1 unstable release
new 0.4.1 | May 9, 2025 |
---|
#48 in Magic Beans
460KB
10K
SLoC
BRC2.0 - Programmable Module
Smart contract execution engine compatible with BRC20 standard.
BRC2.0 programmable module provides smart contract execution capabilities for BRC20 indexers.
This module allows users to inscribe smart contracts and function calls on Bitcoin blockchain to implement decentralised applications.
BRC2.0 runs on a custom EVM
execution engine using revm
. Our main reasons for choosing EVM
are listed below:
- Rich open-source ecosystem for tooling, including several different execution engines
- Heavily tested open-source smart contract libraries that are readily available for various financial applications
- Large and active developer community - many smart contract developers are already familiar with
EVM
andSolidity
EVM
is deterministic and Turing complete.
See our proposal at bestinslot-xyz/brc20-prog-module-proposal for detailed information about how the BRC2.0 programmable module works.
See Indexer Integration Guide on how to integrate the programmable module your BRC20 indexer.
For questions, comments and requests, use the issues section or Best in Slot discord server.
[!WARNING] This module is not currently enabled on Bitcoin mainnet.
Usage
BRC2.0 Programmable Module is written in Rust, so you need Cargo installed in order to build and run the server.
Precompiled contracts require environment variables to work properly, see the Precompiles section and Indexer Integration Guide to learn how to set them up, otherwise precompiled contracts will fail.
Build and run brc20_prog:
cargo run --release
[!NOTE] You must use clang as CC. Try installing clang
sudo apt install clang
before runningbrc20_prog
.Eg. CC=/usr/bin/clang CXX=/usr/bin/clang++. Clang llvm version must be the same as the one used by rust compiler. On the rust side you should use RUSTFLAGS="-Clinker-plugin-lto -Clinker=clang -Clink-arg=-fuse-ld=lld".
Supported JSON-RPC methods
BRC2.0 provides a JSON-RPC 2.0 server to interact with the indexers, and chain explorers at localhost:18545
. eth_*
methods are supported to provide information on blocks and transactions, while brc20_*
methods are used for adding new transactions and blocks to run in the execution engine.
eth_* methods
BRC2.0 implements the Ethereum JSON-RPC API.
JSON-RPC methods work the same way as the official implementation, e.g. eth_blockNumber
will return the latest indexed block height, eth_getBlockByNumber
or eth_getBlockByHash
will return an indexed block and all the indexed transactions, and eth_getTransactionReceipt
will return the transaction receipt for given transaction, including logs and status.
eth_call
can be used to interact with the contracts.
[!WARNING] Filter methods such as
eth_newFilter
,eth_getFilterChanges
are not supported yet, but they are planned for after release.
debug_* methods
BRC2.0 can record traces of transactions and serve a callTracer result via debug_traceTransaction
method similar to Geth.
This needs to be enabled by setting EVM_RECORD_TRACES
environment variable to true
.
[!NOTE] Currently, only
debug_traceTransaction
method with acallTracer
is supported.
brc20_* methods (for indexers)
BRC2.0 implements following brc20_*
JSON-RPC methods intended for indexer usage
Mine empty blocks
Method: brc20_mine
Description: Inserts empty blocks with unknown/unimportant hashes, this method can be used to speed up the initialisation process by skipping unnecessary blocks and moving the block height to given point for indexing purposes.
Parameters:
- block_count (
int
): Number of empty blocks to insert - timestamp (
int
): Timestamp for the empty blocks
Initialise and deploy BRC20_Controller contract
Method: brc20_initialise
Description: Initialises the execution engine with a known block height and hash, deploys the BRC20_Controller
contract at address 0xc54dd4581af2dbf18e4d90840226756e9d2b3cdb
. This method can be called before or after brc20_mine
, but subsequent calls to it must have the same genesis parameters, otherwise it will fail.
Parameters:
- genesis_hash (
string
): Block hash - genesis_timestamp (
int
): Timestamp - genesis_height (
int
): Block height
Returns:
- Error if block info doesn't match a previous
brc20_initialise
call.
Deploy contract
Method: brc20_deploy
Description: Used to deploy a contract, this adds a transaction to current block.
Parameters:
- from_pkscript (
string
): Bitcoin pkscript that created the deploy/call inscription - data (
string
): Call or deploy data for EVM - timestamp (
int
): Current block timestamp - hash (
string
): Current block hash - tx_idx (
int
): Transaction index, starts from 0 every block, and needs to be incremented for every transaction - inscription_id (Optional
string
): Source inscription ID that triggered this transaction, will be recorded for easier contract address retrieval - inscription_byte_len (Optional
number
): Length of the insription content, used to determine the gas limit for this transaction
Returns:
- Receipt for the executed transaction, see eth_getTransactionReceipt for details.
Call contract
Method: brc20_call
Description: Used to call a contract, this adds a transaction to current block.
Parameters:
- from_pkscript (
string
): Bitcoin pkscript that created the deploy/call inscription - contract_address (
string
): Address of the contract to call, corresponds to the "c" (Contract Address) field of a call inscription - contract_inscription_id (
string
): Contract deployed by the inscription ID to call, corresponds to the "i" (Inscription ID) field of a call inscription - data (
string
): Call or deploy data for EVM, corresponds to the "d" (Data) field of a call inscription - timestamp (
int
): Current block timestamp - hash (
string
): Current block hash - tx_idx (
int
): Transaction index, starts from 0 every block, and needs to be incremented for every transaction - inscription_id (Optional
string
): Inscription ID that triggered this transaction, will be recorded for easier transaction receipt retrieval - inscription_byte_len (Optional
number
): Length of the insription content, used to determine the gas limit for this transaction
Returns:
- Receipt for the executed transaction, see eth_getTransactionReceipt for details.
[!NOTE]
inscription_byte_len
parameter is used to determine the gas limit forbrc20_deploy
andbrc20_call
transactions, currently BRC2.0 sets an allowance of 12000 gas per byte (object to change, but generously set). In case of calling expensive methods and contracts, inscriptions should be padded to increase the gas allowance. Minimum gas limit is set to 32 bytes per transaction.eth_estimateGas
JSON-RPC method can be used to estimate how much gas this transaction might consume.
Get Transaction Receipt by Inscription ID
Method: brc20_getTxReceiptByInscriptionId
Description: Returns the transaction receipt for given inscription ID, previously sent via brc20_deploy
or brc20_call
. This makes it easier to work with inscriptions rather than transactions in BRC2.0 applications.
Parameters:
- inscription_id (
string
): Inscription ID previously added viabrc20_deploy
,brc20_call
,brc20_deposit
, orbrc20_withdraw
.
Returns:
- Transaction receipt, following
eth_getTransactionReceipt
structure. - None if the inscription isn't added yet, i.e. it doesn't match previous calls.
Get Inscription ID by Transaction Hash
Method: brc20_getInscriptionIdByTxHash
Description: Returns the inscription ID for given transaction, previously sent via brc20_deploy
or brc20_call
. This makes it easier to work with inscriptions rather than transactions in BRC2.0 applications.
Parameters:
- tx_hash (
string
): Transaction hash previously added viabrc20_deploy
,brc20_call
,brc20_deposit
, orbrc20_withdraw
.
Returns:
- Inscription ID, as string
- None if the transaction doesn't have an inscription
Finalise Block
Method: brc20_finaliseBlock
Description: Finalises a block, this should be called after all the transactions in the block are added via brc20_deploy
, brc20_call
, brc20_deposit
, or brc20_withdraw
.
Parameters:
- timestamp (
int
): Current block timestamp - hash (
string
): Current block hash - block_tx_count (
int
): Number of transactions added to this block
Returns:
- Error if any of the
timestamp
orhash
parameters don't match previous calls. - Error if
block_tx_count
doesn't match transaction count for this block.
Commit to Database
Method: brc20_commitToDatabase
Description: Writes pending changes to disk.
Parameters:
- None
Clear Caches
Method: brc20_clearCaches
Description: Removes pending changes. Can be used to clear recently added transactions and revert to last saved state.
Parameters:
- None
Reorg
Method: brc20_reorg
Description: Reverts to a previous state at the given block. Should be used when a reorg is detected.
Parameters:
- latest_valid_block_number (
int
): Block height to revert the state to
[!NOTE] Not all of the history is stored, and reorg is only supported up to 10 blocks earlier (this can be modified in code if needed, but will result in increased storage), otherwise this method will fail and return an error.
BRC20 Deposit
Method: brc20_deposit
Description: Deposits (mints) BRC20 tokens to given bitcoin pkscript. This is a convenience method to replace brc20_call
calls for BRC20 transactions, and used to transfer BRC20 tokens into BRC2.0 module.
Parameters:
- to_pkscript (
string
): Bitcoin pkscript to receive BRC20 tokens - ticker (
string
): Ticker for the BRC20 token - amount (
string
): Amount of BRC20 tokens - timestamp (
int
): Current block timestamp - hash (
string
): Current block hash (starting with 0x) - tx_idx (
int
): Transaction index - inscription_id (
string
): Inscription ID that triggered this transaction
Returns:
- Receipt for the executed transaction, see eth_getTransactionReceipt for details.
BRC20 Withdraw
Method: brc20_withdraw
Description: Withdraws (burns) BRC20 tokens from given bitcoin pkscript. Method returns an error if the given pkscript doesn't have enough tokens. This is a convenience method to replace brc20_call
calls for BRC20 transactions, and used to transfer BRC20 tokens out of BRC2.0 module.
Parameters:
- from_pkscript (
string
): Bitcoin pkscript to burn BRC20 tokens - ticker (
string
): Ticker for the BRC20 token - amount (
string
): Amount of BRC20 tokens - timestamp (
int
): Current block timestamp - hash (
string
): Current block hash (starting with 0x) - tx_idx (
int
): Transaction index - inscription_id (
string
): Inscription ID that triggered this transaction
Returns:
- Receipt for the executed transaction, see eth_getTransactionReceipt for details.
BRC20 Balance
Method: brc20_balance
Description: Returns a transaction receipt for retrieving current BRC20 balance (in-module) for the given pkscript and ticker.
Parameters:
- pkscript (
string
): Bitcoin pkscript - ticker (
string
): BRC20 ticker
Returns:
- (string) BRC20 balance of the bitcoin pkscript for the given ticker
Precompiles
Execution engine has precompiled contracts deployed at given addresses to make it easier to work with bitcoin transactions.
Precompile | Address |
---|---|
BRC20_Balance | 0x00000000000000000000000000000000000000ff |
BIP322_Verifier | 0x00000000000000000000000000000000000000fe |
BTC_Transaction | 0x00000000000000000000000000000000000000fd |
BTC_LastSatLoc | 0x00000000000000000000000000000000000000fc |
BTC_LockedPkScript | 0x00000000000000000000000000000000000000fb |
BRC20 Balance Contract
BRC20_Balance
contract can be used to retrieve non-module BRC20 balance for a given pkscript. BRC2.0 makes an HTTP call to the server at BRC20_PROG_BALANCE_SERVER_URL
environment variable.
> curl "http://localhost:18546/?pkscript=1234567890ABCDEF&ticker=0x12345678"
86
[!NOTE]
ticker
parameter is hex encoded to avoid passing invalid URL strings.
BRC20 indexers should expose this HTTP server and set the environment variable accordingly.
[!WARNING] BRC20 Balance Server exposed by the indexer should return BRC20 balance at the time of current transaction after processing all the BRC20 events up until this point, and NOT the BRC20 balance at the start of the block.
Contract interface:
/**
* @dev Get non-module BRC-20 balance of a given Bitcoin wallet script and BRC-20 ticker.
*/
interface IBRC20_Balance {
function balanceOf(
bytes calldata ticker,
bytes calldata pkscript
) external view returns (uint256);
}
[!WARNING]
BRC20_PROG_BALANCE_SERVER_URL
must be set for this precompile to work.
BIP322 Verifier Contract
BIP322_Verifier
contract can be used to verify a BIP322 signature. This precompile uses the rust-bitcoin/bip322 library.
Contract interface:
/**
* @dev BIP322 verification method
*/
interface IBIP322_Verifier {
function verify(
bytes calldata pkscript,
bytes calldata message,
bytes calldata signature
) external returns (bool success);
}
[!WARNING] Currently rust-bitcoin/bip322 and this precompile only supports
P2TR
,P2WPKH
andP2SH-P2WPKH
single-sig addresses.
Bitcoin Contracts
BRC2.0 has a set of precompiles that make it easier to work with bitcoin transactions within a smart contract. These can be used to retrieve transaction details, track satoshis across transactions and calculate locked pkscripts. These allow BRC2.0 smart contracts to be aware of the transactions, ordinals and ordinal lockers that happen outside the execution engine.
[!WARNING]
BTC_Transaction
andBTC_LastSatLoc
precompiles use Bitcoin JSON-RPC calls to calculate results, so an RPC server needs to be specified in the environment variables.Associated environment variables are
BITCOIN_RPC_URL
,BITCOIN_RPC_USER
,BITCOIN_RPC_PASSWORD
andBITCOIN_RPC_NETWORK
. See env.sample for a sample environment.
Transaction details
BTC_Transaction
contract can be used to retrieve details for a bitcoin transaction. Returns block height, and vin
, vout
txids, scriptPubKeys and values as arrays.
Contract interface:
/**
* Get Bitcoin transaction details using tx ids.
*/
interface IBTC_Transaction {
function getTxDetails(
bytes32 txid
)
external
view
returns (
uint256 block_height,
bytes32[] memory vin_txids,
uint256[] memory vin_vouts,
bytes[] memory vin_scriptPubKeys,
uint256[] memory vin_values,
bytes[] memory vout_scriptPubKeys,
uint256[] memory vout_values
);
}
Last sat location
BTC_LastSatLoc
contract can be used to retrieve previous location of a satoshi at given txid
, vout
and sat
number using the rules detailed at ordinals/ord/blob/master/bip.mediawiki.
Contract interface:
/**
* @dev Get last satoshi location of a given sat location in a transaction.
*/
interface IBTC_LastSatLoc {
function getLastSatLocation(
bytes32 txid,
uint256 vout,
uint256 sat
) external view returns (
bytes32 last_txid,
uint256 last_vout,
uint256 last_sat,
bytes memory old_pkscript,
bytes memory new_pkscript
);
}
Get locked pkscript
BTC_LockedPkScript
contract can be used to calculate lock pkscripts for given pkscript and block count.
Contract interface:
/**
* @dev Get locked pkscript of a given Bitcoin wallet script.
*/
interface IBTC_LockedPkscript {
function getLockedPkscript(
bytes calldata pkscript,
uint256 lock_block_count
) external view returns (bytes memory locked_pkscript);
}
Indexer Integration Guide
BRC2.0 execution engine is designed to work together with a BRC20 indexer, and the indexer should recognise inscriptions that are intended for BRC2.0 and execute transactions, deposit and withdraw BRC20 tokens.
Deploy/Call inscriptions
Defined in the proposal, deploy inscriptions have the following structure:
{
"p": "brc20-prog",
"op": "deploy",
"d": "<bytecode + constructor_args in hex>"
}
Whenever an indexer encounters a deploy inscription, it should inform the programmable module via the brc20_deploy
JSON-RPC method, this will allow the EVM to deploy a new smart contract.
Once an inscription is deployed as a smart contract, then methods can be called via call inscriptions with the following structure:
{
"p": "brc20-prog",
"op": "call",
"c": "<contract_addr>",
"i": "<inscription_id>",
"d": "<call data>"
}
Call inscriptions should be added as transactions to the EVM using brc20_call
JSON-RPC method. BRC2.0 maintains a map of contract addresses and deploy inscriptions, so at least one of the "c"
or "i"
fields should be set to call the contract "c"
, or a contract deployed by the inscription "i"
.
Deposit/Withdrawal inscriptions
Deposit inscriptions are standard BRC20 transfer inscriptions that are sent to OP_RETURN "BRC20PROG"
:
{
"p": "brc-20",
"op": "transfer",
"tick": "ordi",
"amt": "10"
}
When an indexer encounters this, it should call brc20_deposit
JSON-RPC method to create the same amount of BRC20 tokens in the execution engine. These BRC20 tokens then can be transferred and manipulated using BRC2.0 call inscriptions.
Withdraw inscriptions have the following structure:
{
"p": "brc20-module",
"op": "withdraw",
"tick": "ordi",
"amt": "10",
"module": "BRC20PROG"
}
When encountered, an indexer can call brc20_withdraw
JSON-RPC method, and verify the result, as this can fail in case there isn't enough funds to withdraw, and increase BRC20 balance for the pkscript this inscription was sent to.
[!WARNING] Tokens should be withdrawn from the sender's pkscript, but deposited to the receiver's pkscript for a withdraw inscription. A withdraw inscription can be sent to the same pkscript, or a different pkscript.
Initialisation and empty blocks
Execution engine deploys a BRC20_Controller
contract for BRC20 deposits, transfers and withdrawals. This deployment should be triggered by an indexer via brc20_initialise
method at any point, before any of the inscriptions take place. This will add a block with a single transaction that is the BRC20_Controller
deployment transaction.
In order to skip initial blocks, i.e. empty blocks, miners can call brc20_mine
to add empty blocks to the system. If the first inscription is at block height 100, then initialisation might look like:
brc20_mine {
block_count: 100,
timestamp: 0
}
brc20_initialise {
genesis_hash: "100TH_BLOCK_HASH",
genesis_timestamp: "100TH_BLOCK_TIMESTAMP",
genesis_height: 100
}
If an indexer wants earlier block hashes and timestamps to be correct, they can also initialise empty blocks using brc20_finaliseBlock
, and pass the correct hashes and timestamps.
brc20_initialise {
genesis_hash: "GENESIS_HASH",
genesis_timestamp: "GENESIS_TIMESTAMP",
genesis_height: 0
}
for all initial blocks:
brc20_finaliseBlock {
hash: "KNOWN_HASH",
timestamp: "KNOWN_TIMESTAMP", block_tx_count: 0
}
Loop for adding transactions and finalising blocks
When a new block arrives, all its deploy/call/deposit/withdraw transactions should be sent to the execution engine in order, with the correct transaction index using the relevant methods such as brc20_deploy
, brc20_call
, brc20_deposit
, and brc20_withdraw
. Once all inscriptions in the block are processed, block should be finalised using the brc20_finaliseBlock
JSON-RPC method.
Indexing for a single block in pseudo code would look like the following (field validation is omitted for simplicity):
block = await_new_block()
current_tx_idx = 0
for (inscription, transfer) in block:
current_inscription_id = transfer.inscription_id
sender = transfer.sender
receiver = transfer.receiver
if inscription.op is 'deploy' and
receiver.pkscript is OP_RETURN "BRC20PROG":
brc20_deploy(
from_pkscript: sender.pkscript,
data: inscription.d,
hash: block.hash,
timestamp: block.timestamp,
tx_idx: current_tx_idx++,
inscription_id: current_inscription_id,
inscription_byte_len: inscription.content.length)
if inscription.op is 'call' and
receiver.pkscript is OP_RETURN "BRC20PROG":
brc20_call(
from_pkscript: sender.pkscript,
contract_address: inscription.c
contract_inscription_id: inscription.i,
data: inscription.d,
hash: block.hash,
timestamp: block.timestamp,
tx_idx: current_tx_idx++,
inscription_id: current_inscription_id,
inscription_byte_len: inscription.content.length)
if inscription.op is 'transfer' and
receiver.pkscript is OP_RETURN "BRC20PROG":
if sender.balance[inscription.tick] > inscription.amt:
sender.balance[inscription.tick] -= inscription.amt;
brc20_deposit(
to_pkscript: sender.pkscript,
ticker: inscription.tick,
amount: inscription.amt (padded to 18 decimals),
hash: block.hash,
timestamp: block.timestamp,
tx_idx: current_tx_idx++,
inscription_id: current_inscription_id)
if inscription.op is 'withdraw' and
inscription.p is 'brc20-module' and
inscription.module is 'BRC20PROG':
# Withdrawals are done from sender's pkscript
result = brc20_withdraw(
from_pkscript: sender.pkscript,
ticker: inscription.tick,
amount: inscription.amt (padded to 18 decimals),
hash: block.hash,
timestamp: block.timestamp,
tx_idx: current_tx_idx++,
inscription_id: current_inscription_id)
# Withdrawals fail if there is not enough funds
if result.status = '0x1':
# Note that withdrawals are sent to receiver's wallet
receiver.balance[inscription.tick] += inscription.amt
# Finalise block at the end
brc20_finaliseBlock(
hash: block.hash,
timestamp: block.timestamp,
block_tx_count: current_tx_idx)
# Committing to database, can be done at any point to write changes to disk
brc20_commitToDatabase()
When a reorg is detected, brc20_reorg
should be called to revert the EVM to a previous state.
BRC20 Balance Server
Indexers should expose a balance server that returns current overall balance for a pkscript and a ticker, and set the BRC20_BALANCE_SERVER_URL
environment variable to make sure the BRC20_Balance
precompiled contract knows where to send these requests to.
> curl "http://localhost:18546/?pkscript=1234567890ABCDEF&ticker=0x123456789"
86
[!WARNING] BRC20 Balance Server exposed by the indexer should return BRC20 balance at the time of current transaction after processing all the BRC20 events up until this point, and NOT the BRC20 balance at the start of the block.
Authorization
brc20_prog
module supports basic username/password HTTP auth. It's turned off by default, but can be enabled using the following environment variables:
BRC20_PROG_RPC_SERVER_ENABLE_AUTH=true
BRC20_PROG_RPC_SERVER_USER="<USER>"
BRC20_PROG_RPC_SERVER_PASSWORD="<PASSWORD>"
Indexer Checklist
- Set environment variables, check env.sample for a list
- Start a BRC20 balance server for BRC20_Balance Contract
- Mine
brc20_mine
or finalise empty blocksbrc20_finaliseBlock
to fill the database before the first inscription height - Deploy the
BRC20_Controller
contract by callingbrc20_initialise
- Index every block for BRC2.0 transactions
- Add deploy/call inscriptions via
brc20_deploy
orbrc20_call
- Deposit/Withdraw BRC20 tokens via
brc20_deposit
andbrc20_withdraw
- Finalise every block via
brc20_finaliseBlock
- Commit changes to database via
brc20_commitToDatabase
- Add deploy/call inscriptions via
- Call
brc20_reorg
when a reorg is detected
Dependencies
~104MB
~2M SLoC