Skip to main content

Crate sidestr_wallet

Crate sidestr_wallet 

Source
Expand description

sidestr-wallet — a wallet for sidestr sidechains, in Rust: the coin set for a script, the reference coin selection, taproot key-path spends and peg-out burns signed behind a signer port, the parent-side peg-in transaction shape, and delivery as a POST /tx body or a kind-23500 event.

A wallet needs a chain id and a relay, and nothing of the producer’s (SPEC 11). It learns the chain from a mirror’s chain.json, held to the signer’s announced tip; asks a producer /coins/<script hex> for what its script owns, or folds the block file itself with sidestr_core::State and gets the same list; builds a transaction Bitcoin’s rules accept, with the fee at the document’s minFeeRate; and hands it over as POST /tx or as an event on a relay whose key is anyone’s, because the transaction authorises itself. A producer includes what validates. A wallet with nothing publishes a kind-23501 request and a faucet may answer it.

This crate is a port of siding, the reference implementation by Melvin Carvalho (https://github.com/sidestr/spec, AGPL-3.0), ported from commit 2de40bdac4cba01be0864156a553d8287c22e279 and brought to SPEC 0.0.4 (@sidestr/spec 0.0.6) at fa86dac83d47b8f70195132e91e9dc083e1d9228 (lib/txsign.mjs) — siding/lib/spend.mjs, lib/address.mjs, the construction halves of lib/parent.mjs and lib/checkpoint.mjs, and the send and faucet commands of bin/siding.mjs — and carries the same licence, AGPL-3.0-only. SPEC.md in that repository is the design; section numbers below are its. Where a function ports a siding function its documentation names it. Consensus types, the markers, addresses and the document come from sidestr_core and are not duplicated here.

§What is here

modulewhatSPECported from
coinsthe coin set for a script: /coins JSON or a state fold; maturity11siding/lib/chain.mjs coins, spend.mjs
selectlargest-first selection to the amount plus a fee bound11spend.mjs buildSpend
spenda key-path spend to an address or script, fee at minFeeRate, signed, as hex11spend.mjs buildSpend, resolveTo; siding send
burna peg-out: OP_RETURN pegout:<parent script hex>, at least pegoutMin7spend.mjs (--pegout)
peginthe parent side: peg output + pegin:<chain id>:<script> marker; the peg-out payment and checkpoint shapes; scanning6, 7, 11parent.mjs, checkpoint.mjs
deliverPOST /tx, /coins, /tip, /chain.json as data; kind 23500 / 23501 templates; HTTP behind feature client11spend.mjs deliver, bin/siding.mjs routes
keythe SpendSigner port, a plain key, pubkey → 5120 script → bech32m, ADR-2101 spend-key derivation3sign.mjs, address.mjs
policythe SpendPolicy hook every builder consults; Permissive—(ADR-2100)

§A payment, end to end

Against a chain held in memory — the same calls work against a producer’s /coins and /tip (see deliver).

use bitcoin::secp256k1::SecretKey;
use sidestr_core::block::{challenge_for, pubkey_of};
use sidestr_core::document::{ChainDocument, Peg};
use sidestr_core::state::{NextBlock, State};
use sidestr_wallet::burn::{build_burn, BurnRequest};
use sidestr_wallet::spend::{build_spend, SpendRequest};
use sidestr_wallet::{coins, key, Error, Permissive, PlainKey, SpendSigner};

// the chain's signer seals blocks; the wallet's spend key is a role key (ADR-2101)
let signer = SecretKey::from_slice(&[7u8; 32]).unwrap();
let root = SecretKey::from_slice(&[0x11u8; 32]).unwrap();
let wallet = PlainKey::new(key::derive_spend_key(&root, &"0".repeat(64), 0).unwrap());

// a throwaway document whose genesis pegs 0.01 BTC to the wallet's script (SPEC 5)
let json = format!(r#"{{"id":"sidestr:example","name":"example","parent":"tbtc4","challenge":"{}",
  "powLimit":"7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff","addressPrefix":"ex",
  "genesisTime":1790000000,"signer":"{}","pegs":[]}}"#, challenge_for(&pubkey_of(&signer)).to_hex_string(), pubkey_of(&signer));
