BINST Pilot

A proof-of-concept for Bitcoin-sovereign institutional design and operations.

Can complex institutional entities — institutions, process templates, running workflows, step-by-step execution — be permanently represented on Bitcoin L1 and have their associated events verified at the Bitcoin layer?

Ultimately, can complex entities of arbitrary structure and their linked events carrying arbitrary content be created, owned, and operated on Bitcoin?

That is the core question. Not "decentralization" in the abstract, but something concrete: can a Bitcoin inscription be the canonical record of an institution's existence, and can the execution of a multi-step process be traced back to Bitcoin cryptographically — without trusting an L2?

The pilot is a proof-of-concept for that claim. An EVM-compatible L2 (Citrea) handles the operational logic as a delegate of the Bitcoin key holder. The L2 is replaceable; the inscription is not.

How it works

ConcernApproach
Institutional identityInscribed on Bitcoin L1 via Ordinals — the inscription IS the entity
MembershipRunes tokens on Bitcoin L1 — holding ≥1 token means membership
Operational logicRuns on Citrea (EVM L2) as a delegate of the Bitcoin key
AuthorityThe Bitcoin key controls the inscription UTXO; the L2 contract obeys it
L2 replaceabilityCreating new process instances on a new L2, bound to the same inscription, preserves identity
UTXO safetyTaproot script tree (NUMS + CSV + multisig) protects the inscription sat
Event verificationL2 batch proofs are written to Bitcoin DA — execution state is ZK-provable from Bitcoin

If the L2 disappears, the inscription remains. If the L2 is replaced, the same Bitcoin identity binds to the new process instances.

What the pilot implements

  • 2 smart contracts deployed and verified on Citrea testnet — BINSTProcessFactory (thin factory) and BINSTProcess (self-contained instance)
  • 4 Rust crates (BINST Protocol) — decoding BINST data directly from Bitcoin transactions, no_std-compatible, WASM-ready
  • 6 TypeScript scripts — end-to-end protocol flows, Bitcoin inscription tooling, finality monitoring
  • binst metaprotocol JSON schema — formal inscription format for four entity types
  • Taproot vault script tree — NUMS + CSV + multisig UTXO protection for inscription sats
  • Rust/WASM webapp — pilot user interface with real wallet integration (UniSat, SafePal, MetaMask), L1 inscription stack (PSBT batching), and L2 EVM queue (review buffer)

What the pilot proves

  1. Institutional identity can be permanently inscribed on Bitcoin L1
  2. L2 contracts can operate as delegates bound to that Bitcoin identity
  3. The L2 choice is non-permanent — switching L2s preserves the identity
  4. Bitcoin transaction data (DA layer) can be decoded to reconstruct full institutional state without trusting the L2
  5. Inscription UTXOs can be protected with Taproot script trees
  6. A browser-native app can route L1 actions (PSBTs) and L2 actions (EVM calls) to the correct wallet with no mocked flows

Source Code

Architecture

The architecture is built on a three-layer sovereignty model following a "Thin L2, Fat L1" philosophy: Bitcoin is the authoritative identity and definition layer; the L2 is a minimal execution delegate.

┌─────────────────────────────────────────────────────────────┐
│              BITCOIN L1 (Authority)                          │
│                                                             │
│  Ordinals Inscriptions ──── Institutional Identity          │
│  Runes Tokens ───────────── Membership & Roles              │
│  Tapscript Vault ────────── UTXO Safety Layer               │
│  BTC Key ────────────────── Root of All Authority           │
└──────────────────────────┬──────────────────────────────────┘
                           │ anchors
┌──────────────────────────▼──────────────────────────────────┐
│           L2 PROCESSING LAYER (Delegate)                     │
│           Currently: Citrea (Chain 5115)                     │
│                                                             │
│  BINSTProcessFactory ─── Thin factory (1 per chain)         │
│  BINSTProcess ────────── Self-contained instance with       │
│                          embedded steps + L1 inscription    │
│                          anchor (templateInscriptionId)     │
│                                                             │
│  No institution or template contracts on L2 — those live    │
│  on Bitcoin as inscriptions. L2 only holds execution state. │
│                                                             │
│  Execution state verified trustlessly via Bitcoin DA proofs  │
└──────────────────────────┬──────────────────────────────────┘
                           │ verified by
┌──────────────────────────▼──────────────────────────────────┐
│         VERIFICATION LAYER                                   │
│                                                             │
│  BitVM2 ─────── Trust-minimized bridge verification         │
│                 (operational on Citrea via Clementine)       │
│  ZK Proofs ──── Batch proofs of L2 state transitions        │
│                 inscribed on Bitcoin (Groth16/RISC Zero)     │
│  Covenants ──── Future: native BTC spending constraints     │
│  SNARK ──────── Future: ZK proof verification in Script     │
└─────────────────────────────────────────────────────────────┘

Each layer serves a distinct purpose:

  • Bitcoin L1 — permanent identity (inscriptions), membership (Runes), and the root of authority
  • L2 Processing — minimal execution delegate; only holds step-by-step process state, not identity
  • Verification — trust-minimized verification between L2 and Bitcoin (BitVM2 operational on Citrea; ZK batch proofs inscribed on Bitcoin)

Thin L2 principle: The L2 has no institution or template contracts. Those concepts live entirely on Bitcoin as inscriptions. Each BINSTProcess instance carries a templateInscriptionId that anchors it back to Bitcoin L1 — the L2 is a pure execution engine.

Pilot scope note: Cross-chain identity mirroring via LayerZero V2 is an architectural plan (Phase 3) — not implemented in the current pilot. The pilot runs on a single L2 (Citrea). See Cross-Chain Synchronization for the design.

The Three-Layer Model

Layer 1: Bitcoin (Authority)

Bitcoin L1 stores three kinds of data:

PrimitiveRoleWhat it represents
Ordinals inscriptionsEntity identity, ownership, metadataInstitutions, process templates, process instances
RunesMembership and fungible roles"Alice is a member of Acme Financial"
ZK batch proofsComputational integrityEvery L2 state transition, ZK-proven
Bitcoin L1
├── Ordinals    → entities EXIST here (identity, ownership — AUTHORITATIVE)
├── Runes       → membership IS here (fungible tokens per institution)
└── ZK proofs   → computation is PROVEN here (L2 batch proofs)

This is the authoritative layer. If there's a conflict between Bitcoin and any L2, Bitcoin wins.

Layer 2: Processing Delegate (Currently Citrea)

The L2 runs a minimal execution layer — only process instance state. Identity (institutions) and definitions (process templates) live on Bitcoin as inscriptions. The L2 contracts are:

  • BINSTProcessFactory — thin factory, deployed once per chain, creates self-contained BINSTProcess instances
  • BINSTProcess — carries its own step definitions + templateInscriptionId anchor back to Bitcoin L1

Each instance is self-contained: it embeds the step names and action types copied from the L1 template at creation time. This means an instance can migrate to another L2 without any pre-deployed contracts on the destination.

The L2 is a processing engine — it does not own the identity. The user can redeploy to a different L2 at any time, pointing the new instance at the same templateInscriptionId. The identity stays on Bitcoin.

Why Citrea?

FeatureWhy it matters
Fully EVM-compatibleSolidity process instances deploy with an RPC endpoint change
Bitcoin Light Client (0x3100…0001)Read Bitcoin block hashes on-chain, verify inclusion proofs
Schnorr precompile (0x…0200)BIP-340 signature verification in Solidity — no other L2 offers this
Clementine Bridge (BitVM2)Trust-minimized BTC ↔ cBTC peg
Testnet uses Bitcoin Testnet4 as DAReal Bitcoin data, not simulated
Three finality levelsSoft confirmation → Committed → ZK-proven on Bitcoin

The L2 choice is explicitly non-permanent. The architecture allows migrating to any EVM-compatible L2 by deploying a new factory and creating instances that reference the same inscription IDs.

Layer 3: Verification

The verification layer provides trust-minimized guarantees that L2 computation was correct. On Citrea, two mechanisms are already operational:

ZK Batch Proofs (operational)

Citrea's batch prover inscribes ZK proofs (Groth16 via RISC Zero) on Bitcoin. Every L2 state transition — including every executeStep() call on a BINSTProcess — is covered by these proofs. The webapp tracks three finality stages per transaction:

StageMeaning
Soft ConfirmationSequencer has ordered the tx
CommittedSequencer commitment inscribed on Bitcoin
ProvenZK proof inscribed on Bitcoin — mathematically verified

BitVM2 / Clementine Bridge (operational)

Citrea's Clementine Bridge implements BitVM2 for trust-minimized BTC ↔ cBTC peg verification. BitVM2 is a fraud-proof system where:

  • Optimistic case: bridge operators post assertions about L2 state; if unchallenged, the assertion is accepted
  • Dispute case: a challenger can force the operator to reveal intermediate computation steps; if the operator is wrong, their bond is slashed on Bitcoin L1
  • Trust assumption: 1-of-N honesty — only one honest verifier needed

This means BINST process execution on Citrea is not just proven by ZK proofs — the bridge itself that connects BTC liquidity to the L2 is secured by BitVM2 fraud proofs on Bitcoin.

Future enhancements

As Bitcoin's scripting capabilities evolve:

  • Covenants (OP_CTV, OP_CAT) — native spending constraints for vaults and trustless bridges
  • On-chain SNARK verification — ZK proof verification within Bitcoin Script
  • BitVM3 / BitVMX — further improvements to fraud-proof and RISC-V verification

What Each Layer Guarantees

LayerWhat it provesTrust assumptionFailure mode
Ordinal inscriptionEntity exists, admin controls UTXOBitcoin consensusUTXO accidentally spent → lose root authority
Rune balanceThis person is a memberBitcoin consensusToken accidentally sent → membership lost
L2 process instanceProcessing delegate executes logicBitcoin consensus + ZK mathL2 down → create instances on another L2
L2 batch proofEvery state transition was correctBitcoin consensus + ZK mathProof missing → state unverifiable until next batch

The Bitcoin key is the single root of authority. L2 process instances are replaceable processing delegates. Losing an L2 is graceful — create new instances elsewhere. Losing the Bitcoin key is catastrophic — the committee multi-sig is the last resort.

Authority Model

The Bitcoin key is sovereign. Everything else is a delegate.

The Hierarchy

Bitcoin secret key (ROOT OF AUTHORITY)
  │
  ├── controls inscription UTXO     → identity, provenance, metadata
  ├── controls Rune distribution    → membership tokens
  └── authorizes L2 contract(s)     → processing delegates
       ├── Citrea     (current)
       ├── BOB        (possible future)
       ├── Rootstock  (possible future)
       └── any L2     (portable)

The user who holds the Bitcoin private key has full control of every element in the protocol. If they decide to use a different L2, they create new process instances on the new chain, point them at the same inscription IDs, and pick up where they left off.

What the Key Controls

LayerWhat it controlsCan the user switch it?
Inscription UTXOIdentity, metadata, provenanceNo — this IS the identity
Rune distributionMembership tokensNo — lives on Bitcoin L1
L2 process instancesProcessing logic (workflows, payments)Yes — create on any L2
Mirror contractsRead-only identity/membership on other L2s (Phase 3 — not yet built)Yes — add/remove mirrors

L2 Portability

Because the root of authority is the Bitcoin key (not the L2 contract address), the protocol is not locked into any specific L2. A user who starts on Citrea can later move to BOB, Rootstock, or any future Bitcoin L2 without losing their institution's identity, provenance, or membership.

Beyond simple portability, BINST supports multi-chain presence via a dual-channel sync model (Phase 3 plan — not yet implemented):

  • LayerZero V2 (future) — syncs identity and membership across L2s in real-time
  • Bitcoin DA — provides trustless execution state verification via ZK batch proofs (available now)

Mirror contracts on other L2s will provide read-only identity and membership verification. Process execution stays on the home chain — single-writer per process instance prevents concurrent mutation conflicts across chains.

See Cross-Chain Synchronization for the full model.

Failure Modes

ScenarioSeverityRecovery
L2 goes downGracefulCreate new instances on another L2; identity survives on Bitcoin
Inscription UTXO lostSeriousRe-inscribe as child of original + create new L2 instances
Bitcoin key lostCatastrophicCommittee 2-of-3 multi-sig recovery (Taproot vault Leaf 1)

The degradation is intentionally hierarchical: losing the L2 is easy to recover from, losing the inscription UTXO is hard but possible, losing the Bitcoin key requires the committee backstop.

Cryptographic Binding: admin Pubkey

The institution inscription body contains an admin field — the 32-byte x-only public key (BIP-340, hex, 64 chars) of the institution admin's Bitcoin key. This key is the root identity anchor for the entire protocol.

This closes the trust gap between Bitcoin identity and L2 execution. Without it, the link between an inscription and an L2 process instance is informational (a stored string). With it, the binding is verifiable:

  1. Read the institution inscription's admin field from Bitcoin L1
  2. Read the inscription UTXO's owner from Bitcoin
  3. Verify they match — no oracle, no trust
  4. L2 process instances carry a templateInscriptionId that chains back to the institution inscription, completing the link

Citrea's Schnorr precompile (0x0000000000000000000000000000000000000200) enables BIP-340 signature verification on-chain, which can be used to verify that an L2 caller is the legitimate admin. The Rust BitcoinIdentity struct requires bitcoin_pubkey — the inscription body stores the same key as admin.

Institution Anchoring Lifecycle

An institution progresses through three anchoring states:

The Three States

State 1: UNANCHORED (L2-only)
  → Process instances exist on Citrea (or any L2)
  → Functional on L2: step execution works
  → Batch proofs reach Bitcoin DA (orphan proofs — see below)
  → No inscription, no rune
  → Status: DRAFT

State 2: BINDING (partially anchored)
  → L2 instances exist + institution inscription created
  → But not yet fully linked (inscription not referenced
    in L2 instances, or rune not yet etched)
  → Status: BINDING

State 3: ANCHORED (fully sovereign)
  → Inscription created + L2 instances reference it via
    templateInscriptionId + rune etched
  → L2 state is provably linked to Bitcoin identity
  → Status: ANCHORED

Design Decision: Progressive Anchoring

Anchoring is not inscription. An institution's processes can execute on L2 from the moment the first BINSTProcess instance is deployed. The institution becomes Bitcoin-anchored when its Ordinals inscription is created and its L2 instances reference it via templateInscriptionId.

This is a deliberate design choice:

  • Lowers the barrier to entry — users can experiment on L2 for just gas costs
  • Creates a natural funnel — experiment → anchor → grow
  • Matches reality — Citrea batches state regardless of inscription status
  • Permissionless — no gatekeeping on who can create institutions

Orphan ZK Proofs

If a user creates L2 process instances on Citrea but never inscribes the institution identity on Bitcoin, the ZK batch proof still reaches Bitcoin DA. This is an orphan proof — valid (it proves the L2 state transition happened) but unanchored (no Bitcoin-native identity to attach it to).

The binst-decoder would see storage slot changes for BINSTProcess instances, but no templateInscriptionId would resolve to a valid Bitcoin inscription.

Orphan proofs are not harmful:

  • They are noise the indexer filters: if templateInscriptionId resolves to nothing → skip
  • They don't affect anchored institutions
  • They represent experimentation — a healthy signal for protocol adoption

Entity Creation Patterns

PatternBitcoin TX needed?L2 TX needed?Example
Full entity creationYes (inscription + rune)Yes (create instance)Anchored institution + process
L2-only creationNoYes (create instance)Unanchored process execution
Step executionNoYes (EVM tx)Execute step in BINSTProcess
VerificationNoNo (read only)Check membership, verify proof

Read/Write Phase Model

Two Transaction Domains

BINST operations happen in two independent domains:

  1. Bitcoin transactions — deliberate, user-initiated actions that create or transfer identity (inscriptions, runes)
  2. L2 transactions — EVM transactions on the processing delegate for institutional logic

These domains are decoupled. A Bitcoin inscription and an L2 process instance are separate operations that can happen in any order. The batch proof that anchors L2 state to Bitcoin happens automatically — the user doesn't trigger it.

USER ACTION           L2 (Citrea)                    Bitcoin
─────────────────────────────────────────────────────────────
Inscribe Institution  (nothing on L2)                ← ordinal inscription
                                                        (user-initiated)

Inscribe Template     (nothing on L2)                ← child inscription
                                                        (linked to institution)

