bsv-rs
0.5.0, a Rust SDK for BSV: keys and cryptography, Script, transactions,
BEEF and SPV, wallets, authentication and overlay clients. Optional features
support native applications and wasm hosts. Compatibility is tested against
shared vectors; the crate also has explicit local evaluator contracts and
documented differences from other SDKs.
Install
[dependencies]
bsv-rs = { version = "=0.5.0", features = ["transaction", "wallet"] }
Defaults enable primitives and script. Cargo.toml lists module
features, full, native transports and wasm. A Worker should select the
features it needs and enable wasm; native HTTP transports are separate.
Proof and evaluator contracts
A one-level merkle path with one hashed leaf is accepted only at offset 0,
representing a one-transaction block. A nonzero lone-leaf offset is refused
with MissingSibling, both when parsing and computing the root. Obtain a
valid proof; do not rewrite an offset on trust.
tests/merkle_path_lone_leaf.rs pins this rule.
SpendParams and TxSpendParams accept work_limit: Option<u64>.
The default None leaves work accounting off. With a bound, reached opcodes
and bytes fed to hashes are charged before that work. Exhaustion returns
ScriptResource::Work, a LOCAL evaluator decline, not an invalid-script
verdict. It is not a wall-clock or total computation bound.
tests/script_work_budget.rs exercises charging
and the distinction from validity.
The streaming BEEF reader verifies element by element, checks proof roots
through supplied headers and executes unproven inputs. It holds an ancestry
index and needed outputs, so streaming does not mean constant memory.
StreamVerifier::set_work_limit and AsyncStreamVerifier::set_work_limit
apply the local work bound per input. Reapply it after resuming a cursor.
Pausing is between elements or inputs; a work decline cannot resume within a
script. The implementation is src/transaction/beef_stream.rs.
Script defaults follow SDK evaluation mode; callers modelling node validation
select explicit ScriptFlags and interpret resource limits separately from
validity. The flag and resource witnesses live in tests.
The node's mining checks
From 0.5.0 the streaming reader refuses an unproven transaction that
bitcoin-sv refuses when mining it: an unproven coinbase, a null outpoint, a
repeated input, an output value or running total outside the money range, an
input total outside it, a P2SH-shaped output after Genesis, an immature
coinbase spend, and a time-locked transaction that is not final for the next
block. Each byte refusal names its offset and kind. A finality verdict needs a
tip: Headers::mining_tip and the async ChainTracker::mining_tip supply a
ChainTip, and without one the reader declines the transaction as
unverifiable instead of guessing. The new kinds and refusals extend public
exhaustive enums; CHANGELOG.md carries the 0.5.0 migration
notes. The node's rules are read through the sibling specification's pins, so
the node implementation decides what the reader refuses.
Build, gate and records
cargo build --examples --all-features
cargo test --test readme_examples --test merkle_path_lone_leaf --test script_work_budget --all-features
cargo fmt --all -- --check
.github/workflows/ci.yml defines PR/main tests,
formatting, clippy, vectors, docs and benchmark checks. It checks out the
pinned external conformance corpus for the vector job; a local run without
that corpus can skip those tests and is not conformance evidence.
RELEASING.md defines the machine gate and trusted publication
from tags. CHANGELOG.md holds release and API migration notes.
Examples
The blocks below are the files in examples, checked byte for byte
by tests/readme_examples.rs. Building them does not
run their network or signing operations. Network examples require a host's
own configuration and authorization.
Keys, hashes, signatures
use bsv_rs::primitives::{sha256, to_hex, PrivateKey};
fn main() {
let private_key = PrivateKey::random();
let public_key = private_key.public_key();
let address = public_key.to_address();
let wif = private_key.to_wif();
assert_eq!(PrivateKey::from_wif(&wif).unwrap().to_wif(), wif);
let digest = sha256(b"Hello, BSV!");
let signature = private_key.sign(&digest).unwrap();
assert!(public_key.verify(&digest, &signature));
assert_eq!(
private_key.sign(&digest).unwrap().to_der(),
signature.to_der()
);
println!("address {address}");
println!("pubkey {}", to_hex(&public_key.to_compressed()));
println!("signature {}", to_hex(&signature.to_der()));
}
Scripts, and the interpreter
use bsv_rs::script::templates::P2PKH;
use bsv_rs::script::{
LockingScript, ProtocolEra, Script, ScriptFlags, Spend, SpendParams, UnlockingScript,
};
fn main() {
let locking = P2PKH::lock_from_address("1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2").unwrap();
let from_hex = Script::from_hex(&locking.to_hex()).unwrap();
assert_eq!(from_hex.to_asm(), locking.to_asm());
assert!(from_hex.is_p2pkh());
println!("p2pkh {}", locking.to_asm());
let mut spend = Spend::new(SpendParams {
source_txid: [0u8; 32],
source_output_index: 0,
source_satoshis: 1,
locking_script: LockingScript::from_asm("OP_1 OP_EQUAL").unwrap(),
transaction_version: 1,
other_inputs: vec![],
outputs: vec![],
input_index: 0,
unlocking_script: UnlockingScript::from_asm("OP_1").unwrap(),
input_sequence: 0xffff_ffff,
lock_time: 0,
memory_limit: None,
work_limit: None,
});
assert!(spend.validate().unwrap());
let mut refused = Spend::new(SpendParams {
source_txid: [0u8; 32],
source_output_index: 0,
source_satoshis: 1,
locking_script: LockingScript::from_asm("OP_1 OP_EQUAL").unwrap(),
transaction_version: 1,
other_inputs: vec![],
outputs: vec![],
input_index: 0,
unlocking_script: UnlockingScript::from_asm("OP_2").unwrap(),
input_sequence: 0xffff_ffff,
lock_time: 0,
memory_limit: None,
work_limit: None,
});
let err = refused.validate().unwrap_err();
assert!(!err.is_resource_limit());
println!("refused {}", err.message);
let untidy = || {
Spend::new(SpendParams {
source_txid: [0u8; 32],
source_output_index: 0,
source_satoshis: 1,
locking_script: LockingScript::from_asm("OP_1 OP_1").unwrap(),
transaction_version: 1,
other_inputs: vec![],
outputs: vec![],
input_index: 0,
unlocking_script: UnlockingScript::new(),
input_sequence: 0xffff_ffff,
lock_time: 0,
memory_limit: None,
work_limit: None,
})
};
assert!(untidy().validate().is_err());
let mut consensus = untidy();
consensus.set_flags(ScriptFlags::block(ProtocolEra::PostChronicle));
assert!(consensus.validate().unwrap());
let mut relay = untidy();
relay.set_flags(ScriptFlags::standard(ProtocolEra::PostChronicle));
println!("relay {}", relay.validate().unwrap_err().message);
}
A transaction: build, sign, verify
use bsv_rs::primitives::PrivateKey;
use bsv_rs::script::templates::P2PKH;
use bsv_rs::script::{SignOutputs, UnlockingScript};
use bsv_rs::transaction::{
MerklePath, MockChainTracker, Transaction, TransactionInput, TransactionOutput,
};
fn main() {
let key = PrivateKey::random();
let address = key.public_key().to_address();
let mut funding = Transaction::new();
let mut mined = TransactionInput::new("11".repeat(32), 0);
mined.unlocking_script = Some(UnlockingScript::new());
funding.add_input(mined).unwrap();
funding
.add_output(TransactionOutput::new(
10_000,
P2PKH::lock_from_address(&address).unwrap(),
))
.unwrap();
let funding_txid = funding.id();
funding.merkle_path = Some(MerklePath::from_coinbase_txid(&funding_txid, 800_000));
let mut spend = Transaction::new();
let mut input = TransactionInput::with_source_transaction(funding, 0);
input.set_unlocking_script_template(P2PKH::unlock(&key, SignOutputs::All, false));
spend.add_input(input).unwrap();
spend.add_p2pkh_output(&address, Some(9_900)).unwrap();
futures::executor::block_on(spend.sign()).unwrap();
println!("txid {}", spend.id());
println!("size {} bytes", spend.to_binary().len());
let mut tracker = MockChainTracker::new(800_000);
tracker.add_root(800_000, funding_txid);
let ok = futures::executor::block_on(spend.verify(&tracker, None)).unwrap();
assert!(ok);
println!("verified {ok}");
}
BRC-42 keys and the ProtoWallet
use bsv_rs::primitives::PrivateKey;
use bsv_rs::wallet::{
Counterparty, CreateSignatureArgs, KeyDeriver, ProtoWallet, Protocol, SecurityLevel,
VerifySignatureArgs,
};
fn main() {
let alice = KeyDeriver::new(Some(PrivateKey::random()));
let bob = KeyDeriver::new(Some(PrivateKey::random()));
let protocol = Protocol::new(SecurityLevel::App, "payment system");
let key_id = "invoice-12345";
let bob_private = bob
.derive_private_key(
&protocol,
key_id,
&Counterparty::Other(alice.identity_key()),
)
.unwrap();
let bob_public = alice
.derive_public_key(
&protocol,
key_id,
&Counterparty::Other(bob.identity_key()),
false,
)
.unwrap();
assert_eq!(
bob_private.public_key().to_compressed(),
bob_public.to_compressed()
);
let alice_wallet = ProtoWallet::new(Some(PrivateKey::random()));
let bob_wallet = ProtoWallet::new(Some(PrivateKey::random()));
let protocol = Protocol::new(SecurityLevel::App, "secure messaging");
let signed = alice_wallet
.create_signature(CreateSignatureArgs {
data: Some(b"Hello, Bob".to_vec()),
hash_to_directly_sign: None,
protocol_id: protocol.clone(),
key_id: "msg-1".to_string(),
counterparty: Some(Counterparty::Other(bob_wallet.identity_key())),
})
.unwrap();
let verified = bob_wallet
.verify_signature(VerifySignatureArgs {
data: Some(b"Hello, Bob".to_vec()),
hash_to_directly_verify: None,
signature: signed.signature,
protocol_id: protocol,
key_id: "msg-1".to_string(),
counterparty: Some(Counterparty::Other(alice_wallet.identity_key())),
for_self: None,
})
.unwrap();
assert!(verified.valid);
println!("alice → bob signature verified: {}", verified.valid);
}
BEEF: write, read, validate, verify
use bsv_rs::primitives::PrivateKey;
use bsv_rs::script::templates::P2PKH;
use bsv_rs::script::{SignOutputs, UnlockingScript};
use bsv_rs::transaction::{
Beef, MerklePath, MockChainTracker, Transaction, TransactionInput, TransactionOutput,
};
fn main() {
let key = PrivateKey::random();
let address = key.public_key().to_address();
let mut funding = Transaction::new();
let mut mined = TransactionInput::new("11".repeat(32), 0);
mined.unlocking_script = Some(UnlockingScript::new());
funding.add_input(mined).unwrap();
funding
.add_output(TransactionOutput::new(
10_000,
P2PKH::lock_from_address(&address).unwrap(),
))
.unwrap();
let funding_txid = funding.id();
let proof = MerklePath::from_coinbase_txid(&funding_txid, 800_000);
funding.merkle_path = Some(proof.clone());
let mut spend = Transaction::new();
let mut input = TransactionInput::with_source_transaction(funding.clone(), 0);
input.set_unlocking_script_template(P2PKH::unlock(&key, SignOutputs::All, false));
spend.add_input(input).unwrap();
spend.add_p2pkh_output(&address, Some(9_900)).unwrap();
futures::executor::block_on(spend.sign()).unwrap();
let subject = spend.id();
let mut beef = Beef::new();
let bump = beef.merge_bump(proof);
beef.merge_raw_tx(funding.to_binary(), Some(bump));
beef.merge_transaction(spend);
let bytes = beef.to_binary_atomic(&subject).unwrap();
println!("beef {} bytes, subject {subject}", bytes.len());
let mut parsed = Beef::from_binary(&bytes).unwrap();
let validation = parsed.verify_valid(false);
assert!(validation.valid);
assert_eq!(validation.roots.get(&800_000), Some(&funding_txid));
let tx = Transaction::from_beef(&bytes, None).unwrap();
assert_eq!(tx.id(), subject);
assert!(tx.inputs[0].source_transaction.is_some());
let mut tracker = MockChainTracker::new(800_000);
tracker.add_root(800_000, funding_txid);
let ok = futures::executor::block_on(tx.verify(&tracker, None)).unwrap();
assert!(ok);
println!("verified {ok}");
}
The overlay network
The overlay example reaches network services when executed.
use bsv_rs::overlay::{
LookupAnswer, LookupQuestion, LookupResolver, LookupResolverConfig, TopicBroadcaster,
TopicBroadcasterConfig,
};
use bsv_rs::transaction::Transaction;
#[tokio::main(flavor = "current_thread")]
async fn main() {
let resolver = LookupResolver::new(LookupResolverConfig::default());
let question = LookupQuestion::new("ls_kvstore", serde_json::json!({ "key": "hello" }));
match resolver.query(&question, Some(5_000)).await {
Ok(LookupAnswer::OutputList { outputs }) => {
println!("lookup answered {} output(s)", outputs.len())
}
Ok(other) => println!("lookup answered a {:?} shape", other.answer_type()),
Err(e) => println!("lookup failed: {e}"),
}
let broadcaster = TopicBroadcaster::new(
vec!["tm_kvstore".to_string()],
TopicBroadcasterConfig::default(),
)
.unwrap();
let tx = Transaction::new();
match broadcaster.broadcast_tx(&tx).await {
Ok(steak) => println!("broadcast acknowledged: {steak:?}"),
Err(e) => println!("broadcast refused: {e:?}"),
}
}
More: BRC-103 over HTTP or Socket.IO (auth, socketio), the wallet wire protocol (wallet::wire), UHRP storage, the registry, the KV stores and identity all carry worked examples in their module docs on docs.rs.
License
Licensed under MIT or Apache-2.0.