34 releases (11 breaking)
| new 0.15.11 | Jan 12, 2026 |
|---|---|
| 0.15.10 | Dec 17, 2025 |
| 0.15.3 | Nov 5, 2025 |
| 0.10.2 | Jul 31, 2025 |
#2 in #bitcoin-transaction
510KB
11K
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
EVMandSolidity EVMis 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 clangbefore 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 useRUSTFLAGS="-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_getFilterChangesare 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_traceTransactionmethod with acallTraceris supported.
txpool_content method
BRC2.0 maintains a pending transaction pool for transactions that are sent out of order, and these can be retrieved using txpool_content method similar to Geth.
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_initialisecall.
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 inscriptiondata(Optionalstring): Call or deploy data for EVM, corresponds to the "d" (Data) field of a deploy inscriptionbase64_data(Optionalstring): Call or deploy data for EVM, encoded in base64 with the compression prefix, corresponds to the "b" (Base64 Data) field of a deploy inscriptiontimestamp(int): Current block timestamphash(string): Current block hashtx_idx(int): Transaction index, starts from 0 every block, and needs to be incremented for every transactioninscription_id(Optionalstring): Source inscription ID that triggered this transaction, will be recorded for easier contract address retrievalinscription_byte_len(Optionalnumber): Length of the insription content, used to determine the gas limit for this transactionop_return_tx_id(Optionalstring): Bitcoin transaction ID that sends the inscription to OP_RETURN, used for the precompile
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 inscriptioncontract_address(Optionalstring): Address of the contract to call, corresponds to the "c" (Contract Address) field of a call inscriptioncontract_inscription_id(Optionalstring): Contract deployed by the inscription ID to call, corresponds to the "i" (Inscription ID) field of a call inscriptiondata(Optionalstring): Call or deploy data for EVM, corresponds to the "d" (Data) field of a call inscriptionbase64_data(Optionalstring): Call or deploy data for EVM, encoded in base64 with the compression prefix, corresponds to the "b" (Base64 Data) field of a call inscriptiontimestamp(int): Current block timestamphash(string): Current block hashtx_idx(int): Transaction index, starts from 0 every block, and needs to be incremented for every transactioninscription_id(Optionalstring): Inscription ID that triggered this transaction, will be recorded for easier transaction receipt retrievalinscription_byte_len(Optionalnumber): Length of the insription content, used to determine the gas limit for this transactionop_return_tx_id(Optionalstring): Bitcoin transaction ID that sends the inscription to OP_RETURN, used for the precompile
Returns:
- Receipt for the executed transaction, see eth_getTransactionReceipt for details.
[!NOTE]
inscription_byte_lenparameter is used to determine the gas limit forbrc20_deployandbrc20_calltransactions, 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_estimateGasJSON-RPC method can be used to estimate how much gas this transaction might consume.
Send raw signed transaction
Method: brc20_transact
Description: Used to send a raw signed transaction, this adds a transaction to current block. This is useful for sending transactions that are pre-signed using ethereum wallets.
Parameters:
raw_tx_data(Optionalstring): Raw signed transaction data, encoded in hex format.base64_raw_tx_data(Optionalstring): Raw signed transaction data, encoded in base64 with the compression prefix.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(Optionalstring): Inscription ID that triggered this transaction, will be recorded for easier transaction receipt retrieval.inscription_byte_len(Optionalnumber): Length of the inscription content, used to determine the gas limit for this transaction.op_return_tx_id(Optionalstring): Bitcoin transaction ID that sends the inscription to OP_RETURN, used for the precompile
Returns:
-
List of receipts for the executed transactions, see eth_getTransactionReceipt for details.
-
If the transaction nonce is not in order, zero receipts will be returned, as transaction will be stored as a pending transaction. In that case,
tx_idxfor the next call shouldn't be incremented in that case. -
Multiple receipts can be returned if the pending transaction pool contains multiple transactions with nonces following the current transaction, as they will be executed together. In that case,
tx_idxfor the next call should be incremented by the number of transactions executed.
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_getTransactionReceiptstructure. - 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
timestamporhashparameters don't match previous calls. - Error if
block_tx_countdoesn'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 |
|---|---|
| Reserved. | 0x00000000000000000000000000000000000000ff |
| BIP322_Verifier | 0x00000000000000000000000000000000000000fe |
| BTC_Transaction | 0x00000000000000000000000000000000000000fd |
| BTC_LastSatLoc | 0x00000000000000000000000000000000000000fc |
| BTC_LockedPkScript | 0x00000000000000000000000000000000000000fb |
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,P2WPKHandP2SH-P2WPKHsingle-sig addresses.
[!WARNING] This precompile supports up to 32 KB input size for combined
pkscript,messageandsignatureparameters. This is to avoid excessive resource usage and potential denial of service attacks.
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_TransactionandBTC_LastSatLocprecompiles 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_PASSWORDandBITCOIN_RPC_NETWORK. See env.sample for a sample environment for Signet.
Get Current Txid
Current_Txid contract can be used to retrieve the current Bitcoin transaction id being executed. This can be useful to enable contracts to be aware of the transaction they are being executed in.
Contract interface:
/**
* @dev Get current Bitcoin transaction id being executed.
*/
interface IBTC_CurrentTxid {
function getTxId() external view returns (bytes32 txid);
}
[!WARNING] This precompile is only usable after block 275_000 on Signet, and 923_369 on Mainnet. This coincides with the Prague upgrade activation on Signet and Mainnet. Proposal: https://github.com/bestinslot-xyz/brc20-proposals/blob/main/004-prog-tx-id-precompile/index.md
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.
[!NOTE] BRC20 indexer in OPI/experimental-signet-brc20-prog branch already has the brc20-prog integration in place.
Deploy/Call inscriptions
Defined in the proposal, deploy inscriptions have the following structure:
{
"p": "brc20-prog",
"op": "deploy (or d)",
"d": "<bytecode + constructor_args in hex>",
"b": "<base64 encoded bytecode + constructor_args with the compression prefix>"
}
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 (or c)",
"c": "<contract_addr>",
"i": "<inscription_id>",
"d": "<call data>",
"b": "<base64 encoded call data with the compression prefix>"
}
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".
Raw Signed Transaction Inscriptions
Raw signed transaction inscriptions are used to send pre-signed transactions to the execution engine. These inscriptions have the following structure:
{
"p": "brc20-prog",
"op": "transact (or t)",
"d": "<raw signed transaction data in hex>",
"b": "<base64 encoded raw signed transaction data with the compression prefix>"
}
When an indexer encounters this inscription, it should call brc20_transact JSON-RPC method to send the raw signed transaction to the execution engine. This allows users to send pre-signed transactions using their EVM compatible wallets, and have them executed in the BRC2.0 module.
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 first, before any blocks are added. This will add a block with a single transaction that is the BRC20_Controller deployment transaction.
In order to skip initial empty blocks, indexers need to 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_initialise {
genesis_hash: "0x0000...0000",
genesis_timestamp: "0",
genesis_height: 0
}
brc20_mine {
block_count: 99,
timestamp: 0
}
This will create a block with the BRC20_Controller contract deployed at address 0xc54dd4581af2dbf18e4d90840226756e9d2b3cdb, and 99 more empty blocks, so the indexer can start processing inscriptions from block height 100.
This also makes sure every block hash before the first inscription is 0, and contracts can only access block hashes after the first BRC2.0 inscription.
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_transact, 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 in ['deploy', 'd'] and
receiver.pkscript is OP_RETURN "BRC20PROG":
brc20_deploy(
from_pkscript: sender.pkscript,
data: inscription.d,
base64_data: inscription.b,
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 in ['call', 'c'] 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,
base64_data: inscription.b,
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 in ['transact', 't'] and
receiver.pkscript is OP_RETURN "BRC20PROG":
receipts = brc20_transact(
raw_tx_data: inscription.d,
base64_raw_tx_data: inscription.b,
hash: block.hash,
timestamp: block.timestamp,
tx_idx: current_tx_idx,
inscription_id: current_inscription_id,
inscription_byte_len: inscription.content.length)
current_tx_idx += receipts.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.
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>"
[!CAUTION] This uses basic auth, so make sure authenticated calls are made through HTTPS or a secure tunnel if the server is exposed to the internet, otherwise credentials can be intercepted.
Indexer Checklist
- Set environment variables, check env.sample for a list
- Mine
brc20_mineor finalise empty blocksbrc20_finaliseBlockto fill the database before the first inscription height - Deploy the
BRC20_Controllercontract by callingbrc20_initialise - Index every block for BRC2.0 transactions
- Add deploy/call inscriptions via
brc20_deployorbrc20_call - Deposit/Withdraw BRC20 tokens via
brc20_depositandbrc20_withdraw - Finalise every block via
brc20_finaliseBlock - Commit changes to database via
brc20_commitToDatabase
- Add deploy/call inscriptions via
- Call
brc20_reorgwhen a reorg is detected
Dependencies
~48–71MB
~1M SLoC