Create Instance       → factory.createInstance()      (nothing yet)
                      → BINSTProcess deployed on L2

                      ... L2 batches state ...     
                      
                      → batch proof inscribed          ← Bitcoin DA write
                                                         (automatic, not 
                                                          user-initiated)

Execute Steps         → instance.executeStep()        (nothing yet)
                      → step state updated on L2

                      ... L2 batches state ...

                      → batch proof inscribed          ← Bitcoin DA write

Write Phases (User-Initiated Transactions)

ActionWhereWho pays / signs
Inscribe institutionBitcoin (ordinal)User, BTC wallet
Inscribe process templateBitcoin (ordinal, child)User, BTC wallet
Etch membership RuneBitcoin (rune)User, BTC wallet
Send Rune to memberBitcoin (rune)Admin, BTC wallet
Create process instanceCitrea (EVM)Admin, EVM wallet
Execute stepCitrea (EVM)Authorized user, EVM wallet

Automatic (No User Action)

ActionWhereWho pays
ZK batch proofBitcoin DACitrea sequencer (periodic, async)

The user does not create a Bitcoin transaction when they interact with Citrea. The batch proof is automatic — the sequencer batches L2 state changes and inscribes the ZK proof on Bitcoin periodically. The user doesn't trigger it or pay for it.

Read Phases (Free)

ActionWhereCost
Verify membershipCitrea (EVM view call)Free
Check process stateCitrea (EVM view call)Free
Verify inscription existsBitcoin (indexer query)Free
Verify batch proofBitcoin DA (decode)Free

Wallet UX

Current: Two Wallets

  • Bitcoin wallet (UniSat, SafePal BTC) — for inscriptions and runes
  • EVM wallet (MetaMask, SafePal EVM) — for L2 transactions

The Schnorr precompile on Citrea (0x5a) means contracts can verify Bitcoin Schnorr signatures, but the current flow requires both wallets.

Future: Single Bitcoin Wallet

Account abstraction or Schnorr-verified sessions will allow the user to sign once with their Bitcoin key, and an AA layer submits to the L2. One wallet, one identity.

Creating an Institution

The full lifecycle from key generation to a fully anchored Bitcoin-sovereign institution.

Step-by-Step Flow

1. Admin generates a Bitcoin key pair (x-only Taproot pubkey)
   → this key IS the institution's identity root
   → everything else derives from this key

2. Admin inscribes institution on Bitcoin (Ordinal)
   → metaprotocol: "binst", body: institution metadata
   → gets inscription ID: abc123...i0
   → inscription lives in a Taproot vault UTXO → admin owns it
   → the inscription is the institution's birth certificate

3. Admin etches membership Rune on Bitcoin
   → INSTITUTION•MEMBER, premine: 1
   → admin holds the initial unit

4. Institution is now discoverable on Bitcoin
   → any Ordinals explorer shows the inscription
   → the admin pubkey in the body identifies the controller
   → the inscription ID is the stable, cross-chain identifier

5. L2 state reaches Bitcoin via batch proof
   → institution is represented on Bitcoin via:
      a) Ordinal inscription (identity — AUTHORITATIVE)
      b) Rune (membership token)
      c) State diff in batch proof (L2 computational state)

Note: The institution inscription ID can be referenced from any L2. If a BINSTProcess instance on Citrea references this inscription via templateInscriptionId, the provenance chain is verifiable.

Note: Steps 2–3 (Bitcoin) are independent of any L2 activity. If a user creates L2 process instances before inscribing, the institution is UNANCHORED (see Institution Anchoring Lifecycle).

Transaction Summary

StepChainTransaction typeCost
Generate keyOfflineNoneFree
Inscribe identityBitcoinOrdinal inscription (~500B)~$2–5
Etch RuneBitcoinRunestone in OP_RETURN~$1–3
Batch proofBitcoinAutomatic (sequencer)User doesn't pay

The Inscription Envelope

Every BINST inscription uses the Ordinals envelope format:

OP_FALSE OP_IF
  OP_PUSH "ord"
  OP_PUSH 1                              ← content type tag
  OP_PUSH "application/json"             ← MIME type
  OP_PUSH 7                              ← metaprotocol tag
  OP_PUSH "binst"                        ← protocol identifier
  OP_PUSH 3                              ← parent tag (optional on root)
  OP_PUSH <binst-root-inscription-id>    ← provenance chain
  OP_PUSH 0                              ← body separator
  OP_PUSH '{
    "v": 0,
    "type": "institution",
    "name": "Acme Financial",
    "admin": "a3f4b2c1d5e6f7890123456789abcdef0123456789abcdef0123456789abcdef",
    "citrea_contract": "0x1234...5678",
    "membership_rune": "ACME•MEMBER",
    "description": "Acme Financial pilot institution",
    "website": "https://acme.example"
  }'
OP_ENDIF

See Inscription Schema for the full JSON schema specification.

Membership & Runes

How Membership Works

Each institution etches a Rune on Bitcoin that represents membership. The Rune is a fungible token — holding ≥1 unit means "you are a member."

Rune: ACME•MEMBER
  Divisibility: 0  (whole units only — member or not)
  Symbol: 🏛
  Premine: 1  (admin gets the first unit)
  Terms: cap=1000, amount=1 (admin mints and distributes)

Operations

  • Check membership: "Does Alice hold ≥1 ACME•MEMBER?" — standard Rune indexer query. No L2 needed.
  • Add member: Admin sends 1 unit to new member's Bitcoin address.
  • Remove member: Admin burns the token via edict, or member sends it back.
  • Visible: Members see membership in any Rune-aware wallet (UniSat, SafePal BTC).

Adding a Member (Full Flow)

1. Admin sends 1 INSTITUTION•MEMBER rune to new member's address
   → member now holds membership token in their Bitcoin wallet
   → visible in any Rune-aware wallet or indexer

2. Membership is verifiable on Bitcoin immediately
   → any Rune indexer can confirm the balance
   → no L2 interaction required for membership checks

3. L2 state reaches Bitcoin via batch proof
   → if any L2 process references member activity, it is ZK-proven on Bitcoin

L1 + L2 Mirroring

Membership exists in two places simultaneously:

LayerHow membership is representedHow to check
Bitcoin L1Rune balance (ACME•MEMBER ≥ 1)Any Rune indexer
L2 (Citrea)On-chain state in ZK batch proofBatch proof decode

Both representations should be kept in sync. The Bitcoin Rune is the authoritative source — if there's a discrepancy, the Rune balance wins.

Future: Governance Tokens

A separate Rune (e.g., ACME•VOTE) with divisibility could represent weighted voting power. Governance becomes a token distribution problem — not a staking competition.

Process Execution

Processes are the core operational primitive of BINST — multi-step workflows defined on Bitcoin L1 and executed on L2.

Concepts

  • ProcessTemplate — an immutable blueprint inscribed on Bitcoin L1, defining a sequence of steps (name, action type). A child inscription of the institution.
  • BINSTProcess (L2) — a self-contained instance deployed on Citrea via BINSTProcessFactory. Carries embedded step definitions + a templateInscriptionId linking back to Bitcoin.

Creating an Instance

1. Admin inscribes a ProcessTemplate on Bitcoin L1 (child of institution)
   → inscription body contains step names, action types, institution_id

2. Admin opens the Execute view in the webapp
   → selects the template → clicks "Create Instance on Citrea"
   → factory.createInstance(inscriptionId, stepNames[], stepActionTypes[])
   → eth_sendTransaction via MetaMask / Brave Wallet / WalletConnect
   → InstanceCreated event → instance address parsed from tx receipt

3. Instance address is stored in localStorage and shown in the UI

Executing Steps

1. Member opens the Execute view, instance is loaded from Citrea
   → on-chain state read: currentStepIndex, totalSteps, completed,
     step names, step states (all via eth_call — no wallet needed)

2. Member clicks "Execute Step →"
   → instance.executeStep(Completed, evidenceData)
   → eth_sendTransaction via EVM wallet (one pop-up per step)
   → StepExecuted event emitted on-chain

3. After tx confirms (~2s on Citrea testnet):
   → UI reloads state from contract (currentStepIndex advances)
   → Tx hash saved to localStorage for finality tracking

4. Bitcoin settlement (automatic, no user action):
   → Soft Confirmation: sequencer orders the tx (~instant)
   → Committed: sequencer commitment inscribed on Bitcoin
   → Proven: ZK batch proof inscribed on Bitcoin
   → The step execution is now part of Bitcoin's permanent record

Signing model: Each step execution requires one EVM wallet signature via eth_sendTransaction. There is no batching or queue — each step fires immediately on button click. The webapp's Execute view delegates all EVM/ABI logic to citrea.rs.

No L1 inscription per step: In the current pilot, step executions are NOT individually inscribed on Bitcoin. They reach Bitcoin automatically via Citrea's ZK batch proofs. A future phase may add optional L1 inscriptions for step evidence.

Entity-to-Primitive Mapping

EntityNatureBitcoin PrimitiveL2 PrimitiveReasoning
InstitutionUnique, one-of-oneOrdinal inscription(none — L1 only)Identity lives on Bitcoin
Process TemplateUnique, immutableOrdinal inscription (child of institution)(none — L1 only)Definition lives on Bitcoin
Process InstanceUnique, mutable state(represented via ZK batch proofs)BINSTProcess contractExecution state on L2, settled to BTC via proofs
Step ExecutionImmutable event record(settled via ZK batch proofs)StepExecuted eventReaches Bitcoin through batch proof, not individual inscription
MembershipFungible relationshipRune balance(none — L1 only)"Hold ≥1 token = member"
Governance voteFungible weightRune balance (separate)(none — L1 only)Transferable, weighted voting power

Cross-Chain Execution Safety

A BINSTProcess instance has exactly one home chain. Steps execute only on that chain. This is enforced by design:

  • Each instance is self-contained: it carries its own step definitions + templateInscriptionId
  • No external contract dependencies on L2 — migration is a single message
  • The type system prevents concurrent mutation across chains
  • No rollback/rewind mechanism is needed — conflicts are prevented, not repaired

If a process on one L2 needs to reference a step completed on another L2, it performs a cross-chain read (via Bitcoin DA batch proof or LayerZero query). No mutation, no conflict.

Migration model (future — Phase 65)

