Introduction

Welcome to the Arch Network developer documentation.

This book will guide you through installation of Arch's developer stack as well as provide you with the essential knowledge to create, deploy and interact with Arch programs.

Arch Network leverages the security and ubiquity of Bitcoin to enhance decentralized applications by providing a computation environment. Arch facilitates complex operations on Bitcoin UTXOs through programs executed within the Arch VM environment.

Prerequirements

Rust

This book explores Arch Network's programs. It is not a Rust tutorial, and it assumes basic Rust knowledge. It is strongly recommended to understand the language first before proceeding.

Bitcoin

Additionally, it is helpful to understand how Bitcoin works, especially the UTXO model. A good resource is Saylor Academy's Bitcoin for Developers course as well as Mastering Bitcoin by Andreas Antonopoulos.

Contributing

Please create an issue or pull request if you find any mistakes, bugs, or ambiguities. See CONTRIBUTING.md for additional details.

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.

Requirements

The following dependencies are needed to proceed. Install these before moving to the next step.

Install Rust

First, to work with Arch programs you will need Rust installed on your machine. If you don't have it, you can find installation instructions on the Rust website.

It is assumed that you are working with a stable Rust channel throughout this book.

Install Docker

Next, Docker is required to run Arch's containerized node infrastructure locally. The desktop client can be installed from the Docker website.

Install C++ Compiler

For MacOS users, this should already be installed alongside [gcc] so you can skip this section.

For Linux (Debian/Ubuntu) users, this must be installed if it isn't already. We will manually install the gcc-multilib.

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

Install Solana CLI

To compile the examples, the Solana CLI toolchain must be installed. Execute the following commands to install the toolchain to your local system.

MacOS & Linux

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

You can replace v1.18.18 with the release tag matching the software version of your desired release, or use one of the three symbolic channel names: stable, beta, or edge.

Ref: Solana Docs.

⚠️ NOTE: Installing rust through Homebrew likely leads to issues working with cargo-build-sbf. Below are some steps to get around this.

Steps:

  1. Uninstall rust
rustup uninstall self
  1. Ensure rust is completely removed
rustup --version

# should result:
zsh: command not found: rustup
  1. Reinstall rust
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)"

If you are still experiencing errors, join our Discord dev-chat channel for more support.

Clone the arch-local repository

Finally, we'll be using a repository specifically made to demonstrate Arch's capabilities and get started quickly. This repo contains a local Arch Network development environment, as well as some example programs that we'll touch on later in this book.

git clone https://github.com/arch-Network/arch-local && \
cd arch-local

Starting the stack

Within arch-local there is a compose.yaml file. This is a descriptor for the pre-configured multi-container definition of the components required for standing up a local development environment.

Configure

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

NOTE: Additionally, if you have an Intel chip (ie, x86_64), you may encounter the following error when executing docker compose up; we recommend removing the --platform=linux/arm64 flag within [Line 1: Dockerfile]:

=> ERROR [init-bootnode 2/3] RUN set -ex && apt-get update && apt-get install -qq --no-install-recommends curl jq               0.6s
------
 > [init-bootnode 2/3] RUN set -ex && apt-get update && apt-get install -qq --no-install-recommends curl jq:
0.314 exec /bin/sh: exec format error
------
failed to solve: process "/bin/sh -c set -ex \t&& apt-get update \t&& apt-get install -qq --no-install-recommends curl jq" did not complete successfully: exit code: 1

Start

Once Docker is up and running, start the stack by issuing the following command:

docker compose up

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