let mut doc = ChainDocument::from_json(&json).unwrap();
doc.pegs.push(Peg { txid: "a".repeat(64), vout: 0, amount: 1_000_000, script: wallet.script().to_hex_string(), extra: Default::default() });
let mut chain = State::with_key(doc.clone(), &signer).unwrap();

// genesis coins are coinbase outputs: mature at 100 (a wallet sees this as `coinbase: true`)
let listed = coins::from_state(&chain, &wallet.script());
assert!(listed[0].coinbase && !listed[0].is_mature(chain.height()));
for i in 1..=100 { chain.produce(&signer, &NextBlock { time: 1790000000 + i, claims: vec![] }, None).unwrap(); }

// pay someone: an address under the chain's prefix, fee at minFeeRate, signed through the port
let you = key::address_for(&PlainKey::new(SecretKey::from_slice(&[8u8; 32]).unwrap()).pubkey(), "ex").unwrap();
let req = SpendRequest { chain: &doc, coins: &listed, tip_height: chain.height(), to: &you, amount: 250_000, fee: None };
let paid = build_spend(&req, &wallet, &Permissive).unwrap();
let ok = chain.submit(paid.tx.clone()).unwrap();          // the producer's mempool policy, and the signature
assert_eq!((ok.txid, ok.fee), (paid.txid, paid.fee));

// once mined, the change is a coin; burn some of it to a parent address: the peg holders owe it on tbtc4 (SPEC 7)
chain.produce(&signer, &NextBlock { time: 1790000200, claims: vec![] }, None).unwrap();
let coins_now = coins::from_state(&chain, &wallet.script());
assert_eq!(coins_now[0].value, paid.change);
let burn = build_burn(&BurnRequest { chain: &doc, coins: &coins_now, tip_height: chain.height(), to: "tb1pvts4e2zcrujj9zey3kadyfgh2xs93v8va8ae9ldhukpxy2n3848qyqurhc", amount: 10_000, fee: None }, &wallet, &Permissive).unwrap();
assert!(chain.submit(burn.tx.clone()).is_ok());

// what the wallet refuses before signing
assert!(matches!(build_spend(&SpendRequest { amount: 5_000_000, ..req }, &wallet, &Permissive), Err(Error::Insufficient { .. })));
assert!(matches!(build_spend(&SpendRequest { amount: 100, ..req }, &wallet, &Permissive), Err(Error::Dust { .. })));

§Conventions that matter

  • Keys stay behind SpendSigner. A builder computes the key-path sighash the chain’s family requires (BIP 341 beside stock Bitcoin, Knots’ unified beside BLAKE2b) and asks the port to sign it; it never holds a secret. PlainKey is the in-memory implementation; a signer that holds a derived role key and permits named operations only (ADR-2101) fits the same trait. key::derive_spend_key is the derivation; the identity key never spends.
  • The wallet builds and signs; it does not decide. Every builder consults a SpendPolicy with the chain, kind, script, amount, fee and input count before signing. Permissive says yes; the authority gate (ADR-2100) is the caller’s implementation.
  • The fee is minFeeRate × vsize, exactly, sized with the 65-byte witness the signer will produce, unless the caller fixes one — and a fixed fee under the rate is refused here rather than by the producer.
  • No I/O by default. deliver returns URLs, bodies and event templates; feature client adds the HTTP calls over ureq. Nothing here publishes to a relay: the event’s signature is sidestr-nostr’s.
  • Amounts are sats, u64; txids in markers are display-order hex, as the markers carry them; structural txids are bitcoin::Txid.

