sidestr-core
sidestr user-activated sidechains beside a Bitcoin-family parent, in Rust: the chain document, the parents table, signed blocks in either header family (a BIP 325 challenge, no subsidy), the peg-in claim and peg-out burn rules, the block file, and an in-memory validating chain with a producer's mempool.
A sidestr chain runs beside a Bitcoin-family chain with Bitcoin's transaction rules, blocks that are valid because they are signed rather than mined, no subsidy, and every coin on it a coin locked on the parent. Signers decide the order of blocks; they do not decide the rules.
[]
= "0.2"
# and, for a chain beside a BLAKE2b parent (xbt, txbt4):
= "0.2"
The rules, state and chain are generic over the header family
(sidestr_core::block::HeaderFamily): State / Chain are the stock
instantiation (btc, tbtc4), and StateOf<Blake2bV2> / ChainOf<Blake2bV2>
with sidestr_header::Blake2bV2 validate a chain beside Knots' BLAKE2b fork.
The dependency edge runs from sidestr-header to this crate, never back.
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
(siding/lib/{parents,block,chain,overlay,marker,records,address,checkpoint}.mjs,
bin/siding.mjs, and the tests in siding/test/). Two parts come from the
engine siding loads, by the same author and under the same licence:
- the block, header and spending checks, and Knots' unified sighash, from
bitcoin-desktop/schema
(
codec/blocks.js,codec/headers.js,codec/interpreter.jssighashUnified,schema/validate.jsonld); - the block file format and the chain state machine from
bitcoin-blake/blaketestnode
(
lib/blockfile.mjs,lib/node.mjs).
SPEC.md in the sidestr repository is the design; the crate documentation
cites its sections, and every ported function names its original.
What changed in the port
- Bitcoin's consensus serialisation, hashes, merkle roots, the taproot sighash
and BIP-340 Schnorr come from
rust-bitcoinand itssecp256k1; bech32 from thebech32crate. Nothing cryptographic is hand-rolled. - Script verification fails closed: taproot key-path spends are verified, every other script type is refused rather than skipped. The reference lets a witness version it cannot verify through.
- The solution's witness decoder refuses truncation, trailing bytes, non-minimal sizes and more than 256 items; the reference reads what it can.
- The mempool judges a spend's sighash type by the family's block rule (unified only beside a BLAKE2b parent); the reference mempool accepts unified on every family while its block rule does not.
- Claim and burn records commit only when a block is applied, not while it is being judged.
- Blocks are a pure function of their inputs and the key: BIP 340 auxiliary randomness is zero for every block, as siding sets it for the genesis.
- A document naming the
assets,poolorevmrules is refused, because this version does not carry them. - Level 2 carries the pure parts of
federation.mjsand not the round (that issidestr-round): the script path is verified for exactly themulti_a(k, …)leaf, an unknown leaf version is refused rather than skipped, andtemplate_idnames what signers authorise separately from the sealed hash (ADR-2101 review). - No I/O in the rules: the filesystem and the clock are behind the
stdfeature (blockfile,chain). - The genesis is judged, not trusted:
from_genesisruns every rule that applies at height 0 (the family's header rules, the signature against the challenge, the pegs as the one subsidy,sidestr:rule-genesis-document) before the hash is held to the document'sgenesisHash; siding applies block 0 on the hash alone. There is no trusted import. - A stock header with version bit 31 set is refused at decode and, on the
typed path, by
btc:rule-header-version— the rule the kernel names, which reads a stock version asi32le. - A mirror's
blocks.datrecord framing ([u32 height][u32 size]) is held toblocks.jsonand to the file's length on every read (Error::BlockFile). - Markers are written with a canonical push (
OP_PUSHDATA1above 75 bytes) and read exactly as siding'sopReturnDatareads them — a bare length byte or anOP_PUSHDATA1prefix, minimal or not — because that is the burn rule's grammar and a burn a reference wallet wrote must be paid. Their text is decoded as siding'sTextDecoderdecodes it, one leading UTF-8 byte-order mark dropped, at exactly the readers that text-decode there (burns, claims, the peg-in remainder's hex-form decision, records) and not at the two compared as bytes (the parent peg-out record, the checkpoint) — so a BOM-led burn is recorded, or refused, alike.tests/audit_regressions_records.rsholds every marker case and every block of an audit corpus to identical derived lists in both engines.
Status — 0.2.2
Level 1 (one signer), both header families, end to end: genesis from the document, block production, validation, the mempool policy, the block file. Proven against the reference:
- the genesis of a throwaway chain rebuilt from its document and key is
byte-identical to the one siding wrote (
tests/oracle.rs); - the estate's sealed
sidestr:dreamlabgenesis replays to its documented hash; - blocks produced here are accepted by siding and blocks siding produces are
accepted here (
tests/interop.rs, needs the reference checkouts); - with
sidestr-header'sBlake2bV2, Melvin Carvalho's livesidestr:txbt4-sidingchain replays from its genesis to its tip with every rule on, spends signed with Knots' unified sighash included (sidestr-header/tests/core_family.rs); - a 2-of-3 federation's challenge, its genesis and two blocks sealed by three
different pairs are byte-identical to siding's (
tests/federation.rs,fixtures/fedtest), every subset and both parities are property-tested (tests/federation_prop.rs), and Bitcoin Core's interpreter agrees with themulti_averifier on 161 differential cases (cargo test --features consensus-oracle); - the five counter-examples of the independent 0.2 audit (an unsigned genesis
accepted on its hash, a stock block with version bit 31 replayed, a corrupt
record prefix replayed, a
u32overflow inclaimable, and the typed v2 genesis bypassing the family rules) are fixed and pinned as regressions (tests/audit_regressions.rs, both crates).
0.2 made the rules, state and chain generic over HeaderFamily (with an
associated header and block type), added Knots' unified sighash and the
family's own rules, tightened the witness decoder, and added level 2's pure
parts (federation: NUMS key, leaf, partial signatures, witness assembly,
sealing, the multi_a verifier, template_id, Chain::open_sealed).
State, Chain and every 0.1 name keep their meaning as the stock
instantiation. The parent view (parent) is behind two traits — the chain
read-only, the peg wallet — with every decision pure (peg-ins found in
decoded blocks, what to claim and lock, the burn payment and the checkpoint
as send outputs, reconciliation) and Bitcoin Core's JSON-RPC as the one
implementation behind the rpc feature; tests/parent_live.rs (ignored,
SIDESTR_PARENT_RPC) finds the estate's peg-wallet funding on a testnet4
node without sending anything.
Elsewhere in the stack: the level-2 co-signing round is sidestr-round,
tips and transactions over Nostr are sidestr-nostr, spending is
sidestr-wallet. Not yet: the assets and pool rules, a full script
interpreter, and the Byzantine-tolerant consensus protocol above the
signature (ADR-2101 review), which is a later crate.
Running the checks
SIDESTR_PARENT_RPC=http://<node>:48332/
# the JS interop test, with the reference checkouts:
SIDESTR_SIDING=<sidestr/spec>/siding SCHEMA=<bitcoin-desktop/schema> BLAKETESTNODE=<bitcoin-blake/blaketestnode> \
RUSTDOCFLAGS="-D warnings"
&&
fixtures/trial is a throwaway chain with its disposable signer key, carried
for the byte-for-byte test. fixtures/dreamlab is a sealed genesis without its
key. Keys are files, never arguments; nothing here prints one.
Licence
AGPL-3.0-only, as a derivative work of siding. See LICENSE. This crate is not dual-licensed: a crate that links it is AGPL-3.0 in effect and should say so.