Welcome to Arch Network

This documentation is actively maintained. If you find any issues or have suggestions for improvements, please visit our GitHub repository.

What is Arch Network?

Arch Network is a computation environment that enhances Bitcoin's capabilities by enabling complex operations on Bitcoin UTXOs through its specialized virtual machine. Unlike Layer 2 solutions, Arch Network provides a native computation layer that works directly with Bitcoin's security model.

Key Features

Bitcoin-Native

Direct integration with Bitcoin through UTXO management

Computation Environment

Execute complex programs within the Arch VM

Security

Leverages Bitcoin's proven security guarantees through multi-signature validation

Developer Tools

Complete development environment with CLI tools and explorer

Choose Your Path

🚀 Quick Start (15 minutes)

🎓 Learning Path

Master Arch development:
  1. Network Architecture - Understand how nodes work together

  2. Bitcoin Integration - Learn Bitcoin interaction

  3. Program Development - Write programs

🛠 Reference

Core Architecture

How Arch Works

Arch Network consists of three main components:
  1. Network Layer
  1. Bitcoin Integration
  • UTXO Management
    • Transaction tracking
    • State anchoring
    • Ownership validation
  • RPC Integration
    • Bitcoin node communication
    • Transaction submission
    • Network synchronization
  1. Computation Layer

Prerequisites

Before you begin, ensure you have:

Next Steps

Need Help?

