← blocktrails.org

Blocktrails (v0.1)

Nostr-native output-key commitment chaining on Bitcoin.

Definition

A Blocktrail is a sequence of P2TR key-path outputs where each output key is derived by cumulatively tweaking the base key. Each state transition adds a new tweak, and spending the output advances the trail to the next committed state.

Key Representation

Blocktrails operate on full secp256k1 private keys and public keys internally, as used by Nostr. State transitions are defined as scalar additions to the private key. When committing state to Bitcoin, the corresponding public key is encoded as a Taproot output key by taking its x-coordinate. Verification compares x-coordinates only.

This preserves:

Parity normalization is an output encoding concern, not a state or semantic concern.

Key encodings such as nsec, npub, or compressed public keys are out of scope and may be used freely by applications.

Mechanism

Blocktrails use chained tweaking: each state transition adds a scalar tweak to the previous key, forming a cryptographic chain that mirrors the spend chain.

For genesis state s₀:

  1. Compute t₀ = scalar(s₀)
  2. Private key: d₀ = d_base + t₀
  3. Public key: P₀ = P_base + t₀·G

For transition to state sᵢ:

  1. Compute tᵢ = scalar(sᵢ)
  2. Private key: dᵢ = dᵢ₋₁ + tᵢ
  3. Public key: Pᵢ = Pᵢ₋₁ + tᵢ·G

Equivalently, the cumulative tweak is the sum of all state scalars:

dₙ = d_base + t₀ + t₁ + ... + tₙ
Pₙ = P_base + (t₀ + t₁ + ... + tₙ)·G

H is SHA-256 unless otherwise specified by the application.

Hash-to-scalar reduction

scalar(s):
  h = sha256(serialize(s))           // 32 bytes
  t = int(h, big-endian) mod n       // n = secp256k1 group order
  if t == 0: reject state as invalid // probability ~2^-256
  return t

The tweak t MUST be in the range [1, n-1]. Implementations MUST reject states where t = 0. This is an application-layer rule — Bitcoin accepts the output regardless.

Note: Since sha256 output is 256 bits and n ≈ 2256, the modular reduction is nearly uniform. No rehashing or counter-based totality mechanism is required.

All subsequent uses of scalar(state) refer to this function.

serialize(s) MUST be canonical and byte-stable: the same logical state MUST always produce identical bytes. Non-deterministic serialization breaks commitment verification.

The base key d_base is immutable for a given trail. Rotation requires starting a new trail.

Fees are paid from the current head output. When balance runs low, top-up (see Scope) adds funds.

Boundary encoding for P2TR output:

p2tr_xonly(P) → bytes32:
  if y(P) is even: return x(P)
  else: return x(-P)

The witness program is p2tr_xonly(P). Signing uses the corresponding (possibly negated) private key per BIP-340.

Spending this output advances the trail to the next committed state.

Constraints

Blocktrails use P2TR key-path only and do not require tapscript, script trees, or script-path spends. The only mechanism used is public-key tweaking.

No domain separation is applied; the commitment binds exactly to the serialized state bytes. This is deliberate — simplicity is a goal, mirroring Bitcoin's sighash minimalism philosophy. Applications requiring domain separation can include it in their serialize() function.

Components

ComponentDescription
Commitment carrierTweaked public key (full point)
State headCurrently unspent output
HistoryThe spend chain

Design Goals

GoalHow
Minimal on-chain footprintOne P2TR output per state update
Standardness-friendlyKey-path spends only
External stateState bytes live off-chain; chain anchors integrity
Single-writerOne controlling base key (multi-writer requires extra protocol)

Scope

Blocktrails intentionally specify only the minimal primitive required for interoperable verification: commitment-by-output-key and linear progression by spends.

Many lifecycle behaviors — top-up, third-party payments, transfer of control, explicit closure — are compositions of ordinary Bitcoin spends and do not change the core verification rule. This document treats them as optional extensions.

