sidestr-wallet
Part of sidestr-rs. Rust port of Melvin Carvalho's sidestr sidechains, AGPL-3.0-only: the economic engine for did:nostr agents. A did:nostr key is a sidechain 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 reads chain.json from a mirror, asks a producer /coins/<script hex> — or
folds the block file with sidestr-core
and gets the same list — builds a transaction Bitcoin's rules accept with the
fee at the document's minFeeRate, and hands it over. The transaction
authorises itself; a producer includes what validates.
[]
= "0.3"
= "0.3"
use ;
let chain = from_json?; // from a mirror
let listed = from_json?; // GET /coins/<my script>
let me = from_hex?; // a derived role key, never the identity key
let paid = build_spend?;
let post = tx_post; // POST post.body to post.url
Attribution
This crate is a port of siding, the reference implementation of sidestr by
Melvin Carvalho — 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 (buildSpend, resolveTo, deliver),
lib/address.mjs, the transaction and marker construction of
lib/parent.mjs (scanPegins, payPegout) and lib/checkpoint.mjs
(sendCheckpoint), and the send and faucet commands of bin/siding.mjs.
The mempool policy it builds to — maturity, pegoutMin, minFeeRate, the
key-path signature check — is siding/lib/chain.mjs Siding.submit(), which
sidestr-core carries as State::submit. SPEC.md in the sidestr repository
is the design; the crate documentation cites its sections, and every ported
function names its original. Consensus types, markers, addresses and the chain
document come from sidestr-core and are not duplicated.
What changed in the port
- Signatures name their hash type. As siding's
lib/txsign.mjsdoes since SPEC 0.0.3: the key-path signature follows the parent's family,0x01(BIP 341) beside stock Bitcoin and0x21(Knots' unified sighash) beside BLAKE2b, always as a 65-byte witness, and the fee is sized for it.PlainKeysigns with zero auxiliary randomness, so a transaction is a pure function of its inputs; given the same randomness the reference signs the same bytes (tests/txsign.rs). - Keys behind a port. A builder computes the sighash and asks a
SpendSignerfor the signature; it never holds a secret.PlainKeyis the in-memory implementation (siding's model), andkey::derive_spend_keyis the HMAC-SHA256 domain-separated role-key derivation of agentbox ADR-2101, with a known-answer vector cross-checked against Node.js. - A policy hook. Every builder consults a
SpendPolicy(chain, kind, script, amount, fee, inputs) before signing.Permissiveis the default and must be named to be used. - Dust is refused, and change under the dust threshold goes to the fee rather than into an unspendable coin. siding does not check.
- A fixed fee is checked against
minFeeRatebefore signing rather than by the producer. - Zero BIP 340 auxiliary randomness in
PlainKey: a spend is a pure function of its inputs and the key. - No I/O by default.
deliverreturns URLs, bodies and unsigned event templates; featureclientadds the HTTP calls overureq. Signing and publishing a Nostr event issidestr-nostr's. Nothing here talks to a parent node:peginproduces the outputs, the unsigned transaction, and the[{address: btc}, {data: hex}]argument Bitcoin Core'ssendtakes, asparent.mjsdoes. - Parent records are bounded before they are built.
pegoutMarkerDataand the checkpoint go on the parent inside its 80-byteOP_RETURNpolicy, and every marker push must fit the one length byte the shared grammar reads (255 bytes);pegin::pegout_payment_outputsreturnsError::MarkerTooLongpast either bound rather than a well-formedOP_PUSHDATA2output thatparse_pegout_markerwould never read back (audit F4, 0.2.1; the checkpoint's 80-byte bound issidestr-core'scheckpoint_data). The reference does not check; itssendfails at the node. - Not carried: the
--evmdeposit branch (ADR-2096 excludes theevmrule), assets (SPEC 12, reserved), the faucet's relay loop and rate state (its payment isbuild_spend; the request template isdeliver::faucet_request).
Status — 0.3.0
Spend, burn, peg-in shape, delivery data, coin listing and selection, the
signer and policy ports. Signatures follow the parent's family (SPEC 0.0.3):
0x01 beside stock Bitcoin, 0x21 beside BLAKE2b. Verified independently before release (GPT-6 Astra, 2026-09-23). The pass
re-ran the gates and checked each 0.0.3 change at its layer against the
reference. It found every block with the same claims, burns and UTXO set on
both engines, for both families. Its probes and its four findings, now
fixed, are kept as tests/audit_regressions_0_0_3.rs. Proven:
-
the signing parity with the reference's
lib/txsign.mjson both families: the reference re-signs each Rust transaction's inputs with its ownkeyPathSighashand zero auxiliary randomness, and the witnesses are byte-identical; each engine verifies the other's signatures under the chain's rule; a unified signature is refused at a stock chain's mempool, a BIP 341 one accepted beside BLAKE2b (tests/txsign.rs); -
a two-input spend and a burn built here on a throwaway chain are accepted by
sidestr-core'sState::submitand mined, and by siding'sSiding.submit()and mined, with the same txid, fee and vsize; a tampered signature is refused by both (tests/oracle.rs,tests/xcheck-wallet.mjs, needs the reference checkouts); -
a peg-in transaction round-trips through rust-bitcoin and its marker parses with
sidestr_core::marker; -
accept and reject for every builder: insufficient funds, dust, below
pegoutMin, wrong parent network, fee belowminFeeRate, policy refusal (tests/builders.rs).
Elsewhere in the stack: the relay client and the level-2 peg-out PSBT round
are sidestr-round's. Not yet: a PSBT for the parent-side peg-in, the peg
holders' descriptor (and_v(v:pk(refund), older(refundBlocks))) and refund
sweep, script-path spends, hardened BIP-32 custody roles (ADR-2101).
Running the checks
SIDESTR_SIDING=<sidestr/spec>/siding SCHEMA=<bitcoin-desktop/schema> BLAKETESTNODE=<bitcoin-blake/blaketestnode> \
RUSTDOCFLAGS="-D warnings"
&&
Keys are files, never arguments; nothing here prints one. The test keys are derived from fixed strings and seal throwaway chains whose coins carry no value.
Licence
AGPL-3.0-only, as the work it derives from. Not dual-licensed. A crate that links this one is AGPL-3.0 in effect and should say so (agentbox ADR-2106).