§Where this port departs from siding

  • The signature carries its hash type explicitly. As txsign.mjs does since 0.0.3: 0x01 beside stock Bitcoin, 0x21 beside a BLAKE2b parent, so every witness is 65 bytes and the fee is sized for that. The message is the reference’s (tests/oracle.rs checks each engine verifies the other’s signature); the signature itself differs, since PlainKey signs with zero auxiliary randomness and siding with fresh randomness, so byte equality of whole transactions is not a goal.
  • Dust is refused. siding will pay 1 sat to a taproot script and return 1 sat of change; this crate refuses an amount under the script’s dust threshold (spend::dust_threshold, 330 sats for taproot) and leaves change under it to the fee instead of minting an unspendable coin. A burn’s OP_RETURN has no dust floor; pegoutMin governs it.
  • A fixed fee is checked against minFeeRate before signing; siding lets the producer refuse it.
  • Zero BIP 340 auxiliary randomness in PlainKey, as sidestr-core seals blocks: a spend is a pure function of its inputs and the key.
  • The EVM deposit branch is not carried (--evm; ADR-2096 excludes the evm rule), nor are assets (SPEC 12, reserved).

Re-exports§

pub use burn::build_burn;
pub use burn::BurnRequest;
pub use coins::Coin;
pub use error::Error;
pub use error::Result;
pub use key::PlainKey;
pub use key::SpendSigner;
pub use pegin::build_pegin;
pub use pegin::PegIn;
pub use policy::Permissive;
pub use policy::SpendPolicy;
pub use spend::build_spend;
pub use spend::Spend;
pub use spend::SpendRequest;

Modules§

asset
Issued assets (SPEC 12): issue one, and move it, keeping the assets rule as sidestr_core::assets::AssetView reads it.
burn
A peg-out (SPEC 7): a sidechain transaction paying a burn output, OP_RETURN pegout:<parent output script hex> with a value of at least pegoutMin. The value leaves the supply; the peg holders owe it to that script on the parent. A port of the pegout branch of siding/lib/spend.mjs buildSpend (siding send --pegout).
coins
The coin set for a script (SPEC 11): what a producer’s /coins/<script hex> returns, or the same list folded from a sidestr-core state, and the maturity filter both wallets apply before choosing.
compose
A spend with a laid-out body: named outputs first, then OP_RETURN records (SPEC 12.1), then change. The general form behind crate::asset: an asset transfer is outputs that carry, a tally: record that says what they carry, and inputs that must be spent because they carry it.
deliver
Delivery (SPEC 11): a transaction reaches a producer by POST /tx, body the hex, or as a kind 23500 event on a relay, content the hex, tagged chain = chain id; a wallet with nothing publishes a kind 23501 event, content an address, and a faucet may answer. A port of siding/lib/spend.mjs deliver and the producer’s routes in siding/bin/siding.mjs (/coins/<script>, /tip, /chain.json, POST /tx).
error
The one error type every fallible function in this crate returns.
external
Spends signed somewhere else: a browser extension behind window.nostr.sidestr.signTransaction (spec proposals/browser-signer.md).
key
Keys behind a port: the SpendSigner every builder signs through, a plain-key implementation for tests and a CLI, the pubkey → script → address chain, and the ADR-2101 spend-key derivation.
pegin
The parent side (SPEC 6, 7, 11): the peg-in transaction shape a wallet builds on the parent, and the shapes of the peg holders’ peg-out payment and the producer’s checkpoint, all with rust-bitcoin for the parent network the chain document names. A port of the transaction and marker construction of siding/lib/parent.mjs (scanPegins, payPegout) and siding/lib/checkpoint.mjs (sendCheckpoint), not of their RPC client: siding hands Bitcoin Core a send call with [{address: btc}, {data: hex}], and PegIn::core_send_outputs is that argument.
policy
The policy hook: the wallet builds and signs, it does not decide.
select
Coin selection, as the reference does it (siding/lib/spend.mjs buildSpend): largest first, until the picked coins cover the amount plus a fee bound.
spend
A key-path taproot spend to an address or script, with the fee sized at the chain’s minFeeRate, signed through the SpendSigner port and encoded as the hex a producer’s POST /tx takes (SPEC 11). A port of siding/lib/spend.mjs buildSpend and resolveTo, and of the shape siding send prints.