- [Join our Discord](https://discord.gg/archnetwork) - [Read the Architecture Overview](concepts/architecture.md) - [View Example Programs](guides/how-to-write-arch-program.md) - [Check Network Status](concepts/network-architecture.md#monitoring-and-telemetry) - [API Reference](rpc/rpc.md)
💡 Pro Tip: Use the search function (press 's' or '/' on your keyboard) to quickly find what you're looking for in the documentation.

Setting up the environment

Here you will find instructions on how to run a local Arch Network development environment.

The core components are outlined within this section to help paint a model for how applications interact with Arch and the Bitcoin network.

System Requirements

Welcome to the Arch Network development guide. This page will walk you through setting up your development environment with all necessary dependencies. Please follow each section carefully to ensure a smooth setup process.

Overview

Before you begin development with Arch Network, you'll need to install and configure the following tools:

RequirementMinimum VersionDescription
RustLatest stableCore development language
DockerLatestContainer runtime for local node infrastructure
C++ Compilergcc/clangRequired for native builds
Node.jsv19+JavaScript runtime for SDK
Solana CLIv1.18.18Solana development tools
[Arch CLI]LatestArch Network development toolkit

Detailed Installation Guide

1. Install Rust

Rust is the primary development language for Arch Network programs.

# Install Rust using rustup
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Verify installation
rustc --version
cargo --version

💡 Note: Make sure you're using the stable channel throughout this book.

2. Install Docker

Docker is essential for running Arch's local node infrastructure.

  1. Download Docker Desktop from the Docker website
  2. Follow the installation wizard for your operating system
  3. Start Docker Desktop
  4. Verify installation:
docker --version

3. C++ Compiler Setup

MacOS Users

The C++ compiler comes pre-installed with Xcode Command Line Tools. Verify with:

gcc --version

If not installed, run:

xcode-select --install

Linux Users (Debian/Ubuntu)

Install the required compiler tools:

sudo apt-get update
sudo apt-get install gcc-multilib build-essential

4. Install Node.js

Node.js is required for working with the arch-typescript-sdk.

# Using nvm (recommended)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
nvm install 19
nvm use 19

# Verify installation
node --version  # Should show v19.x.x or higher
npm --version

5. Install Solana CLI

The Solana CLI is required for program compilation and deployment.

sh -c "$(curl -sSfL https://release.solana.com/v1.18.18/install)"

⚠️ Important Notes:

  • Solana v2.x is not supported
  • You can use stable, beta, or edge channels instead of v1.18.18
  • Add Solana to your PATH as instructed after installation

Troubleshooting Solana Installation

If you installed Rust through Homebrew and encounter cargo-build-sbf issues:

  1. Remove existing Rust installation:
rustup self uninstall
  1. Verify removal:
rustup --version  # Should show "command not found"
  1. Perform clean Rust installation:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
  1. Reinstall Solana:
sh -c "$(curl -sSfL https://release.solana.com/v1.18.18/install)"

6. Install Arch CLI

The Arch CLI provides essential development tools and a local development environment.

# Clone the repository
git clone https://github.com/arch-Network/arch-cli
cd arch-cli

# Install the CLI
cargo install --path .

# Verify installation
arch-cli --version

Features

The Arch CLI provides:

  • Local Arch Network development environment
  • Project setup and deployment tools
  • Example dapp with Arch functionality
  • Mini block explorer for transaction monitoring

Need Help?

Setting up an Arch Network Project

This guide will walk you through setting up a new Arch Network project using the arch-cli tool.

Project Setup Animation

Prerequisites

Before starting, ensure you have the following dependencies installed:

  • Docker (v27.3.1 or later)
  • Docker Compose (v2.29.7 or later)
  • Node.js (v22.9.0 or later)
  • Solana CLI (v1.18.22 or later)
  • Cargo (v1.81.0 or later)

Project Setup Process

1. Initialize the CLI Environment

First, run the initialization command:

arch-cli init

This command will:

  1. Create the necessary folder structure
  2. Set up boilerplate code
  3. Configure Docker settings
  4. Create a default configuration file

2. Configuration Setup

The CLI uses a config.toml file for managing settings. This file will be automatically created during initialization.

Configuration File Location

The default locations for config.toml are:

  • Linux: ~/.config/arch-cli/config.toml
  • macOS: ~/Library/Application Support/arch-cli/config.toml
  • Windows: C:\Users\<User>\AppData\Roaming\arch-cli\config.toml

You can specify a custom location using:

export ARCH_CLI_CONFIG=/path/to/your/config.toml

Key Configuration Settings

The config.toml file contains important settings for:

  • Network configuration
  • Bitcoin node settings
  • Program keys
  • Service ports
  • Arch network parameters

For reference, here's the default configuration structure:

[network]
type = "development"

[bitcoin]
docker_compose_file = "./bitcoin-docker-compose.yml"
network = "regtest"
rpc_endpoint = "http://localhost:18443"
rpc_port = "18443"
rpc_user = "bitcoin"
rpc_password = "password"
rpc_wallet = "devwallet"
services = ["bitcoin", "electrs", "btc-rpc-explorer"]

# ... Additional configuration sections ...
# See full example in documentation

3. Create a New Project

Once the CLI is initialized, you can create a new project using:

arch-cli project create --name my_app

This will:

  1. Create a new project directory in your configured location
  2. Set up the necessary project structure
  3. Configure project-specific settings

Project Structure

Your project will be organized as follows:

sample/
├─ app/                    # Frontend application
│  ├─ frontend/
│  │  ├─ node_modules/    # Frontend dependencies
│  │  ├─ public/         # Static assets
│  │  ├─ src/           # Frontend source code
│  │  ├─ eslint.config.js
│  │  ├─ index.html
│  │  ├─ package-lock.json
│  │  ├─ package.json
│  │  ├─ README.md
│  │  └─ vite.config.js
├─ program/              # Backend program
│  ├─ src/
│  │  └─ lib.rs        # Rust source code
│  ├─ Cargo.lock
│  └─ Cargo.toml

Next Steps

After setting up your project:

  1. Review the generated config.toml file
  2. Familiarize yourself with the project structure
  3. Begin development in your project directory

For more detailed information about specific components, refer to their respective documentation sections.

Starting the stack

Configure

Docker

First, ensure that your Docker client is up-to-date and that the DOCKER_DEFAULT_PLATFORM environment variable is properly set (within your ~/.bashrc or shell of choice) to your machine's architecture.

# Eg, for Apple-Silicon users:
export DOCKER_DEFAULT_PLATFORM=linux/amd64

Start the validator

This spins up a lightweight validator that effectively serves the purpose of testing program deployment and functionality by simulating a single-node blockchain environment locally.

This setup is much less resource intensive than running the Self-contained Arch Network and includes only the VM component needed to test business logic.

Note: If you are looking to work on core components of Arch Network or would like to understand how Arch validators communicate with one another, we recommend looking into the Self-contained Arch Network setup.

The following commands will assist you in provisioning the local validator. Simply start the validator to begin testing your program logic.

arch-cli validator start [options]

If everything pulls and builds correctly, you should see something resembling the following in your logs:

Welcome to the Arch Network CLI
  → Loading configuration from /Users/jr/Library/Application Support/arch-cli/config.toml
Starting the local validator...
Local validator started successfully!

To stop the validator, simply issue the corresponding stop command.

arch-cli validator stop

If everything stops correctly, you should something resembling the following in your logs:

Welcome to the Arch Network CLI
  → Loading configuration from /Users/jr/Library/Application Support/arch-cli/config.toml
Stopping the local validator...
Local validator stopped successfully!

Now that everything is configured and the local validator is up and running, it's time learn how to build, deploy and interact with a program.

Nodes

Let's introduce the nodes that comprise the Arch Network stack in greater detail.

Bootnode

The bootnode works similarly to DNS seeds in Bitcoin whereby the server handles the first connection to nodes joining the Arch Network.

Leader

All signing is coordinated by the leader. Ultimately, the leader submits signed Bitcoin transactions to the Bitcoin network following program execution.

Validator

This node represents a generic node operated by another party. It performs the validator role and has a share in the network's distributed signing key. The leader node passes transactions to validator nodes to validate and sign. After enough signatures have been collected (a threshold has been met), the leader can then submit a fully signed Bitcoin transaction to the Bitcoin network.

The validator node also runs the eBPF virtual machine and executes the transactions asynchronously alongside the other validator nodes in the network.

Lightweight Validator

This validator is a lightweight server that only serves as an RPC for developers to get up and running quickly with the least amount of overhead. It simulates a single-node blockchain environment that is meant for efficient, rapid development.

Note: the Lightweight Validator node uses the same image as the Validator node though operates singularly for maximum efficiency.

More can be read about the Arch Network architecture in our docs.

Resources

Bitcoin mempool and blockchain explorer

Architecture Overview

Core Components

graph TB
    subgraph "Arch Network"
        VM[Arch VM<br/>eBPF-based]
        BTC[Bitcoin Integration]
        
        subgraph "Validator Network"
            L[Leader Node]
            V1[Validator Node 1]
            V2[Validator Node 2]
            V3[Validator Node ...]
            B[Bootnode]
        end
        
        VM --> BTC
        L --> V1
        L --> V2
        L --> V3
        B --> V1
        B --> V2
        B --> V3
    end

Arch VM

The Arch Virtual Machine (VM) is built on eBPF technology, providing a secure and efficient environment for executing programs.

Key features:

  • 🔄 Manages program execution
  • ⚡ Handles state transitions
  • 🎯 Ensures deterministic computation
  • 🔗 Provides syscalls for Bitcoin UTXO operations

Bitcoin Integration

Arch Network interacts directly with Bitcoin through:

  • 💼 Native UTXO management
  • ✅ Transaction validation
  • 🔐 Multi-signature coordination
  • 📝 State commitment to Bitcoin

Validator Network

The validator network consists of multiple node types that work together:

Node Types

Node TypePrimary Responsibilities
Leader Node• Coordinates transaction signing
• Submits signed transactions to Bitcoin
• Manages validator communication
Validator Nodes• Execute programs in the Arch VM
• Validate transactions
• Participate in multi-signature operations
• Maintain network state
Bootnode• Handles initial network discovery
• Similar to Bitcoin DNS seeds
• Helps new nodes join the network

Transaction Flow

sequenceDiagram
    participant C as Client
    participant L as Leader
    participant V as Validators
    participant B as Bitcoin Network
    
    C->>L: 1. Submit Transaction
    L->>V: 2. Distribute to Validators
    V->>V: 3. Execute in Arch VM
    V->>L: 4. Sign Results
    L->>B: 5. Submit to Bitcoin

Security Model

Arch Network implements a robust multi-layered security model that directly leverages Bitcoin's security guarantees:

1. UTXO Security

  • 🔒 Ownership Verification

    • Public key cryptography using secp256k1
    • BIP322 message signing for secure ownership proofs
    • Double-spend prevention through UTXO consumption tracking
  • 🔗 State Management

    • State anchoring to Bitcoin transactions
    • Atomic state transitions with rollback capability
    • Cross-validator state consistency checks

2. Transaction Security

pub struct SecurityParams {
    pub min_confirmations: u32,    // Required Bitcoin confirmations
    pub signature_threshold: u32,   // Multi-sig threshold
    pub timelock_blocks: u32,      // Timelock requirement
    pub max_witness_size: usize    // Maximum witness data size
}
  • 📝 Multi-signature Validation
    • ROAST protocol for distributed signing
    • Threshold signature scheme (t-of-n)
    • Malicious signer detection and removal
    • Binding factor verification for signature shares

3. Network Security

  • 🌐 Validator Selection

    pub struct ValidatorSet {
        pub validators: Vec<ValidatorInfo>,
        pub threshold: u32
    }
    • Stake-weighted validator participation
    • Dynamic threshold adjustment
    • Automatic malicious node detection
  • 🛡️ State Protection

    • Multi-stage transaction verification
    • Bitcoin-based finality guarantees
    • State root commitment to Bitcoin
    • Mandatory signature verification for all state changes

4. Best Practices

  • UTXO Management

    • Minimum 6 confirmations for finality
    • Comprehensive UTXO validation
    • Double-spend monitoring
    • Reorg handling for UTXO invalidation
  • 🔍 Transaction Processing

    • Full signature verification
    • Input/output validation
    • Proper error handling
    • Network partition handling

Network Architecture

Arch Network operates as a distributed system with different types of nodes working together to provide secure and efficient program execution on Bitcoin. This document details the network's architecture and how different components interact.

Network Overview

flowchart TB
    subgraph Core["Core Components"]
        direction TB
        BN[Bitcoin Network]
        Boot[Bootnode]
    end

    subgraph Leader["Leader Node Services"]
        direction LR
        TC[Transaction\nCoordination]
        MS[MultiSig\nAggregation]
    end
    
    subgraph Validators["Validator Network"]
        direction TB
        V1[Validator 1]
        V2[Validator 2]
        V3[Validator 3]
        VN[Validator N]
    end

    BN --> LN[Leader Node]
    Boot --> LN
    
    LN --> TC
    LN --> MS
    
    LN --> V1
    LN --> V2
    LN --> V3
    LN --> VN

    %% Styling
    classDef core fill:#e1f5fe,stroke:#01579b
    classDef leader fill:#fff3e0,stroke:#e65100
    classDef validators fill:#f3e5f5,stroke:#4a148c
    
    class BN,Boot core
    class TC,MS leader
    class V1,V2,V3,VN validators

Node Types

1. Bootnode

The bootnode serves as the network's entry point, similar to DNS seeds in Bitcoin:

  • Handles initial network discovery
  • Maintains whitelist of valid validators
  • Coordinates peer connections
  • Manages network topology
flowchart LR
    subgraph Bootnode["Bootnode Services"]
        direction TB
        PR[Peer Registry]
        WL[Validator Whitelist]
    end

    NN[New Node]
    VN[Validator Network]

    %% Connections
    NN <--> PR
    PR <--> VN
    WL -.-> PR

    %% Styling
    classDef bootnode fill:#e1f5fe,stroke:#01579b
    classDef external fill:#f5f5f5,stroke:#333
    classDef connection stroke-width:2px
    
    class PR,WL bootnode
    class NN,VN external

Configuration:

cargo run -p bootnode -- \
    --network-mode localnet \
    --p2p-bind-port 19001 \
    --leader-peer-id "<LEADER_ID>" \
    --validator-whitelist "<VALIDATOR_IDS>"

2. Leader Node

The leader node coordinates transaction processing and Bitcoin integration:

flowchart TB
    %% Main Components
    BN[Bitcoin Network]
    LN[Leader Node]
    VN[Validator Network]
    PE[Program Execution]
    
    %% Leader Node Services
    subgraph Leader["Leader Node Services"]
        direction LR
        TC[Transaction\nCoordination]
        MS[Multi-sig\nAggregation]
    end
    
    %% Connections
    BN <--> LN
    LN --> Leader
    TC --> VN
    MS --> VN
    VN --> PE
    
    %% Styling
    classDef bitcoin fill:#f7931a,stroke:#c16c07,color:white
    classDef leader fill:#fff3e0,stroke:#e65100
    classDef validator fill:#f3e5f5,stroke:#4a148c
    classDef execution fill:#e8f5e9,stroke:#1b5e20
    
    class BN bitcoin
    class LN,TC,MS leader
    class VN validator
    class PE execution

Key responsibilities:

  • Transaction coordination
  • Multi-signature aggregation
  • Bitcoin transaction submission
  • Network state management

3. Validator Nodes

Validator nodes form the core of the network's computation and validation:

flowchart TB
    subgraph ValidatorNode["Validator Node"]
        direction TB
        
        subgraph Execution["Execution Layer"]
            direction LR
            VM["Arch VM\nExecution"]
            SV["State\nValidation"]
        end
        
        NP["Network Protocol"]
        P2P["P2P Network"]
        
        %% Connections within validator
        VM --> NP
        SV --> NP
        NP --> P2P
    end
    
    %% Styling
    classDef validator fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
    classDef execution fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px
    classDef network fill:#e3f2fd,stroke:#0d47a1,stroke-width:2px
    
    class ValidatorNode validator
    class VM,SV execution
    class NP,P2P network

Types:

  1. Full Validator

    • Participates in consensus
    • Executes programs
    • Maintains full state
  2. Lightweight Validator

    • Local development use
    • Single-node operation
    • Simulated environment

Network Communication

P2P Protocol

The network uses libp2p for peer-to-peer communication:

pub const ENABLED_PROTOCOLS: [&str; 2] = [
    ArchNetworkProtocol::STREAM_PROTOCOL,
    ArchNetworkProtocol::VALIDATOR_PROTOCOL,
];

// Protocol versions
pub const PROTOCOL_VERSION: &str = "/arch/1.0.0";
pub const VALIDATOR_VERSION: &str = "/arch/validator/1.0.0";

Message Types

  1. Network Messages

    pub enum NetworkMessage {
        Discovery(DiscoveryMessage),
        State(StateMessage),
        Transaction(TransactionMessage),
    }
  2. ROAST Protocol Messages

    pub enum RoastMessage {
        KeyGeneration(KeyGenMessage),
        Signing(SigningMessage),
        Aggregation(AggregationMessage),
    }

Network Modes

1. Devnet

  • Local development environment
  • Single validator setup
  • Simulated Bitcoin interactions
  • Fast block confirmation

2. Testnet

  • Test environment with multiple validators
  • Bitcoin testnet integration
  • Real network conditions
  • Test transaction processing

3. Mainnet

  • Production network
  • Full security model
  • Bitcoin mainnet integration
  • Live transaction processing

Security Model

1. Validator Selection

pub struct ValidatorInfo {
    pub peer_id: PeerId,
    pub pubkey: Pubkey,
    pub stake: u64,
}

pub struct ValidatorSet {
    pub validators: Vec<ValidatorInfo>,
    pub threshold: u32,
}

2. Transaction Security

  • Multi-signature validation using ROAST protocol
  • Threshold signing (t-of-n)
  • Bitcoin-based finality
  • Double-spend prevention

3. State Protection

pub struct StateUpdate {
    pub block_height: u64,
    pub state_root: Hash,
    pub bitcoin_height: u64,
    pub signatures: Vec<Signature>,
}

Monitoring and Telemetry

1. Node Metrics

pub struct NodeMetrics {
    pub peer_id: PeerId,
    pub network_mode: ArchNetworkMode,
    pub bitcoin_block_height: u64,
    pub arch_block_height: u64,
    pub peers_connected: u32,
    pub transactions_processed: u64,
    pub program_count: u32,
}

2. Network Health

pub struct NetworkHealth {
    pub validator_count: u32,
    pub active_validators: u32,
    pub network_tps: f64,
    pub average_block_time: Duration,
    pub fork_count: u32,
}

3. Monitoring Endpoints

  • /metrics - Prometheus metrics
  • /health - Node health check
  • /peers - Connected peers
  • /status - Network status

Best Practices

1. Node Operation

  • Secure key management
  • Regular state verification
  • Proper shutdown procedures
  • Log management

2. Network Participation

  • Maintain node availability
  • Monitor Bitcoin integration
  • Handle network upgrades
  • Backup critical data

3. Development Setup

  • Use lightweight validator for testing
  • Monitor resource usage
  • Handle network modes properly
  • Implement proper error handling

Bitcoin Integration

Arch Network provides direct integration with Bitcoin, enabling programs to interact with Bitcoin's UTXO model while maintaining Bitcoin's security guarantees. This document details how Arch Network integrates with Bitcoin.

Architecture Overview

flowchart TB
    subgraph BN[Bitcoin Network]
        BNode[Bitcoin Node]
    end

    subgraph AN[Arch Network]
        LN[Leader Node\nBitcoin Integration]
        subgraph VN[Validator Network]
            P1[Program 1]
            PN[Program N]
        end
    end

    BNode <--> LN
    LN <--> VN
    P1 --- PN

Core Components

1. UTXO Management

Arch Network manages Bitcoin UTXOs through a specialized system:

flowchart LR
    subgraph UTXO[Bitcoin UTXO]
        TxID[Transaction\nID]
        OutIdx[Output\nIndex]
    end

    subgraph Account[Arch Account]
        Meta[UTXO\nMeta]
        State[Program\nState]
    end

    TxID --> Meta
    OutIdx --> State
// UTXO Metadata Structure
pub struct UtxoMeta {
    pub txid: [u8; 32],  // Transaction ID
    pub vout: u32,       // Output index
    pub amount: u64,     // Amount in satoshis
    pub script_pubkey: Vec<u8>, // Output script
    pub confirmation_height: Option<u32>, // Block height of confirmation
}

// UTXO Account State
pub struct UtxoAccount {
    pub meta: UtxoMeta,
    pub owner: Pubkey,
    pub delegate: Option<Pubkey>,
    pub state: Vec<u8>,
    pub is_frozen: bool,
}

Key operations:

// UTXO Operations
pub trait UtxoOperations {
    fn create_utxo(meta: UtxoMeta, owner: &Pubkey) -> Result<()>;
    fn spend_utxo(utxo: &UtxoMeta, signature: &Signature) -> Result<()>;
    fn freeze_utxo(utxo: &UtxoMeta, authority: &Pubkey) -> Result<()>;
    fn delegate_utxo(utxo: &UtxoMeta, delegate: &Pubkey) -> Result<()>;
}

2. Bitcoin RPC Integration

flowchart LR
    AP[Arch\nProgram]
    RPC[Bitcoin RPC\nInterface]
    BN[Bitcoin\nNode]
    Config[Configuration]
    Network[Bitcoin\nNetwork]

    AP --> RPC
    RPC --> BN
    AP --> Config
    Config --> RPC
    BN --> Network

    style AP fill:#f9f9f9,stroke:#333,stroke-width:2px
    style RPC fill:#f9f9f9,stroke:#333,stroke-width:2px
    style BN fill:#f9f9f9,stroke:#333,stroke-width:2px
    style Config fill:#f9f9f9,stroke:#333,stroke-width:2px
    style Network fill:#f9f9f9,stroke:#333,stroke-width:2px

Programs can interact with Bitcoin through RPC calls:

// Bitcoin RPC Configuration
pub struct BitcoinRpcConfig {
    pub endpoint: String,
    pub port: u16,
    pub username: String,
    pub password: String,
    pub wallet: Option<String>,
    pub network: BitcoinNetwork,
    pub timeout: Duration,
}

// RPC Interface
pub trait BitcoinRpc {
    fn get_block_count(&self) -> Result<u64>;
    fn get_block_hash(&self, height: u64) -> Result<BlockHash>;
    fn get_transaction(&self, txid: &Txid) -> Result<Transaction>;
    fn send_raw_transaction(&self, tx: &[u8]) -> Result<Txid>;
    fn verify_utxo(&self, utxo: &UtxoMeta) -> Result<bool>;
}

Transaction Flow

sequenceDiagram
    participant Program
    participant Leader
    participant Validator
    participant Bitcoin

    Program->>Leader: Create UTXO
    Leader->>Validator: Validate
    Validator->>Leader: Sign
    Leader->>Bitcoin: Submit TX
    Bitcoin-->>Program: Confirmation

1. Transaction Creation

// Create new UTXO transaction
pub struct UtxoCreation {
    pub amount: u64,
    pub owner: Pubkey,
    pub metadata: Option<Vec<u8>>,
}

impl UtxoCreation {
    pub fn new(amount: u64, owner: Pubkey) -> Self {
        Self {
            amount,
            owner,
            metadata: None,
        }
    }

    pub fn with_metadata(mut self, metadata: Vec<u8>) -> Self {
        self.metadata = Some(metadata);
        self
    }
}

2. Transaction Validation

// Validation rules
pub trait TransactionValidation {
    fn validate_inputs(&self, tx: &Transaction) -> Result<()>;
    fn validate_outputs(&self, tx: &Transaction) -> Result<()>;
    fn validate_signatures(&self, tx: &Transaction) -> Result<()>;
    fn validate_script(&self, tx: &Transaction) -> Result<()>;
}

3. State Management

// State transition
pub struct StateTransition {
    pub previous_state: Hash,
    pub next_state: Hash,
    pub utxos_created: Vec<UtxoMeta>,
    pub utxos_spent: Vec<UtxoMeta>,
    pub bitcoin_height: u64,
}

Security Model

1. UTXO Security

  • Ownership verification through public key cryptography
  • Double-spend prevention through UTXO consumption
  • State anchoring to Bitcoin transactions
  • Threshold signature requirements

2. Transaction Security

// Transaction security parameters
pub struct SecurityParams {
    pub min_confirmations: u32,
    pub signature_threshold: u32,
    pub timelock_blocks: u32,
    pub max_witness_size: usize,
}

3. Network Security

  • Multi-signature validation
  • Threshold signing (t-of-n)
  • Bitcoin-based finality
  • Cross-validator consistency

Error Handling

1. Bitcoin Errors

pub enum BitcoinError {
    ConnectionFailed(String),
    InvalidTransaction(String),
    InsufficientFunds(u64),
    InvalidUtxo(UtxoMeta),
    RpcError(String),
}

2. UTXO Errors

pub enum UtxoError {
    NotFound(UtxoMeta),
    AlreadySpent(UtxoMeta),
    InvalidOwner(Pubkey),
    InvalidSignature(Signature),
    InvalidState(Hash),
}

Best Practices

1. UTXO Management

  • Always verify UTXO ownership
  • Wait for sufficient confirmations
  • Handle reorganizations gracefully
  • Implement proper error handling

2. Transaction Processing

  • Validate all inputs and outputs
  • Check signature thresholds
  • Maintain proper state transitions
  • Monitor Bitcoin network status

3. Security Considerations

  • Protect private keys
  • Validate all signatures
  • Monitor for double-spend attempts
  • Handle network partitions

Nodes

Let's introduce the nodes that comprise the Arch Network stack in greater detail.

Bootnode

The bootnode works similarly to DNS seeds in Bitcoin whereby the server handles the first connection to nodes joining the Arch Network.

Leader

All signing is coordinated by the leader. Ultimately, the leader submits signed Bitcoin transactions to the Bitcoin network following program execution.

Validator

This node represents a generic node operated by another party. It performs the validator role and has a share in the network's distributed signing key. The leader node passes transactions to validator nodes to validate and sign. After enough signatures have been collected (a threshold has been met), the leader can then submit a fully signed Bitcoin transaction to the Bitcoin network.

The validator node also runs the eBPF virtual machine and executes the transactions asynchronously alongside the other validator nodes in the network.

Lightweight Validator

This validator is a lightweight server that only serves as an RPC for developers to get up and running quickly with the least amount of overhead. It simulates a single-node blockchain environment that is meant for efficient, rapid development.

Note: the Lightweight Validator node uses the same image as the Validator node though operates singularly for maximum efficiency.

More can be read about the Arch Network architecture in our docs.

Program

A program is a special kind of account that contains executable eBPF bytecode, denoted by the Account.is_executable: true field. This allows an account to receive arbitrary instruction data via a transaction to be processed by the runtime.

Every program is stateless, meaning that it can only read/write data to other accounts and that it cannot write to its own account; this, in-part, is how parallelized execution is made possible (see State for more info).

💡 Additionally, programs can send instructions to other programs which, in turn, receive instructions and thus extend program composability further. This is known as cross-program invocation (CPI) and will be detailed in future sections.

Components:

1. Entrypoint

Every Arch program includes a single entrypoint used to invoke the program. A handler function, often named process_instruction, is then used to handle the data passed into the entrypoint.

These parameters are required for every instruction to be processed._

use arch_program::entrypoint;
entrypoint!(process_instruction);

pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> Result<(), ProgramError> {
    // Program logic here
}

lib.rs

2. Instruction

The instruction_data is deserialized after being passed into the entrypoint. From there, if there are multiple instructions, a match statement can be utilized to point the logic flow to the appropriate handler function previously defined within the program which can continue processing the instruction.

3. Process Instruction

If a program has multiple instructions, a corresponding handler function should be defined to include the specific logic unique to the instruction.

4. State

Since programs are stateless, a "data" account is needed to hold state for a user. This is a non-executable account that holds program data.

If a program receives instruction that results in a user's state being altered, the program would manage this user's state via a mapping within the program's logic. This mapping would link the user's pubkey with a data account where the state would live for that specific program.

The program will likely include a struct to define the structure of its state and make it easier to work with. The deserialization of account data occurs during program invocation. After an update is made, state data gets re-serialized into a byte array and stored within the data field of the account.

UTXO (Unspent Transaction Output)

UTXOs (Unspent Transaction Outputs) are fundamental to Bitcoin's transaction model and serve as the foundation for state management in Arch Network. Unlike account-based systems that track balances, UTXOs represent discrete "coins" that must be consumed entirely in transactions.

Core Concepts

What is a UTXO?

  • A UTXO represents an unspent output from a previous transaction
  • Each UTXO is uniquely identified by a transaction ID (txid) and output index (vout)
  • UTXOs are immutable - they can only be created or spent, never modified
  • Once spent, a UTXO cannot be reused (prevents double-spending)

Role in Arch Network

  • UTXOs anchor program state to Bitcoin's security model
  • They provide deterministic state transitions
  • Enable atomic operations across the network
  • Allow for provable ownership and state validation

UTXO Structure

The UtxoMeta struct encapsulates the core UTXO identification data:

use arch_program::utxo::UtxoMeta;
use bitcoin::Txid;

#[derive(Debug, Clone, PartialEq)]
pub struct UtxoMeta {
    pub txid: [u8; 32],  // Bitcoin transaction ID (32 bytes)
    pub vout: u32,       // Output index in the transaction
}

impl UtxoMeta {
    /// Creates a new UTXO metadata instance
    pub fn new(txid: [u8; 32], vout: u32) -> Self {
        Self { txid, vout }
    }

    /// Deserializes UTXO metadata from a byte slice
    /// Format: [txid(32 bytes)][vout(4 bytes)]
    pub fn from_slice(data: &[u8]) -> Self {
        let mut txid = [0u8; 32];
        txid.copy_from_slice(&data[0..32]);
        let vout = u32::from_le_bytes([
            data[32], data[33], data[34], data[35]
        ]);
        Self { txid, vout }
    }
}

UTXO Lifecycle

1. Creation Process

Creating a UTXO with Bitcoin RPC

use bitcoincore_rpc::{Auth, Client as RpcClient, RpcApi};
use bitcoin::{Amount, Address};
use arch_program::pubkey::Pubkey;

// Initialize Bitcoin RPC client
let rpc = RpcClient::new(
    "http://localhost:18443",  // Bitcoin node RPC endpoint
    Auth::UserPass(
        "user".to_string(),
        "pass".to_string()
    )
).expect("Failed to create RPC client");

// Generate a new account address
let account_address = Pubkey::new_unique();
let btc_address = Address::from_pubkey(&account_address);

// Create UTXO by sending Bitcoin
// Parameters explained:
// - address: Destination Bitcoin address
// - amount: Amount in satoshis (3000 sats = 0.00003 BTC)
// - comment: Optional transaction comment
// - replaceable: Whether the tx can be replaced (RBF)
let txid = rpc.send_to_address(
    &btc_address,
    Amount::from_sat(3000),
    Some("Create Arch UTXO"),  // Comment
    None,                      // Comment_to
    Some(true),               // Replaceable
    None,                     // Fee rate
    None,                     // Fee estimate mode
    None                      // Avoid reuse
)?;

// Wait for confirmation (recommended)
rpc.wait_for_confirmation(&txid, 1)?;

Creating an Arch Account with UTXO

use arch_program::{
    system_instruction::SystemInstruction,
    pubkey::Pubkey,
    transaction::Transaction,
};

// Create new program account backed by UTXO
let account_pubkey = Pubkey::new_unique();
let instruction = SystemInstruction::new_create_account_instruction(
    txid.try_into().unwrap(),
    0,  // vout index
    account_pubkey,
    // Additional parameters like:
    // - space: Amount of space to allocate
    // - owner: Program that owns the account
);

// Build and sign transaction
let transaction = Transaction::new_signed_with_payer(
    &[instruction],
    Some(&payer.pubkey()),
    &[&payer],
    recent_blockhash
);

2. Validation & Usage

Programs must implement proper UTXO validation:

fn validate_utxo(utxo: &UtxoMeta) -> Result<(), ProgramError> {
    // 1. Verify UTXO exists on Bitcoin
    let btc_tx = rpc.get_transaction(&utxo.txid)?;
    
    // 2. Check confirmation count
    if btc_tx.confirmations < MIN_CONFIRMATIONS {
        return Err(ProgramError::InsufficientConfirmations);
    }
    
    // 3. Verify output index exists
    if utxo.vout as usize >= btc_tx.vout.len() {
        return Err(ProgramError::InvalidVout);
    }
    
    // 4. Verify UTXO is unspent
    if is_spent(utxo) {
        return Err(ProgramError::UtxoAlreadySpent);
    }
    
    Ok(())
}

3. State Management

// Example UTXO state tracking
#[derive(Debug)]
pub struct UtxoState {
    pub meta: UtxoMeta,
    pub status: UtxoStatus,
    pub owner: Pubkey,
    pub created_at: i64,
    pub spent_at: Option<i64>,
}

#[derive(Debug)]
pub enum UtxoStatus {
    Pending,    // Waiting for confirmations
    Active,     // Confirmed and spendable
    Spent,      // UTXO has been consumed
    Invalid,    // UTXO was invalidated (e.g., by reorg)
}

Best Practices

  1. Validation

    • Always verify UTXO existence on Bitcoin
    • Check for sufficient confirmations (recommended: 6+)
    • Validate ownership and spending conditions
    • Handle Bitcoin reorgs that might invalidate UTXOs
  2. State Management

    • Implement robust UTXO tracking
    • Handle edge cases (reorgs, conflicting txs)
    • Consider implementing UTXO caching for performance
    • Maintain accurate UTXO sets for your program
  3. Security

    • Never trust client-provided UTXO data without verification
    • Implement proper access controls
    • Consider timelock constraints for sensitive operations
    • Monitor for suspicious UTXO patterns
  4. Performance

    • Batch UTXO operations when possible
    • Implement efficient UTXO lookup mechanisms
    • Consider UTXO consolidation strategies
    • Cache frequently accessed UTXO data

Error Handling

Common UTXO-related errors to handle:

pub enum UtxoError {
    NotFound,                    // UTXO doesn't exist
    AlreadySpent,               // UTXO was already consumed
    InsufficientConfirmations,  // Not enough confirmations
    InvalidOwner,               // Unauthorized attempt to spend
    Reorged,                    // UTXO invalidated by reorg
    InvalidVout,                // Output index doesn't exist
    SerializationError,         // Data serialization failed
}

Basics

To illustrate the basic process involved in developing an Arch Program, we're going to build, deploy and interact with our demo app: GraffitiWall.

Building, deploying and interfacing

Now that all of the dependencies are installed and we have successfully started our development stack, we can finally discuss program development, including compiling, deploying and interacting with it.

The arch-cli comes with a demo dapp called GraffitiWall; each message written to the wall contains a timestamp, name and note.

Find the program's logic within the app/program/src/lib.rs file.

Build

In order to compile the program, we'll make use of the cargo-build-sbf a binary, a tool that comes with the Solana-CLI that installs the toolchain needed to produce Executable and Linkable Format (ELF) files which consist of eBPF bytecode.

Access the app/program/src folder:

cd app/program/src

Build the program

cargo-build-sbf

You will find the generated shared object file at: ./target/deploy/arch_network_app.so

If you are experiencing issues with this step, we recommend returning to review the requirements page or hopping in our Discord dev-chat channel for support.

Deploy

In this step, we will be submitting a transaction to store our program's logic on the Arch Network.

Note: make sure you have arch validator running before deploying the program, if you don't run arch-cli validator start

arch-cli deploy

Output:

Welcome to the Arch Network CLI
  → Loading configuration from /Users/jr/Library/Application Support/arch-cli/config.toml
Deploying your Arch Network app...
  ℹ Building program...
  ✓ Program built successfully
  ℹ Program ID: 3688ef8de06d56e32a765243e900875c4fefc6aa9c83dfbc2643f661c5b4982e
  ℹ Failed to load wallet 'devwallet'. Error: JSON-RPC error: RPC error response: RpcError { code: -18, message: "Wallet file verification failed. Failed to load database path '/home/bitcoin/.bitcoin/regtest/wallets/devwallet'. Path does not exist.", data: None }
Attempting to resolve the issue...
  ✓ Wallet 'devwallet' created successfully.
  → Generating initial blocks for mining rewards...
  ✓ Initial blocks generated
  ✓ Transaction sent: db69c08c4ff7df6081499d20c7f544f91f167fd184cbf3487f1ced8f1e75c848
  ✓ Transaction confirmed with 1 confirmations
    Creating program account...
    Program account created successfully
    Deploying program transactions...
Starting program deployment
  ℹ ELF file size: 175640 bytes
  → Deploying program with 210 transactions
  Transactions sent successfully                                                                                                                                             ✓ Successfully sent 210 transactions for program deployment
  [00:00:11] [########################################] 210/210 (100%)                                                                                                         Program transactions deployed successfully
    Making program executable...
    Transaction sent: 23de0a1d6fe6ec1f7304488aa223d092bcaa64ea63a23c61b765a248063e6e9c
    Program made executable successfully
  ✓ Program deployed successfully
  ✓ Wallet 'devwallet' unloaded successfully.
Your app has been deployed successfully!
  ℹ Program ID: 3688ef8de06d56e32a765243e900875c4fefc6aa9c83dfbc2643f661c5b4982e

Copy the Program ID from the output as you will need this again later.

The Program ID can be thought of as a uniform resource locator (URL) for your deployed program on the Arch Network.

Create an Account

An account is used to store the state for your dapp.

Obtain the Program ID from the deployment step output and use it within this command.

arch-cli account create --name graffiti --program-id <program-id>

Start the demo application

arch-cli demo start

Output:

Welcome to the Arch Network CLI
  → Loading configuration from /Users/jr/Library/Application Support/arch-cli/config.toml
Starting the frontend application...
  → Copying .env.example to .env...
  ✓ .env file created
  → Installing npm packages...
npm warn deprecated noble-secp256k1@1.2.14: Switch to namespaced @noble/secp256k1 for security and feature updates
npm warn deprecated @types/tailwindcss@3.1.0: This is a stub types definition. tailwindcss provides its own type definitions, so you do not need this installed.

added 393 packages, and audited 394 packages in 3s

131 packages are looking for funding
  run `npm fund` for details

2 vulnerabilities (1 moderate, 1 high)

To address all issues, run:
  npm audit fix

Run `npm audit` for details.
npm notice
npm notice New patch version of npm available! 10.8.2 -> 10.8.3
npm notice Changelog: https://github.com/npm/cli/releases/tag/v10.8.3
npm notice To update run: npm install -g npm@10.8.3
npm notice
  ✓ npm packages installed
  → Building and starting the Vite server...
  ✓ Vite server started
  → Opening application in default browser...
  ✓ Application opened in default browser
Frontend application started successfully!
Press Ctrl+C to stop the server and exit.

If the window doesn't pop up, navigate to: http://localhost:5173 to interface with your deployed program via the web frontend.

🎨

Now you're ready to tag the wall!

Program interaction

Table of Contents:

Description

Continuing with our example program, GraffitiWall, we find an implementation example of how to communicate with a deployed Program by looking at the frontend code; specifically, we'll look at the GrafittiWallComponent.tsx file.

Logic

const client = new RpcConnection((import.meta as any).env.VITE_RPC_URL || 'http://localhost:9002');
const PROGRAM_PUBKEY = (import.meta as any).env.VITE_PROGRAM_PUBKEY;
const WALL_ACCOUNT_PUBKEY = (import.meta as any).env.VITE_WALL_ACCOUNT_PUBKEY;

Here we initialize a new RPC connection and pass in the RPC URL that we wish to connect to; in this case, the URL is pulled from the environment variables or defaults to our locally running validator.

We then import the Pubkeys of the Graffiti Wall Program as well as the Wall state Account. The Wall state Account stores the state of the Graffiti Wall and the Program's Pubkey serves as the owner of the Graffiti Wall.

class GraffitiMessage {
  constructor(
    public timestamp: number,
    public name: string,
    public message: string
  ) {}

  static schema = new Map([
    [
      GraffitiMessage,
      {
        kind: 'struct',
        fields: [
          ['timestamp', 'i64'],
          ['name', ['u8', 16]],
          ['message', ['u8', 64]]
        ]
      }
    ]
  ]);
}

// Define the schema for the wall containing messages
class GraffitiWall {
  constructor(public messages: GraffitiMessage[]) {}

  static schema = new Map([
    [
      GraffitiWall,
      {
        kind: 'struct',
        fields: [
          ['messages', [GraffitiMessage]]
        ]
      }
    ]
  ]);
}

We then define the schemas for handling the Wall's message data- these schemas mirror the data structure that are found within the GraffitiWall program which ensures data uniformity between the application frontend and program backend during serialization/deserialization.

#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)]
pub struct GraffitiMessage {
    pub timestamp: i64,
    pub name: [u8; 16],
    pub message: [u8; 64],
}

Above is the data structure as defined in src/app/program/src/lib.rs, our Program.

  const accountPubkey = PubkeyUtil.fromHex(WALL_ACCOUNT_PUBKEY);

  const schema = {
    struct: {
      messages: {
        seq: {
          struct: {
            timestamp: 'i64',
            name: { array: { type: 'u8', len: 16 } },
            message: { array: { type: 'u8', len: 64 } }
          }
        }
      }
    }
  };

Moving along, we set the accountPubkey variable with our previously imported Wall account Pubkey converted from hexidecimal and then set a new schema for to handle the Graffiti Wall message data.

const checkProgramDeployed = useCallback(async () => {
  try {
    const pubkeyBytes = PubkeyUtil.fromHex(PROGRAM_PUBKEY);
    const accountInfo = await client.readAccountInfo(pubkeyBytes);
    if (accountInfo) {
      setIsProgramDeployed(true);
      setError(null);
    }
  } catch (error) {
    console.error('Error checking program:', error);
    setError('The Arch Graffiti program has not been deployed to the network yet. Please run `arch-cli deploy`.');
  }
}, []);

We then submit our first request to the Arch RPC service: readAccountInfo.

We pass the pubkeyBytes- which represents the program_id, indicating the unique resource location of the Program within the Arch Network- as the argument to readAccountInfo in order to obtain the AccountInfo.

If we are able to read the accountInfo successfully, then we can be sure the program was deployed.

  const checkAccountCreated = useCallback(async () => {
    try {
      const pubkeyBytes = PubkeyUtil.fromHex(WALL_ACCOUNT_PUBKEY);
      const accountInfo = await client.readAccountInfo(pubkeyBytes);
      if (accountInfo) {
        setIsAccountCreated(true);
        setError(null);
      }
    } catch (error) {
      console.error('Error checking account:', error);
      setIsAccountCreated(false);
      setError('The wall account has not been created yet. Please run the account creation command.');
    }
  }, []);

Similarly, we perform the same check against the Wall account, ensuring that the Graffiti wall has an account provisioned to manage the program's state. Without this state account the program would not have any data to work with; as a reminder, every Arch Program is stateless.

const fetchWallData = useCallback(async () => {
    try {
        const userAccount = await client.readAccountInfo(accountPubkey);
        if (!userAccount) {
            setError('Account not found.');
            return;
        }
        const wallData = userAccount.data;
        
        console.log(`Wall data: ${wallData}`);
...

We then begin to retrieve the Graffiti Wall data from the Wall account.

We make an RPC call to read in the account info just as we did in the previous step, only this time we access the data stored within the AccountInfo. As of now, this data is not yet parsed, so it comes back as bytes which will need to be handled.

        // If data is empty or invalid length, just set empty messages without error
        if (!wallData || wallData.length < 4) {
            setWallData([]);
            setError(null); // Clear any existing errors
            return;
        }

We perform a check against the length of this bytedata to ensure that it is not empty, meaning there is at least some data stored within the Wall account.

        // Deserialize the wall data using borsh
        // Read data directly from the buffer
        const messages = [];
        let offset = 0;

        // First 4 bytes are the array length
        const messageCount = new DataView(wallData.buffer).getUint32(offset, true);
        offset += 4;

        for (let i = 0; i < messageCount; i++) {
            // Read timestamp (8 bytes)
            const timestamp = new DataView(wallData.buffer).getBigInt64(offset, true);
            offset += 8;

            // Read name (16 bytes)
            const nameBytes = wallData.slice(offset, offset + 16);
            const name = new TextDecoder().decode(nameBytes.filter(x => x !== 0));
            offset += 16;

            // Read message (64 bytes)
            const messageBytes = wallData.slice(offset, offset + 64);
            const message = new TextDecoder().decode(messageBytes.filter(x => x !== 0));
            offset += 64;

            messages.push(new GraffitiMessage(
                Number(timestamp),
                name,
                message
            ));
        }

        messages.sort((a, b) => b.timestamp - a.timestamp);

        setWallData(messages);
    } catch (error) {
        console.error('Error fetching wall data:', error);
        setError(`Failed to fetch wall data: ${error instanceof Error ? error.message : String(error)}`);
    }
    ...

We now need to deserialize the bytedata into a structure that is more manageable, in this case, we'll make use of the GraffitiMessage schema we set earlier.

  const handleAddToWall = async () => {
    if (!message.trim() || !name.trim() || !isAccountCreated || !wallet.isConnected) {
      setError("Name and message are required, account must be created, and wallet must be connected.");
      return;
    }

We'll again skip over some React state management.

handleAddToWall contains the lion's share of the logic for serializing data and submitting this data to the Program.

  const serializeGraffitiData = (name: string, message: string): number[] => {
    // Create fixed-size arrays
    const nameArray = new Uint8Array(16).fill(0);
    const messageArray = new Uint8Array(64).fill(0);
    
    // Convert strings to bytes
    const nameBytes = new TextEncoder().encode(name);
    const messageBytes = new TextEncoder().encode(message);
    
    // Copy bytes into fixed-size arrays (will truncate if too long)
    nameArray.set(nameBytes.slice(0, 16));
    messageArray.set(messageBytes.slice(0, 64));
    
    // Create the params object matching the Rust struct
    const params = {
        name: Array.from(nameArray),
        message: Array.from(messageArray)
    };
    
    // Define the schema for borsh serialization
    const schema = {
        struct: {
            name: { array: { type: 'u8', len: 16 } },
            message: { array: { type: 'u8', len: 64 } }
        }
    };
    
    return Array.from(borsh.serialize(schema, params));
    ...

In this anonymous function we pass in our dapp data, name and message in order to prepare it for submission to the Program.

We create two new Uint8 byte arrays and initialize their appropriate lengths with placeholder zeros, eventually copying the encoded name and message data into into fixed-size arrays and storing them within the params object.

We define the schema for the data and serialize the scheme alongside the params object which we will use within the following try block.

    try {
      const data = serializeGraffitiData(name, message);
    
      const instruction: Instruction = {
        program_id: PubkeyUtil.fromHex(PROGRAM_PUBKEY),
        accounts: [
          { 
            pubkey: PubkeyUtil.fromHex(wallet.publicKey!), 
            is_signer: true, 
            is_writable: false 
          },
          { 
            pubkey: accountPubkey, 
            is_signer: false, 
            is_writable: true 
          },
        ],
        data: new Uint8Array(data),
      };
...

Stepping into our try block, we serialize our post data, in this case including the name of the author as well as the message they wish to post.

We construct an Instruction object, containing our program_id, serialized data, as well as the accounts involved, in this case, the signing Pubkey of our user's wallet as well as the accountPubkey, the Pubkey of the Wall state account.

   const messageObj : Message = {
        signers: [PubkeyUtil.fromHex(wallet.publicKey!)],
        instructions: [instruction],
      };

We then construct our Message object to hold the needed signers (our user) as well as the previously formed Instruction.

const messageBytes = MessageUtil.serialize(messageObj);
...
const signature = await wallet.signMessage(Buffer.from(MessageUtil.hash(messageObj)).toString('hex'));

We then serialize our Message and then craft our Signature.

const signatureBytes = new Uint8Array(Buffer.from(signature, 'base64')).slice(2);
console.log(`Signature bytes: ${signatureBytes}`);

const result = await client.sendTransaction({
  version: 0,
  signatures: [signatureBytes],
  message: messageObj,
});

We then store our Signature within a new Uint8 array and create a slice from it in order to segregate the last 64-bytes of the base64 decoded Signature.

const result = await client.sendTransaction({
  version: 0,
  signatures: [signatureBytes],
  message: messageObj,
});

We then craft our Transaction object within the RPC call to sendTransaction, passing in our sliced Signature and serialized Message, along with the correct Transaction version (0), successfully submitting our state change to the Arch Network for processing.

🎨

This concludes the logic walkthrough of the Program interaction component of our GraffitiWall.

Guides

In this section, we'll provide a few guides that can step through constructing an Arch program, as well as deploying and interacting with your program.

How to write an Arch program

Table of Contents:


The Arch Book can serve as a reference for concepts introduced here as well as our docs for high-level architecture diagrams and comparisons to other similar projects building on Bitcoin.

For this guide, we will be walking through an example program: helloworld.

Logic

A smart contract on Arch is known as a Program.

use arch_program::{
    account::AccountInfo,
    entrypoint, msg,
    helper::add_state_transition,
    input_to_sign::InputToSign,
    program::{
        get_account_script_pubkey, get_bitcoin_block_height,
        next_account_info, set_transaction_to_sign, invoke
    },
    program_error::ProgramError,
    pubkey::Pubkey, utxo::UtxoMeta, 
    transaction_to_sign::TransactionToSign,
    system_instruction::SystemInstruction,
};
use bitcoin::{self, Transaction, transaction::Version, absolute::LockTime};
use borsh::{BorshDeserialize, BorshSerialize};

entrypoint!(process_instruction);
pub fn process_instruction(
    _program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> Result<(), ProgramError> {
    if accounts.len() != 2 {
        return Err(ProgramError::Custom(501));
    }

    let bitcoin_block_height = get_bitcoin_block_height();
    msg!("bitcoin_block_height {:?}", bitcoin_block_height);

    let account_iter = &mut accounts.iter();
    let account = next_account_info(account_iter)?;
    let account2 = next_account_info(account_iter)?;

    msg!("account {:?}", account);
    msg!("account2 {:?}", account2);

    if account2.utxo.clone() != UtxoMeta::from_slice(&[0; 36]) {
        msg!("UTXO {:?}", account2.utxo.clone());
        return Err(ProgramError::Custom(502));
    }

    let params: HelloWorldParams = borsh::from_slice(instruction_data).unwrap();
    let fees_tx: Transaction = bitcoin::consensus::deserialize(&params.tx_hex).unwrap();

    let new_data = format!("Hello {}", params.name);

    // Extend the account data to fit the new data
    let data_len = account.data.try_borrow().unwrap().len();
    if new_data.as_bytes().len() > data_len {
        account.realloc(new_data.len(), true)?;
    }

    let script_pubkey = get_account_script_pubkey(account.key);
    msg!("script_pubkey {:?}", script_pubkey);

    account
        .data
        .try_borrow_mut()
        .unwrap()
        .copy_from_slice(new_data.as_bytes());

    if account2.is_writable {

        invoke(
            &SystemInstruction::new_create_account_instruction(
                params.utxo.txid().try_into().unwrap(), 
                params.utxo.vout(), account2.key.clone()
            ), 
            &[account2.clone()]
        ).expect("failed");
    }

    let mut tx = Transaction {
        version: Version::TWO,
        lock_time: LockTime::ZERO,
        input: vec![],
        output: vec![],
    };
    add_state_transition(&mut tx, account);
    tx.input.push(fees_tx.input[0].clone());

    let tx_to_sign = TransactionToSign {
        tx_bytes: &bitcoin::consensus::serialize(&tx),
        inputs_to_sign: &[InputToSign {
            index: 0,
            signer: account.key.clone(),
        }],
    };

    msg!("tx_to_sign{:?}", tx_to_sign);

    set_transaction_to_sign(accounts, tx_to_sign)
}

#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)]
pub struct HelloWorldParams {
    pub name: String,
    pub tx_hex: Vec<u8>,
    pub utxo: UtxoMeta,
}

Imports

First, let's bring our arch_program, borsh and bitcoin crates into local namespace.

use arch_program::{
    account::AccountInfo,
    entrypoint, msg,
    helper::add_state_transition,
    input_to_sign::InputToSign,
    program::{
        get_account_script_pubkey, get_bitcoin_block_height,
        next_account_info, set_transaction_to_sign, invoke
    },
    program_error::ProgramError,
    pubkey::Pubkey, utxo::UtxoMeta, 
    transaction_to_sign::TransactionToSign,
    system_instruction::SystemInstruction,
};
use bitcoin::{self, Transaction, transaction::Version, absolute::LockTime};
use borsh::{BorshDeserialize, BorshSerialize};

Before we continue, let's quickly introduce some helpful resources that we are importing:

  • entrypoint: a macro used for invoking our program.
  • msg: a macro used for logging messages; these are visible within the node logs of your local validator.
  • borsh: a crate for serialization/deserialization of data passed to/from our program.
  • bitcoin: a crate for working with the Bitcoin blockchain.

Entrypoint

Every Arch program includes a single entrypoint used to invoke the program.

This tells Arch that the entrypoint to this program is the the process_instruction function, our handler.

entrypoint!(process_instruction);

Handler

Each handler function's parameters must match what is required for a transaction Instruction.

  • program_id - Unique identifier of the currently executing program.
  • accounts - Slice reference containing accounts needed to execute an instruction.
  • instruction_data - Serialized data containing program instructions.
pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> Result<(), ProgramError> {
    ...
}

Now that we're inside the function scope, first, we check that there are a sufficient number of accounts are passed into our program.

We perform a Syscall to retrieve the latest Bitcoin block height (this can be omitted though is helpful for debugging) and then iterate over the accounts passed in to the program and retrieve the first one.

if accounts.len() != 2 {
    return Err(ProgramError::Custom(501));
}

let bitcoin_block_height = get_bitcoin_block_height();
msg!("bitcoin_block_height {:?}", bitcoin_block_height);

let account_iter = &mut accounts.iter();
let account = next_account_info(account_iter)?;
let account2 = next_account_info(account_iter)?;

msg!("account {:?}", account);
msg!("account2 {:?}", account2);

Next, we perform a check to ensure that the UTXO passed into the Account is not the default value of a 36-zero byte slice. This step is done to ensure that the UTXO is properly unititialized before continuing.

if account2.utxo.clone() != UtxoMeta::from_slice(&[0; 36]) {
    msg!("UTXO {:?}", account2.utxo.clone());
    return Err(ProgramError::Custom(502));
}

Next, we deserialize our instruction_data into a newly initialized instance of HelloWorldParams to hold our program state and more easily manage it within our program logic.

let params: HelloWorldParams = borsh::from_slice(instruction_data).unwrap();

Now that our instruction_data has been deserialized, we can access the fields, such as params.tx_hex.

In this step, we will use the Bitcoin crate to further deserialize a reference to the tx_hex field into an instance of a Bitcoin transaction; this represents the fees that need to be paid to execute the program instruction.

let fees_tx: Transaction = bitcoin::consensus::deserialize(&params.tx_hex).unwrap();

Note: tx_hex represents a serialized Bitcoin UTXO that is used to pay the fee for updating state/executing a transaction; it is a fully-signed Bitcoin UTXO that gets sent directly to Arch. The Arch leader node then submits this fee UTXO alongside the other state/asset UTXOs as a result of the program execution.

Including tx_hex is a convention, not a requirement.

Program invocation can be paid for by another source, although in the majority of cases it is most practical to have the caller be prepared to pay this.

We'll create our new message with the name we wish to store.

let new_data = format!("Hello {}", params.name);

Next, we'll extend the account data to fit the new data that we wish to store from the previous step.

We check whether the length of the new data that we wish to pass to the program exceeds the length that is stored within the existing account's data (ie, the program state) by comparing byte lengths.

If the new data exceeds the length of what was currently stored in the account's data field, then we re-allocate the account's data as well as zero-initialize the new memory. This is done to ensure that no stale data remains in the account.

Read more about memory reallocation and zero-initialization.

let data_len = account.data.try_borrow().unwrap().len();
if new_data.as_bytes().len() > data_len {
    account.realloc(new_data.len(), true)?;
}

Next, we retrieve the script_pubkey from the key field of the Account. This tells us how the Bitcoin can be spent; we log this out for debugging.

let script_pubkey = get_account_script_pubkey(account.key);
msg!("script_pubkey {:?}", script_pubkey);

Next, we attempt a mutated borrow of the account data in order to copy contents in from the data passed into our program.

account.data.try_borrow_mut().unwrap().copy_from_slice(new_data.as_bytes());

We then perform a check to ensure that the account is writable, if it is, we invoke a SystemInstruction to create a new account instruction.

To create a new account instruction, we provide the txid and vout (the output index for identification) of our UTXO for the instruction data, and include a copy of the account's Pubkey.

if account2.is_writable {

    invoke(
        &SystemInstruction::new_create_account_instruction(
            params.utxo.txid().try_into().unwrap(), 
            params.utxo.vout(), 
            account2.key.clone(),
        ), 
        &[account2.clone()]
    ).expect("failed");
}

Next, we initialize a new instance of a Bitcoin transaction that we'll fill in over the next few steps.

let mut tx = Transaction {
    version: Version::TWO,
    lock_time: LockTime::ZERO,
    input: vec![],
    output: vec![],
};

We then modify the previously initialized Bitcoin transaction by updating input and output fields with the UTXO data of the account, including the fees_tx that is needed to pay for the Bitcoin transaction.

add_state_transition(&mut tx, account);
tx.input.push(fees_tx.input[0].clone());

Now, we're ready to sign and submit the transaction to Bitcoin which will cement our state alteration.

Here, we construct a new Arch transaction that includes our serialized Bitcoin transaction alongside our program's Pubkey serving as the signer.

let tx_to_sign = TransactionToSign {
    tx_bytes: &bitcoin::consensus::serialize(&tx),
    inputs_to_sign: &[InputToSign {
        index: 0,
        signer: account.key.clone()
    }]
};

Finally, we pass in the list of accounts our program received initially alongside the previously constructed transaction (tx_to_sign) into a helper function that will serialize it and set the UTXOs to the account.

set_transaction_to_sign(accounts, tx_to_sign);

🎉🎉🎉

Congratulations, you've walked through constructing the our helloworld program. In a future guide, we'll walk you through how to test the logic of your program.

How to write an oracle program

This guide walks through the innerworkings of an oracle program as well as details how oracle data can be utilized by other programs on Arch Network.

Table of Contents:


Description

Two important aspects of understanding how this oracle example is implemented within Arch:

  1. The oracle is a program that updates an account which holds the data
  2. No cross-program invocation occurs since only the account is updated and read from versus this being another program that gets interacted with from another program

The source code can be found within the arch-examples repo.

Flow

  • Project deploys oracle program
  • Project creates state account that the oracle program will control in order to write state to it
  • Projects submit data to the oracle state account by submitting instructions to the oracle program
  • Programs include oracle state account alongside their program instructions in order to use this referenced data stored in the oracle state account within their program
  • Projects submit instructions to oracle program periodically to update oracle state account with fresh data

Logic

If you haven't already read How to write an Arch program, we recommend starting there to get a basic understanding of the program anatomy before going further.

We'll look closely at the logic block contained within the update_data handler.

pub fn update_data(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> Result<(), ProgramError> {
    let account_iter = &mut accounts.iter();
    let oracle_account = next_account_info(account_iter)?;

    assert!(oracle_account.is_signer);
    assert_eq!(instruction_data.len(), 8);

    ...
}

First, we'll iterate over the accounts that get passed into the function, which includes the newly created state account that will be responsible for managing the oracle's data.

We then assert that the oracle state account has the appropriate authority to be written to and update what it stores within its data field. Additionally, we assert that the data we wish to update the account with is at least a certain number of bytes.

let data_len = oracle_account.data.try_borrow().unwrap().len();
if instruction_data.len() > data_len {
    oracle_account.realloc(instruction_data.len(), true)?;
}

Next, we calculate the length of the new data that we are looking to store in the account and reallocate memory to the account if the new data is larger than the data currently existing within the account. This step is important for ensuring that there is no remaining, stale data stored in the account before adding new data to it.

oracle_account
    .data
    .try_borrow_mut()
    .unwrap()
    .copy_from_slice(instruction_data);

msg!("updated");

Ok(())

Lastly, we store the new data that is passed into the program via the instruction to the state account for management, thus marking the end of the oracle update process.

Implementation

Let's look at an example implementation of this oracle program. This includes:

Create oracle project

First, we'll need to create a new project using the arch-cli to hold our oracle logic.

arch-cli project create --name oracle

Example output:

Welcome to the Arch Network CLI
Loading config for network: development
  → Loading configuration from /Users/jr/Library/Application Support/arch-cli/config.toml
  ✓ Loaded network-specific configuration for development
Creating a new project...
  ✓ Updated configuration with project directory
  ✓ Created project directory at "/Users/jr/Documents/ArchNetwork/oracle"
Creating Vite application...
  ✓ Created Vite application
  ✓ Installed base dependencies
  ✓ Installed additional packages
New project created successfully! 🎉
  ℹ Project location: "/Users/jr/Documents/ArchNetwork/oracle"

Next steps:
  1. Navigate to /Users/jr/Documents/ArchNetwork/oracle/app/program to find the Rust program template
  2. Edit the source code to implement your program logic
  3. When ready, run arch-cli deploy to compile and deploy your program to the network

Need help? Check out our documentation at https://arch-network.github.io/docs/

We can then proceed to replace the logic in oracle/app/program/lib.rs with our example oracle code as well as update the dependencies (oracle/app/program/Cargo.toml), both found within the arch-examples repo.

Deploy program

After the project is created, the program is written and the Cargo.toml is set with the proper dependencies, we can use the arch-cli to deploy the program.

arch-cli deploy 

Example output:

Welcome to the Arch Network CLI
Loading config for network: development
  → Loading configuration from /Users/jr/Library/Application Support/arch-cli/config.toml
  ✓ Loaded network-specific configuration for development
Deploying your Arch Network app...
Available folders to deploy:
  1. demo
  2. helloworld
  3. oracle
  4. my_app
Enter the number of the folder you want to deploy (or 'q' to quit): 3
Deploying from folder: "/Users/jr/Documents/ArchNetwork/oracle"
  ℹ Building program...
  ℹ Cargo.toml found at: /Users/jr/Documents/ArchNetwork/oracle
  ℹ Current working directory: /Users/jr/Documents/ArchNetwork/oracle
  ✓ Program built successfully
Select a key to use as the program key: oracle
  ℹ Program ID: e46ed1e7441ac5d583961122bc1b63a46a84ec5d33a1d8967d2a827e65297531
Wallet RPC URI: http://bitcoin-node.dev.aws.archnetwork.xyz:18443/wallet/testwallet
Client connected: 03a06383512c806931d88f55013670454cd95c73611c54ce917552ce9843b50e
  ✓ Wallet 'testwallet' loaded successfully.
  ✓ Transaction sent: f3695398563199274125d69e04769c303167f1de599158c3627e83f7493c448d
  ✓ Transaction confirmed with 1 confirmations
    Creating program account...
    Program account created successfully
    Deploying program transactions...
 [00:00:01] Successfully Processed Deployment Transactions : [####################################################################################################] 10/10 (0s)    Program transactions deployed successfully
    Making program executable...
    Transaction sent: 11488915c4535479023ec264e2c65519748c655531ddf4b6f0516d36c4740a41
    Program made executable successfully
  ✓ Program deployed successfully
  ✓ Wallet 'testwallet' unloaded successfully.
Your app has been deployed successfully!
  ℹ Program ID: e46ed1e7441ac5d583961122bc1b63a46a84ec5d33a1d8967d2a827e65297531

During the deployment step, the arch-cli creates an account for the deployed program logic and sets the account to be executable, making the distinction that the account is to be considered a [Program] rather than a data [Account].

Create state account

From the above output, we should obtain the program_id. We can use this program_id in order to create a state account that is owned and updated by the program.

The oracle state account can then be read from by any program in order to retrieve the associated oracle data.

arch-cli account create --name oracle-state-account --program-id e46ed1e7441ac5d583961122bc1b63a46a84ec5d33a1d8967d2a827e65297531

Example output:

Welcome to the Arch Network CLI
Loading config for network: development
  → Loading configuration from /Users/jr/Library/Application Support/arch-cli/config.toml
  ✓ Loaded network-specific configuration for development
Creating account for dApp...
  ℹ Account address: bcrt1pz853jlekzq2c9rvx5lz644qc9c3qx6n28g48jv3hyyknzvhm93rsg7r04f
Wallet RPC URI: http://bitcoin-node.dev.aws.archnetwork.xyz:18443/wallet/testwallet
Client connected: 79d37c5aa2b9216b1f4d66cfdfd1e125f9b241536de3ca81ab1a6887881e3e53
  ✓ Wallet 'testwallet' loaded successfully.
Please send funds to the following address:
  → Bitcoin address: bcrt1pz853jlekzq2c9rvx5lz644qc9c3qx6n28g48jv3hyyknzvhm93rsg7r04f
  ℹ Minimum required: 3000 satoshis
  ⏳ Waiting for funds...
  ✓ Transaction sent: fb4f176a0f1a6ed355987c4bfa24491a1e01484b624ffaa00e62d9554e411db1
  ✓ Transaction confirmed with 1 confirmations
  ✓ Account created with Arch Network transaction ID: c6033bc2acfb12f9f330a7b79c25287e1126dcb1ee42f64d2ebf206dd3fc55cb
  ℹ Account public key: "50130456b1bae1cb7ec5b8d2c4afaf08301e899423d1c5908995bc198b6a3326"
Account created and ownership transferred successfully!
IMPORTANT: Please save your private key securely. It will not be displayed again.
  🔑 Private Key: ...
  🔑 Public Key: 50130456b1bae1cb7ec5b8d2c4afaf08301e899423d1c5908995bc198b6a3326
  ✓ Wallet 'testwallet' unloaded successfully.

In this step, the account is created and ownership is transferred to the program. This allows the program to update the account's data field which holds state for the program.

Update the state account

Now that we have created an account and the oracle program has authority to update it, we now want to update the data that the account holds.

In order to update the data stored in the account, we simply need to make a transaction that includes the data that we wish to update the oracle state account to hold, and submit this within the context of an instruction.

As an example, below we have a sample rust program that we'll use to fetch the Bitcoin fees from the mempool.space API and store this fee data in our oracle state account that was created during deployment.

Note: The below is a rust program and is not an Arch program.

The call to update the oracle state account can be written in any programming language as it is simply an RPC call. For sake of continuity, we're using rust along with methods from both the program and sdk crates.

use bitcoincore_rpc::{Auth, Client};

let mut old_feerate = 0;

let body: Value = reqwest::blocking::get("https://mempool.space/api/v1/fees/recommended").unwrap().json().unwrap();
let feerate = body.get("fastestFee").unwrap().as_u64().unwrap();

if old_feerate != feerate {
    let (txid, instruction_hash) = sign_and_send_instruction(
        Instruction {
            program_id: program_pubkey.clone(),
            accounts: vec![AccountMeta {
                pubkey: caller_pubkey.clone(),
                is_signer: true,
                is_writable: true
            }],
            data: feerate.to_le_bytes().to_vec()
        },
        vec![caller_keypair],
    ).expect("signing and sending a transaction should not fail");

    let processed_tx = get_processed_transaction(NODE1_ADDRESS, txid.clone()).expect("get processed transaction should not fail");
    println!("processed_tx {:?}", processed_tx);

    println!("{:?}", read_account_info(NODE1_ADDRESS, caller_pubkey.clone()));

    old_feerate = feerate;
}

Read from the state account

Below is an example of a different program (we'll call this app-program) that would like to access the oracle data.

Essentially, what happens here is that when we pass an instruction into our app-program, we must also include the oracle state account alongside any other account that we need for the app-program. In this way, the oracle state account is now in-scope and its data can be read from.

pub fn process_instruction(
    _program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> Result<(), ProgramError> {
    let account_iter = &mut accounts.iter();

    // our app-program's state account
    let app_program_account = next_account_info(account_iter)?;

    // our oracle data account
    let oracle_account = next_account_info(account_iter)?; 

    // our oracle data that can now be used within the context of
    // app-program's business logic
    let oracle_data = oracle_account.data.try_borrow().unwrap();

    let msg_str = format!("Oracle data: {}", oracle_data);

    msg!(msg_str);
    ...
}

How to create a fungible token

This guide walks through how to implement the Fungible Token Standard program, part of the Arch Program Library, or APL.

Table of Contents:


Description

The Fungible Token Standard program provides a consistent interface for implementing fungible tokens on Arch. As with all programs within the APL, this program is predeployed and is tested against the Arch runtime.

The source code can be found within the arch-examples repo.

Logic

If you haven't already read How to write an Arch program, we recommend starting there to get a basic understanding of the program anatomy before going further.

Implementation

Deploy

Although the Fungible Token Standard program is part of the APL, and is there predeployed by the validators, for local testing, we can deploy it ourselves. Move to Mint if you'd like to skip this step.

To demonstrate a deploy, we'll reference: deploy.rs

We make use of try_deploy_program, a helper function from the ebpf-counter example to deploy our program.

pub const ELF_PATH: &str = "./program/target/sbf-solana-solana/release/fungible-token-standard-program.so";

fn deploy_standard_program() {
    let program_pubkey =
        try_deploy_program(ELF_PATH, PROGRAM_FILE_PATH, "Fungible-Token-Standard").unwrap();

    println!(
        "Deployed Fungible token standard program account id {:?}!",
        program_pubkey.serialize()
    );
...
}

Mint

To mint tokens, we must supply a few pieces of information:

  • Owner
  • Supply
  • Ticker
  • Decimals

This data gets stored in the InitializeMintInput struct, which will be used to generate a new instance of the Fungible Token Standard.

#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)]
pub struct InitializeMintInput {
    owner: [u8; 32],
    supply: u64, // in lowest denomination
    ticker: String,
    decimals: u8,
}

To demonstrate a mint, we'll reference: tests_mint.rs

We initialize a new instance of InitializeMintInput and pass in the necessary data. In the below case, our owner account will create the token "SPONK," with a total supply of 1,000,000, which will have only a single decimal, meaning it is divisible by 1.

// deploy.rs
let mint_input = InitializeMintInput::new(
    mint_account_pubkey.serialize(),
    1000000,
    "SPONK".to_string(),
    1,
);

We then serialize mint_input so that we can pass it as instruction_data within an Instruction which then gets submitted to the deployed Fungible Token Standard program.

let mut instruction_data = vec![0u8];

mint_input
    .serialize(&mut instruction_data)
    .expect("Couldnt serialize mint input");

let initialize_mint_instruction = Instruction {
    program_id: program_pubkey.clone(),
    accounts: vec![AccountMeta {
        pubkey: mint_account_pubkey,
        is_signer: true,
        is_writable: true,
    }],
    data: instruction_data,
};

Next, we build a transaction using build_transaction and then submit the transaction with build_and_send_block, both helper function from the ebpf-counter example.

let transaction = build_transaction(
    vec![mint_account_keypair],
    vec![initialize_mint_instruction],
);

let block_transactions = build_and_send_block(vec![transaction]);

We fetch the result of the transaction with fetch_processed_transactions helper function (ebpf-counter) and then obtain the mint details by passing the Pubkey of the token owner.

let processed_transactions = fetch_processed_transactions(block_transactions).unwrap();

assert!(matches!(
    processed_transactions[0].status,
    Status::Processed
));

let mint_details = get_mint_info(&mint_account_pubkey).expect("Couldnt deserialize mint info");

println!("Mint account {:?}", mint_account_pubkey.serialize());

Transfer

To demonstrate a transfer, we'll reference: tests_transfer.rs

We obtain a mint_account_pubkey, made possible by using the try_create_mint_account helper function. We pass true as this is a one-time mint event and this will generate a new keypair and Pubkey.

This step will actually create a new token with the following details:

  • Supply: 1,000,000
  • Ticker: "ARCH"
  • Decimals: 2
  • Mint Price: 1000 sats
let mint_account_pubkey = try_create_mint_account(true).unwrap();

We then fetch the token mint details with get_mint_info.

let previous_mint_details = get_mint_info(&mint_account_pubkey).unwrap();

Now, let's provision our two accounts: the sender and the receiver.

// sending account
let (first_account_owner_key_pair, first_account_owner_pubkey, _first_account_owner_address) =
        generate_new_keypair();

let first_balance_account_pubkey = create_balance_account(
        &first_account_owner_pubkey,
        first_account_owner_key_pair,
        &mint_account_pubkey,
        &program_pubkey,
)
.unwrap();

// receiving account
let (second_account_owner_key_pair, second_account_owner_pubkey, _second_account_owner_address) =
    generate_new_keypair();

let second_balance_account_pubkey = create_balance_account(
    &second_account_owner_pubkey,
    second_account_owner_key_pair,
    &mint_account_pubkey,
    &program_pubkey,
)
.unwrap();

We then procure funds for the sending account. In this case, we'll mint 10 tokens.

let mint_amount = 10u64;

let mint_instruction = mint_request_instruction(
    &mint_account_pubkey,
    &program_pubkey,
    &first_balance_account_pubkey,
    &first_account_owner_pubkey,
    mint_amount,
)
.unwrap();

We utilize the transfer_request_instruction helper function to generate a transfer Instruction.

let transfer_instruction = transfer_request_instruction(
    &mint_account_pubkey,
    &program_pubkey,
    &first_balance_account_pubkey,
    &first_account_owner_pubkey,
    &second_balance_account_pubkey,
    mint_amount,
)
.unwrap();

We build the transaction by passing in the newly created transfer Instruction as well as the keypair of the sending account, necessary for authorizing the fund transfer.

let transfer_transaction = build_transaction(
    vec![first_account_owner_key_pair],
    vec![transfer_instruction],
);

Next, we then submit the transaction with build_and_send_block and then fetch the processed transaction to get the result.

let block_transactions = build_and_send_block(vec![transfer_transaction]);

let processed_transactions = fetch_processed_transactions(block_transactions).unwrap();

assert!(matches!(
    processed_transactions[0].status,
    Status::Processed
));

Balance check

In order to check the token balance of an account, we'll make use of the get_balance_account function and pass in the account we are looking to query the balance of; in the below example, we'll fetch the balances of both the sending and receiving accounts.

let resulting_sender_balance = get_balance_account(&first_balance_account_pubkey).unwrap();

let resulting_receiver_balance = get_balance_account(&second_balance_account_pubkey).unwrap();

assert_eq!(resulting_receiver_balance.current_balance, mint_amount);

assert_eq!(resulting_sender_balance.current_balance, 0);

Building Your First Bitcoin Runes Swap Application

Welcome to this hands-on tutorial! Today, we're going to build a decentralized application that enables users to swap Bitcoin Runes tokens on the Arch Network. By the end of this lesson, you'll understand how to create a secure, trustless swap mechanism for Runes tokens.

Class Prerequisites

Before we dive in, please ensure you have:

  • Completed the environment setup
  • A basic understanding of Bitcoin Integration
  • Familiarity with Rust programming language
  • Your development environment ready with the Arch CLI installed

Lesson 1: Understanding the Basics

What are Runes?

Before we write any code, let's understand what we're working with. Runes is a Bitcoin protocol for fungible tokens, similar to how BRC-20 works. Each Rune token has a unique identifier and can be transferred between Bitcoin addresses.

What are we building?

We're creating a swap program that will:

  1. Allow users to create swap offers ("I want to trade X amount of Rune A for Y amount of Rune B")
  2. Enable other users to accept these offers
  3. Let users cancel their offers if they change their mind
  4. Ensure all swaps are atomic (they either complete fully or not at all)

Lesson 2: Setting Up Our Project

Let's start by creating our project structure. Open your terminal and run:

# Create a new Arch project
arch-cli project create --name runes-swap
cd runes-swap

# Your project structure should look like this:
# runes-swap/
# ├── Cargo.toml
# ├── src/
# │   └── lib.rs

Lesson 3: Defining Our Data Structures

Now, let's define the building blocks of our swap program. In programming, it's crucial to plan our data structures before implementing functionality.

use arch_program::{
    account::AccountInfo,
    entrypoint,
    msg,
    program_error::ProgramError,
    pubkey::Pubkey,
    utxo::UtxoMeta,
    borsh::{BorshDeserialize, BorshSerialize},
};

/// This structure represents a single swap offer in our system
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct SwapOffer {
    // Unique identifier for the offer
    pub offer_id: u64,
    // The public key of the person creating the offer
    pub maker: Pubkey,
    // The Rune ID they want to give
    pub rune_id_give: String,
    // Amount of Runes they want to give
    pub amount_give: u64,
    // The Rune ID they want to receive
    pub rune_id_want: String,
    // Amount of Runes they want to receive
    pub amount_want: u64,
    // When this offer expires (in block height)
    pub expiry: u64,
    // Current status of the offer
    pub status: OfferStatus,
}

Let's break down why we chose each field:

  • offer_id: Every offer needs a unique identifier so we can reference it later
  • maker: We store who created the offer to ensure only they can cancel it
  • rune_id_give/want: These identify which Runes are being swapped
  • amount_give/want: The quantities of each Rune in the swap
  • expiry: Offers shouldn't live forever, so we add an expiration

Lesson 4: Implementing the Swap Logic

Now that we understand our data structures, let's implement the core swap functionality. We'll start with creating an offer:

fn process_create_offer(
    accounts: &[AccountInfo],
    instruction: SwapInstruction,
) -> Result<(), ProgramError> {
    // Step 1: Get all the accounts we need
    let account_iter = &mut accounts.iter();
    let maker = next_account_info(account_iter)?;
    let offer_account = next_account_info(account_iter)?;
    
    // Step 2: Verify the maker has the Runes they want to swap
    if let SwapInstruction::CreateOffer { 
        rune_id_give, 
        amount_give,
        rune_id_want,
        amount_want,
        expiry 
    } = instruction {
        // Security check: Ensure the maker owns enough Runes
        verify_rune_ownership(maker, &rune_id_give, amount_give)?;
        
        // Step 3: Create and store the offer
        let offer = SwapOffer {
            offer_id: get_next_offer_id(offer_account)?,
            maker: *maker.key,
            rune_id_give,
            amount_give,
            rune_id_want,
            amount_want,
            expiry,
            status: OfferStatus::Active,
        };
        
        store_offer(offer_account, &offer)?;
    }

    Ok(())
}

Understanding the Create Offer Process

  1. First, we extract the accounts passed to our program
  2. We verify that the maker actually owns the Runes they want to trade
  3. We create a new SwapOffer with an Active status
  4. Finally, we store this offer in the program's state

Lesson 5: Testing Our Program

Testing is crucial in blockchain development because once deployed, your program can't be easily changed. Let's write comprehensive tests for our swap program.

#[cfg(test)]
mod tests {
    use super::*;
    use arch_program::test_utils::{create_test_account, create_test_pubkey};

    /// Helper function to create a test offer
    fn create_test_offer() -> SwapOffer {
        SwapOffer {
            offer_id: 1,
            maker: create_test_pubkey(),
            rune_id_give: "RUNE1".to_string(),
            amount_give: 100,
            rune_id_want: "RUNE2".to_string(),
            amount_want: 200,
            expiry: 1000,
            status: OfferStatus::Active,
        }
    }

    #[test]
    fn test_create_offer() {
        // Arrange: Set up our test accounts
        let maker = create_test_account();
        let offer_account = create_test_account();
        
        // Act: Create an offer
        let result = process_create_offer(
            &[maker.clone(), offer_account.clone()],
            SwapInstruction::CreateOffer {
                rune_id_give: "RUNE1".to_string(),
                amount_give: 100,
                rune_id_want: "RUNE2".to_string(),
                amount_want: 200,
                expiry: 1000,
            },
        );
        
        // Assert: Check the result
        assert!(result.is_ok());
        // Add more assertions here to verify the offer was stored correctly
    }
}

Understanding Our Test Structure

We follow the "Arrange-Act-Assert" pattern:

  1. Arrange: Set up the test environment and data
  2. Act: Execute the functionality we're testing
  3. Assert: Verify the results match our expectations

Lesson 6: Implementing Offer Acceptance

Now let's implement the logic for accepting an offer. This is where atomic swaps become crucial:

fn process_accept_offer(
    accounts: &[AccountInfo],
    instruction: SwapInstruction,
) -> Result<(), ProgramError> {
    // Step 1: Get all required accounts
    let account_iter = &mut accounts.iter();
    let taker = next_account_info(account_iter)?;
    let maker = next_account_info(account_iter)?;
    let offer_account = next_account_info(account_iter)?;
    
    if let SwapInstruction::AcceptOffer { offer_id } = instruction {
        // Step 2: Load and validate the offer
        let mut offer = load_offer(offer_account)?;
        require!(
            offer.status == OfferStatus::Active,
            ProgramError::InvalidAccountData
        );
        require!(
            offer.offer_id == offer_id,
            ProgramError::InvalidArgument
        );
        
        // Step 3: Verify the taker has the required Runes
        verify_rune_ownership(taker, &offer.rune_id_want, offer.amount_want)?;
        
        // Step 4: Perform the atomic swap
        // Transfer Runes from maker to taker
        transfer_runes(
            maker,
            taker,
            &offer.rune_id_give,
            offer.amount_give,
        )?;
        
        // Transfer Runes from taker to maker
        transfer_runes(
            taker,
            maker,
            &offer.rune_id_want,
            offer.amount_want,
        )?;
        
        // Step 5: Update offer status
        offer.status = OfferStatus::Completed;
        store_offer(offer_account, &offer)?;
    }
    
    Ok(())
}

Understanding Atomic Swaps

An atomic swap ensures that either:

  • Both transfers complete successfully, or
  • Neither transfer happens at all

This is crucial for preventing partial swaps where one party could lose their tokens.

Lesson 7: Implementing Offer Cancellation

Finally, let's implement the ability to cancel offers:

fn process_cancel_offer(
    accounts: &[AccountInfo],
    instruction: SwapInstruction,
) -> Result<(), ProgramError> {
    let account_iter = &mut accounts.iter();
    let maker = next_account_info(account_iter)?;
    let offer_account = next_account_info(account_iter)?;
    
    if let SwapInstruction::CancelOffer { offer_id } = instruction {
        // Load the offer
        let mut offer = load_offer(offer_account)?;
        
        // Security checks
        require!(
            offer.maker == *maker.key,
            ProgramError::InvalidAccountData
        );
        require!(
            offer.status == OfferStatus::Active,
            ProgramError::InvalidAccountData
        );
        require!(
            offer.offer_id == offer_id,
            ProgramError::InvalidArgument
        );
        
        // Update offer status
        offer.status = OfferStatus::Cancelled;
        store_offer(offer_account, &offer)?;
    }
    
    Ok(())
}

Final Steps: Building and Deploying

Now that we've implemented our swap program, let's build and deploy it:

# Build the program
cargo build-bpf

# Deploy to your local test validator
arch-cli program deploy target/deploy/runes_swap.so

Testing the Deployed Program

Here's a simple script to test our deployed program:

import { Connection, PublicKey, Transaction } from '@archway/web3.js';
import { RunesSwapProgram } from './program';

async function testSwap() {
    // Connect to local test validator
    const connection = new Connection('http://localhost:8899', 'confirmed');
    
    // Create a new swap offer
    const offer = await RunesSwapProgram.createOffer({
        runeIdGive: 'RUNE1',
        amountGive: 100,
        runeIdWant: 'RUNE2',
        amountWant: 200,
        expiry: Date.now() + 3600000, // 1 hour from now
    });
    
    console.log('Created offer:', offer);
}

testSwap().catch(console.error);

Conclusion

Congratulations! You've built a complete Runes swap program. This program demonstrates several important blockchain concepts:

  1. Atomic transactions
  2. State management
  3. Security checks
  4. Program testing

Remember to always:

  • Test thoroughly before deployment
  • Consider edge cases
  • Implement proper error handling
  • Add detailed documentation

Next Steps

To further improve your program, consider adding:

  1. A UI for interacting with the swap program
  2. More sophisticated offer matching
  3. Order book functionality
  4. Price oracle integration
  5. Additional security features

Questions? Feel free to ask in the comments below!

How to configure the local validator with Bitcoin Testnet4

This guide is intended for those wishing to view logs from their programs while benefitting from being connected to Bitcoin testnet4 and therefore gaining access to ordinals/runes helper tools.

Table of Contents:

Config

First, edit the arch-cli configuration file and insert the following details into the testnet section.

arch-cli config edit

Note: We have redacted our Bitcoin node password to prevent abuse; contact us if you need this, otherwise provide your own node credentials and use the below as a reference.

Your arch-cli configuration file should resemble something like the following, with the leader_rpc_endpoint being the endpoint for reaching your Local validator, which defaults to port 9002.

[networks.testnet]
type = "testnet"
bitcoin_rpc_endpoint = "bitcoin-node.test.aws.archnetwork.xyz"
bitcoin_rpc_port = "49332"
bitcoin_rpc_user = "bitcoin"
bitcoin_rpc_password = "redacted"
bitcoin_rpc_wallet = "testwallet"
leader_rpc_endpoint = "http://localhost:9002"

Local validator

Note: the arch-cli (and Docker) can be used to run the local validator.

Additionally, if you do not already have the local validator installed, please pull it from the arch-node releases page.

Be sure to download the local variant, not the regular validator.

Run the local validator

Use the arch-cli command to run the local validator. You'll need to have Docker installed and running.

arch-cli validator start

The validator logs can be viewed easily within the Docker desktop dashboard.

Note: You can also run the standalone local validator binary where the logs will be streamed to stdout unless otherwise redirected.

Steps for running standalone validator binary:

  1. Download the appropriate binary as well as the system_program.so file from arch-node releases page.

  2. Store the system_program.so file within a new directory called /ebpf.

    Your directory structure should resemble the following:

    tmp/
    ├─ ebpf/
    │  ├─ system_program.so
    ├─ local_validator
    
  3. Run the binary and pass the relevant flags dependening on your target network.

    RUST_LOG=info \
    ./local_validator \
    --network-mode testnet \
    --rpc-bind-ip 127.0.0.1 \
    --rpc-bind-port 9002 \
    --bitcoin-rpc-endpoint bitcoin-node.test.aws.archnetwork.xyz \
    --bitcoin-rpc-port 49332 \
    --bitcoin-rpc-username bitcoin \
    --bitcoin-rpc-password redacted
    

Help commands

This section includes some helpful material when needing to restart the node state or better ensure our infrastructure is operational before proceeding.

Arch node

The below commands can be used to assist with running the Local validator.

Start fresh

By removing the /.arch_data directory, we can wipe the state and effective start the node again from genesis (block: 0).

rm -rf .arch_data && RUST_LOG=info \
./local_validator \
...
Pulse check

This cURL command will allow us to ensure that our Local validator is up and running correctly. We can use this to effective get a pulse check on the node which is helpful for debugging.

curl -vL POST -H 'Content-Type: application/json' -d '
{
    "jsonrpc":"2.0",
    "id":1,
    "method":"is_node_ready",
    "params":[]
}' \
http://localhost:9002/

Log assistance

Ordinarily, the arch-node logs will flood your terminal screen (or the Docker logs). This is less than idea when needing to review them carefully, so you can also direct the stdout to a file for later reading.

Here's an example of how to do this:

rm -rf .arch_data && RUST_LOG=info \
./local_validator \
--network-mode testnet \
--rpc-bind-ip 127.0.0.1 \
--rpc-bind-port 9002 \
--bitcoin-rpc-endpoint bitcoin-node.test.aws.archnetwork.xyz \
--bitcoin-rpc-port 49332 \
--bitcoin-rpc-username bitcoin \
--bitcoin-rpc-password redacted \
> node-logs.txt

Then you can tail the output and view the logs as they stream in.

tail -f node-logs.txt

Deploy + interact

Now that everything is setup correctly, we can now deploy our program and begin interacting with it. The deploy step will prove everything works correctly.

arch-cli deploy --network testnet

And if you are running the local validator binary directly from the command-line, set the --rpc-endpoint flag so it overwrites the leader_rpc_endpoint in the arch-cli config if this information was not changed:

arch-cli deploy --network testnet --rpc-url http://localhost:9002

We hope this guide has been helpful, but as always, feel free to ask question within our Discord dev-chat or submit issues within out public-issues repo.

How to Build a Bitcoin Lending Protocol

This guide walks through building a lending protocol for Bitcoin-based assets (BTC, Runes, Ordinals) on Arch Network. We'll create a decentralized lending platform similar to Aave, but specifically designed for Bitcoin-based assets.

Prerequisites

Before starting, ensure you have:

  • Completed the environment setup
  • A basic understanding of Bitcoin Integration
  • Familiarity with Rust programming language
  • Your development environment ready with the Arch CLI installed

System Overview

Basic User Flow

flowchart TD
    subgraph Depositing
        A[User wants to lend] -->|1. Deposits BTC| B[Lending Pool]
        B -->|2. Receives interest| A
    end

    subgraph Borrowing
        C[User needs loan] -->|3. Provides collateral| B
        B -->|4. Lends BTC| C
        C -->|5. Repays loan + interest| B
    end

    style A fill:#b3e0ff
    style B fill:#98FB98
    style C fill:#b3e0ff

Safety System

flowchart LR
    subgraph "Price Monitoring"
        direction TB
        A[Price Oracle] -->|1. Updates prices| B[Health Checker]
    end

    subgraph "Health Check"
        direction TB
        B -->|2. Monitors positions| C[User Position]
        C -->|3. If position unsafe| D[Liquidator]
    end

    style A fill:#FFB6C1
    style B fill:#FFB6C1
    style C fill:#b3e0ff
    style D fill:#FFB6C1

Simple Example

Let's say Alice wants to borrow BTC and Bob wants to earn interest:

  1. Bob (Lender)

    • Deposits 1 BTC into pool
    • Earns 3% APY interest
  2. Alice (Borrower)

    • Provides 1.5 BTC as collateral
    • Borrows 1 BTC
    • Pays 5% APY interest
  3. Safety System

    • Monitors BTC price
    • Checks if Alice's collateral stays valuable enough
    • If BTC price drops too much, liquidates some collateral to protect Bob's deposit

Architecture Overview

Our lending protocol consists of several key components:

1. Pool Accounts

Pool accounts are the core of our lending protocol. They serve as liquidity pools where users can:

  • Deposit Bitcoin-based assets (BTC, Runes, Ordinals)
  • Earn interest on deposits
  • Borrow against their collateral
  • Manage protocol parameters

Each pool account maintains:

  • Total deposits and borrows
  • Interest rates and utilization metrics
  • Collateral factors and liquidation thresholds
  • Asset-specific parameters

The pool account manages both state and UTXOs:

  • State Management: Tracks deposits, withdrawals, and user positions
  • UTXO Management:
    • Maintains a collection of UTXOs for the pool's Bitcoin holdings
    • Manages UTXO creation for withdrawals
    • Handles UTXO consolidation for efficient liquidity management

2. Price Oracle

Track asset prices for liquidation calculations

3. User Positions

User positions track all user interactions with the lending pools:

  • Active deposits and their earned interest
  • Outstanding borrows and accrued interest
  • Collateral positions and health factors
  • Liquidation thresholds and warnings

Each user can have multiple positions across different pools, and the protocol tracks:

  • Position health through real-time monitoring
  • Collateralization ratios
  • Interest accrual
  • Liquidation risks

Core Data Structures

#[derive(BorshSerialize, BorshDeserialize)]
pub struct LendingPool {
    pub pool_pubkey: Pubkey,
    pub asset_type: AssetType, // BTC, Runes, Ordinals
    pub total_deposits: u64,
    pub total_borrows: u64,
    pub interest_rate: u64,
    pub utilization_rate: u64,
    pub liquidation_threshold: u64,
    pub collateral_factor: u64,
    pub utxos: Vec<UtxoMeta>,
    pub validator_signatures: Vec<Signature>,
    pub min_signatures_required: u32,
}

#[derive(BorshSerialize, BorshDeserialize)]
pub struct UserPosition {
    pub user_pubkey: Pubkey,
    pub pool_pubkey: Pubkey,
    pub deposited_amount: u64,
    pub borrowed_amount: u64,
    pub collateral_amount: u64,
    pub last_update: i64,
}

#[derive(BorshSerialize, BorshDeserialize)]
pub struct InterestRateModel {
    pub base_rate: u64,
    pub multiplier: u64,
    pub jump_multiplier: u64,
    pub optimal_utilization: u64,
}

// Additional helper structures for managing positions
#[derive(BorshSerialize, BorshDeserialize)]
pub struct PositionHealth {
    pub health_factor: u64,
    pub liquidation_price: u64,
    pub safe_borrow_limit: u64,
}

#[derive(BorshSerialize, BorshDeserialize)]
pub struct PoolMetrics {
    pub total_value_locked: u64,
    pub available_liquidity: u64,
    pub utilization_rate: u64,
    pub supply_apy: u64,
    pub borrow_apy: u64,
}

Custom Scoring and Risk Management

LTV (Loan-to-Value) Scoring System

flowchart TD
    subgraph Core_Factors[Core Factors]
        P1[Transaction History]
        P2[Asset Quality]
        P3[Market Volatility]
        P4[Position Size]
    end
    
    subgraph User_Metrics[User Metrics]
        P5[Account History]
        P6[Repayment Record]
        P7[Portfolio Health]
    end
    
    subgraph Market_Context[Market Context]
        P8[Market Conditions]
        P9[Price Impact]
        P10[Network Status]
    end
    
    Core_Factors --> SC[Scoring Engine]
    User_Metrics --> SC
    Market_Context --> SC
    SC --> WF[Weight Calculation]
    WF --> NM[Risk Normalization]
    NM --> LTV[Final LTV Ratio]
    
    style Core_Factors fill:#e1f3d8
    style User_Metrics fill:#fff7e6
    style Market_Context fill:#e6f3ff
    style SC fill:#f9f9f9
    style WF fill:#f9f9f9
    style NM fill:#f9f9f9
    style LTV fill:#d4edda

Health Score Monitoring

sequenceDiagram
    participant User
    participant HealthMonitor
    participant PriceOracle
    participant LiquidationEngine
    participant Market

    loop Every Block
        PriceOracle->>HealthMonitor: Update Asset Prices
        HealthMonitor->>HealthMonitor: Calculate Health Score
        
        alt Health Score < Threshold
            HealthMonitor->>LiquidationEngine: Trigger Liquidation
            LiquidationEngine->>User: Lock Account
            LiquidationEngine->>Market: List Assets
            Market-->>LiquidationEngine: Asset Sale Complete
            LiquidationEngine->>User: Update Position
        else Health Score >= Threshold
            HealthMonitor->>User: Position Safe
        end
    end

Liquidation Process

stateDiagram-v2
    [*] --> Monitoring
    Monitoring --> Warning: Health Score Declining
    Warning --> AtRisk: Below Warning Threshold
    AtRisk --> Liquidation: Below Critical Threshold
    Liquidation --> Step1: Lock Account
    Step1 --> Step2: List Assets
    Step2 --> Recovery: Asset Sale
    Recovery --> [*]: Position Cleared
    
    Warning --> Monitoring: Health Restored
    AtRisk --> Warning: Health Improved

Custom Scoring Implementation

#[derive(BorshSerialize, BorshDeserialize)]
pub struct UserScore {
    pub historical_data_score: u64,
    pub asset_quality_score: u64,
    pub market_volatility_score: u64,
    pub position_size_score: u64,
    pub account_age_score: u64,
    pub liquidation_history_score: u64,
    pub repayment_history_score: u64,
    pub cross_margin_score: u64,
    pub portfolio_diversity_score: u64,
    pub market_condition_score: u64,
    pub collateral_quality_score: u64,
    pub platform_activity_score: u64,
    pub time_weighted_score: u64,
    pub price_impact_score: u64,
    pub network_status_score: u64,
}

pub fn calculate_ltv_ratio(score: &UserScore) -> Result<u64> {
    // Weighted calculation of LTV based on all scoring parameters
    let weighted_score = calculate_weighted_score(score)?;
    let normalized_score = normalize_score(weighted_score)?;
    
    // Convert normalized score to LTV ratio
    let ltv_ratio = convert_score_to_ltv(normalized_score)?;
    
    // Apply market condition adjustments
    let adjusted_ltv = apply_market_adjustments(ltv_ratio)?;
    
    Ok(adjusted_ltv)
}

pub fn monitor_health_score(
    ctx: Context<HealthCheck>,
    position: &UserPosition,
    score: &UserScore,
) -> Result<()> {
    let health_score = calculate_health_score(position, score)?;
    
    if health_score < CRITICAL_THRESHOLD {
        trigger_full_liquidation(ctx, position)?;
        lock_account(ctx.accounts.user_account)?;
    } else if health_score < WARNING_THRESHOLD {
        emit_warning(ctx.accounts.user_account)?;
    }
    
    Ok(())
}

pub fn trigger_full_liquidation(
    ctx: Context<Liquidation>,
    position: &UserPosition,
) -> Result<()> {
    // Step 1: Lock the account
    lock_account(ctx.accounts.user_account)?;
    
    // Step 2: Calculate current position value
    let position_value = calculate_position_value(position)?;
    
    // Step 3: List assets on marketplace
    list_assets_for_liquidation(
        ctx.accounts.marketplace,
        position.assets,
        position_value,
    )?;
    
    // Step 4: Monitor recovery process
    start_recovery_monitoring(ctx.accounts.recovery_manager)?;
    
    Ok(())
}

## Health Score Calculation
The health score is calculated using a combination of factors:

```rust,ignore
pub fn calculate_health_score(
    position: &UserPosition,
    score: &UserScore,
) -> Result<u64> {
    // 1. Calculate base health ratio
    let base_health = calculate_base_health_ratio(
        position.collateral_value,
        position.borrowed_value,
    )?;
    
    // 2. Apply user score modifiers
    let score_adjusted_health = apply_score_modifiers(
        base_health,
        score,
    )?;
    
    // 3. Apply market condition adjustments
    let market_adjusted_health = apply_market_conditions(
        score_adjusted_health,
        &position.asset_type,
    )?;
    
    // 4. Apply time-weighted factors
    let final_health_score = apply_time_weights(
        market_adjusted_health,
        position.last_update,
    )?;
    
    Ok(final_health_score)
}

Liquidation Implementation

The two-step liquidation process is implemented as follows:

pub struct LiquidationConfig {
    pub warning_threshold: u64,
    pub critical_threshold: u64,
    pub recovery_timeout: i64,
    pub minimum_recovery_value: u64,
}

pub fn handle_liquidation(
    ctx: Context<Liquidation>,
    config: &LiquidationConfig,
) -> Result<()> {
    // Step 1: Asset Recovery
    let recovery_listing = create_recovery_listing(
        ctx.accounts.marketplace,
        ctx.accounts.user_position,
        config.minimum_recovery_value,
    )?;
    
    // Step 2: Monitor Recovery
    start_recovery_monitoring(
        recovery_listing,
        config.recovery_timeout,
    )?;
    
    // Lock account until recovery complete
    lock_user_account(ctx.accounts.user_account)?;
    
    Ok(())
}

Implementation Steps

1. Initialize Lending Pool

First, we'll create a function to initialize a new lending pool:

pub fn initialize_lending_pool(
    ctx: Context<InitializeLendingPool>,
    asset_type: AssetType,
    initial_interest_rate: u64,
    liquidation_threshold: u64,
    collateral_factor: u64,
) -> Result<()> {
    let lending_pool = &mut ctx.accounts.lending_pool;
    
    lending_pool.pool_pubkey = ctx.accounts.pool.key();
    lending_pool.asset_type = asset_type;
    lending_pool.total_deposits = 0;
    lending_pool.total_borrows = 0;
    lending_pool.interest_rate = initial_interest_rate;
    lending_pool.utilization_rate = 0;
    lending_pool.liquidation_threshold = liquidation_threshold;
    lending_pool.collateral_factor = collateral_factor;
    
    Ok(())
}

// Initialize pool metrics
pub fn initialize_pool_metrics(
    ctx: Context<InitializePoolMetrics>,
) -> Result<()> {
    let pool_metrics = &mut ctx.accounts.pool_metrics;
    
    pool_metrics.total_value_locked = 0;
    pool_metrics.available_liquidity = 0;
    pool_metrics.utilization_rate = 0;
    pool_metrics.supply_apy = 0;
    pool_metrics.borrow_apy = 0;
    
    Ok(())
}

2. Manage User Positions

Functions to handle user position management:

pub fn create_user_position(
    ctx: Context<CreateUserPosition>,
    pool_pubkey: Pubkey,
) -> Result<()> {
    let user_position = &mut ctx.accounts.user_position;
    
    user_position.user_pubkey = ctx.accounts.user.key();
    user_position.pool_pubkey = pool_pubkey;
    user_position.deposited_amount = 0;
    user_position.borrowed_amount = 0;
    user_position.collateral_amount = 0;
    user_position.last_update = Clock::get()?.unix_timestamp;
    
    Ok(())
}

pub fn update_position_health(
    ctx: Context<UpdatePositionHealth>,
) -> Result<()> {
    let position = &ctx.accounts.user_position;
    let pool = &ctx.accounts.lending_pool;
    let health = &mut ctx.accounts.position_health;
    
    // Calculate health factor based on current prices and positions
    let collateral_value = calculate_collateral_value(
        position.collateral_amount,
        pool.asset_type,
    )?;
    
    let borrow_value = calculate_borrow_value(
        position.borrowed_amount,
        pool.asset_type,
    )?;
    
    health.health_factor = calculate_health_factor(
        collateral_value,
        borrow_value,
        pool.collateral_factor,
    )?;
    
    health.liquidation_price = calculate_liquidation_price(
        position.borrowed_amount,
        position.collateral_amount,
        pool.liquidation_threshold,
    )?;
    
    health.safe_borrow_limit = calculate_safe_borrow_limit(
        collateral_value,
        pool.collateral_factor,
    )?;
    
    Ok(())
}

3. Pool and Position Utilities

Helper functions for managing pools and positions:

// Calculate the utilization rate of a pool
pub fn calculate_utilization_rate(pool: &LendingPool) -> Result<u64> {
    if pool.total_deposits == 0 {
        return Ok(0);
    }
    
    Ok((pool.total_borrows * 10000) / pool.total_deposits)
}

// Calculate the health factor of a position
pub fn calculate_health_factor(
    collateral_value: u64,
    borrow_value: u64,
    collateral_factor: u64,
) -> Result<u64> {
    if borrow_value == 0 {
        return Ok(u64::MAX);
    }
    
    Ok((collateral_value * collateral_factor) / (borrow_value * 10000))
}

// Update pool metrics
pub fn update_pool_metrics(
    pool: &LendingPool,
    metrics: &mut PoolMetrics,
) -> Result<()> {
    metrics.total_value_locked = pool.total_deposits;
    metrics.available_liquidity = pool.total_deposits.saturating_sub(pool.total_borrows);
    metrics.utilization_rate = calculate_utilization_rate(pool)?;
    
    // Update APY rates based on utilization
    let (supply_apy, borrow_apy) = calculate_apy_rates(
        metrics.utilization_rate,
        pool.interest_rate,
    )?;
    
    metrics.supply_apy = supply_apy;
    metrics.borrow_apy = borrow_apy;
    
    Ok(())
}

4. Deposit Assets

Create a deposit function to allow users to provide liquidity:

pub fn deposit(
    ctx: Context<Deposit>,
    amount: u64,
    btc_txid: [u8; 32],
    vout: u32,
) -> Result<()> {
    let pool = &mut ctx.accounts.lending_pool;
    let user_position = &mut ctx.accounts.user_position;
    
    // Verify the UTXO belongs to the user
    require!(
        verify_utxo_ownership(
            &ctx.accounts.user.key(),
            &btc_txid,
            vout
        )?,
        ErrorCode::InvalidUTXO
    );

    // Create deposit account to hold the UTXO
    invoke(
        &SystemInstruction::new_create_account_instruction(
            btc_txid,
            vout,
            pool.pool_pubkey,
        ),
        &[ctx.accounts.user.clone(), ctx.accounts.pool.clone()]
    )?;

    // Update pool state
    pool.total_deposits = pool.total_deposits
        .checked_add(amount)
        .ok_or(ErrorCode::MathOverflow)?;
    
    // Update user position
    user_position.deposited_amount = user_position.deposited_amount
        .checked_add(amount)
        .ok_or(ErrorCode::MathOverflow)?;
    
    // Update utilization metrics
    update_utilization_rate(pool)?;
    
    Ok(())
}

5. Borrow Assets

Implement borrowing functionality:

pub fn borrow(
    ctx: Context<Borrow>,
    amount: u64,
    collateral_utxo: UtxoMeta,
) -> Result<()> {
    let pool = &mut ctx.accounts.lending_pool;
    let borrower_position = &mut ctx.accounts.user_position;

    // Verify collateral UTXO ownership
    require!(
        verify_utxo_ownership(
            &ctx.accounts.borrower.key(),
            &collateral_utxo.txid,
            collateral_utxo.vout,
        )?,
        ErrorCode::InvalidCollateral
    );

    // Check collateral requirements
    require!(
        is_collateral_sufficient(borrower_position, pool, amount)?,
        ErrorCode::InsufficientCollateral
    );

    // Create collateral account
    invoke(
        &SystemInstruction::new_create_account_instruction(
            collateral_utxo.txid,
            collateral_utxo.vout,
            pool.pool_pubkey,
        ),
        &[ctx.accounts.borrower.clone(), ctx.accounts.pool.clone()]
    )?;

    // Create borrow UTXO for user
    let mut btc_tx = Transaction::new();
    add_state_transition(&mut btc_tx, ctx.accounts.pool);

    // Set transaction for validator signing
    set_transaction_to_sign(
        ctx.accounts,
        TransactionToSign {
            tx_bytes: &bitcoin::consensus::serialize(&btc_tx),
            inputs_to_sign: &[InputToSign {
                index: 0,
                signer: pool.pool_pubkey
            }]
        }
    );

    // Update states
    pool.total_borrows = pool.total_borrows
        .checked_add(amount)
        .ok_or(ErrorCode::MathOverflow)?;
    
    borrower_position.borrowed_amount = borrower_position.borrowed_amount
        .checked_add(amount)
        .ok_or(ErrorCode::MathOverflow)?;

    update_utilization_rate(pool)?;
    update_interest_rate(pool)?;

    Ok(())
}

6. Liquidation Logic

Implement liquidation for underwater positions:

pub fn liquidate(
    ctx: Context<Liquidate>,
    repay_amount: u64,
) -> Result<()> {
    let pool = &mut ctx.accounts.lending_pool;
    let liquidated_position = &mut ctx.accounts.liquidated_position;
    
    // Check if position is liquidatable
    require!(
        is_position_liquidatable(liquidated_position, pool)?,
        ErrorCode::PositionNotLiquidatable
    );
    
    // Calculate liquidation bonus
    let bonus = calculate_liquidation_bonus(repay_amount, pool.liquidation_threshold)?;
    
    // Process liquidation
    process_liquidation(
        pool,
        liquidated_position,
        repay_amount,
        bonus,
    )?;
    
    Ok(())
}

Testing

Create comprehensive tests for your lending protocol:

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_initialize_lending_pool() {
        // Test pool initialization
    }

    #[test]
    fn test_deposit() {
        // Test deposit functionality
    }

    #[test]
    fn test_borrow() {
        // Test borrowing
    }

    #[test]
    fn test_liquidation() {
        // Test liquidation scenarios
    }
}

Security Considerations

  1. Collateral Safety: Implement strict collateral requirements and regular position health checks
  2. Price Oracle Security: Use reliable price feeds and implement safeguards against price manipulation
  3. Interest Rate Model: Ensure the model can handle extreme market conditions
  4. Access Control: Implement proper permission checks for all sensitive operations
  5. Liquidation Thresholds: Set appropriate thresholds to maintain protocol solvency

Next Steps

  1. Implement additional features:

    • Flash loans
    • Multiple collateral types
    • Governance mechanisms
  2. Deploy and test on testnet:

    • Monitor pool performance
    • Test liquidation scenarios
    • Validate interest rate model
  3. Security audit:

    • Contract review
    • Economic model analysis
    • Risk assessment

Process Descriptions

1. Pool Initialization Process

The pool initialization process involves several steps:

%%{init: {
  'theme': 'base',
  'themeVariables': { 'fontSize': '16px'},
  'flowchart': {
    'curve': 'basis',
    'nodeSpacing': 50,
    'rankSpacing': 50,
    'animation': {
      'sequence': true,
      'duration': 1000,
      'ease': 'linear',
      'diagramUpdate': 200
    }
  }
}}%%
graph LR
    A[Admin] -->|Create Pool| B[Initialize Pool Account]
    B -->|Set Parameters| C[Configure Pool]
    C -->|Initialize Metrics| D[Create Pool Metrics]
    D -->|Enable Oracle| E[Connect Price Feed]
    E -->|Activate| F[Pool Active]

    classDef default fill:#f9f9f9,stroke:#333,stroke-width:2px;
    classDef active fill:#4a9eff,color:white,stroke:#3182ce,opacity:0;
    classDef complete fill:#98FB98,stroke:#333;

  1. Admin creates a new pool account
  2. Pool parameters are set (interest rates, thresholds)
  3. Pool metrics are initialized
  4. Price oracle connection is established
  5. Pool is activated for user operations

2. Deposit and Borrow Flow

The lending and borrowing process follows this sequence:

graph TD
    A[User] -->|Deposit Assets| B[Lending Pool]
    B -->|Create Position| C[User Position]
    C -->|Calculate Capacity| D[Borrow Limit]
    D -->|Enable Borrowing| E[Borrow Assets]
    E -->|Update Metrics| F[Pool Metrics]
    F -->|Adjust Rates| G[Interest Rates]

Key steps:

  1. User deposits assets into the pool
  2. System creates or updates user position
  3. Calculates borrowing capacity based on collateral
  4. Enables borrowing up to the limit
  5. Updates pool metrics and interest rates

3. Health Monitoring System

Continuous health monitoring process:

graph TD
    A[Price Oracle] -->|Update Prices| B[Position Valuation]
    B -->|Calculate Ratios| C[Health Check]
    C -->|Evaluate| D{Health Factor}
    D -->|>1| E[Healthy]
    D -->|<1| F[At Risk]
    F -->|<Threshold| G[Liquidatable]
    G -->|Notify| H[Liquidators]

The system:

  1. Continuously monitors asset prices
  2. Updates position valuations
  3. Calculates health factors
  4. Triggers liquidations when necessary

Withdrawal Process

The withdrawal process in our lending protocol involves two key components:

  1. State management through program accounts
  2. Actual BTC transfer through UTXOs
rust,ignore
#[derive(BorshSerialize, BorshDeserialize)]
pub struct WithdrawRequest {
    pub user_pubkey: Pubkey,
    pub pool_pubkey: Pubkey,
    pub amount: u64,
    pub recipient_btc_address: String,
}

pub fn process_withdrawal(
    ctx: Context<ProcessWithdraw>,
    request: WithdrawRequest,
) -> Result<()> {
    let pool = &mut ctx.accounts.lending_pool;
    let user_position = &mut ctx.accounts.user_position;

    // 1. Validate user position
    require!(
        user_position.deposited_amount >= request.amount,
        ErrorCode::InsufficientBalance
    );

    // 2. Check pool liquidity
    require!(
        pool.available_liquidity() >= request.amount,
        ErrorCode::InsufficientLiquidity
    );

    // 3. Find available UTXOs from pool
    let selected_utxos = select_utxos_for_withdrawal(
        &pool.utxos,
        request.amount
    )?;

    // 4. Create Bitcoin withdrawal transaction
    let mut btc_tx = Transaction::new();
    
    // Add inputs from selected UTXOs
    for utxo in selected_utxos {
        btc_tx.input.push(TxIn {
            previous_output: OutPoint::new(utxo.txid, utxo.vout),
            script_sig: Script::new(),
            sequence: Sequence::MAX,
            witness: Witness::new(),
        });
    }

    // Add withdrawal output to user's address
    let recipient_script = Address::from_str(&request.recipient_btc_address)?
        .script_pubkey();
    btc_tx.output.push(TxOut {
        value: request.amount,
        script_pubkey: recipient_script,
    });

    // Add change output back to pool if needed
    let total_input = selected_utxos.iter()
        .map(|utxo| utxo.amount)
        .sum::<u64>();
    if total_input > request.amount {
        btc_tx.output.push(TxOut {
            value: total_input - request.amount,
            script_pubkey: get_account_script_pubkey(&pool.pool_pubkey),
        });
    }

    // 5. Set transaction for validator signing
    set_transaction_to_sign(
        ctx.accounts,
        TransactionToSign {
            tx_bytes: &bitcoin::consensus::serialize(&btc_tx),
            inputs_to_sign: &selected_utxos.iter()
                .enumerate()
                .map(|(i, _)| InputToSign {
                    index: i as u32,
                    signer: pool.pool_pubkey,
                })
                .collect::<Vec<_>>()
        }
    );

    // 6. Update pool state
    pool.total_deposits = pool.total_deposits
        .checked_sub(request.amount)
        .ok_or(ErrorCode::MathOverflow)?;

    // 7. Update user position
    user_position.deposited_amount = user_position.deposited_amount
        .checked_sub(request.amount)
        .ok_or(ErrorCode::MathOverflow)?;

    // 8. Remove spent UTXOs from pool
    pool.utxos.retain(|utxo| !selected_utxos.contains(utxo));

    Ok(())
}

fn select_utxos_for_withdrawal(
    pool_utxos: &[UtxoMeta],
    amount: u64,
) -> Result<Vec<UtxoMeta>> {
    let mut selected = Vec::new();
    let mut total_selected = 0;

    for utxo in pool_utxos {
        if total_selected >= amount {
            break;
        }
        
        // Verify UTXO is still valid and unspent
        validate_utxo(utxo)?;
        
        selected.push(utxo.clone());
        total_selected += utxo.amount;
    }

    require!(
        total_selected >= amount,
        ErrorCode::InsufficientUtxos
    );

    Ok(selected)
}

Program

A program is a special kind of account that contains executable eBPF bytecode, denoted by the Account.is_executable: true field. This allows an account to receive arbitrary instruction data via a transaction to be processed by the runtime.

Every program is stateless, meaning that it can only read/write data to other accounts and that it cannot write to its own account; this, in-part, is how parallelized execution is made possible (see State for more info).

💡 Additionally, programs can send instructions to other programs which, in turn, receive instructions and thus extend program composability further. This is known as cross-program invocation (CPI) and will be detailed in future sections.

Components:

1. Entrypoint

Every Arch program includes a single entrypoint used to invoke the program. A handler function, often named process_instruction, is then used to handle the data passed into the entrypoint.

These parameters are required for every instruction to be processed._

use arch_program::entrypoint;
entrypoint!(process_instruction);

pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> Result<(), ProgramError> {
    // Program logic here
}

lib.rs

2. Instruction

The instruction_data is deserialized after being passed into the entrypoint. From there, if there are multiple instructions, a match statement can be utilized to point the logic flow to the appropriate handler function previously defined within the program which can continue processing the instruction.

3. Process Instruction

If a program has multiple instructions, a corresponding handler function should be defined to include the specific logic unique to the instruction.

4. State

Since programs are stateless, a "data" account is needed to hold state for a user. This is a non-executable account that holds program data.

If a program receives instruction that results in a user's state being altered, the program would manage this user's state via a mapping within the program's logic. This mapping would link the user's pubkey with a data account where the state would live for that specific program.

The program will likely include a struct to define the structure of its state and make it easier to work with. The deserialization of account data occurs during program invocation. After an update is made, state data gets re-serialized into a byte array and stored within the data field of the account.

Account Structure

Navigation: ReferenceProgram → Account Structure

For a comprehensive guide on working with accounts, see the Account Guide.

Accounts are a fundamental data structure in Arch that store state and are owned by [programs]. Each account has a unique address ([pubkey]) and contains data that can be modified by its owner program.

Account Structure

#[derive(Clone)]
#[repr(C)]
pub struct AccountInfo<'a> {
    pub key: &'a Pubkey, // address of the account
    pub utxo: &'a UtxoMeta, // utxo has this account key in script_pubkey
    pub data: Rc<RefCell<&'a mut [u8]>>, 
    pub owner: &'a Pubkey, 
    pub is_signer: bool,
    pub is_writable: bool,
    pub is_executable: bool, // true: program; false: data account
}

AccountMeta

#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
#[repr(C)]
pub struct AccountMeta {
    pub pubkey: Pubkey,
    pub is_signer: bool,
    pub is_writable: bool,
}

account.rs

Account Guide

Navigation: ReferenceProgram → Account Guide

For the core account structure and data types, see Account Structure.

Accounts are the fundamental building blocks for state management and program interaction in Arch Network. They serve as containers for both program code and state data, bridging the gap between Bitcoin's UTXO model and modern programmable state machines.

Note: For detailed documentation on core system functions used to interact with accounts (like invoke, new_create_account_instruction, add_state_transition, and set_transaction_to_sign), see System Functions.

flowchart TD
    A[Account] --> B[Program Account]
    A --> C[Data Account]
    A --> D[Native Account]
    B --> E[Executable Code]
    C --> F[Program State]
    C --> G[UTXOs]
    D --> H[System Operations]

    style A fill:#f9f,stroke:#333,stroke-width:2px
    style B fill:#f5f5f5,stroke:#666
    style C fill:#f5f5f5,stroke:#666
    style D fill:#f5f5f5,stroke:#666

Core Concepts

Account Fundamentals

Every account in Arch Network is uniquely identified by a public key (pubkey) and contains four essential components:

pub struct Account {
    /// The program that owns this account
    pub owner: Pubkey,
    /// Number of lamports assigned to this account
    pub lamports: u64,
    /// Data held in this account
    pub data: Vec<u8>,
    /// Whether this account can process instructions
    pub executable: bool,
}

Component Details:

  1. Owner (Pubkey)

    • Controls account modifications
    • Determines which program can modify data
    • Can be transferred to new programs
    • Required for all accounts
  2. Lamports (u64)

    • Native token balance
    • Used for:
      • Transaction fees
      • Rent payments
      • State storage costs
      • Program execution fees
  3. Data (Vec)

    • Flexible byte array for state storage
    • Common uses:
      • Program code (if executable)
      • Program state
      • UTXO metadata
      • Configuration data
    • Size determined at creation
  4. Executable Flag (bool)

    • Determines if account contains program code
    • Immutable after deployment
    • Controls instruction processing capability
flowchart LR
    A[Account Creation] --> B[Initial State]
    B --> C[Runtime Operations]
    C --> D[State Updates]
    D --> E[Account Closure]
    
    subgraph Lifecycle
        A -. Initialize .-> B
        B -. Process Instructions .-> C
        C -. Modify State .-> D
        D -. Cleanup .-> E
    end

    style A fill:#f9f,stroke:#333,stroke-width:2px
    style E fill:#9ff,stroke:#333,stroke-width:2px
    style Lifecycle fill:#f5f5f5,stroke:#666,stroke-width:1px

Account Types & Use Cases

1. Program Accounts

Program accounts contain executable code and form the backbone of Arch Network's programmable functionality.

// Example program account creation
let program_account = SystemInstruction::CreateAccount {
    lamports: rent.minimum_balance(program_data.len()),
    space: program_data.len() as u64,
    owner: bpf_loader::id(),  // BPF Loader owns program accounts
    executable: true,
    data: program_data,
};

Key characteristics:

  • Immutable after deployment
  • Owned by BPF loader
  • Contains verified program code
  • Processes instructions

2. Data Accounts

Data accounts store program state and user data. They're highly flexible and can be structured to meet various needs.

// Example data structure for a game account
#[derive(BorshSerialize, BorshDeserialize)]
pub struct GameAccount {
    pub player: Pubkey,
    pub score: u64,
    pub level: u8,
    pub achievements: Vec<Achievement>,
    pub last_played: i64,
}

// Creating a data account
let game_account = SystemInstruction::CreateAccount {
    lamports: rent.minimum_balance(size_of::<GameAccount>()),
    space: size_of::<GameAccount>() as u64,
    owner: game_program::id(),
    executable: false,
    data: Vec::new(),  // Will be initialized by program
};

Common use cases:

  • Player profiles
  • Game state
  • DeFi positions
  • NFT metadata
  • Configuration settings

3. UTXO Accounts

Special data accounts that bridge Bitcoin UTXOs with Arch Network state.

#[derive(BorshSerialize, BorshDeserialize)]
pub struct UtxoAccount {
    pub meta: UtxoMeta,
    pub owner: Pubkey,
    pub delegate: Option<Pubkey>,
    pub state: UtxoState,
    pub created_at: i64,
    pub last_updated: i64,
    pub constraints: Vec<UtxoConstraint>,
}

// Example UTXO account creation
let utxo_account = SystemInstruction::CreateAccount {
    lamports: rent.minimum_balance(size_of::<UtxoAccount>()),
    space: size_of::<UtxoAccount>() as u64,
    owner: utxo_program::id(),
    executable: false,
    data: Vec::new(),
};

Account Interactions

Account interactions in Arch Network are facilitated through a set of core system functions. These functions handle everything from account creation to state transitions and are documented in detail in System Functions. Below are common patterns for account interactions:

1. Creation Patterns

// 1. Basic account creation
pub fn create_basic_account(
    payer: &Keypair,
    space: u64,
    owner: &Pubkey,
) -> Result<Keypair, Error> {
    let account = Keypair::new();
    let rent = banks_client.get_rent().await?;
    let lamports = rent.minimum_balance(space as usize);
    
    let ix = system_instruction::create_account(
        &payer.pubkey(),
        &account.pubkey(),
        lamports,
        space,
        owner,
    );
    
    let tx = Transaction::new_signed_with_payer(
        &[ix],
        Some(&payer.pubkey()),
        &[payer, &account],
        recent_blockhash,
    );
    
    banks_client.process_transaction(tx).await?;
    Ok(account)
}

// 2. PDA (Program Derived Address) creation
pub fn create_pda_account(
    program_id: &Pubkey,
    seeds: &[&[u8]],
    space: u64,
) -> Result<Pubkey, Error> {
    let (pda, bump) = Pubkey::find_program_address(seeds, program_id);
    
    let ix = system_instruction::create_account(
        &payer.pubkey(),
        &pda,
        lamports,
        space,
        program_id,
    );
    
    // Include the bump seed for deterministic address
    let seeds_with_bump = &[&seeds[..], &[&[bump]]].concat();
    let signer_seeds = &[&seeds_with_bump[..]];
    
    invoke_signed(&ix, &[payer, pda], signer_seeds)?;
    Ok(pda)
}

2. State Management

// Example of managing account state
pub trait AccountState: Sized {
    fn try_from_slice(data: &[u8]) -> Result<Self, Error>;
    fn try_serialize(&self) -> Result<Vec<u8>, Error>;
    
    fn load(account: &AccountInfo) -> Result<Self, Error> {
        Self::try_from_slice(&account.data.borrow())
    }
    
    fn save(&self, account: &AccountInfo) -> Result<(), Error> {
        let data = self.try_serialize()?;
        let mut account_data = account.data.borrow_mut();
        account_data[..data.len()].copy_from_slice(&data);
        Ok(())
    }
}

// Implementation example
impl AccountState for GameAccount {
    fn update_score(&mut self, new_score: u64) -> Result<(), Error> {
        self.score = new_score;
        self.last_played = Clock::get()?.unix_timestamp;
        Ok(())
    }
}

3. Cross-Program Invocation (CPI)

// Example of one program calling another
pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    // Deserialize accounts
    let account_info_iter = &mut accounts.iter();
    let source_info = next_account_info(account_info_iter)?;
    let dest_info = next_account_info(account_info_iter)?;
    let system_program = next_account_info(account_info_iter)?;
    
    // Create CPI context
    let cpi_accounts = Transfer {
        from: source_info.clone(),
        to: dest_info.clone(),
    };
    let cpi_program = system_program.clone();
    let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
    
    // Perform cross-program invocation
    transfer(cpi_ctx, amount)?;
    
    Ok(())
}

Security Considerations

1. Access Control

fn verify_account_access(
    account: &AccountInfo,
    expected_owner: &Pubkey,
    writable: bool,
) -> ProgramResult {
    // Check account ownership
    if account.owner != expected_owner {
        return Err(ProgramError::IncorrectProgramId);
    }
    
    // Verify write permission if needed
    if writable && !account.is_writable {
        return Err(ProgramError::InvalidAccountData);
    }
    
    // Additional checks...
    Ok(())
}

2. Data Validation

fn validate_account_data<T: AccountState>(
    account: &AccountInfo,
    validate_fn: impl Fn(&T) -> bool,
) -> ProgramResult {
    // Load and validate account data
    let data = T::load(account)?;
    if !validate_fn(&data) {
        return Err(ProgramError::InvalidAccountData);
    }
    Ok(())
}

Best Practices

1. Account Management

  • Always validate account ownership before modifications
  • Use PDAs for deterministic addresses
  • Implement proper error handling
  • Close unused accounts to reclaim rent

2. Data Safety

  • Validate all input data
  • Use proper serialization
  • Handle account size limits
  • Implement atomic operations

3. Performance

  • Minimize account creations
  • Batch operations when possible
  • Use appropriate data structures
  • Cache frequently accessed data

4. Upgrades

  • Plan for version management
  • Implement migration strategies
  • Use flexible data structures
  • Document state changes

Common Patterns

1. Account Initialization

pub fn initialize_account<T: AccountState>(
    program_id: &Pubkey,
    account: &AccountInfo,
    initial_state: T,
) -> ProgramResult {
    // Verify account is uninitialized
    if !account.data_is_empty() {
        return Err(ProgramError::AccountAlreadyInitialized);
    }
    
    // Set account owner
    account.set_owner(program_id)?;
    
    // Initialize state
    initial_state.save(account)?;
    
    Ok(())
}

2. Account Updates

pub fn update_account<T: AccountState>(
    account: &AccountInfo,
    update_fn: impl FnOnce(&mut T) -> ProgramResult,
) -> ProgramResult {
    // Load current state
    let mut state = T::load(account)?;
    
    // Apply update
    update_fn(&mut state)?;
    
    // Save updated state
    state.save(account)?;
    
    Ok(())
}

3. Account Closure

pub fn close_account(
    account: &AccountInfo,
    destination: &AccountInfo,
) -> ProgramResult {
    // Transfer lamports
    let dest_starting_lamports = destination.lamports();
    **destination.lamports.borrow_mut() = dest_starting_lamports
        .checked_add(account.lamports())
        .ok_or(ProgramError::Overflow)?;
    **account.lamports.borrow_mut() = 0;
    
    // Clear data
    account.data.borrow_mut().fill(0);
    
    Ok(())
}
  • UTXOs - How UTXOs integrate with accounts
  • Programs - Programs that own and modify accounts
  • Instructions - How to interact with accounts

System Functions

Navigation: ReferenceProgram → System Functions

Core system functions that enable program interactions, account management, and state transitions in Arch Network.

Overview

These functions form the foundation for program-to-program communication, account management, and state transitions in Arch Network:

  1. invoke - Cross-program invocation
  2. new_create_account_instruction - Account creation
  3. add_state_transition - State management
  4. set_transaction_to_sign - Transaction preparation

Detailed Function Documentation

1. invoke

pub fn invoke(instruction: &Instruction, account_infos: &[AccountInfo]) -> ProgramResult

The invoke function enables cross-program communication and execution:

  • Purpose: Allows one program to call another program securely
  • Key Features:
    • Validates account permissions
    • Manages account borrowing
    • Handles cross-program context
    • Provides error handling

Example Usage:

// Invoke system program to create account
invoke(
    &SystemInstruction::new_create_account_instruction(
        txid.try_into().unwrap(),
        vout,
        account_pubkey
    ),
    &[account_info.clone()]
)?;

2. new_create_account_instruction

pub fn new_create_account_instruction(
    txid: [u8; 32],
    vout: u32,
    pubkey: Pubkey,
) -> Instruction

Creates instructions for new account initialization:

  • Purpose: Creates new accounts with UTXO backing
  • Key Features:
    • Sets up UTXO metadata
    • Configures permissions
    • Associates with system program
    • Prepares initialization

Example Usage:

let instruction = SystemInstruction::new_create_account_instruction(
    txid.try_into().unwrap(),
    0,  // vout index
    account_pubkey,
);

3. add_state_transition

pub fn add_state_transition(transaction: &mut Transaction, account: &AccountInfo)

Manages state transitions for accounts:

  • Purpose: Updates Bitcoin transactions with account changes
  • Key Features:
    • Adds UTXO inputs
    • Sets up script signatures
    • Configures outputs
    • Manages state

Example Usage:

let mut tx = Transaction {
    version: Version::TWO,
    lock_time: LockTime::ZERO,
    input: vec![],
    output: vec![],
};
add_state_transition(&mut tx, account);

4. set_transaction_to_sign

pub fn set_transaction_to_sign(
    accounts: &[AccountInfo],
    transaction_to_sign: TransactionToSign,
) -> ProgramResult

Prepares transactions for signing:

  • Purpose: Sets up transaction metadata and permissions
  • Key Features:
    • Validates size limits
    • Checks signer permissions
    • Sets up metadata
    • Manages requirements

Example Usage:

let transaction_to_sign = TransactionToSign {
    tx_bytes: serialized_tx,
    inputs_to_sign: vec![
        InputToSign {
            signer: account.key,
            ..Default::default()
        }
    ],
};
set_transaction_to_sign(accounts, transaction_to_sign)?;
graph LR
    A[Program Request] ==> B[new_create_account_instruction]
    B ==> C[invoke]
    C ==> D[add_state_transition]
    D ==> E[set_transaction_to_sign]
    E ==> F[Bitcoin Transaction]

    style A fill:#4a9eff,stroke:#3182ce,stroke-width:2px,color:#fff
    style B fill:#ffffff,stroke:#ccd7e0,stroke-width:2px
    style C fill:#ffffff,stroke:#ccd7e0,stroke-width:2px
    style D fill:#ffffff,stroke:#ccd7e0,stroke-width:2px
    style E fill:#ffffff,stroke:#ccd7e0,stroke-width:2px
    style F fill:#f687b3,stroke:#d53f8c,stroke-width:2px,color:#fff

Best Practices

  1. Validation

    • Always check account permissions
    • Verify transaction limits
    • Validate UTXO states
    • Handle errors properly
  2. State Management

    • Use atomic operations
    • Maintain state consistency
    • Handle failures gracefully
    • Implement rollbacks
  3. Security

    • Validate all signatures
    • Check account ownership
    • Verify transaction data
    • Handle edge cases

Entrypoint and Handler Functions

Entrypoint

Every Arch program includes a single entrypoint used to invoke the program. A handler function is then used to process the data passed into the entrypoint.

entrypoint!(process_instruction);

lib.rs

Initialization and Data Reading:

The entrypoint begins by initializing and reading serialized data that is passed in, which includes everything needed for program execution. It then deserializes this data to obtain the instruction, an object which contains all necessary details like the program_id and associated UTXO information.

It passes in all deserialized data in to the handler function for processing of the program's business logic; this could involve transactions, state updates, or other program-specific operations.

Handler function

Here we'll discuss the dispatcher function, in our case, named: process_instruction.

This dispatcher function requires the following parameters:

  • program_id - Unique identifier of the currently executing program.
  • accounts - Slice reference containing accounts needed to execute an instruction.
  • instruction_data - Serialized data containing program instructions.

This returns a Result representing success (Ok) or failture (ProgramError).

pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> Result<(), ProgramError> {
...
}

lib.rs

This function is responsible for parsing and directing the execution flow based on the type of transaction or method specified. It is a critical component that developers must implement to ensure that their programs can appropriately respond to different operational requests.

The handler function is defined as part of the program and is facilitated via the entrypoint!. This macro binds the process_instruction function to be the first receiver of any execution call made by the Arch virtual machine, effectively making it the gatekeeper for all incoming instructions.

Deserialize Input Data

The function starts by deserializing the input data (instruction_data) into a known format, typically a custom struct that represents different methods or commands the program can execute.

Method Dispatch

Based on the deserialized data, the function determines which specific method to execute. This is often handled through a match statement that routes to different functions or modules within the program.

Execute Business Logic

Each routed function performs specific business logic related to the program's purpose, such as managing assets, updating state, or interacting with other program or tokens.

Result Commitment:

Upon successful execution, the new UTXO authorities, new UTXO Data and a Bitcoin transaction are committed back to the network.

Instructions and Messages

Instructions and messages are fundamental components of Arch's transaction processing system that enable communication between clients and programs. They form the basis for all state changes and interactions within the Arch network.

Instructions

An instruction is the basic unit of program execution in Arch. It contains all the information needed for a program to execute a specific operation. Instructions are processed atomically, meaning they either complete entirely or have no effect.

Structure

pub struct Instruction {
    /// Program ID that executes this instruction
    pub program_id: Pubkey,
    /// Accounts required for this instruction
    pub accounts: Vec<AccountMeta>,
    /// Instruction data
    pub data: Vec<u8>,
}

Components:

  1. Program ID: The pubkey of the program that will process the instruction
  2. Accounts: List of accounts required for the instruction, with their metadata
  3. Instruction Data: Custom data specific to the instruction, typically serialized using Borsh or another format

Account Metadata

pub struct AccountMeta {
    pub pubkey: Pubkey,
    pub is_signer: bool,
    pub is_writable: bool,
}
  • pubkey: The account's public key
  • is_signer: Whether the account must sign the transaction
  • is_writable: Whether the account's data can be modified

Messages

A message is a collection of instructions that form a transaction. Messages ensure atomic execution of multiple instructions, meaning either all instructions succeed or none take effect.

Structure

pub struct Message {
    /// List of account keys referenced by the instructions
    pub account_keys: Vec<Pubkey>,
    /// Recent blockhash
    pub recent_blockhash: Hash,
    /// List of instructions to execute
    pub instructions: Vec<CompiledInstruction>,
}

Components:

  1. Account Keys: All unique accounts referenced across instructions
  2. Recent Blockhash: Used for transaction uniqueness and timeout
  3. Instructions: List of instructions to execute in sequence

Instruction Processing Flow:

  1. Client creates an instruction with:

    • Program ID to execute the instruction
    • Required accounts with appropriate permissions
    • Instruction-specific data (serialized parameters)
  2. Instruction(s) are bundled into a message:

    • Multiple instructions can be atomic
    • Account permissions are consolidated
    • Blockhash is included for uniqueness
  3. Message is signed to create a transaction:

    • All required signers must sign
    • Transaction size limits apply
    • Fees are calculated
  4. Transaction is sent to the network:

    • Validated by validators
    • Processed in parallel when possible
    • Results are confirmed
  5. Program processes the instruction:

    • Deserializes instruction data
    • Validates accounts and permissions
    • Executes operation
    • Updates account state

Best Practices:

  1. Account Validation

    • Always verify account ownership
    • Check account permissions
    • Validate account relationships
  2. Data Serialization

    • Use consistent serialization format (preferably Borsh)
    • Include version information
    • Handle errors gracefully
    • Validate data lengths
  3. Error Handling

    • Return specific error types
    • Provide clear error messages
    • Handle all edge cases
    • Implement proper cleanup

Cross-Program Invocation (CPI)

Instructions can invoke other programs through CPI, enabling composability:

  1. Create new instruction for target program:

    • Specify program ID
    • Include required accounts
    • Prepare instruction data
  2. Pass required accounts:

    • Include all necessary accounts
    • Set proper permissions
    • Handle PDA derivation
  3. Invoke using invoke or invoke_signed:

    • For regular accounts: invoke
    • For PDAs: invoke_signed
    • Handle return values
  4. Handle results:

    • Check return status
    • Process any returned data
    • Handle errors appropriately

Security Considerations:

  1. Account Verification

    • Verify all account permissions
    • Check ownership and signatures
    • Validate account relationships
    • Prevent privilege escalation
  2. Data Validation

    • Sanitize all input data
    • Check buffer lengths
    • Validate numerical ranges
    • Prevent integer overflow
  3. State Management

    • Maintain atomic operations
    • Handle partial failures
    • Prevent race conditions
    • Ensure consistent state

Common Patterns:

  1. Initialization

    • Create necessary accounts
    • Set initial state
    • Assign proper ownership
  2. State Updates

    • Validate permissions
    • Update account data
    • Maintain invariants
  3. Account Management

    • Close accounts when done
    • Manage PDAs properly

Pubkey

A pubkey, or public key, is a custom type that contains a 256-bit (32 bytes) integer derived from the private key.

#[derive(Clone, Debug, Eq, PartialEq, BorshSerialize, BorshDeserialize, Serialize, Deserialize)]
pub struct Pubkey([u8; 32]);

pubkey.rs

Syscalls

A syscall is a function that can be used to obtain information from the underlying virtual machine.

// Used for cross-program invocation (CPI)

// Invokes a cross-program call
define_syscall!(fn sol_invoke_signed_rust(instruction_addr: *const u8, account_infos_addr: *const u8, account_infos_len: u64) -> u64);

// Sets the data to be returned for the cross-program invocation
define_syscall!(fn sol_set_return_data(data: *const u8, length: u64));

// Returns the cross-program invocation data
define_syscall!(fn sol_get_return_data(data: *mut u8, length: u64, program_id: *mut Pubkey) -> u64);

// Arch

// Validates and sets up transaction for being signed
define_syscall!(fn arch_set_transaction_to_sign(transaction_to_sign: *const TransactionToSign));

// Retrieves raw Bitcoin transaction from RPC and copies into memory buffer
define_syscall!(fn arch_get_bitcoin_tx(data: *mut u8, length: u64, txid: &[u8; 32]) -> u64);

// Retrieves the multi-sig public key and copies into memory buffer
define_syscall!(fn arch_get_network_xonly_pubkey(data: *mut u8) -> u64);

// Validates ownership of a Bitcoin UTXO against a public key
define_syscall!(fn arch_validate_utxo_ownership(utxo: *const UtxoMeta, owner: *const Pubkey) -> u64);

// Generates a Bitcoin script public key and copies into memory buffer
define_syscall!(fn arch_get_account_script_pubkey(script: *mut u8, pubkey: *const Pubkey) -> u64);

// Retrieves the latest Bitcoin block height
define_syscall!(fn arch_get_bitcoin_block_height() -> u64);

// logs

// Prints the hexidecimal representation of a string slice to stdout
define_syscall!(fn sol_log_(message: *const u8, len: u64));

// Prints 64-bit values represented as hexadecimal to stdout
define_syscall!(fn sol_log_64_(arg1: u64, arg2: u64, arg3: u64, arg4: u64, arg5: u64));

// Prints the hexidecimal representation of a public key to stdout
define_syscall!(fn sol_log_pubkey(pubkey_addr: *const u8));

// Prints the base64 representation of a data array to stdout
define_syscall!(fn sol_log_data(data: *const u8, data_len: u64));

syscalls/definition.rs

System Instruction

By default, every account is owned by the System Program (Pubkey). Only the System Program can create a new account.

#[derive(Clone, PartialEq, Eq, Debug)]
pub enum SystemInstruction {
    CreateAccount(UtxoMeta),
    ExtendBytes(Vec<u8>),
}

...

pub fn new_create_account_instruction(
    txid: [u8; 32],
    vout: u32,
    pubkey: Pubkey,
) -> Instruction {
    Instruction {
        program_id: Pubkey::system_program(),
        accounts: vec![AccountMeta {
            pubkey,
            is_signer: true,
            is_writable: true,
        }],
        data: SystemInstruction::CreateAccount(UtxoMeta::from(txid, vout)).serialise(),
    }
}

system_instruction.rs

UTXO (Unspent Transaction Output)

UTXOs (Unspent Transaction Outputs) are fundamental to Bitcoin's transaction model and serve as the foundation for state management in Arch Network. Unlike account-based systems that track balances, UTXOs represent discrete "coins" that must be consumed entirely in transactions.

Core Concepts

What is a UTXO?

  • A UTXO represents an unspent output from a previous transaction
  • Each UTXO is uniquely identified by a transaction ID (txid) and output index (vout)
  • UTXOs are immutable - they can only be created or spent, never modified
  • Once spent, a UTXO cannot be reused (prevents double-spending)

Role in Arch Network

  • UTXOs anchor program state to Bitcoin's security model
  • They provide deterministic state transitions
  • Enable atomic operations across the network
  • Allow for provable ownership and state validation

UTXO Structure

The UtxoMeta struct encapsulates the core UTXO identification data:

use arch_program::utxo::UtxoMeta;
use bitcoin::Txid;

#[derive(Debug, Clone, PartialEq)]
pub struct UtxoMeta {
    pub txid: [u8; 32],  // Bitcoin transaction ID (32 bytes)
    pub vout: u32,       // Output index in the transaction
}

impl UtxoMeta {
    /// Creates a new UTXO metadata instance
    pub fn new(txid: [u8; 32], vout: u32) -> Self {
        Self { txid, vout }
    }

    /// Deserializes UTXO metadata from a byte slice
    /// Format: [txid(32 bytes)][vout(4 bytes)]
    pub fn from_slice(data: &[u8]) -> Self {
        let mut txid = [0u8; 32];
        txid.copy_from_slice(&data[0..32]);
        let vout = u32::from_le_bytes([
            data[32], data[33], data[34], data[35]
        ]);
        Self { txid, vout }
    }
}

UTXO Lifecycle

1. Creation Process

Creating a UTXO with Bitcoin RPC

use bitcoincore_rpc::{Auth, Client as RpcClient, RpcApi};
use bitcoin::{Amount, Address};
use arch_program::pubkey::Pubkey;

// Initialize Bitcoin RPC client
let rpc = RpcClient::new(
    "http://localhost:18443",  // Bitcoin node RPC endpoint
    Auth::UserPass(
        "user".to_string(),
        "pass".to_string()
    )
).expect("Failed to create RPC client");

// Generate a new account address
let account_address = Pubkey::new_unique();
let btc_address = Address::from_pubkey(&account_address);

// Create UTXO by sending Bitcoin
// Parameters explained:
// - address: Destination Bitcoin address
// - amount: Amount in satoshis (3000 sats = 0.00003 BTC)
// - comment: Optional transaction comment
// - replaceable: Whether the tx can be replaced (RBF)
let txid = rpc.send_to_address(
    &btc_address,
    Amount::from_sat(3000),
    Some("Create Arch UTXO"),  // Comment
    None,                      // Comment_to
    Some(true),               // Replaceable
    None,                     // Fee rate
    None,                     // Fee estimate mode
    None                      // Avoid reuse
)?;

// Wait for confirmation (recommended)
rpc.wait_for_confirmation(&txid, 1)?;

Creating an Arch Account with UTXO

use arch_program::{
    system_instruction::SystemInstruction,
    pubkey::Pubkey,
    transaction::Transaction,
};

// Create new program account backed by UTXO
let account_pubkey = Pubkey::new_unique();
let instruction = SystemInstruction::new_create_account_instruction(
    txid.try_into().unwrap(),
    0,  // vout index
    account_pubkey,
    // Additional parameters like:
    // - space: Amount of space to allocate
    // - owner: Program that owns the account
);

// Build and sign transaction
let transaction = Transaction::new_signed_with_payer(
    &[instruction],
    Some(&payer.pubkey()),
    &[&payer],
    recent_blockhash
);

2. Validation & Usage

Programs must implement proper UTXO validation:

fn validate_utxo(utxo: &UtxoMeta) -> Result<(), ProgramError> {
    // 1. Verify UTXO exists on Bitcoin
    let btc_tx = rpc.get_transaction(&utxo.txid)?;
    
    // 2. Check confirmation count
    if btc_tx.confirmations < MIN_CONFIRMATIONS {
        return Err(ProgramError::InsufficientConfirmations);
    }
    
    // 3. Verify output index exists
    if utxo.vout as usize >= btc_tx.vout.len() {
        return Err(ProgramError::InvalidVout);
    }
    
    // 4. Verify UTXO is unspent
    if is_spent(utxo) {
        return Err(ProgramError::UtxoAlreadySpent);
    }
    
    Ok(())
}

3. State Management

// Example UTXO state tracking
#[derive(Debug)]
pub struct UtxoState {
    pub meta: UtxoMeta,
    pub status: UtxoStatus,
    pub owner: Pubkey,
    pub created_at: i64,
    pub spent_at: Option<i64>,
}

#[derive(Debug)]
pub enum UtxoStatus {
    Pending,    // Waiting for confirmations
    Active,     // Confirmed and spendable
    Spent,      // UTXO has been consumed
    Invalid,    // UTXO was invalidated (e.g., by reorg)
}

Best Practices

  1. Validation

    • Always verify UTXO existence on Bitcoin
    • Check for sufficient confirmations (recommended: 6+)
    • Validate ownership and spending conditions
    • Handle Bitcoin reorgs that might invalidate UTXOs
  2. State Management

    • Implement robust UTXO tracking
    • Handle edge cases (reorgs, conflicting txs)
    • Consider implementing UTXO caching for performance
    • Maintain accurate UTXO sets for your program
  3. Security

    • Never trust client-provided UTXO data without verification
    • Implement proper access controls
    • Consider timelock constraints for sensitive operations
    • Monitor for suspicious UTXO patterns
  4. Performance

    • Batch UTXO operations when possible
    • Implement efficient UTXO lookup mechanisms
    • Consider UTXO consolidation strategies
    • Cache frequently accessed UTXO data

Error Handling

Common UTXO-related errors to handle:

pub enum UtxoError {
    NotFound,                    // UTXO doesn't exist
    AlreadySpent,               // UTXO was already consumed
    InsufficientConfirmations,  // Not enough confirmations
    InvalidOwner,               // Unauthorized attempt to spend
    Reorged,                    // UTXO invalidated by reorg
    InvalidVout,                // Output index doesn't exist
    SerializationError,         // Data serialization failed
}

SDK

This section includes reference documentation for our SDK.

Processed Transaction

A processed transaction is a custom data type that contains a runtime transaction, a status, denoting the result of executing this runtime transaction, as well as a collection of Bitcoin transaction IDs.

#[derive(Clone, Debug, Deserialize, Serialize, BorshDeserialize, BorshSerialize)]
pub enum Status {
    Processing,
    Processed,
}

#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub struct ProcessedTransaction {
    pub runtime_transaction: RuntimeTransaction,
    pub status: Status,
    pub bitcoin_txids: Vec<String>,
}

processed_transaction.rs

Runtime Transaction

A runtime transaction includes a version number, a slice of signatures included on the transaction and a message field, which details a list of instructions to be processed atomically.

#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, BorshDeserialize, BorshSerialize)]
pub struct RuntimeTransaction {
    pub version: u32,
    pub signatures: Vec<Signature>,
    pub message: Message,
}

runtime_transaction.rs

Signature

A signature is a custom data type that holds a slice of 64 bytes.

#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub struct Signature(pub Vec<u8>);

signature.rs

System Program

The Arch System Program is Arch's core program. This program contains a set of variants that can be thought-of as native functionality that can be used within any Arch program.

The System Program creates new accounts, assigns accounts to owning programs, marks accounts as executable, and writes data to the accounts.

In order to make calls to the System Program, the following mapping can help you point to the correct functionality.

indexmethod
0CreateAccount
1WriteBytes
2MakeExecutable
3AssignOwnership

CreateAccount

Index: 0

Create a new account.

Below, within the Instruction data field, we find a local variable instruction_data that contains vec![0], the correct index for making a call to SystemProgram::CreateAccount.

let instruction_data = vec![0];

let instruction = Instruction {
    program_id: Pubkey::system_program(),
    accounts: vec![AccountMeta {
        pubkey,
        is_signer: true,
        is_writable: true,
    }],
    data: instruction_data,
}

WriteBytes

Index: 1

Writes bytes to an array and serializes it.

Below, within the Instruction data field, we find a local variable instruction_data that contains vec![1], the correct index for making a call to SystemProgram::WriteBytes.

let offset = 4u32.to_le_bytes();
let mut instruction_data = vec![1];
instruction_data.extend_from_slice(&offset);

let instruction = Instruction {
    program_id: Pubkey::system_program(),
    accounts: vec![AccountMeta {
        pubkey,
        is_signer: true,
        is_writable: true,
    }],
    data: instruction_data,
}

MakeExecutable

Index: 2

Sets the account as executable, marking it as a program.

Below, within the Instruction data field, we find a local variable instruction_data that contains vec![2], the correct index for making a call to SystemProgram::MakeExecutable.

let instruction_data = vec![2];

let instruction = Instruction {
    program_id: Pubkey::system_program(),
    accounts: vec![AccountMeta {
        pubkey,
        is_signer: true,
        is_writable: true,
    }],
    data: instruction_data,
}

We can proceed to confirm that the program is executable with read_account_info which returns an AccountInfoResult that gets parsed to obtain the is_executable value.

assert!(
    read_account_info("node_url", program_pubkey)
        .unwrap()
        .is_executable
);

AssignOwnership

Index: 3

Sets a Pubkey to be the owner of an account.

Below, within the Instruction data field, we find a local variable instruction_data that contains vec![3], the correct index for making a call to SystemProgram::AssignOwnership.

The instruction_data also contains the serialized Pubkey of the owner account.

let mut instruction_data = vec![3];
instruction_data.extend(program_pubkey.serialize());

let instruction = Instruction {
    program_id: Pubkey::system_program(),
    accounts: vec![AccountMeta {
        pubkey,
        is_signer: true,
        is_writable: true,
    }],
    data: instruction_data,
}

RPC

Interact with Arch nodes directly with the JSON RPC API via the HTTP methods.

HTTP Methods

Interact with Arch nodes directly with the JSON RPC API via this list of available HTTP methods.

Note: For client-side needs, use the @saturnbtcio/arch-sdk library as an interface for the RPC methods to interact with an Arch node.

Endpoint

Default port: 9002

  • http://localhost:9002

Request Format:

To make a JSON-RPC request, send an HTTP POST request with a Content-Type: application/json header.

The JSON request data should contain 4 fields:

  • jsonrpc: <string> - set to "2.0."
  • id: <number> - a unique client-generated identifying integer.
  • method: <string> - a string containing the method to be invoked.
  • params: <array> - a JSON array of ordered parameter values.

Response Format:

The response output will be a JSON object with the following fields:

  • jsonrpc: <string> - matching the value set in the request.
  • id: <number> - matching the value set in the request.
  • result: <array|boolean|number|object|string> - requested data, success confirmation or boolean flag.

sendTransaction

Description: Relays a single transaction to the nodes for execution.

The following pre-flight checks are performed:

  1. It verifies the transaction size limit.
  2. It verifies the transaction signatures.
  3. It verifies that the UTXOs are not already spent.

The same checks are performed on sendTransactions.

If these checks pass, the transaction is forwarded to the rest of the nodes for processing.

Method: POST

Parameters: params: <serialized_object> - A serialized Runtime Transaction object representing the transaction to be sent.

Returns: A string containing the transaction IDs (txid) of the submitted transaction.

Request:

curl -vL POST -H 'Content-Type: application/json' -d '
{
  "jsonrpc":"2.0",
  "id":1,
  "method":"send_transaction",
  "params": [
    [1,2,3,4,...]
  ]
}' \
http://localhost:9002/

Response:

{
  "jsonrpc": "2.0",
  "result": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
  "id": "1"
}

sendTransactions

Description: Sends multiple transactions in a single batch request.

The following pre-flight checks are performed:

  1. It verifies the transaction size limit.
  2. It verifies the transaction signatures.
  3. It verifies that the UTXOs are not already spent.

The same checks are performed on sendTransaction.

If these checks pass, the transaction is forwarded to the rest of the nodes for processing.

Method: POST

Parameters: params: <array[serialized_object]> - An array of serialized Runtime Transaction objects to be sent.

Returns: An array of strings containing the transaction IDs (txids) of the submitted transactions.

Request:

curl -vL POST -H 'Content-Type: application/json' -d '
{
    "jsonrpc":"2.0",
    "id":1,
    "method":"send_transactions",
    "params": [
        [
            [1,2,3,4,...],
            [5,6,7,8,...]
        ]
    ]
}' \
http://localhost:9002/

Response:

{
  "jsonrpc": "2.0",
  "result": [
    "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
    "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
  ],
  "id": "1"
}

getAccountAddress

Description: Fetches the account address associated with a public key.

Method: POST

Parameters: pubkey: <byte_array> - The public key (Pubkey) of the account.

Returns: The account address as a string.

Request:

curl -vL POST -H 'Content-Type: application/json' -d '
{
    "jsonrpc":"2.0",
    "id":1,
    "method":"get_account_address",
    "params":[
        [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32] 
    ]
}' \
http://localhost:9002/

Response:

{
  "jsonrpc": "2.0",
  "result": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
  "id": "1"
}

getProgramAccounts

⚠️ Note: This endpoint is not available for local validators.

Description: Fetches all accounts owned by the specified program ID.

Parameters: program_id: <byte_array> - Pubkey of the program to query, as an array of 32 bytes.

filters (optional) - Array of filter objects, each filter should be either:

  • { "DataSize": <size> } where <size> is the required size of the account data
  • { "DataContent": { "offset": <offset>, "bytes": <byte_array> } } where <offset> is the offset into the account data, and <byte_array> is an array of bytes to match

Returns: An array of account objects, each containing:

  • pubkey: <byte_array>: The account's public key.
  • account: <object>: An object containing the account's data and metadata.

Request:

curl -vL POST -H 'Content-Type: application/json' -d '
{
  "jsonrpc":"2.0",
  "id":1,
  "method": "get_program_accounts",
  "params": [
    [80,82,242,228,43,246,248,133,88,238,139,124,88,96,107,32,71,40,52,251,90,42,66,176,66,32,147,203,137,211,253,40],
    [
      {
        "DataSize": 165
      },
      {
        "DataContent": {
          "offset": 0,
          "bytes": [1, 2, 3, 4]
        }
      }
    ]
  ]
}' \
http://localhost:9002/

Response:

{
  "jsonrpc": "2.0",
  "result": [
    {
      "pubkey": [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32],
      "account": {
        "data": [1,2,3,4,...],
        "owner": [80,82,242,228,43,246,248,133,88,238,139,124,88,96,107,32,71,40,52,251,90,42,66,176,66,32,147,203,137,211,253,40],
        "utxo": "txid:vout",
        "is_executable": false
      }
    }
  ],
  "id": "1"
}

getBlock

Description: Retrieves block data based on a block hash.

Method: POST

Parameters: blockHash: <string> - A string representing the block hash.

Returns: A Block object or undefined if the block is not found.

Error Handling: Returns undefined if the block is not found (404).

Request:

curl -vL POST -H 'Content-Type: application/json' -d '
{
    "jsonrpc":"2.0",
    "id":1,
    "method":"get_block",
    "params":[
        "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f" 
    ]
}' \
http://localhost:9002/

Response:

{
  "jsonrpc": "2.0",
  "result": {
    /* Block object */
  },
  "id": "1"
}

getBlockCount

Description: Retrieves the current block count.

Method: POST

Parameters: None.

Returns: The current block count as a number.

Request:

curl -vL POST -H 'Content-Type: application/json' -d '
{
    "jsonrpc":"2.0",
    "id":1,
    "method":"get_block_count",
    "params":[]
}' \
http://localhost:9002/

Response:

{
  "jsonrpc": "2.0",
  "result": 680000,
  "id": "1"
}

getBlockHash

Description: Retrieves the block hash at a specific block height.

Method: POST

Parameters: blockHeight: <number> - The block height for which to retrieve the block hash.

Returns: A string representing the block hash.

Request:

curl -vL POST -H 'Content-Type: application/json' -d '
{
    "jsonrpc":"2.0",
    "id":1,
    "method":"get_block_hash",
    "params":["680000"]
}' \
http://localhost:9002/

Response:

{
  "jsonrpc": "2.0",
  "result": "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
  "id": "1"
}

getCurrentState

️️⚠️ Note: This endpoint is not available for local validators.

Description: Retrieves the current state of the node, including information about the blockchain and network status.

Method: POST

Parameters: None.

Returns: A CurrentState object.

Request:

curl -vL POST -H 'Content-Type: application/json' -d '
{
    "jsonrpc":"2.0",
    "id":1,
    "method":"get_current_state",
    "params":[]
}' \
http://localhost:9002/

Response:

{
  "jsonrpc": "2.0",
  "result": {
    "state": "Running",
    "last_state_transition": "2023-10-01T12:00:00Z"
  },
  "id": 1
}

getPeers

️️⚠️ Note: This endpoint is not available for local validators.

Description: Retrieves a list of peers currently connected to the node.

Method: POST

Parameters: None.

Returns: An array of PeerStats.

Request:

curl -vL POST -H 'Content-Type: application/json' -d '
{
    "jsonrpc":"2.0",
    "id":1,
    "method":"get_peers",
    "params":[]
}' \
http://localhost:9002/

Response:

{
  "jsonrpc": "2.0",
  "result": [
    {
      "peer_id": "12D3KooW...",
      "address": "/ip4/192.168.1.1/tcp/30303",
      "status": "connected"
    }
  ],
  "id": 1
}

getProcessedTransaction

Description: Fetches details of a processed transaction using a transaction ID.

Method: POST

Parameters: txid: <string> - A string representing the transaction ID.

Returns: A ProcessedTransaction object or undefined if the transaction is not found.

Error Handling: Returns undefined if the transaction is not found (404).

Request:

curl -vL POST -H 'Content-Type: application/json' -d '
{
    "jsonrpc":"2.0",
    "id":1,
    "method":"get_processed_transaction",
    "params":[
        "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
    ]
}' \
http://localhost:9002/

Response:

{
  "jsonrpc": "2.0",
  "result": {
    "runtime_transaction": { /* RuntimeTransaction object */ },
    "status": "Confirmed",
    "bitcoin_txids": ["txid1", "txid2"]
  },
  "id": "1"
}

readAccountInfo

Description: Retrieves detailed information for the specified account.

Method: POST

Parameters: pubkey: <byte_array> - The public key (Pubkey) of the account to query, as an array of 32 bytes.

Returns: An object containing the account's information:

  • data: The account's data as a byte array.
  • owner: The account's owner as a byte array (program_id).
  • utxo: The UTXO associated with this account.
  • is_executable: A boolean indicating if the account contains executable code.

Request:

curl -vL POST -H 'Content-Type: application/json' -d '
{
    "jsonrpc":"2.0",
    "id":1,
    "method":"read_account_info",
    "params":[
        [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32]
    ]
}' \
http://localhost:9002/

Response:

{
  "jsonrpc": "2.0",
  "result": {
    "data": [1,2,3,4,...],
    "owner": [80,82,242,228,43,246,248,133,88,238,139,124,88,96,107,32,71,40,52,251,90,42,66,176,66,32,147,203,137,211,253,40],
    "utxo": "txid:vout",
    "is_executable": false
  },
  "id": "1"
}

startDkg

️️⚠️ Note: This endpoint is not available for local validators.

Description: Initiates the Distributed Key Generation (DKG) process.

Method: POST

Parameters: None.

Returns: A success message if the DKG process is initiated.

Request:

curl -vL POST -H 'Content-Type: application/json' -d '
{
    "jsonrpc":"2.0",
    "id":1,
    "method":"start_dkg",
    "params":[]
}' \
http://localhost:9002/

Response:

{
  "jsonrpc": "2.0",
  "result": "DKG process initiated",
  "id": "1"
}

isNodeReady

Description: Checks if the node is ready to process requests.

Parameters: None.

Returns: A boolean indicating whether the node is ready.

Request:

curl -vL POST -H 'Content-Type: application/json' -d '
{
    "jsonrpc":"2.0",
    "id":1,
    "method":"is_node_ready",
    "params":[]
}' \
http://localhost:9002/

Response:

{
  "jsonrpc": "2.0",
  "result": true,
  "id": "1"
}

resetNetwork

⚠️ Note:

This method is only callable by the Leader node, which for the time being will be Arch. This method is used for internal debugging purposes as we get the Testnet operational.

This endpoint is also not available for local validators.

Description: Resets the network state.

Parameters: None.

Returns: A success message if the network reset is successful.

Request:

curl -vL POST -H 'Content-Type: application/json' -d '
{
  "jsonrpc":"2.0",
  "id":1,
  "method":"reset_network",
  "params":[]
}' \
http://localhost:9002/

Response:

{
  "jsonrpc": "2.0",
  "result": "Success!",
  "id": "1"
}

Deprecated Methods

The following includes a list of RPC methods which are no longer supported.

getAccountInfo

Description: Retrieves detailed information for the specified account.

Method: POST

Parameters: pubkey: <byte_array> - The public key (Pubkey) of the account to query, as an array of 32 bytes.

Returns: An object containing the account's information:

  • data: The account's data as a byte array.
  • owner: The account's owner as a byte array (program_id).
  • utxo: The UTXO associated with this account.
  • is_executable: A boolean indicating if the account contains executable code.

Request:

curl -vL POST -H 'Content-Type: application/json' -d '
{
    "jsonrpc":"2.0",
    "id":1,
    "method":"get_account_info",
    "params":[
        [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32]
    ]
}' \
http://localhost:9001/

Response:

{
  "jsonrpc": "2.0",
  "result": {
    "data": [1,2,3,4,...],
    "owner": [80,82,242,228,43,246,248,133,88,238,139,124,88,96,107,32,71,40,52,251,90,42,66,176,66,32,147,203,137,211,253,40],
    "utxo": "txid:vout",
    "is_executable": false
  },
  "id": "1"
}