leader-1       | 2024-08-22T00:22:08.858084Z  INFO validator::roast::roast_leader: validator/src/roast/roast_leader.rs:54: Starting a new session with id 2
leader-1       | 2024-08-22T00:22:08.861441Z  INFO validator::roast::roast_entry_generation: validator/src/roast/roast_entry_generation.rs:65: Generated 1 block commitments for block id #3c2360fc4938d5f08a2ab8b0bc15f5ee54b42dc1cd61a8b906952e068f2a92d9 session #2
validator-2-1  | 2024-08-22T00:22:08.863250Z  INFO validator::roast::roast_entry_generation: validator/src/roast/roast_entry_generation.rs:65: Generated 1 block commitments for block id #3c2360fc4938d5f08a2ab8b0bc15f5ee54b42dc1cd61a8b906952e068f2a92d9 session #1
leader-1       | 2024-08-22T00:22:08.870662Z  INFO validator::roast::roast_leader: validator/src/roast/roast_leader.rs:152: Session 1 is ready for aggregation
validator-1-1  | 2024-08-22T00:22:08.870958Z  INFO validator::roast::roast_entry_generation: validator/src/roast/roast_entry_generation.rs:65: Generated 1 block commitments for block id #3c2360fc4938d5f08a2ab8b0bc15f5ee54b42dc1cd61a8b906952e068f2a92d9 session #2
leader-1       | 2024-08-22T00:22:08.874029Z  INFO validator::roast::roast_leader: validator/src/roast/roast_leader.rs:233: Successfully finished signatures in session 1
leader-1       | 2024-08-22T00:22:08.874064Z  INFO validator::roast::roast_verification: validator/src/roast/roast_verification.rs:199: Execution time for verify_and_prepare_block: 1.4333e-5 seconds
leader-1       | 2024-08-22T00:22:08.874071Z  INFO validator::utils: validator/src/utils.rs:320: Execution time for submit_block_to_btc: 2.1834e-5 seconds
leader-1       | 2024-08-22T00:22:08.875352Z  INFO validator::roast::roast_verification: validator/src/roast/roast_verification.rs:308: Successfully verified the block #3c2360fc4938d5f08a2ab8b0bc15f5ee54b42dc1cd61a8b906952e068f2a92d9 signature !
leader-1       | 2024-08-22T00:22:08.875367Z  INFO validator::roast::roast_block_result: validator/src/roast/roast_block_result.rs:69: 0 Transactions were submitted to btc network : []
leader-1       | 2024-08-22T00:22:08.876336Z  INFO validator::roast::roast_block_result: validator/src/roast/roast_block_result.rs:117: Block #3c2360fc4938d5f08a2ab8b0bc15f5ee54b42dc1cd61a8b906952e068f2a92d9 was finalized in session 1, I got 1 signatures and successfully verified the block signature !

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.

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

Compiling and executing

Now that all of the dependencies are installed and we have successfully started our stack of development nodes, we can finally compile our program and interact with it.

Build

Access the examples/helloworld/program folder:

cd examples/helloworld/program

Build the example program

cargo-build-sbf

This step will compile the example helloworld program into a eBPF ELF file (the executable format expected by the Arch VM).

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

⚠️ NOTE: If you are a Linux user and do not already have gcc-multilib installed you will see an error like the below when trying to execute cargo-build-sbf.

cargo:warning=/usr/include/stdint.h:26:10: fatal error: 'bits/libc-header-start.h' file not found
  cargo:warning=   26 | #include <bits/libc-header-start.h>
  cargo:warning=      |          ^~~~~~~~~~~~~~~~~~~~~~~~~~
  cargo:warning=1 error generated.

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.

Test

Now that our program is successfully compiled, we can run the corresponding test which will submit execute the program and submit transactions to the network.

# return to the helloworld dir and run test
cd .. && cargo test -- --nocapture

NOTE: If the test succeeds, you should be presented with the following:

test tests::test_deploy_call ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 77.21s

   Doc-tests helloworld

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

NOTE: If you encounter an error like the following: linking with cc failed, you may need to update your ~/.cargo/config to include the correct architecture of your machine:

[target.x86_64-apple-darwin]
rustflags = [
  "-C", "link-arg=-undefined",
  "-C", "link-arg=dynamic_lookup",
]

[target.aarch64-apple-darwin]
rustflags = [
  "-C", "link-arg=-undefined",
  "-C", "link-arg=dynamic_lookup",
]

Resources

Bitcoin mempool and blockchain explorer

Basics

Now that we've successfully compiled a program and executed it within the context of our Arch VM, let's dig into the details of how an Arch program is written and discuss some best practices.

In this section, we'll guide you through the basics of writing functions and handling input parameters effectively within the example program that we worked with in the previous section: helloworld.

Program interaction

Continuing with our example program: helloworld, we find an implementation example of how to communicate with a program within the test_deploy_call() function.

#[test]
fn test_deploy_call() { 
...
}

lib.rs