BINSTProcess includes sourceChainId and sourceAddress fields for LayerZero migration. The full state (templateInscriptionId, steps, currentStepIndex, stepStates, creator`) is sent to the destination chain, which deploys a new instance pre-loaded with that state.

See Cross-Chain Synchronization and Switching L2s for the full model.

Switching L2s

One of BINST's core properties: the L2 is replaceable. Here's how migration works.

Flow

1. Admin decides to move from Citrea to another L2 (e.g., BOB)

2. BINSTProcess instance carries full state:
   → templateInscriptionId (L1 anchor — same on any chain)
   → steps[] (embedded at creation, no external dependency)
   → currentStepIndex + stepStates[] (current execution state)
   → creator address

3. Migration via LayerZero (Phase 65 — future):
   → _lzSend() on source chain carries the full state
   → Destination chain deploys new BINSTProcess with that state
   → sourceChainId + sourceAddress fields link back to origin

4. The Bitcoin-layer identity is unchanged:
   → same inscription, same UTXO, same admin key
   → same membership Rune, same member balances
   → provenance chain is intact

5. The old L2 instance becomes historical — its batch proofs
   remain on Bitcoin as a permanent record of past operations

6. The new L2 instance continues execution from where it left off

What Survives

ElementAfter migration
Inscription ID✅ Unchanged — same identity on Bitcoin
Admin key✅ Unchanged — same UTXO, same authority
Membership Runes✅ Unchanged — live on Bitcoin, not on any L2
Provenance chain✅ Unchanged — parent/child inscriptions intact
Old L2 state✅ Preserved — batch proofs on Bitcoin are permanent
New L2 instance🆕 Deployed with full state, same templateInscriptionId

Why This Works

The Bitcoin key is the root of authority, not the L2 contract address. The inscription is the institution's identity, not the Solidity code. When you move L2s, you're changing the processing engine, not the institution itself.

Because BINSTProcess instances are self-contained (embedded step definitions, no dependency on external L2 contracts), migration is a single message. The destination chain doesn't need a pre-deployed factory, institution, or template — it just deploys a new BINSTProcess pre-loaded with the migrated state.

Admin Transfer

Transferring institutional control from one admin to another.

Flow

1. Current admin transfers the inscription UTXO to new admin's vault
   → on Bitcoin: new admin now controls the UTXO (Leaf 0 spend)
   → the inscription ID stays the same; the controlling key changes

2. New admin calls transferAdmin() on the L2 contract
   → L2 contract updates admin address to match new key holder

3. Both layers now agree: the new admin controls the institution
   on Bitcoin (UTXO) and on the L2 (contract state)

Conflict Resolution

If the L2 contract admin disagrees with the UTXO owner, the UTXO owner is authoritative. The L2 contract is expected to be updated to match.

A future version could enforce this via Bitcoin-key-based signature verification on the L2 (using Citrea's Schnorr precompile).

Security Considerations

The Taproot vault provides safety for admin transfers:

  • Leaf 0 (admin): CSV-delayed (~24 hours / 144 blocks). The delay gives time to abort if the transfer was unauthorized.
  • Leaf 1 (committee): Immediate 2-of-3 multisig. Emergency override if the admin key is compromised during transfer.

See Taproot Vault for the full vault specification.

Cross-Chain Synchronization

⚠ Architectural vision — not built in the pilot. This page describes Phase 3 design. The current pilot runs on a single L2 (Citrea). Cross-chain relay and mirror contracts do not exist yet. The Bitcoin DA verification path is live; the LayerZero identity sync is not.

BINST institutions can be omnipresent across multiple L2s simultaneously, not just portable between them.

The Dual-Channel Model

Bitcoin (inscription + rune) = AUTHORITY
    ↓
Home L2 (e.g., Citrea) = PRIMARY DELEGATE (read + write)
    ↓ LayerZero V2 (identity)     ↓ Bitcoin DA (execution proof)
Mirror L2s (BOB, Rootstock, etc.) = READ-ONLY MIRRORS

Two sync channels, each optimized for different needs:

ChannelWhat it syncsSpeedTrust model
LayerZero V2Identity: name, admin, inscriptionIdFast (real-time)DVN-configurable
Bitcoin DAExecution: process step states, completion proofsSlow (batch interval)Trustless (ZK-proven)

LayerZero V2 on Citrea

LayerZero V2 has deployed endpoints on Citrea mainnet (Chain ID 4114, Endpoint ID 30403, endpoint address 0x6F475642a6e85809B1c36Fa62763669b1b48DD5B) and supports 8+ Bitcoin L2s:

  • Citrea, BOB, Bitlayer, BEVM, Merlin, Rootstock, Hemi, Corn, Goat

This maps directly to BINST's L2 portability promise.

The Three State Tiers

TierDataSync methodWritable?
IdentityinscriptionId, admin, nameLayerZero (fast)Home chain only
MembershipRune balanceBitcoin L1 (authoritative)Bitcoin only
ExecutionstepStates[], process progressBitcoin DA (trustless)Home chain only

Single-Writer Rule

Critical invariant: A BINSTProcess instance lives on exactly one home chain. It is created there, executed there, completed there. Mirror chains can read process state but cannot mutate it.

This prevents the core distributed systems problem: two L2s executing the same step simultaneously and producing conflicting state.

Why not rewind/rollback?

A rollback mechanism would mean:

  1. You allowed the conflict to happen
  2. You detected it after the fact
  3. You unwound one or both executions

This adds enormous complexity and violates the simplicity principle. Instead, architectural prevention eliminates the problem entirely:

  • Mirror contracts expose only view functions: read-only access to process state, step status, and completion
  • No executeStep(), no createInstance(), no mutating functions
  • The type system enforces the invariant at compile time

Cross-chain process references

If a process on BOB needs to verify a step completed on Citrea (e.g., "KYC must complete before this audit"), it performs a cross-chain read:

  • Query the Citrea mirror via LayerZero (fast, real-time)
  • Or verify the step state from Bitcoin DA batch proof (slower, trustless)

No mutation, no conflict.

Trust Considerations

LayerZero introduces a dependency outside Bitcoin — messages go through DVNs (Decentralized Verifier Networks), not Bitcoin DA. This is acceptable because:

  • The authority remains on Bitcoin (inscription UTXO ownership)
  • Mirrors are convenience, not consensus — any L2 can independently verify the inscription on Bitcoin
  • If LayerZero goes down, institutions still function on their home L2
  • LayerZero's principles align with BINST: permissionless, immutable endpoints, censorship-resistant

Implementation Plan

Phase 3 will introduce:

  • BINSTRelay — an OApp that listens for institution events on the home L2 and broadcasts identity state to registered mirror chains
  • ProcessMirror — a read-only contract on non-home chains that receives and exposes process state and institution identity data

This turns BINST from "portable across L2s" (manual redeploy) into "omnipresent across L2s" (automatic sync).

Inscription Schema

The binst metaprotocol defines a formal JSON schema for Ordinals inscriptions.

Envelope Format

Every BINST inscription uses the Ordinals envelope:

  • Content type (tag 1) = application/json
  • Metaprotocol (tag 7) = binst
  • Parent (tag 3) = parent inscription ID (provenance chain)
  • Metadata (tag 5) = optional CBOR-encoded metadata
  • Body = JSON matching the schema below

Entity Types

TypeParent requirementPurpose
institutionNone (root of its tree)Institution identity and metadata
process_templateInstitution inscriptionImmutable process blueprint
process_instanceProcess template inscriptionRunning execution of a template
step_executionProcess instance inscriptionRecord of a step execution (optional)

Provenance Hierarchy

institution (root — no parent required)
 └─ process_template (child of institution)
     └─ process_instance (child of template)
         └─ step_execution (child of instance)

Schema Version

"v": 0 — pilot / testnet4. Breaking changes increment the version.

Example: Institution

{
  "v": 0,
  "type": "institution",
  "name": "Acme Financial",
  "admin": "a3f4b2c1d5e6f7890123456789abcdef0123456789abcdef0123456789abcdef",
  "citrea_contract": "0x1234...5678",
  "membership_rune": "ACME•MEMBER",
  "description": "Acme Financial pilot institution",
  "website": "https://acme.example"
}

Example: Process Template

{
  "v": 0,
  "type": "process_template",
  "name": "KYC Onboarding",
  "institution_inscription_id": "abc123...i0",
  "steps": [
    { "name": "Submit Documents", "action_type": "upload" },
    { "name": "Review", "action_type": "approval" },
    { "name": "Final Approval", "action_type": "approval" }
  ]
}

Example: Process Instance

{
  "v": 0,
  "type": "process_instance",
  "template_inscription_id": "def456...i0",
  "created_by": "a3f4b2c1...",
  "status": "in_progress"
}

Example: Step Execution

{
  "v": 0,
  "type": "step_execution",
  "instance_inscription_id": "ghi789...i0",
  "step_index": 0,
  "actor": "b5e6c7d8...",
  "status": "completed",
  "timestamp": "2026-03-15T14:30:00Z"
}

Validation

The full JSON Schema (2020-12) is available at binst-metaprotocol.json.

# Validate with ajv-cli
ajv validate -s binst-metaprotocol.json -d examples/institution.json

Ordinals — Entity Identity

Every BINST entity is a permanent Ordinals inscription on Bitcoin. The inscription is the entity's birth certificate, identity anchor, and metadata carrier.

How It Works

BINST inscriptions use the Ordinals protocol with these conventions:

  • Metaprotocol (tag 7) = "binst" — filterable by any indexer
  • Content type = application/json
  • Metadata (tag 5) = CBOR-encoded structured data
  • Parent (tag 3) = parent inscription ID (provenance chain)

Provenance Hierarchy

Entities form a parent/child tree rooted at the institution inscription:

Institution "Acme Financial" (root inscription)
 ├── Process Template "KYC Onboarding" (child)
 │    ├── Instance #1 (grandchild)
 │    │    ├── Step 1 executed by Alice (event)
 │    │    └── Step 2 executed by Bob (event)
 │    └── Instance #2
 └── Process Template "Loan Approval"

Anyone running ord can verify the full provenance chain — "KYC Onboarding was created by Acme Financial" — without touching any L2.

Security note — tag 3 is self-declared, not cryptographic. Writing tag 3 bytes into the Ordinals envelope requires no private key; any party can claim any parent inscription ID. The only cryptographically secure provenance proof is the parent sat being spent as an input of the child's reveal transaction — that requires the holder's private key. The BINST webapp distinguishes these with a three-level badge system: ⛓ provenance verified (sat-spend proven), ✓ unverified (tag 3 only), and ⬡ declared (JSON field only). See Provenance & Parent-Child Security for the full security model, verification algorithm, and real-world testnet4 examples.

Each entity in the tree gets its own sat, own UTXO, own vault. The parent-child relationship links them, but each inscription is independently held and independently protected. New information about an institution (a new process, a completed instance) is always a new child inscription — never a modification of the parent.

This keeps the institution's root inscription UTXO locked and safe while the child tree grows as the institution operates.

What gets inscribed vs what gets ZK-proven

Not every level of the tree needs its own Ordinals inscription. ZK batch proofs already carry all L2 state changes to Bitcoin automatically (the sequencer pays, not the user). The practical split:

LevelOrdinals inscription?Rationale
InstitutionYes — alwaysThis IS the identity. Permanent, inscribed once.
Process TemplateYesProves "this process belongs to this institution" on Bitcoin. Inscribed once, never changes.
Process InstanceOptionalCreated frequently. L2 state + ZK batch proof is sufficient.
Step ExecutionNoHigh-frequency L2 operations. Already ZK-proven via batch proofs.

The top two levels justify the inscription cost — infrequent, high-value, permanent identity. The bottom two are operational data better served by the L2 + ZK proof path:

Institution ─────── Ordinals inscription (identity)
Process Template ── Ordinals inscription (blueprint)
Process Instance ── L2 state, verified via ZK batch proofs
Step Execution ──── L2 state, verified via ZK batch proofs

Ownership

The inscription UTXO is controlled by the admin's Bitcoin key. This key is the canonical authority — whoever controls this UTXO controls the institution, its child entities, and any L2 contracts bound to it.

  • Transfer the UTXO = transfer admin rights (on Bitcoin and all L2s)
  • L2 contracts derive their authority from this key, not the other way around
  • A Bitcoin maximalist holds their institution in their Bitcoin wallet

Reinscription Policy

The first inscription is canonical (per Ordinals protocol). Reinscriptions append to the history — they do not overwrite:

  • Inscription 1 (canonical): "Created Acme Financial, admin=pk1"
  • Reinscription 2: "Updated description"
  • Reinscription 3: "Admin transferred to pk2"

Institutions cannot erase their history. The append-only model matches the transparency requirement.

Rule: use child inscriptions for data updates. Reserve reinscription for ownership events only (admin transfer, key rotation). This avoids spending the parent inscription's vault UTXO — the riskiest operation in the protocol — for routine updates. A new process template is a new child, not a reinscription of the institution.

Discovery

BINST inscriptions are discoverable through standard tooling:

  • Ordinals explorers (ordinals.com, ord.io, Hiro) — search by metaprotocol
  • Ordinals wallets (UniSat, SafePal BTC) — shows as an asset
  • Self-hosted ord indexer — trustless, complete access
  • No custom BINST software needed for basic discovery

See Mempool Pre-Consolidation for the rules and risk model that apply while a commit/reveal chain is in-flight between broadcast and confirmation.

Provenance & Parent-Child Security

BINST uses a three-level provenance system to classify how strongly a process template's claimed relationship to its institution has been verified on-chain. Understanding the security guarantees of each level is critical: not all "on-chain" claims are equally trustworthy.


The Core Question

When a process template inscription declares "institution_id": "abc…i0", how do we know the inscription was really created by the institution's admin, and not by an arbitrary third party mimicking the relationship?

Bitcoin provides two mechanisms for this — and they have very different security properties.


Mechanism 1 — JSON field (institution_id)

The simplest declaration: the JSON body contains a field pointing to the parent institution.

{
  "type": "process_template",
  "name": "KYC Onboarding",
  "institution_id": "abc123...i0"
}

Security: none. Anyone can write any string into a JSON body. This is a self-declaration with no on-chain enforcement.


Mechanism 2 — Ordinals tag 3 (OP_RETURN parent)

The Ordinals protocol defines tag 3 in the inscription envelope as a "parent" pointer. Indexers like Xverse and Ordiscan expose this as a parents array on the inscription metadata.

OP_FALSE OP_IF
  ...
  03 <parent_inscription_id_bytes>   ← tag 3: parent pointer
  ...
OP_ENDIF

Security: none. Tag 3 is arbitrary bytes written into the witness script of the reveal transaction. No private key is required to write them. Any inscription can claim any parent by copying 32 bytes into the envelope.

This is often confused with cryptographic proof because it appears "on-chain" — but the chain only records that the bytes were written, not that the writer held any relevant key.


Mechanism 3 — Parent sat spent as reveal tx input ✓

The only cryptographically secure provenance proof in Ordinals:

The sat currently holding the parent inscription must be spent as an input (vin) of the child's reveal transaction.

Spending a UTXO requires a valid signature from the private key controlling that address. If the institution's sat appears as a vin of the template's reveal tx, it is mathematically proven that the reveal tx was authorized by whoever held the institution's private key at that moment.

This is what the Ordinals protocol calls parent provenance and what indexers use for their /children endpoint results.


The Three-Level Badge System

The BINST webapp assigns a provenance badge to every process template card:

BadgeLabelMechanismForgeable?
greenprovenance verifiedParent sat spent as vin of reveal tx❌ No — requires private key
amberunverifiedTag 3 in envelope, confirmed by indexer parents field⚠️ Yes — any witness script can contain tag 3
greydeclaredJSON institution_id field only⚠️ Yes — arbitrary JSON

Only the green ⛓ provenance verified badge constitutes a cryptographic security proof of institutional authorship.


How "verified" is determined in the webapp

The webapp uses two independent paths to establish sat-spend proof:

Stage A — Indexer /children endpoint

GET {xverse_base}/v1/inscriptions/{institution_id}/children

Returns inscriptions for which the indexer has traced the institution's sat through the reveal tx inputs — the same sat-tracking that powers the entire Ordinals protocol. This is authoritative.

Limitation: CORS-blocked from localhost during development. Works correctly from any deployed domain.

Stage B — Direct Mempool tx input check

For every template in localStorage, the webapp independently verifies the sat-spend without relying on an indexer:

  1. Derive the reveal txid from the inscription ID. Inscription IDs always have the format {reveal_txid}i{vout}, so the reveal txid is simply the part before the i.

    "46f5557a10e7a75ee0b03abea6c0ecaf9f74ce7ec7d3353359744cc518ca5226i0"
     ├── reveal_txid = "46f5557a10e7a75ee0b03abea6c0ecaf9f74ce7ec7d3353359744cc518ca5226"
     └── vout        = 0
    
  2. Fetch the reveal tx from the Mempool.space API — no CORS restriction, no API key required:

    GET https://mempool.space/testnet4/api/tx/{reveal_txid}
    
  3. Check the inputs (vin array) against two known locations of the institution's sat:

    • Genesis UTXO{inst_reveal_txid}:0 — where the institution sat landed when the institution was first inscribed. Matches the first child inscription (before the sat has moved).

    • Current UTXO — the currentOutput field from the Xverse indexer for the institution inscription. Matches the most recent child inscription (the one that last moved the sat).

    reveal_tx.vin[i].txid == institution_genesis_txid && vin[i].vout == 0
    OR
    reveal_tx.vin[i].txid == institution_current_txid && vin[i].vout == current_vout
    
  4. If any vin matches"verified". The reveal tx was signed by the holder of the institution's private key.

  5. If no vin matches → fall back to Xverse parents tag-3 check → "linked" (on-chain declared, not cryptographically secured).

Coverage matrix

Child position in chainStage A (prod)Stage B genesisStage B current
First child
Middle child
Most recent child

Stage B covers the most common cases without requiring an indexer. Stage A provides complete coverage in production.


Why tag 3 alone is insufficient

The on-chain analysis of the BINST testnet4 pilot confirmed this directly. Inscriptions 62 and 63, both created by the webapp and pointing to institution inscription 6 via tag 3, were examined:

Institution 6 genesis UTXO: {inst_txid}:0

Inscription 62 reveal tx (46f5…):
  vin[0]: 7bcd…:0    ← commit UTXO only; no institution sat

Inscription 63 reveal tx chain (150213… → 32ce…):
  vin[0]: 87981…:1   ← change UTXO only; no institution sat

Neither reveal tx spent the institution's sat as an input. Both inscriptions have tag 3 in their envelope pointing to institution 6, and Xverse confirms this in the parents field — yet neither constitutes cryptographic proof of authorship.

The webapp correctly shows amber ✓ unverified for these inscriptions, not green ⛓ provenance verified. The badge is honest: the link is declared on-chain, but not cryptographically secured by a key-spend.


Implications for the protocol

ClaimStrengthWhat it proves
institution_id in JSONWeakNothing — self-declared
Tag 3 in envelopeWeakThat the inscriber knew the parent's ID — nothing more
Parent sat spent as reveal vinStrongThe inscriber held the private key for the institution's sat at the time of inscription

For a production BINST deployment, only sat-spend-proven templates should be trusted as legitimately authored by the institution admin. The "verified" badge is the minimum bar for institutional trust.


Relationship to Ordinals provenance rules

The sat-spend rule is not a BINST invention — it is the core mechanism of the Ordinals parent provenance specification:

For a child to have a recognised parent, the parent's inscription sat must be an input in the child inscription's reveal transaction.

BINST's Stage B simply re-implements this check client-side against the Mempool.space API, without needing a full Ordinals indexer.

Mempool Pre-Consolidation

Every BINST inscription is a two-transaction chain: a commit transaction and a reveal transaction. Between broadcast and confirmation there is a window — the pre-consolidation stage — during which the inscription is live on the network but not yet settled on-chain. This page documents the rules, assumptions, and risks of that window, especially when parent and child inscriptions are chained through it together.


The Two-Transaction Chain

Ordinals inscriptions cannot be created in a single transaction because the inscription script must be committed to before it is revealed, to prevent fee-sniping. The pattern is:

Step 1 — Commit tx
  Input:  wallet UTXO (funds)
  Output 0: P2TR UTXO whose key-path key commits to the reveal script
  Output 1: change back to wallet

Step 2 — Reveal tx
  Input 0: output 0 of the commit tx (spends the script-path)
  Input 1: parent sat UTXO (optional — required for Ordinals provenance)
  Output 0: new inscription sat (dust, 546 sats) — this is the child's identity
  Output 1: parent sat returned to change address (parent inscription survives)
  Witness:  the envelope script carrying the JSON body + tag 3

Each step requires one wallet signature.
2 signatures per inscription, minimum.

For a batch of N inscriptions with no parent/child relationships: N × 2 signatures total, all independent.

For a parent + child pair (e.g. institution + process template): 2 + 2 = 4 signatures, sequentially dependent — the child reveal must spend the parent sat as input 1, which only exists after the parent reveal is broadcast. Children must be inscribed one at a time in sequence; two children cannot be signed in parallel because both would attempt to spend the same UTXO.


Signature Count Reference

Batch contentsMinimum signatures
1 root inscription (institution)2
1 institution + 1 process template4
2 institutions + 2 process templates8
N institutions + M templates(N + M) × 2

These counts are irreducible when Ordinals provenance (parent sat spend) is required. The parent sat must be an input of the child's reveal tx, and it only exists as a spendable UTXO after the parent reveal is broadcast.


The Pre-Consolidation Window

After the parent reveal is broadcast but before it is confirmed, the network holds its output in the unconfirmed UTXO set. During this window:

  • Bitcoin nodes accept transactions that spend unconfirmed outputs (standard CPFP / child-pays-for-parent behaviour)
  • The child reveal can be broadcast immediately, referencing the parent's unconfirmed output as input 1
  • Both transactions sit in the mempool together and are typically mined in the same block or consecutive blocks

This is the pre-consolidation stage: the parent/child inscription chain exists as a coherent unit in the mempool, not yet confirmed, but fully valid.


In-Session vs Cross-Session Chaining

There are two ways a child inscription can reach the pre-consolidation stage.

In-session (both inscriptions signed in the same stack run)

The planner assigns ParentSource::InBatch. After the parent reveal succeeds, its reveal_utxo is passed directly to the child reveal without any network fetch. The child is broadcast immediately after the parent.

Institution reveal → broadcast → reveal_utxo captured in memory
                                       ↓
Process template reveal ← parent_utxo passed in-process
                        → broadcast (parent UTXO unconfirmed, accepted by nodes)

No mempool fetch, no race condition. The parent and child are always consistent with each other. This is the safe path.

Cross-session (parent was broadcast in a prior session)

The parent inscription is already in the mempool or confirmed. The user opens a new session, adds a child to the stack, and signs. The planner assigns ParentSource::FetchMempool: at sign time it calls fetch_inscription_sat_utxo(inscription_id), which queries the Xverse Ordinals indexer for the currentOutput field — the live location of the parent sat — then fetches that UTXO from Mempool.space.

This is the correct approach because the parent sat moves after every child inscription (see Multiple Children below). Fetching vout 0 of the institution's own reveal tx would fail for any institution that already has at least one child.

This also works, but with one additional risk compared to in-session chaining — see the risk table below.


Multiple Children (Fan-Out)

An institution can have any number of process templates. All children reference the same parent inscription ID in their tag 3 field and institution_id JSON body field. The tree is a flat fan-out at the first level of nesting:

Institution  (inst_txid:0)
  ├── Template A  [tag 3 = inst_txid:0]
  ├── Template B  [tag 3 = inst_txid:0]
  └── Template C  [tag 3 = inst_txid:0]

Each child independently claims the same parent. The Ordinals indexer recognises all of them as children of the institution.

The sat travels forward through the child chain

The parent sat is spent as input 1 and returned as output 1 of every child's reveal tx. After each child inscription it lives at a new UTXO:

After institution reveal:    inst_reveal_txid : 0   ← sat starts here

After Template A inscribed:  tmpl_A_reveal_txid : 1  ← sat moved here

After Template B inscribed:  tmpl_B_reveal_txid : 1  ← sat moved here

After Template C inscribed:  tmpl_C_reveal_txid : 1  ← sat moves here

The institution's inscription ID (inst_reveal_txid:0) is permanent and never changes — it is the identity of the institution. Only the UTXO holding its sat changes as children are added.

Sequential constraint

Because each child spends the parent sat as an input, two children cannot be signed in parallel — both would reference the same UTXO and only the first to broadcast would be accepted. Children must be inscribed one at a time, each waiting for the previous child's reveal to be broadcast before the next one is signed.

How the webapp resolves the current sat location

Before signing a child's reveal, the webapp calls the Xverse Ordinals indexer:

GET /v1/inscriptions/{institution_inscription_id}
→ { "currentOutput": "{txid}:{vout}", ... }

currentOutput is updated by the indexer after every confirmed block. The webapp then calls Mempool.space to fetch the scriptpubkey and value for that output:

GET /testnet4/api/tx/{txid}  →  vout[{vout}].scriptpubkey + value

This two-step lookup (fetch_inscription_sat_utxo) is correct for any number of existing children. For in-batch parents (institution not yet broadcast), the sat location is held in memory directly after the parent reveal succeeds — no indexer call needed.


Risk Table

RiskIn-sessionCross-sessionMitigation
Parent UTXO not yet availableNone — held in memoryLow — Mempool.space returns unconfirmed txsfetch_inscription_sat_utxo falls back to vout 0 of institution reveal; clear error if fetch fails
Wrong UTXO fetched (sat already moved to later child)None — in-memory reveal UTXO is always currentWould fail if vout 0 of institution reveal used directlyResolved: Xverse currentOutput always returns the live sat location
Parent gets RBF-replaced before child is signedNone — both signed atomicallyPossible — all txs are RBF-enabled (sequence = 0xFFFFFFFD)Only the key-holder can initiate RBF; typical flow is uninterrupted
Two children signed in parallel (double-spend of parent sat)Not possible — stack executes sequentiallyNot possible — UI enforces one-at-a-time signingSequential execution is enforced by the async loop in on_sign_inscribe
Child references evicted parent (mempool full, low fee)Very low — both broadcast immediatelyLow on testnet4; possible on mainnet at high-fee periodsCPFP the child; both parent and child get mined together
institution_id field empty in JSON bodyNot possible if parent_ref set correctlyNot possible if inscription ID stored in localStoragePlanner validates and warns; stack UI shows ↑ batch / ↑ mempool
Ordinals indexer sees child before parentIndexers process by block, not mempoolSameNon-issue — indexers only act on confirmed blocks

RBF and Our Transactions

Every transaction produced by the BINST webapp sets:

sequence = 0xFFFFFFFD   (Sequence::ENABLE_RBF_NO_LOCKTIME)

This opts every transaction into Replace-by-Fee signalling. The practical effect:

  • You can fee-bump a stuck transaction via RBF
  • If you RBF the parent reveal after the child reveal is already in the mempool, the child reveal becomes invalid (it references an output that no longer exists in the mempool after the replacement)
  • The child reveal would need to be rebuilt and re-signed against the replacement parent

Rule: do not RBF a parent inscription reveal if a child has already been broadcast against it. If you need to fee-bump, use CPFP on the child instead (add a new output spend that carries a higher fee), which pulls both the parent and child into the next block.


Assumptions Made by the Planner

The stack_plan::build_plan function makes these assumptions at planning time:

  1. In-batch parents always precede their children in the stack (enforced by the UI — institution must be added before its templates).

  2. Cross-session parents are findable in localStorage with a valid reveal_txid. If not present, a warning is emitted and the parent source falls back to an external fetch attempt.

  3. The Xverse Ordinals indexer is available at sign time. If it is unreachable, fetch_inscription_sat_utxo falls back to vout 0 of the institution's own reveal tx — which is correct for a fresh institution with no children, but will fail for one that already has children. A clear error is returned and no malformed transaction is broadcast.

  4. The parent inscription was not RBF'd between planning and signing. Safe in practice: only the key-holder can initiate RBF, and the typical flow is sequential (plan → sign → broadcast without interruption).


What "Pre-Consolidation" Means for Ordinals Provenance

Ordinals provenance requires two things:

  1. Tag 3 in the reveal script envelope — the parent inscription ID encoded as a script push. This is present in the JSON body field (institution_id) and as an Ordinals envelope tag, set by the txbuilder at build time.

  2. The parent sat spent as an input of the child's reveal tx. This is what the UTXO-spend proof adds: the parent sat moves from the parent reveal's output into the child reveal's input, and the Ordinals indexer traces sat ownership to confirm the relationship.

Both are satisfied during pre-consolidation:

  • Tag 3 is in the witness script — it is final the moment the PSBT is signed
  • The parent sat is in the mempool UTXO set and is legally spendable

The Ordinals indexer does not act on mempool state — it only processes confirmed blocks. So from the indexer's perspective, both the parent and child appear in confirmed blocks (possibly the same block, possibly consecutive blocks). Provenance is established cleanly either way.


Summary

The pre-consolidation stage is a normal and safe operating condition for BINST inscriptions, subject to the following practical rules:

  • Preferred: sign institution + templates together in one stack session (in-batch path, zero network dependency, zero RBF risk)
  • Acceptable: broadcast child against an unconfirmed parent from a prior session (cross-session path, works because mempool accepts unconfirmed UTXO spends)
  • Avoid: RBF-bumping a parent after its child has been broadcast
  • Avoid: broadcasting a child when the parent is not yet in the mempool (nodes will reject it — the UTXO does not exist yet)

Runes — Membership Tokens

Each institution etches a Rune that represents membership on Bitcoin L1.

Configuration

Rune: ACME•MEMBER
  Divisibility: 0  (whole units only — member or not)
  Symbol: 🏛
  Premine: 1  (admin gets the first unit)
  Terms: cap=1000, amount=1 (admin mints and distributes)

Operations

ActionHowWho
Check membershipQuery Rune balance ≥ 1Anyone
Add memberSend 1 unit to member's Bitcoin addressAdmin
Remove memberBurn via edict, or member sends backAdmin or member
View membershipAny Rune-aware walletMember

No Custom Software Needed

Membership is a standard Rune balance check — any Rune indexer, any Rune-aware wallet (UniSat, SafePal BTC), or a self-hosted ord can verify it. No BINST-specific tooling required for basic membership queries.

Future: Governance Tokens

A separate Rune (e.g., ACME•VOTE) with divisibility could represent weighted voting power. Governance becomes a token distribution problem — not a staking competition.

Taproot Vault — UTXO Safety

The inscription UTXO is the root of authority. Losing it means losing the institution. The Taproot vault prevents accidental spending at the consensus level.

The Risk

An admin who spends the inscription UTXO in a non-Ordinals-aware wallet loses control of the inscription. The data remains on Bitcoin permanently, but the UTXO tracking it moves to an unknown party.

This is the most critical risk in the protocol. The vault is not optional — it is essential infrastructure.

Miniscript Policy

The vault is defined as a BIP 379 miniscript policy:

or(
  and(pk(admin), older(144)),       ← admin transfer, ~24h CSV delay
  thresh(2, pk(A), pk(B), pk(C))    ← 2-of-3 committee, immediate
)

This policy is compiled to a Taproot descriptor using rust-miniscript:

tr(NUMS, { and(pk(admin),older(144)), thresh(2,pk(A),pk(B),pk(C)) })
  • Internal key = NUMS point (unspendable — disables key-path spend)
  • Leaf 0 = admin single-sig + 144-block CSV delay
  • Leaf 1 = 2-of-3 committee multisig (immediate)

Why Miniscript?

The previous implementation used hand-rolled Tapscript (taproot-vault.ts). Miniscript gives us:

  1. Wallet compatibility — the descriptor is importable into Sparrow, Liana, Nunchuk, and any BIP 379 wallet. Users can sign vault spends with standard software.
  2. Compiler-verified correctness — the miniscript compiler guarantees the spending conditions match the policy. No hand-rolled opcode bugs.
  3. Witness size analysis — the compiler provides worst-case witness sizes for fee estimation.
  4. Extensibility — new policies (timelocked multisig, decay trees) are a one-line policy change.

Script Structure

The compiled descriptor produces the same Taproot structure:

Taproot output:
  Internal key: NUMS point (unspendable — disables key-path spend)

  Script tree:
    Leaf 0 (admin transfer — time-delayed):
      <admin_pubkey> OP_CHECKSIG
      <144> OP_CHECKSEQUENCEVERIFY OP_DROP     ← ~24h delay

    Leaf 1 (committee override — immediate):
      <key_A> OP_CHECKSIG
      <key_B> OP_CHECKSIGADD
      <key_C> OP_CHECKSIGADD
      <2> OP_NUMEQUAL

How It Works

PathWhoDelayPurpose
Key pathNobodyDisabled (NUMS internal key) — no accidental spend possible
Leaf 0Admin (single key)~24 hours (144 blocks CSV)Deliberate admin transfer with safety delay
Leaf 12-of-3 committeeImmediateEmergency override for key loss or compromise

Rust Implementation

The vault module lives in binst-decoder/src/vault.rs:

#![allow(unused)]
fn main() {
use binst_decoder::vault::{VaultPolicy, parse_xonly};

let policy = VaultPolicy::new(
    parse_xonly("79be667e…").unwrap(),
    [parse_xonly("c6047f94…").unwrap(),
     parse_xonly("f9308a01…").unwrap(),
     parse_xonly("e493dbf1…").unwrap()],
);

let desc = policy.compile().unwrap();
println!("{}", desc.descriptor);        // tr(NUMS, {…})
println!("{}", desc.address_testnet);   // tb1p…
println!("{}", desc.address_mainnet);   // bc1p…

for path in &desc.spending_paths {
    println!("{}: {} keys, {:?} CSV, ~{} vbytes",
        path.name, path.required_keys.len(),
        path.timelock_blocks, path.witness_size);
}
}

The module also compiles to WASM (--features wasm) for in-browser vault generation.

Why This Works

  • No accidental spending — the key path is dead. Regular wallets can't spend it.
  • Admin retains control — Leaf 0 allows deliberate moves with a CSV safety net.
  • Committee backstop — Leaf 1 is the "break glass" emergency path.
  • Standard Bitcoin — uses only Taproot (BIP 341/342), OP_CHECKSIG, OP_CSV, OP_CHECKSIGADD. No soft fork needed.
  • Wallet-native — the descriptor imports directly into BIP 379 wallets (Sparrow, Liana, Nunchuk).
  • Ordinals-compatibleord tracks inscriptions regardless of spending script.

Future enhancement: When OP_CTV or OP_CAT activates, the vault can enforce that the UTXO is only spendable to pre-approved addresses (true covenant protection).

See also: Vault Unlock Flows · Sat Isolation · Graceful Degradation

Vault Unlock Flows

The vault locks the inscription UTXO but does not make it permanently frozen.

Path A — Admin Transfer (Leaf 0, ~24h delay)

1. Admin decides to move the inscription (transfer, reinscribe, rotate key)

2. Admin waits until the UTXO is at least 144 blocks old

3. Admin constructs a Bitcoin transaction:
   - Input: the vault UTXO (nSequence = 144)
   - Witness: <admin_signature> <leaf_0_script> <control_block>
   - Output 0: new destination (fresh vault or new owner's vault)

4. Bitcoin consensus validates:
   a) admin_signature valid for admin_pubkey  → OP_CHECKSIG ✓
   b) nSequence ≥ 144 blocks passed           → OP_CSV ✓
   c) Taproot script-path commitment correct   → control_block ✓

5. Transaction confirms. Inscription sat moves to new output.

Path B — Committee Override (Leaf 1, immediate)

1. Emergency: admin key lost or compromised

2. Two of three committee members co-sign

3. Committee constructs a transaction:
   - Input: the vault UTXO (nSequence = 0)
   - Witness: <sig_C_or_empty> <sig_B_or_empty> <sig_A> <leaf_1_script> <control_block>
   - Output 0: recovery destination

4. Bitcoin consensus validates:
   a) 2-of-3 via OP_CHECKSIGADD    → accumulated count ≥ 2 ✓
   b) Script-path commitment        → control_block ✓

5. Transaction confirms immediately.

Re-locking After a Spend

Each vault-to-vault transfer resets the CSV timer:

Vault A ──(admin spends after CSV)──▶ Vault B ──(...)──▶ Vault C

When Would the Admin Unlock?

ScenarioPathWhat happens next
Transfer to new adminLeaf 0Send to new admin's vault; call transferAdmin() on L2
Rotate admin keyLeaf 0Send to vault with new admin pubkey
Reinscribe (update metadata)Leaf 0Spend → new reveal TX → re-vault
Admin key compromisedLeaf 1Committee moves to safe address
Admin key lostLeaf 1Committee recovers to new admin's vault
Migrate to covenant vaultLeaf 0Move to OP_CTV vault when available

Atomic Institution Transfer

An institution accumulates vault UTXOs over time — the institution inscription itself plus child inscriptions (process templates). Transferring admin means moving all of them.

Atomicity comes from Bitcoin itself: any transaction with multiple inputs and multiple outputs is all-or-nothing by consensus rules. A single transaction spending every vault UTXO guarantees that either all inscriptions move to the new admin or none do:

Single transaction (all-or-nothing):

  Inputs:                              Outputs:
  ─────────                            ────────
  vault UTXO 1 (Institution sat)  →   new vault 1 → new admin's key
  vault UTXO 2 (Template A sat)   →   new vault 2 → new admin's key
  vault UTXO 3 (Template B sat)   →   new vault 3 → new admin's key
  fee input (admin's spending)    →   change → admin

Each input uses the Leaf 0 script-path spend. The new admin verifies that every inscription is routed to the correct new vault before the transaction is broadcast.

CSV maturity constraint

Every vault UTXO must be ≥ 144 blocks old for Leaf 0 spends. If a child inscription was created recently (e.g., a new process template), its vault may not have matured yet. In that case, the transfer is split into two transactions:

  • TX 1: all matured vault UTXOs (transfers immediately)
  • TX 2: remaining UTXOs (transfers once CSV matures)

Committee recovery

If the admin is uncooperative, the committee (Leaf 1, 2-of-3 multisig) can construct the same multi-input transaction. Leaf 1 has no CSV delay — committee transfers are immediate.

PSBT as a signing tool

A PSBT (BIP-174/371) is not required for atomicity — any Bitcoin transaction is inherently atomic. PSBT is a workflow format: an ephemeral container for building, inspecting, and co-signing a transaction before broadcast. It is recommended when:

  • Cold keys — the admin signs offline and passes the file to a watch-only node for broadcast.
  • Committee spends — two members sign independently, combine signatures, then finalize — without ever being online at the same time.
  • Audit before broadcast — any party can decode the PSBT and verify inputs, outputs, and script paths before a signature is added.

Once broadcast, the PSBT ceases to exist. The vaults and their Tapscript leaves are what protect the UTXOs — PSBT is just the recommended tool for spending from them.

Sat Isolation

The inscribed satoshi lives on its own dedicated, minimal UTXO — separate from spending funds.

Reveal Transaction Layout

Reveal TX:
  Input 0:  commit UTXO (inscription envelope in witness)

  Output 0: 546 sats → Taproot vault address (script-guarded)
             ↑ the inscribed sat lives HERE, alone
             ├── NUMS internal key (no key-path spend)
             ├── Leaf 0: admin + 144-block CSV delay
             └── Leaf 1: 2-of-3 committee multisig

  Output 1: change → admin's regular spending wallet
             Normal sats, freely spendable, no inscription.

The pointer tag (Ordinals tag 2) explicitly binds the inscription to the first satoshi of output 0.

Why 546 sats?

546 sats is Bitcoin's dust limit — the minimum UTXO value that Bitcoin Core nodes will relay. A 1-sat UTXO would be rejected by the mempool as "dust." The inscription lives on exactly one sat (the first sat of the output, per Ordinals ordinal theory), but the UTXO must hold ≥ 546 sats to exist on the network. The other 545 sats are padding — locked in the vault, recoverable when the UTXO is spent.

Two Layers of Protection

  1. Economic — dust-limit UTXO (546 sats) has no spending value to attract
  2. Consensus — Taproot vault script prevents spending even if attempted

Ordinals vs Runes: UTXO Sharing

A single UTXO can carry multiple ordinals (one per sat) and multiple Rune types simultaneously. The risk profile differs:

OrdinalsRunes
GranularityIndividual satsFungible balances per UTXO
Multiple per UTXOYes (one per sat)Yes (multiple Rune types)
Risk of co-locationHigh — sat ordering is complex, accidental transferLow — Runestone edicts are explicit
BINST approachOne inscription per isolated UTXOMultiple Runes per member UTXO is fine

Merging inscribed UTXOs is dangerous because spending scatters sats across outputs unpredictably (ordinal numbering follows first-in-first-out rules). The pilot isolates each inscription in its own vault UTXO to prevent accidental loss.

Graceful Degradation

The protocol degrades hierarchically — the closer to Bitcoin you lose, the harder the recovery.

Losing the L2 (Graceful)

If Citrea goes down or the user wants to switch L2:

  1. Inscription data is permanent and readable on Bitcoin forever
  2. Membership Runes continue to function on Bitcoin L1
  3. Admin creates new process instances on another L2
  4. New instances reference the same inscription ID via templateInscriptionId
  5. Institution continues with full identity and membership intact

Losing the Inscription UTXO (Serious)

If the admin accidentally spends the inscription UTXO despite vault protection:

  1. Inscription data is permanent and readable forever
  2. L2 process instances continue to function short-term
  3. Admin re-inscribes a recovery record (child of original)
  4. New L2 instances reference the new inscription ID
  5. Original provenance chain is preserved

The vault script exists specifically to make this scenario extremely unlikely.

Losing the Bitcoin Key (Catastrophic)

  1. Committee (Leaf 1, 2-of-3 multisig) recovers the inscription to a new key's vault
  2. Admin creates new L2 process instances from the new key
  3. This is the hardest recovery — the committee backstop is the last resort

The Hierarchy

L2 down              → create instances elsewhere, identity survives on Bitcoin
Inscription UTXO lost → re-inscribe + new L2 instances (harder, but recoverable)
Bitcoin key lost      → committee recovery (hardest, requires multi-sig)

Taproot Coverage Audit

BINST was designed to live as close to Bitcoin L1 as possible. This page cross-references every feature introduced by the Taproot upgrade (BIPs 340, 341, 342) against what the pilot actually uses, deliberately skips, or defers to production.

Features We Use

Taproot FeatureBIPWhere We Use It
P2TR output (SegWit v1)341Vault address (tb1p... / bc1p...) — every inscription UTXO is locked to a Pay-to-Taproot output
x-only public keys (32 bytes)340Admin key, committee keys, NUMS internal key — all stored and transmitted as 32-byte x-only keys, saving 1 byte vs compressed ECDSA
Schnorr signatures (64 bytes)340Admin single-sig (Leaf 0) and committee multi-sig (Leaf 1) — 64-byte Schnorr sigs replace 71–72-byte ECDSA sigs
MAST (Merkelized Alternative Script Tree)3412-leaf script tree: Leaf 0 (admin + CSV) and Leaf 1 (committee override). Only the exercised leaf is revealed on-chain — the other stays hidden
Tagged hashes341TapLeaf, TapBranch, TapTweak — domain-separated SHA-256 used for tree construction and output key derivation
Taptweak (Q = P + t·G)341NUMS internal key + Merkle root → tweaked output key. This is the core of BIP 341 key commitment
NUMS internal key341Provably unspendable point — disables key-path spend entirely, forcing all spends through the script tree
Script-path spend341Both vault unlock paths (admin and committee) use Taproot script-path spending with control blocks
Control blocks341(parity | leaf_version) || internal_key || sibling_hash — built for both leaves, enabling Taproot proof-of-inclusion
Leaf version 0xc0342All leaf scripts use the BIP 342 Tapscript leaf version
OP_CHECKSIG (Schnorr variant)342Admin leaf — single-key Schnorr signature check
OP_CHECKSIGADD342Committee leaf — the BIP 342 replacement for OP_CHECKMULTISIG (which is disabled in Tapscript). Accumulates a counter across multiple signature checks
OP_CHECKSEQUENCEVERIFY (CSV)112Admin leaf — enforces 144-block (~24h) relative timelock before the admin can move the inscription UTXO
Schnorr precompile on Citrea340Citrea's precompile at 0x…0200 can verify BIP-340 Schnorr signatures in Solidity — used for Bitcoin-key-based L2 authorization
Ordinals inscription in witness341The inscription envelope lives inside Tapscript witness data. Taproot's witness discount makes inscriptions economically viable
Bech32m address encoding341All P2TR addresses use Bech32m (BIP 350), distinct from SegWit v0's Bech32

Features We Deliberately Skip

Taproot FeatureBIPWhy We Skip It
Key-path spend341We kill it with the NUMS internal key. The entire purpose of the vault is to prevent accidental spending — a live key-path would let any Taproot-aware wallet move the inscription UTXO.
Key aggregation / MuSig2340MuSig2 aggregates N public keys into a single key for key-path spend. Since we disable the key-path, there is no aggregated key to spend with. For the committee, we use OP_CHECKSIGADD in a script leaf instead — this is simpler, independently auditable, and requires no interactive MuSig signing rounds between committee members.
Batch signature verification340Batch verification is a node-level optimization: Bitcoin Core can verify N Schnorr signatures faster than verifying them individually. This is transparent to script authors — our transactions benefit automatically when nodes use batch validation. There is nothing to implement.
Annex field341The annex is a reserved witness field (identified by the 0x50 prefix) with no current consensus meaning. It is reserved for future soft-fork extensions. No use case today.
OP_SUCCESS opcodes342These are upgrade placeholders that make unrecognized opcodes succeed unconditionally, allowing future soft forks to assign them new semantics. They exist precisely so that new opcodes can be added without a hard fork. Nothing to use today.

Design Rationale

Why kill the key-path?

In a normal P2TR workflow, the key-path is the happy path — it looks like a regular single-sig spend and provides maximum privacy. BINST deliberately sacrifices this because the vault's primary job is to prevent spending:

  • The inscription UTXO is the root of authority
  • Accidental spending = loss of the institution
  • The NUMS point makes the key-path provably dead
  • All spends must go through a script leaf with explicit conditions

This is the correct tradeoff for a protocol where the UTXO represents identity, not money.

Why OP_CHECKSIGADD instead of MuSig2?

MuSig2 would produce a cleaner on-chain footprint (single key, single sig), but it requires:

  1. Interactive signing rounds between committee members
  2. Nonce commitment coordination (two rounds minimum)
  3. Specialized MuSig2 software on each signer's machine
  4. All parties online at roughly the same time

OP_CHECKSIGADD in a Tapscript leaf is:

  1. Non-interactive — each member signs independently
  2. Standard tooling — any BIP-340 Schnorr signer works
  3. Auditable — the script is human-readable and each signature is individually verifiable
  4. PSBT-compatible — members sign via PSBT (BIP 174/371) without being online simultaneously

For an emergency recovery mechanism (which the committee path is), simplicity and independence matter more than on-chain size.

Potential Future Enhancements

These Taproot-adjacent features are not in the pilot but could be adopted in production:

EnhancementBasisWhat It EnablesComplexity
MuSig2 aggregated key-path (separate vault variant)BIP 340Aggregate committee keys into one key for key-path spend. Looks like a regular P2TR on-chain → maximum privacy. Script tree remains as fallback.High — requires MuSig2 signing infrastructure
Deeper MAST trees (3+ leaves)BIP 341Add a third leaf: e.g., a dead-man switch that becomes spendable after 1 year of inactivity, or a dedicated "migrate to covenant vault" leafMedium — straightforward script extension
Schnorr-signed L2 actions (single-wallet UX)BIP 340Admin signs L2 transactions with their Bitcoin Schnorr key via Citrea's precompile → one wallet, one identity, both layersMedium — needs account abstraction on L2
Covenants (OP_CTV / OP_CAT)ProposedRestrict the output of a vault spend — the UTXO can only move to a pre-approved address. True on-chain spending constraints.Requires soft fork (not yet activated)
Batch inscriptions in one treeBIP 341Embed multiple inscription commitments in different MAST leaves of a single transaction, reducing on-chain cost for multi-template institutionsLow–Medium

Summary

BINST uses every active Taproot feature relevant to its design goal of UTXO-locked institutional identity:

  • ✅ P2TR outputs with Bech32m
  • ✅ MAST script tree (2 leaves, hidden until spent)
  • ✅ Schnorr signatures (64 bytes, provably secure)
  • ✅ OP_CHECKSIGADD (BIP 342 multisig)
  • ✅ Tagged hashes and taptweak for key commitment
  • ✅ NUMS point to disable key-path
  • ✅ Control blocks for script-path proofs
  • ✅ CSV timelocks in Tapscript
  • ✅ Schnorr precompile on L2

The features we skip (key-path, MuSig2, annex, OP_SUCCESS) are either deliberately disabled for security, irrelevant to script authors, or reserved for future upgrades. No Taproot capability is left on the table without a documented reason.


Post-Quantum Considerations

See the dedicated Post-Quantum Analysis page for a full assessment of how Taproot's P2PK structure affects BINST and what mitigations are already in place.

References

Post-Quantum Analysis

The Taproot P2PK Problem

On April 2026, Bitcoin developers @niftynei and @JeremyRubin highlighted a structural property of Taproot that is relevant to every protocol built on top of it:

Every P2TR output is fundamentally a Pay-to-Public-Key (P2PK).

The 32-byte x-only public key is committed directly in the output script. A sufficiently powerful quantum computer could, in theory, derive the private key from this exposed curve point before the owner spends it.

This was a deliberate 2018/2019 design choice. The Taproot authors traded quantum resistance for on-chain privacy: all Taproot outputs look identical regardless of the complexity of the underlying script tree. At the time, post-quantum (PQ) cryptography was considered too immature and too unpredictable to constrain the design.

@niftynei summarised the rationale:

"Taproot accomplished its design goals to increase privacy. Being quantum safe wasn't on the list of design goals. Now we're in the era where PQ is a design goal."

Jeremy Rubin added that OP_CTV (his covenant proposal) can be used in a quantum-proof way, while OP_TEMPLATEHASH (a competing Taproot- only proposal) cannot — precisely because OP_TEMPLATEHASH inherits the P2PK exposure of Taproot.

How This Affects BINST

Attack Surface Map

BINST ComponentP2PK Exposed?RiskNotes
Institution inscription UTXOYes — tb1p… outputLowNUMS internal key — no private key exists to derive
ProcessTemplate inscription UTXOYes — same P2TRLowSame NUMS mitigation
Taproot vault (admin leaf)Yes — admin pubkey in scriptMediumKey revealed only at spend time, not before
Taproot vault (committee leaf)Yes — 3 pubkeys in OP_CHECKSIGADDMediumSame: revealed only when the committee path is exercised
Citrea DA inscriptionsYes — sequencer's Taproot outputNot oursCitrea controls this key
Inscription content (JSON body)No — pure dataNoneData is not a curve point
State digest merkle rootsNo — SHA-256 hashesNoneHash-based commitments are quantum-resistant
L2 contract stateNo — EVM/ECDSASeparate concernCitrea's EVM would need its own PQ migration
Bitcoin DA batch dataNo — Merkle commitmentsNoneHash-based, quantum-resistant

Why BINST Is Better Positioned Than Most

1. NUMS key kills the key-path attack.

The primary P2PK risk is that a quantum attacker sees the output key on-chain and derives the private key. In BINST, the internal key is the provably unspendable NUMS point:

H = lift_x(SHA256("BINST/nums"))

There is no discrete logarithm relationship between this point and any known generator. A quantum computer running Shor's algorithm needs a known group structure to exploit — NUMS provides none. The tweaked output key Q = H + t·G inherits this property: knowing Q does not help derive a private key because H has no known private key to start with.

2. Script-path keys are only revealed at spend time.

The admin and committee public keys live inside the Taptree leaves, not in the output. They are only revealed in the witness when the script-path is actually exercised. An attacker would need to:

  1. Wait for a vault spend transaction to appear in the mempool
  2. Extract the public key from the witness
  3. Run Shor's algorithm before the transaction is mined

With current block times (~10 minutes) and projected quantum timelines, this is not a practical attack vector in the near term.

3. Our covenant roadmap uses OP_CTV.

The BINST roadmap (Phase 5) plans to use OP_CTV for covenant-locked vaults. Per Jeremy Rubin's analysis, OP_CTV can be constructed in a quantum-proof way because it commits to the transaction template hash rather than requiring a public key. This means our planned upgrade path does not inherit the P2PK vulnerability.

4. DA proofs use hash commitments, not keys.

The state_digest inscriptions commit to Merkle roots over instance states. SHA-256 Merkle trees are quantum-resistant (Grover's algorithm provides only a quadratic speedup, requiring ~2¹²⁸ operations to break SHA-256, which is still infeasible). The entire DA verification chain — from state diffs to batch proofs to digest roots — is hash-based.

What Would Need to Change

If Bitcoin ships a post-quantum address format (e.g. a new SegWit version using hash-based or lattice-based signatures), BINST would need to:

Migration Steps

  1. Move inscription UTXOs to PQ-safe addresses. This requires spending the current Taproot outputs (which reveals the script-path keys) and sending the sats to new PQ outputs. The inscription content transfers with the sat.

  2. Update vault scripts to use PQ signature verification opcodes (if available). The MAST structure would remain; only the leaf scripts change from OP_CHECKSIG (Schnorr) to whatever PQ opcode Bitcoin adopts.

  3. Rotate admin pubkey bindings in inscription bodies. The admin field is currently a 32-byte x-only Schnorr key set at inscription time. A PQ migration would require re-inscribing with a PQ public key in the admin field.

  4. Update the metaprotocol schema to support PQ key formats in the admin field of inscription bodies (currently a 32-byte x-only Schnorr key).

What We Can Do Now

ActionComplexityWhen
Document the migration path (this page)DoneNow
Avoid key reuse — each vault uses fresh keysAlready in placeNow
Don't rely on key-path spend — NUMS is already defaultAlready in placeNow
Design admin key rotation — allow re-inscription with timelocked delay for key migrationLowPhase 4
Monitor BIPs for PQ address proposalsOngoing
Prototype PQ inscription transfer when a PQ address format is available on signetMediumWhen available

Timeline Assessment

MilestoneEstimatedSource
Cryptographically relevant quantum computer2030–2040NIST, IBM roadmaps
Bitcoin PQ address soft fork proposal2026–2028Community discussion active
Bitcoin PQ soft fork activation2029+Typical activation timelines
BINST production deployment2026–2027Project roadmap

The gap between BINST deployment and quantum threat is at least 4–5 years, and Bitcoin itself will need to solve this problem for all Taproot users. BINST's migration path is no harder than any other Taproot-based protocol, and easier than most because:

  • NUMS removes the key-path attack surface
  • OP_CTV covenants (roadmap) are quantum-compatible
  • DA verification is entirely hash-based
  • Inscription UTXOs can be migrated by simple spend-and-resend

References

ZK Batch Proofs — Computational Integrity

The L2 (Citrea) periodically writes ZK batch proofs to Bitcoin — mathematical guarantees that every state transition was computed correctly.

How It Works

  1. Every BINST transaction lives in a Citrea L2 block
  2. The sequencer inscribes Sequencer Commitments (Merkle roots) on Bitcoin — pins ordering
  3. The batch prover inscribes ZK proofs (Groth16 via RISC Zero) on Bitcoin with state diffs — proves correctness
  4. Anyone with a Bitcoin node can reconstruct the entire L2 state including all BINST data

Finality Levels

LevelWhat happensHow to verifyTrust assumption
Soft ConfirmationSequencer signs the L2 blockTransaction receiptTrust sequencer
CommittedSequencer commitment inscribed on Bitcoincitrea_getLastCommittedL2HeightBitcoin consensus + sequencer honesty
ZK-ProvenZK batch proof inscribed on Bitcoincitrea_getLastProvenL2HeightBitcoin consensus + math only

Each BINST process step execution progresses through these three levels automatically — no user action required. The webapp tracks and displays the finality stage of each transaction (instance creation + each step) in the Execute view's finality card.

Querying finality programmatically

# Get the last committed and proven L2 heights
citrea_getLastCommittedL2Height  →  { height, idx }
citrea_getLastProvenL2Height     →  { height, idx }

# Get the block number for a specific tx
eth_getTransactionReceipt(txHash)  →  receipt.blockNumber

# Classification:
if tx_block <= proven_height    →  "Proven"     (green)
if tx_block <= committed_height →  "Committed"  (blue)
otherwise                       →  "Soft Confirmation" (yellow)

Event log recovery

When the webapp loads an instance that was created before finality tracking was added, it recovers tx hashes by scanning on-chain event logs from the factory (InstanceCreated) and instance (StepExecuted) contracts, using eth_getLogs in 1000-block chunks scanning backwards from the current block.

BitVM2 and the Clementine Bridge

Citrea's Clementine Bridge implements BitVM2 — a trust-minimized fraud-proof system that secures the BTC ↔ cBTC peg:

Operator posts assertion about L2 state on Bitcoin
  ├─ No challenge within timeout → assertion accepted (optimistic case)
  └─ Challenger disputes → operator must reveal intermediate steps
       ├─ Operator correct → challenger loses bond
       └─ Operator wrong  → operator's bond slashed on Bitcoin L1

Trust model: 1-of-N honesty — only one honest verifier is needed to catch fraud. This is stronger than multisig bridges (which require M-of-N honesty) and weaker only than a native Bitcoin covenant (which requires zero trust beyond Bitcoin consensus).

Why BitVM2 matters for BINST

Every BINSTProcess.executeStep() call on Citrea is:

  1. Soft-confirmed by the sequencer (~instant)
  2. Committed to Bitcoin via sequencer commitment inscription
  3. ZK-proven on Bitcoin via batch proof inscription
  4. Bridge-secured by BitVM2 fraud proofs (for any BTC ↔ cBTC flows)

This means a completed BINST process instance has its execution integrity guaranteed by ZK math on Bitcoin — and any BTC liquidity involved is secured by BitVM2's fraud-proof mechanism. The institution's identity (inscription) and the execution proof (ZK batch) both live on Bitcoin L1.

Why This Matters for BINST

Process instance state reaches Bitcoin via batch proof. This means:

  • A second L2 can verify process execution by reading Bitcoin DA — no messaging protocol needed
  • The binst-protocol CLI can reconstruct which steps were executed, by whom, at what timestamp
  • Cross-chain execution verification is trustless — it goes through Bitcoin, not through any relay
  • The Execute view shows real-time finality progression for every transaction

See: Citrea DA Format · Decoding Procedure

Citrea DA Transaction Format

Citrea serializes a Borsh enum called DataOnDa inside taproot script-path reveals on Bitcoin.

The Five Variants

VariantIDPayloadPurpose
Complete0Vec<u8> (compressed proof)Full ZK batch proof in one tx
Aggregate1Vec<[u8;32]> txids + wtxidsReferences chunk txs for large proofs
Chunk2Vec<u8> (fragment)Fragment of a large proof
BatchProofMethodId3Method ID + signatures + pubkeysSecurity council metadata
SequencerCommitment4Merkle root + index + L2 end blockMost common — anchors L2 batch to Bitcoin

Tapscript Layout

PUSH32 <x_only_pubkey>           32 bytes — Citrea DA pubkey
OP_CHECKSIGVERIFY
PUSH2  <kind_bytes_le>           2 bytes LE — transaction kind (u16)
OP_FALSE
OP_IF
  PUSH <schnorr_signature>       64 bytes
  PUSH <signer_pubkey>           33 bytes compressed
  PUSH <body_chunk> ...          one or many pushdata entries
OP_ENDIF
PUSH8  <nonce_le>                8 bytes LE — wtxid mining nonce
OP_NIP

Identification

Citrea reveal transactions are identified by their wtxid prefix — the sequencer picks a nonce so the wtxid starts with 0x0202 (production). This allows fast filtering without full parsing.

Implementation Notes

  • Borsh discriminant is 1 byte (not 4)
  • Body may be split into ≤520-byte pushdata chunks — concatenate before deserializing
  • Always compute wtxid (witness hash), not txid
  • Support OP_PUSHBYTES_N, OP_PUSHDATA1, and OP_PUSHDATA2 encodings

Decoding Procedure

Step-by-step process for finding and decoding Citrea DA inscriptions from Bitcoin.

1. Choose a Scanning Strategy

  • Tip-based (continuous): scan from last seen block to current tip
  • Range scan (investigation): specific block range with --from/--to
  • Targeted: only blocks with nTx > 1 (many testnet blocks are coinbase-only)

2. Fetch the Block

Use getblockhash(height) then getblock(hash) to get the full block with witness data.

3. Filter by wtxid Prefix

For each non-coinbase transaction, compute wtxid = SHA256d(serialized_tx_with_witness). Check if it starts with 0x0202.

4. Extract Tapscript

For script-path spends, the tapscript is the second-to-last witness element: witness[witness.len() - 2].

5. Parse Structured Tapscript

Walk pushdata opcodes in order: pubkey → signature → signer → body chunks. Concatenate body chunks.

6. Borsh Deserialize

First byte = variant discriminant (0–4). Deserialize to typed DataOnDa value.

7. Map to Protocol Events

VariantWhat it means
SequencerCommitmentNew L2 batch finalized up to l2_end_block_number
CompleteFull ZK batch proof — verify to confirm correctness
Aggregate + ChunkLarge proof assembly instructions
BatchProofMethodIdSecurity council / method-ID update

CLI Usage

Bitcoin Core mode (local full node)

# Scan a single block (uses cookie auth by default)
cargo run --bin citrea-scanner -- --block 127600

# Scan a range with explicit auth, JSON output
cargo run --bin citrea-scanner -- --from 127600 --to 127761 --format json \
  --rpc-user <user> --rpc-pass <pass>

Citrea RPC mode (no Bitcoin node required)

# Query batch proofs via Citrea RPC
cargo run --bin citrea-scanner -- \
  --citrea-rpc https://rpc.testnet.citrea.xyz \
  --discover \
  --factory 0x6a1d2adbac8682773ed6700d2118c709c8ce5000 \
  --block 127848

# Manual instance addresses (skip discovery)
cargo run --bin citrea-scanner -- \
  --citrea-rpc https://rpc.testnet.citrea.xyz \
  --factory 0x... --instance 0x... \
  --from 127840 --to 127850

The --discover flag crawls the factory contract on-chain: factory.getInstanceCount()factory.allInstances(i) → instance addresses.

8. Human-Readable Value Decoding

Once BINST storage slot changes are identified, raw hex values are automatically decoded into human-readable form. Example output from block 127848:

BINSTProcess.templateInscriptionId = "abc123…i0"
BINSTProcess.creator = 0x8cf6fe5cd0905b6bfb81643b0dcda64af32fd762
BINSTProcess.stepStates[0] = Completed by 0x8cf6fe5cd0905b6bfb81643b0dcda64af32fd762
BINSTProcess.currentStepIndex = 4
BINSTProcess.completed = true
BINSTProcess.totalSteps = 4

Supported Solidity types

Solidity typeDecoded form
address0x8cf6fe5c…d762
uint2564, 1774750572
booltrue / false
string (short ≤31 bytes)"KYC Verification"
string (long >31 bytes)<string, 62 bytes>
StepState (packed struct)Completed by 0x8cf6…d762
bytes320x… hex

Citrea little-endian word order

Key discovery: Citrea / Sovereign SDK stores EVM storage values in little-endian word order — the entire 32-byte slot is byte-reversed compared to standard Solidity ABI encoding.

uint256 value 1:
  Standard Solidity (BE): 0x00000000…00000001
  Citrea state trie (LE): 0x01000000…00000000

The decoder pads to 32 bytes (state diffs may trim trailing LE zeros) and reverses before interpreting according to the field's Solidity type.

Packed struct layout (StepState)

Solidity packs uint8 status + address actor right-aligned in one slot:

BE word (after LE→BE reversal):
  [00 × 11 bytes][actor × 20 bytes][status × 1 byte]
   ↑ padding      ↑ bytes 11..31    ↑ byte 31

Status: 0 = Pending, 1 = Completed, 2 = Rejected.

See the binst-protocol/crates/cli/ directory in the pilot repository.

Discovery

How to find and verify BINST entities — from casual browsing to full trustless verification.

Who Needs What

What you want to knowWhere to lookFull node needed?
Does institution X exist?Ordinals explorer❌ No
Who is the admin?Inscription UTXO owner❌ No
Am I a member?Rune balance in wallet❌ No
Who are all members?Rune indexer query❌ No
What processes exist?Child inscriptions❌ No
What step is instance Y on?L2 RPC (Citrea)❌ No
Is institution X anchored?Inscription exists + L2 instances reference it❌ No
Was step execution valid?ZK batch proof decode✅ Yes
Full trustless verification?binst-protocol CLI✅ Yes
Which L2 is processing?Inscription metadata❌ No
Is this person a member? (cross-chain)Rune balance via indexer❌ No
Did step X complete? (cross-chain)Bitcoin DA batch proof✅ Yes

Two Tiers

Tier 1 (standard tooling):  Ordinals explorer + Rune wallet
  → identity, ownership, membership — no full node needed

Tier 2 (L2 RPC):  Citrea RPC + citrea-scanner --discover
  → process state, step execution, BINST contract discovery — no full node needed

Tier 3 (verification):  Bitcoin full node + binst-protocol CLI
  → ZK proof verification, state diff decoding — trustless

Basic discovery requires no custom software and no full node. Standard Ordinals and Rune tooling covers identity, ownership, membership, and provenance. Tier 2 adds on-chain contract discovery via Citrea RPC. The full node is only needed for the strongest verification tier.

Auto-discovery with --discover

The citrea-scanner CLI can automatically crawl the factory contract to find all BINST process instances registered on-chain:

cargo run --bin citrea-scanner -- \
  --citrea-rpc https://rpc.testnet.citrea.xyz \
  --discover \
  --factory 0x6a1d2adbac8682773ed6700d2118c709c8ce5000 \
  --block 127848

Discovery chain:

  1. factory.getInstanceCount() → total number of instances
  2. factory.allInstances(i) → instance address by index
  3. factory.getTemplateInstances(inscriptionId) → instances for a specific L1 template
  4. factory.getUserInstances(address) → instances created by a specific user

All discovered addresses are merged into the BINST registry, which pre-computes storage slot hashes for matching against state diffs in ZK batch proofs.

Matched state diff values are automatically decoded into human-readable form — addresses, integers, booleans, short Solidity strings, and packed StepState structs. See Decoding Procedure § Human-Readable Value Decoding.

Critically: none of this depends on any specific L2 being online.

Cost Analysis

Approximate costs for BINST operations at 10 sat/vB fee rate.

OperationMechanismApprox. cost
Create institutionOrdinal inscription (~500B text)~$2–5
Create process templateChild inscription (~300B)~$1–3
Record step executionChild inscription (~200B)~$0.50–2
Etch membership RuneRunestone in OP_RETURN~$1–3
Mint membership for 1 userRunestone transaction~$0.50–1
Transfer institution adminSend UTXO (standard tx)~$0.30–1
Total: institution + template + 10 members~$15–30

Testnet4 is free during development. Mainnet costs scale with fee rates but remain reasonable for institutional operations that happen infrequently.

Locked Sats at Scale

Each entity UTXO locks 546 sats (the dust limit). These sats are not burned — they are recoverable when the vault UTXO is spent (admin transfer, reinscription, etc.).

ScaleSats lockedBTC lockedAt $100K/BTC
1 institution5460.00000546$0.55
100 institutions54,6000.00054600$54.60
10,000 institutions5,460,0000.05460000$5,460

Transaction fees (commit + reveal) dwarf the dust padding by 5–10×. The real cost optimization target is batching and fee rate timing, not the locked sats.

Future Optimization: Batching

Bitcoin-side operations (inscribe + etch + distribute runes) can be grouped into fewer transactions:

Single Bitcoin TX:
  Input:  admin's UTXO (from taproot vault)
  Output 0: inscription envelope (institution identity)
  Output 1: rune etch (INSTITUTION•MEMBER)
  Output 2: rune transfer to member_1
  Output 3: rune transfer to member_2
  Output 4: change back to vault

This is planned for Phase 3 — a BINST-aware transaction builder that composes Bitcoin-side operations into minimal transactions.

Smart Contracts

Architecture: Thin L2, Fat L1

BINST follows a "Thin L2, Fat L1" architecture. Bitcoin L1 is the source of truth for identity (institutions) and definitions (process templates). The L2 only holds execution state.

Two contracts make up the L2 layer:

ContractPurpose
BINSTProcessFactoryThin factory — deploys process instances, indexes by user and template
BINSTProcessSelf-contained instance — embedded steps, L1 anchor, migration-ready

Bitcoin owns identity and definitions; L2 only owns execution.


Architecture

BINSTProcessFactory

A thin factory deployed once per L2 chain. Creates self-contained process instances and maintains indexing by user and by L1 template.

Deployed addresses:

NetworkAddressChain ID
Citrea testnet0x6a1d2adbac8682773ed6700d2118c709c8ce50005115
Hardhat local0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e031337

Key functions:

FunctionSelectorPurpose
createInstance(string, string[], string[])0x6f794b70Deploy a new BINSTProcess instance
getTemplateInstances(string)0xb43bed00All instances for a given L1 template inscription ID
getUserInstances(address)0xfceaae17All instances created by an address
getInstanceCount()0xae34325cTotal instance count
allInstances(uint256)0x9b0dc489Instance address by global index

Events:

EventTopic0
InstanceCreated(address indexed, address indexed, string, uint256)0xa3de24f67beb…0700f

BINSTProcess

A self-contained process instance anchored to a Bitcoin L1 inscription. Each instance carries its own step definitions — no dependency on separate contracts on L2.

Why self-contained? Cross-chain migration. When a partially-executed process migrates from Citrea to another L2 (via LayerZero), the receiving chain only needs one message:

{templateInscriptionId, steps[], currentStepIndex, stepStates[], creator}

No pre-deployed contracts are required on the destination chain.

Provenance chain (L2 → L1):

BINSTProcess.templateInscriptionId
  → look up on Bitcoin (mempool.space / ord indexer)
  → read inscription body → institution_id, steps, admin pubkey
  → verify the L1 hierarchy: institution → template → (this instance)

Key state:

string public templateInscriptionId;   // L1 anchor — Bitcoin inscription ID
Step[] public steps;                    // Step definitions (name + actionType)
address public creator;                 // Who created this instance
uint256 public currentStepIndex;        // Current step (0-based)
bool public completed;                  // True when all steps done
mapping(uint256 => StepState) public stepStates;

// Migration fields (future — Phase 65)
uint32 public sourceChainId;           // 0 if native, non-zero if migrated
address public sourceAddress;          // Address on source chain

Key functions:

FunctionSelectorPurpose
executeStep(uint8, string)0xf16e3a23Execute the current step with status + evidence
currentStepIndex()0x334f45ecRead current step index
completed()0x9d9a7fe9Check if all steps are done
totalSteps()0x6931b3aeTotal number of steps
creator()0x02d05d3fAddress of the instance creator
templateInscriptionId()0x0270a0b3The L1 inscription this instance is anchored to

Step status enum: 0 = Pending, 1 = Completed, 2 = Rejected

Events:

EventTopic0
StepExecuted(uint256 indexed, address indexed, uint8, string)0x5b89aea5…1b82
ProcessCompleted(address indexed, uint256)(keccak256 of signature)

Instance Lifecycle

1. Factory deployed once per L2 chain
   └─ Citrea testnet: 0x6a1d…5000

2. User creates instance from the Execute view
   └─ factory.createInstance(inscriptionId, stepNames[], stepActionTypes[])
   └─ InstanceCreated event → instance address parsed from receipt
   └─ Instance address saved to localStorage and shown in the UI

3. Steps executed sequentially
   └─ instance.executeStep(Completed, evidenceData)
   └─ StepExecuted event emitted per step
   └─ currentStepIndex advances; UI reloads state from contract

4. Process completes
   └─ instance.completed == true after final step
   └─ ProcessCompleted event emitted

5. Bitcoin settlement (automatic — no user action)
   └─ Soft Confirmation: sequencer signs the L2 block (~instant)
   └─ Committed: sequencer commitment inscribed on Bitcoin
   └─ Proven: ZK batch proof inscribed on Bitcoin
   └─ All L2 state is now part of the Bitcoin permanent record

6. Migration (future — Phase 65)
   └─ LayerZero _lzSend() carries full state to destination L2
   └─ Destination deploys new BINSTProcess pre-loaded with state

Bitcoin Settlement Finality

Every L2 transaction goes through three finality stages before reaching Bitcoin permanence:

StageMeaningVerification
Soft ConfirmationSequencer has ordered the tx in an L2 blockTransaction receipt
CommittedSequencer commitment inscribed on Bitcoincitrea_getLastCommittedL2Height
ProvenZK proof of correct execution inscribed on Bitcoincitrea_getLastProvenL2Height

The webapp tracks these stages per-transaction (instance creation and each step execution) and displays finality badges in the Execute view. See ZK Batch Proofs for the full model.


EVM Configuration

Citrea does not support Cancun opcodes. Target Shanghai:

solidity: {
  version: "0.8.24",
  settings: { evmVersion: "shanghai", optimizer: { enabled: true, runs: 200 } }
}

Solidity Tests

24 tests in test/BINSTPilot.ts (Hardhat 3, node:test):

  • 10 active contract tests (BINSTProcessFactory, BINSTProcess)
  • 14 additional tests covering extended contract lifecycles

BINST Protocol (Rust)

The Rust protocol crates live in a standalone repository: github.com/Bitcoin-Institutions/BINST-protocol

This separation means any project — CLI tooling, WASM webapp, future mobile app — can depend on the protocol crates via a single git dependency, with a local [patch] override for development.

Repository structure

binst-protocol/          ← standalone Cargo workspace
  Cargo.toml             ← workspace root (resolver = "2")
  crates/
    citrea-decoder/      ← Citrea DA inscription parser (no_std, WASM-ready)
    binst-decoder/       ← Storage slots → protocol entities + miniscript vault
    binst-inscription/   ← Ordinals envelope parser for binst metaprotocol
    cli/                 ← citrea-scanner binary (Bitcoin Core RPC + Citrea RPC)
  schema/                ← JSON schema for binst metaprotocol entities
  BITCOIN-IDENTITY.md
  DECODING.md
  conceptual.md

Previously these crates lived inside binst-pilot/taproot-reader/. They were extracted into their own repository to be reusable as a dependency of the WASM webapp and any future project.

Using the crates

In a project (git dependency)

[dependencies]
binst-inscription = { git = "https://github.com/Bitcoin-Institutions/BINST-protocol.git" }
binst-decoder     = { git = "https://github.com/Bitcoin-Institutions/BINST-protocol.git",
                      default-features = false, features = ["wasm"] }

Local development override ([patch])

[patch."https://github.com/Bitcoin-Institutions/BINST-protocol.git"]
binst-inscription = { path = "../../binst-protocol/crates/binst-inscription" }
binst-decoder     = { path = "../../binst-protocol/crates/binst-decoder" }
citrea-decoder    = { path = "../../binst-protocol/crates/citrea-decoder" }

The [patch] block redirects git deps to the local checkout so you can iterate on protocol crates and the webapp simultaneously without pushing. Remove or comment it out when testing against the published repo.

citrea-decoder

Parses Citrea DA inscriptions from raw tapscript witness data. Handles all five DataOnDa variants, pushdata chunking, and wtxid prefix filtering. Also decodes batch proof output (Brotli decompression, journal extraction, state diff parsing).

  • no_std compatible, WASM-ready
  • 7 tests

binst-decoder

Maps L2 storage slot diffs to BINST entities. Given a state diff from a batch proof, reconstructs InstitutionState, ProcessTemplateState, etc. Parses Citrea JMT keys and builds forward-hash lookup tables for matching state diff entries to known BINST contracts.

  • Computes Solidity storage slot positions (keccak256-based)
  • JMT key parsing: E/s/ (storage), E/H/ (headers), E/a/ (accounts)
  • Forward-hash lookup: SHA-256 precomputation of all known (address, slot) pairs
  • Human-readable value decoding (value module): decodes raw Citrea LE storage values to addresses, uints, bools, Solidity strings, and packed StepState structs
  • Key discovery: Citrea stores EVM slot values in little-endian word order (entire 32-byte word byte-reversed vs. standard Solidity ABI)
  • Carries BitcoinIdentity struct linking entities across layers
  • Miniscript vault module (vault module): compiles BIP 379 spending policies to Taproot descriptors using rust-miniscript. Generates wallet-compatible descriptors, derives addresses, and analyzes spending paths. WASM-exportable for in-browser vault generation.
  • 52 tests (27 unit + 5 e2e + 9 value decoding + 11 vault)

binst-inscription

Parses Ordinals envelopes for binst metaprotocol inscriptions. Extracts typed entity bodies (institution, template, instance, step execution).

  • Validates metaprotocol field, content type, parent chain
  • 10 tests

cli (citrea-scanner)

Binary that scans for Citrea DA transactions. Supports two modes:

  • Bitcoin Core mode: connects to a local full node via RPC
  • Citrea RPC mode: queries batch proofs directly from a Citrea node (no Bitcoin node required)

The --discover flag auto-discovers all BINST contracts by crawling the deployer on-chain.

# Bitcoin Core mode (uses cookie auth by default)
cargo run --bin citrea-scanner -- --block 127600

# Citrea RPC mode with auto-discovery
cargo run --bin citrea-scanner -- \
  --citrea-rpc https://rpc.testnet.citrea.xyz \
  --discover \
  --deployer 0xd0abca83bd52949fcf741d6da0289c5ec7235aaf \
  --block 127848
  • 5 tests
cd binst-protocol && cargo test    # runs all 79 protocol tests

See also: BitcoinIdentity Type

BitcoinIdentity Type

Every BINST entity in the decoder carries a BitcoinIdentity struct linking it across all reachability layers.

#![allow(unused)]
fn main() {
pub struct BitcoinIdentity {
    /// Taproot x-only public key (32 bytes) — ROOT OF AUTHORITY.
    /// Controls the inscription UTXO and is the canonical identity.
    pub bitcoin_pubkey: [u8; 32],

    /// Ordinals inscription ID (e.g., "abc123...i0")
    pub inscription_id: Option<String>,

    /// Rune ID for membership token (e.g., "840000:20")
    pub membership_rune_id: Option<String>,

    /// EVM address on the current L2 (derived from or authorized by the BTC key)
    pub evm_address: Option<[u8; 20]>,

    /// HD derivation path hint (e.g., "m/86'/0'/0'/0/0")
    pub derivation_hint: Option<String>,
}
}

Field Ordering = Authority Hierarchy

  1. bitcoin_pubkey — the root of authority (controls inscription UTXO) — required
  2. inscription_id — permanent identity on Bitcoin
  3. membership_rune_id — membership token on Bitcoin
  4. evm_address — current L2 delegate (optional — changes if L2 changes)
  5. derivation_hint — wallet recovery aid

The required/optional distinction encodes the sovereignty model: Bitcoin identity is mandatory, L2 address is a transient delegate reference.

WASM Webapp

A Rust/WASM single-page application that implements the BINST user-facing pilot flows — institution creation, process design, and step execution. Built with Trunk, targeting wasm32-unknown-unknown.

Network: Bitcoin Testnet4 and Citrea testnet.

Purpose

The webapp is the pilot's honest frontend: every write operation that touches Bitcoin or Citrea goes through a real wallet. There are no mocked broadcasts, no simulated signing prompts. If an action requires a transaction, the wallet pops up.


Signing model — what requires a signature and where

Two entirely different signing mechanisms are used, one per layer:

LayerTechnologyWhat is signedWallets
L1 BitcoinPSBT (BIP 174)Commit + Reveal transaction pairUniSat, Xverse, Leather
L2 Citrea EVMeth_sendTransactionIndividual EVM contract callMetaMask, Brave Wallet, WalletConnect

These cannot be mixed. A Bitcoin wallet cannot sign an EVM transaction, and an EVM wallet cannot sign a PSBT. The webapp maintains two separate wallet slots and routes each action to the correct one.

Serverless inscription pipeline (L1)

A Bitcoin inscription is not a simple transfer. Every BINST entity (institution, process template) that lands on Bitcoin L1 requires:

  1. A commit transaction — funds a Taproot output whose script contains the entity JSON inside an Ordinals envelope
  2. A reveal transaction — spends that Taproot output via the script-path, embedding the inscription in the witness data

Both transactions are built entirely in-browser by txbuilder.rs, signed by the connected Bitcoin wallet, and broadcast directly to https://mempool.space/testnet4/api/tx. There is no local ord daemon or bridge server.

The Ordinals envelope includes:

  • content-type: application/json header
  • The entity JSON as content
  • Tag 3 (parent) when a parent inscription ID is set (for hierarchical linking)

When a parent inscription exists, its UTXO is also spent as a second input of the reveal transaction so Ordinals indexers recognise the provenance relationship.

Why individual EVM calls for L2?

EVM transactions are not natively batchable — each call has its own nonce and msg.sender check. Each L2 operation requires its own wallet pop-up.


Wallet integration

L1 Bitcoin wallets

L1 wallets are detected via EIP-6963 (provider announcement) plus manual window.* injection detection as a fallback:

WalletDetectionSigning method
UniSatwindow.unisatsignPsbt(hex)
XverseEIP-6963 / sats-connectsats-connect SignPsbt request
LeatherEIP-6963request('signPsbt', …)

Xverse note: Xverse exposes two addresses — a payment address (P2WPKH, m/84') and an ordinals address (P2TR, m/86'). Inscriptions must be funded from the ordinals address (tb1p…).

L2 EVM wallets

L2 wallets are detected and connected through the wallet picker modal:

WalletMethod
MetaMaskwindow.ethereum (isMetaMask)
Brave Walletwindow.ethereum (isBraveWallet)
WalletConnect@walletconnect/universal-provider

Dual wallet bar

[ L1  tb1p…a4f2  ✕ ]  [ L2  0x3f…9a1c  ✕ ]  Citrea Testnet

Both slots can be connected simultaneously and disconnected independently.


L1 Stack — institution-aware inscription batching

The L1 stack accumulates multiple inscription intents before signing. When ready, each entry gets its own commit+reveal PSBT pair signed and broadcast sequentially.

Create Institution ──┐
Design Process ──────┼──→  L1 Stack  ──→  stack_plan.rs  ──→  txbuilder.rs  ──→  wallet sign
Design Process ──────┘    (grouped)       (plan + route)      (PSBT pair)

Stack intelligence layer (stack_plan.rs)

stack_plan::build_plan analyses the stack before signing and produces an ExecutionPlan — a flat sequence of PlannedSteps in broadcast order, each annotated with exactly how to resolve its parent UTXO:

Institution stateParentSource
Institution is also in this batchInBatch { batch_idx } — live reveal UTXO from same run
Institution in mempool (unconfirmed)FetchMempool { txid } — fetch output 0 at sign time
Institution confirmedFetchMempool { txid } — same fetch, always works
Unknown parentNone + warning surfaced before signing

The stack panel groups entries by institution and shows a state badge (in-batch / mempool / confirmed / external) per group.

Confirmation polling (bridge.rs)

After broadcast, bridge.rs polls https://mempool.space/testnet4/api/tx/{txid} until 6 confirmations. The pipeline panel shows only active (unconfirmed

  • < 6 conf) inscriptions and auto-removes entries at 6 confs. Polling resumes on wallet reconnect via resume_pending_polls.

localStorage registry (storage.rs)

Every inscription is persisted in localStorage with:

  • reveal_txid — transaction ID of the reveal tx
  • inscription_id — Ordinals inscription ID
  • confirmed flag — updated by the polling loop
  • Entity metadata (label, type, parent)

The registry survives page reloads and is the primary source for parent UTXO lookup during stack planning.


L2 Execute — direct EVM calls

L2 process execution goes through the Execute view, not through the stack. Each EVM transaction fires immediately on button click — no batching, no queue. The L1 stack is Bitcoin-only.

ActionContract callTrigger
Create Instancefactory.createInstance(inscriptionId, names[], types[])"Create Instance on Citrea" button
Execute Stepinstance.executeStep(status, evidence)"Execute Step →" button

All EVM/ABI logic lives in citrea.rs. The execute.rs module is a thin UI wiring layer that delegates to citrea.rs for every on-chain call. Instance state (steps, completion, finality) is read directly from the contract via eth_call — no wallet needed for reads.


Module structure

ModulePurpose
lib.rsWASM entry point — boots all views
auth.rsAuthentication state
bridge.rsConfirmation polling loop + pipeline panel DOM; resume_pending_polls on connect
citrea.rsAll L2 EVM/ABI logic: create_instance, execute_step, ABI encoding, finality queries, event log recovery
create.rsCreate Institution view — form, validates, pushes to stack
decode.rsJSON / witness / vault decoder UI
design.rsDesign Process view — form, fetches parent UTXO, pushes to stack
dom.rsDOM helpers, toast notifications, clipboard
effects.rsVisual effects (logo tilt, spark effect)
execute.rsExecute view — thin UI wiring, delegates to citrea.rs for all on-chain calls; finality status display
fetch.rsShared HTTP fetch helpers (Mempool.space API)
inscribe.rsFull serverless pipeline: fetch UTXOs → build PSBTs → sign → broadcast; fetch_tx_output
institution.rsInstitution card rendering and navigation
l2_queue.rsPure Rust L2 action queue data model (not wired to UI — kept for tests only)
nav.rsView routing and bottom nav, 4 tests
process.rsProcess view + instance creation via factory flow
search.rsInstitution search against Ordiscan API + localStorage, 4 tests
stack.rsPure Rust L1 inscription stack — StackEntry, ordering, validation, 21 tests
stack_plan.rsPure-Rust execution planner — institution grouping, InstitutionState, ParentSource, 12 tests
stack_ui.rsStack panel UI — institution-grouped render, plan-driven on_sign_inscribe
storage.rslocalStorage registry: inscriptions, templates, L2 instance tx hashes, 12 tests
txbuilder.rsCommit+reveal PSBT construction; parent UTXO as second reveal input, 9 tests
wallet.rsL1 BTC wallet — EIP-6963 + manual detection, connect/disconnect, pubkey extraction
wallet_picker.rsWallet picker modal; L2 EVM wallets (WalletConnect, MetaMask, Brave)

Build

# Development server (port 8081)
CC_wasm32_unknown_unknown=/opt/homebrew/opt/llvm/bin/clang \
  trunk serve --port 8081

# Release build
CC_wasm32_unknown_unknown=/opt/homebrew/opt/llvm/bin/clang \
  trunk build --release

Requires LLVM's clang for the wasm32-unknown-unknown target because the system Apple clang does not support that target.


Test suite

cargo test  # runs on native target — no browser needed

97 tests across 11 modules:

ModuleTests
stack21
l2_queue13
storage12
stack_plan12
txbuilder9
decode9
auth7
dom6
search4
nav4
institution3

UI code (DOM manipulation, wallet calls, async flows) runs only in WASM and is not unit-tested.

Dependencies

rust-bitcoin (v0.32)

The WASM webapp uses rust-bitcoin v0.32 as its Bitcoin primitive library. This is the only crate in the project that works directly with Bitcoin transaction types in Rust.

bitcoin = { version = "0.32", features = ["serde", "base64"] }

What it provides

Type / functionUsed for
bitcoin::TransactionConstructing commit and reveal transactions
bitcoin::psbt::PsbtBuilding partially-signed transactions for wallet signing
bitcoin::taproot::TaprootSpendInfoComputing the Taproot output key from the inscription script leaf
bitcoin::key::XOnlyPublicKeyValidating the admin public key before inscription
bitcoin::AddressDeriving commit output and change addresses from the connected pubkey
bitcoin::ScriptBufBuilding Tapscript inscription envelopes (OP_FALSE OP_IF … OP_ENDIF)
bitcoin::Amount / bitcoin::OutPointUTXO representation for fee calculation
bitcoin::NetworkSwitching between Testnet4, Signet, and Mainnet address formats

Why v0.32 specifically

v0.32 introduced bitcoin::psbt::Psbt with the updated BIP 174 API. The webapp's txbuilder.rs uses Psbt::serialize_hex() and Psbt::from_str() for the round-trip test. v0.31 and earlier have a different PSBT serialization API.

Feature flags

  • serde — enables Serialize/Deserialize on Bitcoin types, used when passing inscription data across the WASM boundary as JSON
  • base64 — enables PSBT base64 encoding, which is the standard format wallets (UniSat, SafePal) expect when receiving a PSBT

WASM webapp dependency graph

binst-pilot-webapp
├── bitcoin 0.32 (serde, base64)          ← Bitcoin transaction primitives
├── binst-inscription  ─── git/[patch]    ← Parse binst metaprotocol inscriptions
├── binst-decoder      ─── git/[patch]    ← Protocol state reconstruction
│   └── citrea-decoder ─── path dep
├── wasm-bindgen 0.2                      ← WASM ↔ JS boundary
├── wasm-bindgen-futures 0.4              ← async/await in WASM
├── web-sys 0.3                           ← DOM, fetch, console, storage APIs
├── js-sys 0.3                            ← JsValue, Promise
├── serde + serde_json                    ← JSON serialization
└── html-escape 0.2                       ← XSS-safe HTML generation

Protocol crate feature flags

binst-decoder ships with two feature sets:

FeaturePurpose
std (default)Standard library — for CLI and native tests
wasmEnables wasm-bindgen exports and serde_json for WASM use

The webapp uses default-features = false, features = ["wasm"] to avoid pulling in std-only code into the WASM binary.


rust-bitcoin vs the protocol crates

The split is intentional:

ResponsibilityCrate
Parse inscription JSON from Bitcoin witnessbinst-inscription (no rust-bitcoin dep)
Decode Citrea DA state diffscitrea-decoder / binst-decoder (no rust-bitcoin dep)
Build commit+reveal PSBTs for wallet signingbinst-pilot-webapp via txbuilder.rs (uses rust-bitcoin)

The protocol crates stay no_std-compatible and do not depend on rust-bitcoin. Only the webapp's transaction builder needs Bitcoin primitives, and it is the only place they are used.

Scripts & Tooling

Six TypeScript scripts demonstrate the protocol end-to-end from the CLI. The WASM Webapp provides the browser-native equivalent for institution creation, process design, and step execution.

ScriptPurposeStatus
demo-flow.tsEnd-to-end: deploy → institution → members → process → execute all stepsActive
inscribe-binst.tsGenerate ord commands to inscribe BINST entities on Bitcoin testnet4Active
taproot-vault.tsBuild Taproot leaf scripts for inscription UTXO safety (NUMS + CSV + multisig)Deprecated — replaced by binst-decoder::vault (Rust miniscript)
psbt-transfer.tsGenerate PSBT commands for atomic vault transfersDeprecated — replaced by wallet-native descriptor signing
bitcoin-awareness.tsRead Bitcoin Light Client, query finality RPCsActive
finality-monitor.tsPoll Citrea RPCs until a watched L2 block is committed / ZK-provenActive
test-protocol.tsQuery live deployed contracts on Citrea testnetActive

Note: taproot-vault.ts and psbt-transfer.ts are kept as reference implementations. The Rust vault module (binst-decoder/src/vault.rs) uses BIP 379 miniscript to produce wallet-compatible Taproot descriptors, replacing the hand-rolled scripts. See Taproot Vault.

Usage Examples

# Run end-to-end demo on local Hardhat
npx hardhat run scripts/demo-flow.ts

# Deploy to Citrea Testnet
npx hardhat run scripts/demo-flow.ts --network citreaTestnet

# Generate inscription command
npx ts-node scripts/inscribe-binst.ts institution "Acme Financial" <admin_pubkey>

# Generate vault descriptor (Rust — replaces taproot-vault.ts)
cd binst-protocol && cargo test -p binst-decoder vault

# Bitcoin awareness (reads Light Client)
npx tsx scripts/bitcoin-awareness.ts

# Monitor finality for a specific L2 block
WATCH_L2=23972426 npx tsx scripts/finality-monitor.ts

Infrastructure & L2 Config

Development Environment

ComponentDetails
Dev machine (macOS)Node.js 22+, Rust 1.94, Hardhat 3.2, ord 0.27
Bitcoin Core testnet4Remote node via SSH tunnel, rpc:48332, fully synced
ord indexLocal, syncing against testnet4 node
Citrea testnetPublic RPC https://rpc.testnet.citrea.xyz, chain 5115

Citrea Testnet Configuration

SettingValue
RPChttps://rpc.testnet.citrea.xyz
Chain ID5115
EVMShanghai (no Cancun)
CurrencycBTC
FaucetCitrea Discord #faucet
Explorerexplorer.testnet.citrea.xyz

Citrea System Contracts

ContractAddress
Bitcoin Light Client0x3100000000000000000000000000000000000001
Clementine Bridge0x3100000000000000000000000000000000000002
Schnorr Precompile (BIP-340)0x0000000000000000000000000000000000000200

BINST Deployed Contracts

ContractAddressNetwork
BINSTProcessFactory0x6a1d2adbac8682773ed6700d2118c709c8ce5000Citrea testnet
BINSTProcessFactory0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0Hardhat local

Clementine Bridge

  • Peg-in (BTC → cBTC): send BTC to deposit address → bridge validates via Light Client → mints cBTC
  • Peg-out (cBTC → BTC): burn cBTC → operator pays BTC → dispute via BitVM if needed
  • Testnet: 100-confirmation depth, non-trivial minimum deposit. Use Discord faucet for cBTC.

Quick Start

npm install
npx hardhat compile
npx hardhat test                        # 24 Solidity tests
cd binst-protocol && cargo test         # 80 Rust tests
cd webapp/binst-pilot-webapp && cargo test  # 97 webapp tests
npx hardhat run scripts/demo-flow.ts    # local demo

Test Suite

Solidity Tests (Hardhat 3, node:test)

24 tests in test/BINSTPilot.ts covering the active contract architecture:

BINSTProcessFactory + BINSTProcess (10 tests)

#TestWhat it verifies
1Deploy factoryFactory deploys successfully
2Create instancecreateInstance() deploys BINSTProcess with correct templateInscriptionId
3Instance step namesStep names match creation args
4Execute stepexecuteStep(Completed, "") advances currentStepIndex
5Complete all stepsSequential execution through all steps marks completed = true
6Get user instancesgetUserInstances() returns correct addresses
7Get template instancesgetTemplateInstances() returns instances by inscription ID
8Step state trackinggetStepState() returns correct status per step
9Access controlNon-creator calls revert
10Instance countgetInstanceCount() tracks total instances

Extended lifecycle tests (14 tests)

#TestWhat it verifies
11Deploy factory (variant)Alternate deploy scenario
12Create institution entityInstitution creation emits event, stores name/admin
13Set Inscription IDAdmin can bind Bitcoin inscription
14Set Rune IDAdmin can bind Rune for membership
15Add MemberAdmin adds member, membership confirmed
16Remove MemberAdmin removes member, membership revoked
17Create Process TemplateTemplate created with correct step names
18Create Process InstanceInstance linked to template
19Execute StepFirst step executed, state updated
20Execute All StepsSequential execution through all steps
21Complete InstanceFinal step marks instance as completed
22Access ControlNon-admin calls revert with correct error
23Set admin pubkeyAdmin can set Bitcoin x-only pubkey
24Admin pubkey validationRejects zero pubkey and non-admin calls
npx hardhat test          # runs all 24

Rust Tests (cargo test)

80 tests across 4 crates in binst-protocol/:

binst-inscription (10 tests)

TestWhat it verifies
extract_binst_inscriptionParses binst metaprotocol envelope from witness
extract_with_parent_tagHandles parent inscription tag
no_envelope_in_random_dataRejects non-envelope witness data
ignore_non_binst_inscriptionSkips non-binst metaprotocol
handle_pushdata1Handles OP_PUSHDATA1 encoding
parse_institutionParses institution JSON body
parse_process_templateParses template JSON body
parse_step_executionParses step execution JSON body
parse_state_digestParses state digest body
reject_unknown_typeRejects unknown entity type

binst-decoder (27 unit + 14 value + 11 vault + 5 e2e = 57 tests)

TestWhat it verifies
registry_build_lookup_populates_tableForward-hash lookup table is populated
registry_lookupRegistry resolves address to contract type
registry_resolves_known_slotKnown slot hash maps to BINST field
map_state_diff_finds_binst_entriesState diff entries matched to BINST
map_state_diff_array_elementArray slot elements decoded correctly
decode_deployer_elementsDeployer storage slots decoded
decode_institution_simple_slotsInstitution simple fields decoded
decode_institution_members_array_elementInstitution members array decoded
decode_instance_completedInstance completion flag decoded
decode_instance_step_stateInstance step state decoded
slot_to_u64_simple/largeU256 → u64 conversion
sub_words_basic/underflowWord subtraction arithmetic
evm_storage_hash_*JMT key computation
parse_evm_storage/header/account/indexJMT key prefix parsing
summarize_mixed_diffJMT diff categorization
keccak256_known_vectorKeccak256 matches known output
array_base/element_slotArray storage layout
mapping_slot_addressMapping storage layout
add_word_offsetU256 word offset addition
full_pipeline_* (5 e2e)End-to-end: proof → registry → BINST changes

Vault module (vault — 11 tests)

TestWhat it verifies
compile_produces_descriptorPolicy compiles to tr(NUMS, {…}) descriptor
testnet_address_starts_with_tb1pTestnet address is valid Taproot bech32m
mainnet_address_starts_with_bc1pMainnet address is valid Taproot bech32m
csv_delay_one_compilesMinimum valid CSV delay compiles
csv_delay_zero_rejectedCSV delay = 0 rejected (miniscript minimum is 1)
analyze_returns_two_pathsTwo spending paths: admin + committee
admin_path_has_timelockAdmin path has CSV = 144, requires 1 key
committee_path_is_immediateCommittee path has no timelock, requires 3 keys
witness_sizes_are_reasonableAll paths < 200 vbytes
descriptor_round_tripsparse(format(descriptor)) == descriptor
address_accessor_worksNetwork-specific address accessor returns correct prefix

Value decoding (value module — 14 tests)

TestWhat it verifies
decode_address_from_wordAddress extracted from last 20 bytes of BE word
decode_uint256_smallSmall integer decoded from big-endian bytes
decode_uint256_zeroZero value decoded correctly
decode_uint256_timestampTimestamp-sized integer decoded
decode_bool_trueNon-zero byte → true
decode_bool_falseZero bytes → false
decode_bytes32_pubkeyFull 32-byte hex preserved
decode_short_stringInline Solidity string (≤31 chars) decoded
decode_short_string_emptyEmpty string slot decoded
decode_long_stringLong string length marker detected
decode_step_state_completedPacked StepState: status + actor extracted
decode_value_deletedNone raw value → DELETED
decode_value_from_hexFull pipeline: Citrea LE hex → BE → decoded address
field_type_coverageEvery FieldChange variant has a type mapping

citrea-decoder (7 tests)

TestWhat it verifies
parse_real_sequencer_commitmentParses real commitment from witness data
reject_too_shortRejects truncated input
batch_proof_output_roundtripProof output serialize/deserialize
heuristic_finds_embedded_journalHeuristic journal extraction
decompress_empty_failsBrotli rejects empty input
decompress_garbage_failsBrotli rejects random data
brotli_roundtripBrotli compress → decompress roundtrip

cli (5 tests)

TestWhat it verifies
state_diff_from_rpc_basicRPC state diff hex map conversion
state_diff_from_rpc_no_prefixHandles keys without 0x prefix
decode_address_array_emptyABI decodes empty address[]
decode_address_array_two_addrsABI decodes two-element address[]
decode_address_array_too_shortRejects truncated ABI data
cd binst-protocol && cargo test    # runs all 80 protocol tests

WASM Webapp Tests (cargo test)

97 tests in webapp/binst-pilot-webapp/, running on the native target (no browser required):

ModuleCountWhat is tested
stack21Stack ordering, parent refs, reorder, validate, summary
l2_queue13L2Queue push/remove/clear/validate/summary, L2ActionKind labels
storage12localStorage save/load/confirm/clear, JSON round-trips
stack_plan12build_plan — all institution states: Root, InBatch, InMempool, Confirmed, External; ParentSource routing; sibling deferral
txbuilder9Commit+reveal PSBT construction, fee calculation, parent UTXO input
decode9JSON/witness/vault decoder helpers
auth7Authentication state transitions
dom6HTML escaping, toast variants, DOM helpers
search4Institution card rendering, HTML escaping, source badges
nav4View routing, URL hash parsing
institution3Institution card rendering

UI code (DOM manipulation, wallet calls, async flows) runs only in WASM and is covered by browser smoke testing, not unit tests.

cd webapp/binst-pilot-webapp && cargo test    # runs all 97 webapp tests

Running Everything

# Solidity
cd binst-pilot && npx hardhat test            # 24 tests

# Protocol crates
cd binst-protocol && cargo test               # 80 tests

# WASM webapp
cd webapp/binst-pilot-webapp && cargo test    # 97 tests

# Total: 201 tests

Use Cases

The pilot's architecture — institution + process template + process instance — is generic. Any multi-step workflow that benefits from on-chain auditability and Bitcoin-anchored identity can be modeled.


What the pilot demonstrates

The demo-flow.ts script runs a complete lifecycle:

  1. Deploy an institution with a named admin
  2. Bind a Bitcoin inscription ID to the institution
  3. Add members to the institution
  4. Create a process template with defined steps
  5. Instantiate the process
  6. Execute each step sequentially, recording actor and timestamp
  7. Complete the process instance

This generic flow maps to any domain where who did what, when, and in what order matters.

Example mappings

DomainInstitutionProcess TemplateSteps
Public adminMunicipal officePermit applicationSubmit → Review → Approve/Reject
Private sectorCompany HRHiring workflowPost → Screen → Interview → Offer
LegalArbitration bodyDispute resolutionFile → Assign mediator → Hear → Decide
GovernanceCommunity orgProposal lifecycleDraft → Discuss → Vote → Execute
Supply chainManufacturerQuality inspectionSample → Test → Report → Certify

Each row is a different parameterization of the same four contracts. The pilot doesn't implement domain-specific logic — it provides the framework that any of these domains can plug into.

Contributing

BINST is an open-source project under the Bitcoin-Institutions organization.

Repository

Areas of contribution

AreaStackDescription
Smart contractsSolidity 0.8.24, Hardhat 3Extend or improve the four core contracts
BINST ProtocolRust, no_stdImprove Bitcoin transaction parsing, add crates
Scripts & toolingTypeScript, ViemNew protocol demonstrations, CLI tools
Inscription toolingRust, ordImprove binst metaprotocol parsing and indexing
WebappRust, WASM, TrunkImprove the pilot web UI
DocumentationMarkdown, mdbookImprove or translate this book
TestingTypeScript, RustExpand test coverage across all layers

Development setup

# Clone the pilot
git clone https://github.com/Bitcoin-Institutions/binst-pilot.git
cd binst-pilot

# Solidity
npm install
npx hardhat compile
npx hardhat test

# Rust
cd binst-protocol
cargo build
cargo test

License

MIT — see LICENSE.

References