In practice, top-up is the only liveness-oriented convention that may be widely needed, since trail progression is fee-bounded.

Operations

Genesis

t₀ = scalar(state₀)
d₀ = d_base + t₀
P₀ = d₀·G
output₀ = p2tr_xonly(P₀)

Create P2TR output with witness program output₀. Holder knows d₀.

Transition

tᵢ = scalar(stateᵢ)
dᵢ = dᵢ₋₁ + tᵢ
Pᵢ = Pᵢ₋₁ + tᵢ·G
outputᵢ = p2tr_xonly(Pᵢ)

Sign with dᵢ₋₁ to spend previous output. Create new output with witness program outputᵢ.

Note: The spending key accumulates all previous tweaks. At state n:

dₙ = d_base + t₀ + t₁ + ... + tₙ

Verify

verify(P_base, genesis_outpoint, states[]) → bool:
  outpoint = genesis_outpoint
  P = P_base

  for state in states:
    t = scalar(state)    // rejects if t = 0
    P = P + t·G          // chain the tweak
    if x(P) ≠ witness_program(outpoint): return false
    outpoint = spending_outpoint(outpoint)

  return is_unspent(outpoint)

Verification chains the tweaks: each state adds to the running public key. Since x(P) == x(-P), comparison uses x-coordinates only. No parity handling needed.

To locate the spending transaction for a given outpoint, implementations MAY use any standard Bitcoin data source: full node with indexing, Electrum-style servers, or externally published transaction feeds.

SPV compatibility and data dependencies

Blocktrails are SPV-compatible. Verification requires:

No full node is necessary, but UTXO status and spend-chain traversal require indexing or proof infrastructure. "SPV-compatible" does not mean "zero external dependencies."

Interface

Applications implement:

serialize(state) → bytes
validate(prev, next) → bool

The primitive handles commitment and chaining. Applications define state format and transition rules.

Guarantees

This primitive provides:

Non-Guarantees

This primitive does not provide:

Security Assumptions

AssumptionGuarantees
Hash preimage resistanceState binding
Discrete log hardnessOutput key security
Bitcoin consensusSpend ordering, double-spend prevention

Threat Model

What Blocktrails protect against

ThreatMitigation
State forgeryHash commitment — cannot find s' where H(s') = H(s)
History rewritingBitcoin immutability — confirmed spends are final
Double-spending stateUTXO model — each output spendable exactly once
Ordering disputesBlockchain provides canonical order

What Blocktrails do NOT protect against

ThreatWhyMitigation
Key compromiseHolder of d_base controls all transitions. Compromise means loss of all funds in the head output and full control over all future state transitions.Secure key storage
State withholdingState lives off-chainRedundant state distribution
Invalid state transitionsValidation is client-sideVerifiers must check validate(prev, next)
CensorshipDepends on Bitcoin transaction inclusionStandard Bitcoin censorship resistance
Front-runningMiner can see pending state in mempoolCommit-reveal if state must be private

Trust boundaries

┌─────────────────────────────────────────────┐ │ Trusted (cryptography) │ │ - Hash binds state to output key │ │ - Signature proves key knowledge │ │ - Bitcoin prevents double-spend │ └─────────────────────────────────────────────┘ │ ┌─────────────────────────────────────────────┐ │ Untrusted (application layer) │ │ - State availability │ │ - Transition validity │ │ - Semantic correctness │ └─────────────────────────────────────────────┘

Failure modes

FailureConsequenceRecovery
Lost d_baseCannot advance trailNone — funds/state locked
Lost state historyCannot verify trailRestore from backup/peers
Invalid transition acceptedVerifiers reject; chain forks semanticallySocial consensus on valid branch
Bitcoin reorgRecent transitions may revertWait for confirmations

Reference Implementation

blocktrails — JavaScript/Node.js implementation with full test suite.

npm install blocktrails

Related Specifications

References