This test initializes a new instance of RPC client, constructs, signs and sends 4 transactions, and then polls the network for the processed transaction results.

After that, the Arch Network validator nodes will execute the program logic within the context of the Arch VM, signing-off on the execution then passing the results to the leader who will ultimately submit signed Bitcoin transactions back to the Bitcoin network.

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.

Once again, we'll utilize the same example program that you are already used to from previous sections: helloworld.

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 the helloworld example program located within the arch-local repository.

Program

A smart contract on Arch is known as a program.

use arch_program::{
    account::{AccountInfo},
    entrypoint,
    msg,
    program::{
        next_account_info,
        get_account_script_pubkey,
        get_state_transition_tx,
    },
    transaction_to_sign::TransactionToSign,
    program_error::ProgramError,
    input_to_sign::InputToSign,
    pubkey::Pubkey,
};
use borsh::{BorshSerialize, BorshDeserialize};
use bitcoin::{Transaction};

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

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

    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);

    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());

    let mut tx = get_state_transition_tx(accounts);
    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);

    Ok(())
}

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

Imports

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

use arch_program::{
    account::{AccountInfo},
    entrypoint,
    msg,
    program::{
        next_account_info,
        get_account_script_pubkey,
        get_state_transition_tx,
    },
    transaction_to_sign::TransactionToSign,
    program_error::ProgramError,
    input_to_sign::InputToSign,
    pubkey::Pubkey,
};
use borsh::{BorshSerialize, BorshDeserialize};
use bitcoin::{Transaction};

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 use for logging messages to the console.
  • 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

The handler (process_instruction) 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 then iterate over the accounts passed in to the program and retrieve the first one.

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

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

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 full-signed Bitcoin UTXO but is sent directly to Arch first then the leader submits it 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 caller be prepared to pay this.

Next, we'll access the data field of the account and attempt to borrow it in order to determine the length of the value stored within it.

We then 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.

Read more about .realloc 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());

Here, we construct our state transition transaction inside of a mutable variable called tx. We then copy over the Bitcoin transaction input to our mutatable state transition transaction: tx.

let mut tx = get_state_transition_tx(accounts);
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 key 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 helloworld program. In the next guide, we'll walk you through how to test the logic of your program.

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._

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

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.

Accounts

An account is a unique 256-bit address that can store arbitrary data.

Everything on Arch is an account, and anyone can publicly read from any account; however, only an account's owner can modify data within an account.

If an account is_executable: true, then the account is considered to be a program; conversely, if an account is_executable: false, then it is considered to be a data account, meaning that it only serves to hold and manage state of a program.

Key Concepts

  • Accounts can store up to 10MB of data, which can consist of either executable program code or program state
  • Every account has a UTXO. The UTXO is used to anchor the state change to bitcoin.
  • New Account Creation: Only the System Program can create a new account
  • Space Allocation: Sets the byte capacity for the data field of an account
  • Data Modification: Modifies the data field of an account
#[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
}

#[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

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

An instruction specifies the program_id, which is a unique resource identifier for the program, a collection of accounts needed to execute the instruction, as well as a slice of bytes which, once deserialized, includes the actions for the program to take.

#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub struct Instruction {
    pub program_id: Pubkey,
    pub accounts: Vec<AccountMeta>,
    pub data: Vec<u8>,
}

instruction.rs

Messages

A message structure contains a slice of signing keys as well as a slice of instruction data.

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub struct Message {
    pub signers: Vec<Pubkey>,
    pub instructions: Vec<Instruction>,
}

message.rs

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);

// 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.

SystemInstruction provides a method to create a new account through the System Program (see Implementation).

#[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

Implementation

let (txid, instruction_hash) = sign_and_send_instruction(
    SystemInstruction::new_create_account_instruction(
        hex::decode(txid).unwrap().try_into().unwrap(),
        vout,
        program_pubkey.clone(),
    ),
    vec![program_keypair],
).expect("signing and sending a transaction should not fail");

helloworld/src/lib.rs

UTXO

A UtxoMeta structure contains a 36-byte u8 array representing the UTXO.

#[derive(Clone, Debug, PartialEq, Eq)]
#[repr(C)]
pub struct UtxoMeta([u8; 36]);

utxo.rs

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