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
| Concern | Approach |
|---|---|
| Institutional identity | Inscribed on Bitcoin L1 via Ordinals — the inscription IS the entity |
| Membership | Runes tokens on Bitcoin L1 — holding ≥1 token means membership |
| Operational logic | Runs on Citrea (EVM L2) as a delegate of the Bitcoin key |
| Authority | The Bitcoin key controls the inscription UTXO; the L2 contract obeys it |
| L2 replaceability | Creating new process instances on a new L2, bound to the same inscription, preserves identity |
| UTXO safety | Taproot script tree (NUMS + CSV + multisig) protects the inscription sat |
| Event verification | L2 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) andBINSTProcess(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
binstmetaprotocol 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
- Institutional identity can be permanently inscribed on Bitcoin L1
- L2 contracts can operate as delegates bound to that Bitcoin identity
- The L2 choice is non-permanent — switching L2s preserves the identity
- Bitcoin transaction data (DA layer) can be decoded to reconstruct full institutional state without trusting the L2
- Inscription UTXOs can be protected with Taproot script trees
- A browser-native app can route L1 actions (PSBTs) and L2 actions (EVM calls) to the correct wallet with no mocked flows
Source Code
| Repository | Link |
|---|---|
| Pilot | github.com/Bitcoin-Institutions/binst-pilot |
| This documentation | github.com/Bitcoin-Institutions/binst-pilot-docs |
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
BINSTProcessinstance carries atemplateInscriptionIdthat 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:
| Primitive | Role | What it represents |
|---|---|---|
| Ordinals inscriptions | Entity identity, ownership, metadata | Institutions, process templates, process instances |
| Runes | Membership and fungible roles | "Alice is a member of Acme Financial" |
| ZK batch proofs | Computational integrity | Every 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-containedBINSTProcessinstancesBINSTProcess— carries its own step definitions +templateInscriptionIdanchor 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?
| Feature | Why it matters |
|---|---|
| Fully EVM-compatible | Solidity 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 DA | Real Bitcoin data, not simulated |
| Three finality levels | Soft 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:
| Stage | Meaning |
|---|---|
| Soft Confirmation | Sequencer has ordered the tx |
| Committed | Sequencer commitment inscribed on Bitcoin |
| Proven | ZK 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
| Layer | What it proves | Trust assumption | Failure mode |
|---|---|---|---|
| Ordinal inscription | Entity exists, admin controls UTXO | Bitcoin consensus | UTXO accidentally spent → lose root authority |
| Rune balance | This person is a member | Bitcoin consensus | Token accidentally sent → membership lost |
| L2 process instance | Processing delegate executes logic | Bitcoin consensus + ZK math | L2 down → create instances on another L2 |
| L2 batch proof | Every state transition was correct | Bitcoin consensus + ZK math | Proof 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
| Layer | What it controls | Can the user switch it? |
|---|---|---|
| Inscription UTXO | Identity, metadata, provenance | No — this IS the identity |
| Rune distribution | Membership tokens | No — lives on Bitcoin L1 |
| L2 process instances | Processing logic (workflows, payments) | Yes — create on any L2 |
| Mirror contracts | Read-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
| Scenario | Severity | Recovery |
|---|---|---|
| L2 goes down | Graceful | Create new instances on another L2; identity survives on Bitcoin |
| Inscription UTXO lost | Serious | Re-inscribe as child of original + create new L2 instances |
| Bitcoin key lost | Catastrophic | Committee 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:
- Read the institution inscription's
adminfield from Bitcoin L1 - Read the inscription UTXO's owner from Bitcoin
- Verify they match — no oracle, no trust
- L2 process instances carry a
templateInscriptionIdthat 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
| Pattern | Bitcoin TX needed? | L2 TX needed? | Example |
|---|---|---|---|
| Full entity creation | Yes (inscription + rune) | Yes (create instance) | Anchored institution + process |
| L2-only creation | No | Yes (create instance) | Unanchored process execution |
| Step execution | No | Yes (EVM tx) | Execute step in BINSTProcess |
| Verification | No | No (read only) | Check membership, verify proof |
Read/Write Phase Model
Two Transaction Domains
BINST operations happen in two independent domains:
- Bitcoin transactions — deliberate, user-initiated actions that create or transfer identity (inscriptions, runes)
- 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)
| Action | Where | Who pays / signs |
|---|---|---|
| Inscribe institution | Bitcoin (ordinal) | User, BTC wallet |
| Inscribe process template | Bitcoin (ordinal, child) | User, BTC wallet |
| Etch membership Rune | Bitcoin (rune) | User, BTC wallet |
| Send Rune to member | Bitcoin (rune) | Admin, BTC wallet |
| Create process instance | Citrea (EVM) | Admin, EVM wallet |
| Execute step | Citrea (EVM) | Authorized user, EVM wallet |
Automatic (No User Action)
| Action | Where | Who pays |
|---|---|---|
| ZK batch proof | Bitcoin DA | Citrea 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)
| Action | Where | Cost |
|---|---|---|
| Verify membership | Citrea (EVM view call) | Free |
| Check process state | Citrea (EVM view call) | Free |
| Verify inscription exists | Bitcoin (indexer query) | Free |
| Verify batch proof | Bitcoin 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
BINSTProcessinstance on Citrea references this inscription viatemplateInscriptionId, 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
| Step | Chain | Transaction type | Cost |
|---|---|---|---|
| Generate key | Offline | None | Free |
| Inscribe identity | Bitcoin | Ordinal inscription (~500B) | ~$2–5 |
| Etch Rune | Bitcoin | Runestone in OP_RETURN | ~$1–3 |
| Batch proof | Bitcoin | Automatic (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:
| Layer | How membership is represented | How to check |
|---|---|---|
| Bitcoin L1 | Rune balance (ACME•MEMBER ≥ 1) | Any Rune indexer |
| L2 (Citrea) | On-chain state in ZK batch proof | Batch 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 + atemplateInscriptionIdlinking 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 tocitrea.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
| Entity | Nature | Bitcoin Primitive | L2 Primitive | Reasoning |
|---|---|---|---|---|
| Institution | Unique, one-of-one | Ordinal inscription | (none — L1 only) | Identity lives on Bitcoin |
| Process Template | Unique, immutable | Ordinal inscription (child of institution) | (none — L1 only) | Definition lives on Bitcoin |
| Process Instance | Unique, mutable state | (represented via ZK batch proofs) | BINSTProcess contract | Execution state on L2, settled to BTC via proofs |
| Step Execution | Immutable event record | (settled via ZK batch proofs) | StepExecuted event | Reaches Bitcoin through batch proof, not individual inscription |
| Membership | Fungible relationship | Rune balance | (none — L1 only) | "Hold ≥1 token = member" |
| Governance vote | Fungible weight | Rune 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
| Element | After 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:
| Channel | What it syncs | Speed | Trust model |
|---|---|---|---|
| LayerZero V2 | Identity: name, admin, inscriptionId | Fast (real-time) | DVN-configurable |
| Bitcoin DA | Execution: process step states, completion proofs | Slow (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
| Tier | Data | Sync method | Writable? |
|---|---|---|---|
| Identity | inscriptionId, admin, name | LayerZero (fast) | Home chain only |
| Membership | Rune balance | Bitcoin L1 (authoritative) | Bitcoin only |
| Execution | stepStates[], process progress | Bitcoin 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:
- You allowed the conflict to happen
- You detected it after the fact
- 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(), nocreateInstance(), 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
| Type | Parent requirement | Purpose |
|---|---|---|
institution | None (root of its tree) | Institution identity and metadata |
process_template | Institution inscription | Immutable process blueprint |
process_instance | Process template inscription | Running execution of a template |
step_execution | Process instance inscription | Record 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:
| Level | Ordinals inscription? | Rationale |
|---|---|---|
| Institution | Yes — always | This IS the identity. Permanent, inscribed once. |
| Process Template | Yes | Proves "this process belongs to this institution" on Bitcoin. Inscribed once, never changes. |
| Process Instance | Optional | Created frequently. L2 state + ZK batch proof is sufficient. |
| Step Execution | No | High-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
ordindexer — 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:
| Badge | Label | Mechanism | Forgeable? |
|---|---|---|---|
⛓ green | provenance verified | Parent sat spent as vin of reveal tx | ❌ No — requires private key |
✓ amber | unverified | Tag 3 in envelope, confirmed by indexer parents field | ⚠️ Yes — any witness script can contain tag 3 |
⬡ grey | declared | JSON 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:
-
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 thei."46f5557a10e7a75ee0b03abea6c0ecaf9f74ce7ec7d3353359744cc518ca5226i0" ├── reveal_txid = "46f5557a10e7a75ee0b03abea6c0ecaf9f74ce7ec7d3353359744cc518ca5226" └── vout = 0 -
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} -
Check the inputs (
vinarray) 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
currentOutputfield 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 -
-
If any vin matches →
"verified". The reveal tx was signed by the holder of the institution's private key. -
If no vin matches → fall back to Xverse
parentstag-3 check →"linked"(on-chain declared, not cryptographically secured).
Coverage matrix
| Child position in chain | Stage A (prod) | Stage B genesis | Stage 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
| Claim | Strength | What it proves |
|---|---|---|
institution_id in JSON | Weak | Nothing — self-declared |
| Tag 3 in envelope | Weak | That the inscriber knew the parent's ID — nothing more |
| Parent sat spent as reveal vin | Strong | The 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 contents | Minimum signatures |
|---|---|
| 1 root inscription (institution) | 2 |
| 1 institution + 1 process template | 4 |
| 2 institutions + 2 process templates | 8 |
| 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
| Risk | In-session | Cross-session | Mitigation |
|---|---|---|---|
| Parent UTXO not yet available | None — held in memory | Low — Mempool.space returns unconfirmed txs | fetch_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 current | Would fail if vout 0 of institution reveal used directly | Resolved: Xverse currentOutput always returns the live sat location |
| Parent gets RBF-replaced before child is signed | None — both signed atomically | Possible — 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 sequentially | Not possible — UI enforces one-at-a-time signing | Sequential execution is enforced by the async loop in on_sign_inscribe |
| Child references evicted parent (mempool full, low fee) | Very low — both broadcast immediately | Low on testnet4; possible on mainnet at high-fee periods | CPFP the child; both parent and child get mined together |
institution_id field empty in JSON body | Not possible if parent_ref set correctly | Not possible if inscription ID stored in localStorage | Planner validates and warns; stack UI shows ↑ batch / ↑ mempool |
| Ordinals indexer sees child before parent | Indexers process by block, not mempool | Same | Non-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:
-
In-batch parents always precede their children in the stack (enforced by the UI — institution must be added before its templates).
-
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. -
The Xverse Ordinals indexer is available at sign time. If it is unreachable,
fetch_inscription_sat_utxofalls 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. -
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:
-
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 thetxbuilderat build time. -
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
| Action | How | Who |
|---|---|---|
| Check membership | Query Rune balance ≥ 1 | Anyone |
| Add member | Send 1 unit to member's Bitcoin address | Admin |
| Remove member | Burn via edict, or member sends back | Admin or member |
| View membership | Any Rune-aware wallet | Member |
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:
- Wallet compatibility — the descriptor is importable into Sparrow, Liana, Nunchuk, and any BIP 379 wallet. Users can sign vault spends with standard software.
- Compiler-verified correctness — the miniscript compiler guarantees the spending conditions match the policy. No hand-rolled opcode bugs.
- Witness size analysis — the compiler provides worst-case witness sizes for fee estimation.
- 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
| Path | Who | Delay | Purpose |
|---|---|---|---|
| Key path | Nobody | ∞ | Disabled (NUMS internal key) — no accidental spend possible |
| Leaf 0 | Admin (single key) | ~24 hours (144 blocks CSV) | Deliberate admin transfer with safety delay |
| Leaf 1 | 2-of-3 committee | Immediate | Emergency 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-compatible —
ordtracks 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?
| Scenario | Path | What happens next |
|---|---|---|
| Transfer to new admin | Leaf 0 | Send to new admin's vault; call transferAdmin() on L2 |
| Rotate admin key | Leaf 0 | Send to vault with new admin pubkey |
| Reinscribe (update metadata) | Leaf 0 | Spend → new reveal TX → re-vault |
| Admin key compromised | Leaf 1 | Committee moves to safe address |
| Admin key lost | Leaf 1 | Committee recovers to new admin's vault |
| Migrate to covenant vault | Leaf 0 | Move 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
- Economic — dust-limit UTXO (546 sats) has no spending value to attract
- 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:
| Ordinals | Runes | |
|---|---|---|
| Granularity | Individual sats | Fungible balances per UTXO |
| Multiple per UTXO | Yes (one per sat) | Yes (multiple Rune types) |
| Risk of co-location | High — sat ordering is complex, accidental transfer | Low — Runestone edicts are explicit |
| BINST approach | One inscription per isolated UTXO | Multiple 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:
- Inscription data is permanent and readable on Bitcoin forever
- Membership Runes continue to function on Bitcoin L1
- Admin creates new process instances on another L2
- New instances reference the same inscription ID via
templateInscriptionId - Institution continues with full identity and membership intact
Losing the Inscription UTXO (Serious)
If the admin accidentally spends the inscription UTXO despite vault protection:
- Inscription data is permanent and readable forever
- L2 process instances continue to function short-term
- Admin re-inscribes a recovery record (child of original)
- New L2 instances reference the new inscription ID
- Original provenance chain is preserved
The vault script exists specifically to make this scenario extremely unlikely.
Losing the Bitcoin Key (Catastrophic)
- Committee (Leaf 1, 2-of-3 multisig) recovers the inscription to a new key's vault
- Admin creates new L2 process instances from the new key
- 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 Feature | BIP | Where We Use It |
|---|---|---|
| P2TR output (SegWit v1) | 341 | Vault address (tb1p... / bc1p...) — every inscription UTXO is locked to a Pay-to-Taproot output |
| x-only public keys (32 bytes) | 340 | Admin 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) | 340 | Admin 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) | 341 | 2-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 hashes | 341 | TapLeaf, TapBranch, TapTweak — domain-separated SHA-256 used for tree construction and output key derivation |
| Taptweak (Q = P + t·G) | 341 | NUMS internal key + Merkle root → tweaked output key. This is the core of BIP 341 key commitment |
| NUMS internal key | 341 | Provably unspendable point — disables key-path spend entirely, forcing all spends through the script tree |
| Script-path spend | 341 | Both vault unlock paths (admin and committee) use Taproot script-path spending with control blocks |
| Control blocks | 341 | (parity | leaf_version) || internal_key || sibling_hash — built for both leaves, enabling Taproot proof-of-inclusion |
| Leaf version 0xc0 | 342 | All leaf scripts use the BIP 342 Tapscript leaf version |
| OP_CHECKSIG (Schnorr variant) | 342 | Admin leaf — single-key Schnorr signature check |
| OP_CHECKSIGADD | 342 | Committee leaf — the BIP 342 replacement for OP_CHECKMULTISIG (which is disabled in Tapscript). Accumulates a counter across multiple signature checks |
| OP_CHECKSEQUENCEVERIFY (CSV) | 112 | Admin leaf — enforces 144-block (~24h) relative timelock before the admin can move the inscription UTXO |
| Schnorr precompile on Citrea | 340 | Citrea's precompile at 0x…0200 can verify BIP-340 Schnorr signatures in Solidity — used for Bitcoin-key-based L2 authorization |
| Ordinals inscription in witness | 341 | The inscription envelope lives inside Tapscript witness data. Taproot's witness discount makes inscriptions economically viable |
| Bech32m address encoding | 341 | All P2TR addresses use Bech32m (BIP 350), distinct from SegWit v0's Bech32 |
Features We Deliberately Skip
| Taproot Feature | BIP | Why We Skip It |
|---|---|---|
| Key-path spend | 341 | We 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 / MuSig2 | 340 | MuSig2 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 verification | 340 | Batch 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 field | 341 | The 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 opcodes | 342 | These 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:
- Interactive signing rounds between committee members
- Nonce commitment coordination (two rounds minimum)
- Specialized MuSig2 software on each signer's machine
- All parties online at roughly the same time
OP_CHECKSIGADD in a Tapscript leaf is:
- Non-interactive — each member signs independently
- Standard tooling — any BIP-340 Schnorr signer works
- Auditable — the script is human-readable and each signature is individually verifiable
- 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:
| Enhancement | Basis | What It Enables | Complexity |
|---|---|---|---|
| MuSig2 aggregated key-path (separate vault variant) | BIP 340 | Aggregate 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 341 | Add a third leaf: e.g., a dead-man switch that becomes spendable after 1 year of inactivity, or a dedicated "migrate to covenant vault" leaf | Medium — straightforward script extension |
| Schnorr-signed L2 actions (single-wallet UX) | BIP 340 | Admin signs L2 transactions with their Bitcoin Schnorr key via Citrea's precompile → one wallet, one identity, both layers | Medium — needs account abstraction on L2 |
| Covenants (OP_CTV / OP_CAT) | Proposed | Restrict 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 tree | BIP 341 | Embed multiple inscription commitments in different MAST leaves of a single transaction, reducing on-chain cost for multi-template institutions | Low–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
- BIP 340 — Schnorr Signatures
- BIP 341 — Taproot
- BIP 342 — Tapscript
- River — What Is Taproot?
- River — BIP 341
- River — BIP 342
- River — BIP 340
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 Component | P2PK Exposed? | Risk | Notes |
|---|---|---|---|
| Institution inscription UTXO | Yes — tb1p… output | Low | NUMS internal key — no private key exists to derive |
| ProcessTemplate inscription UTXO | Yes — same P2TR | Low | Same NUMS mitigation |
| Taproot vault (admin leaf) | Yes — admin pubkey in script | Medium | Key revealed only at spend time, not before |
| Taproot vault (committee leaf) | Yes — 3 pubkeys in OP_CHECKSIGADD | Medium | Same: revealed only when the committee path is exercised |
| Citrea DA inscriptions | Yes — sequencer's Taproot output | Not ours | Citrea controls this key |
| Inscription content (JSON body) | No — pure data | None | Data is not a curve point |
| State digest merkle roots | No — SHA-256 hashes | None | Hash-based commitments are quantum-resistant |
| L2 contract state | No — EVM/ECDSA | Separate concern | Citrea's EVM would need its own PQ migration |
| Bitcoin DA batch data | No — Merkle commitments | None | Hash-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:
- Wait for a vault spend transaction to appear in the mempool
- Extract the public key from the witness
- 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
-
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.
-
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. -
Rotate
adminpubkey bindings in inscription bodies. Theadminfield 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 theadminfield. -
Update the metaprotocol schema to support PQ key formats in the
adminfield of inscription bodies (currently a 32-byte x-only Schnorr key).
What We Can Do Now
| Action | Complexity | When |
|---|---|---|
| Document the migration path (this page) | Done | Now |
| Avoid key reuse — each vault uses fresh keys | Already in place | Now |
| Don't rely on key-path spend — NUMS is already default | Already in place | Now |
Design admin key rotation — allow re-inscription with timelocked delay for key migration | Low | Phase 4 |
| Monitor BIPs for PQ address proposals | Ongoing | — |
| Prototype PQ inscription transfer when a PQ address format is available on signet | Medium | When available |
Timeline Assessment
| Milestone | Estimated | Source |
|---|---|---|
| Cryptographically relevant quantum computer | 2030–2040 | NIST, IBM roadmaps |
| Bitcoin PQ address soft fork proposal | 2026–2028 | Community discussion active |
| Bitcoin PQ soft fork activation | 2029+ | Typical activation timelines |
| BINST production deployment | 2026–2027 | Project 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
- @niftynei on Taproot P2PK design
- @JeremyRubin on CTV quantum safety
- BIP-340: Schnorr Signatures
- BIP-341: Taproot
- NIST Post-Quantum Cryptography
- Shor's Algorithm (Wikipedia)
- Grover's Algorithm (Wikipedia)
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
- Every BINST transaction lives in a Citrea L2 block
- The sequencer inscribes Sequencer Commitments (Merkle roots) on Bitcoin — pins ordering
- The batch prover inscribes ZK proofs (Groth16 via RISC Zero) on Bitcoin with state diffs — proves correctness
- Anyone with a Bitcoin node can reconstruct the entire L2 state including all BINST data
Finality Levels
| Level | What happens | How to verify | Trust assumption |
|---|---|---|---|
| Soft Confirmation | Sequencer signs the L2 block | Transaction receipt | Trust sequencer |
| Committed | Sequencer commitment inscribed on Bitcoin | citrea_getLastCommittedL2Height | Bitcoin consensus + sequencer honesty |
| ZK-Proven | ZK batch proof inscribed on Bitcoin | citrea_getLastProvenL2Height | Bitcoin 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:
- Soft-confirmed by the sequencer (~instant)
- Committed to Bitcoin via sequencer commitment inscription
- ZK-proven on Bitcoin via batch proof inscription
- 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-protocolCLI 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
| Variant | ID | Payload | Purpose |
|---|---|---|---|
| Complete | 0 | Vec<u8> (compressed proof) | Full ZK batch proof in one tx |
| Aggregate | 1 | Vec<[u8;32]> txids + wtxids | References chunk txs for large proofs |
| Chunk | 2 | Vec<u8> (fragment) | Fragment of a large proof |
| BatchProofMethodId | 3 | Method ID + signatures + pubkeys | Security council metadata |
| SequencerCommitment | 4 | Merkle root + index + L2 end block | Most 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, andOP_PUSHDATA2encodings
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
| Variant | What it means |
|---|---|
| SequencerCommitment | New L2 batch finalized up to l2_end_block_number |
| Complete | Full ZK batch proof — verify to confirm correctness |
| Aggregate + Chunk | Large proof assembly instructions |
| BatchProofMethodId | Security 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 type | Decoded form |
|---|---|
address | 0x8cf6fe5c…d762 |
uint256 | 4, 1774750572 |
bool | true / false |
string (short ≤31 bytes) | "KYC Verification" |
string (long >31 bytes) | <string, 62 bytes> |
StepState (packed struct) | Completed by 0x8cf6…d762 |
bytes32 | 0x… 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 know | Where to look | Full 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:
factory.getInstanceCount()→ total number of instancesfactory.allInstances(i)→ instance address by indexfactory.getTemplateInstances(inscriptionId)→ instances for a specific L1 templatefactory.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.
| Operation | Mechanism | Approx. cost |
|---|---|---|
| Create institution | Ordinal inscription (~500B text) | ~$2–5 |
| Create process template | Child inscription (~300B) | ~$1–3 |
| Record step execution | Child inscription (~200B) | ~$0.50–2 |
| Etch membership Rune | Runestone in OP_RETURN | ~$1–3 |
| Mint membership for 1 user | Runestone transaction | ~$0.50–1 |
| Transfer institution admin | Send 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.).
| Scale | Sats locked | BTC locked | At $100K/BTC |
|---|---|---|---|
| 1 institution | 546 | 0.00000546 | $0.55 |
| 100 institutions | 54,600 | 0.00054600 | $54.60 |
| 10,000 institutions | 5,460,000 | 0.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:
| Contract | Purpose |
|---|---|
BINSTProcessFactory | Thin factory — deploys process instances, indexes by user and template |
BINSTProcess | Self-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:
| Network | Address | Chain ID |
|---|---|---|
| Citrea testnet | 0x6a1d2adbac8682773ed6700d2118c709c8ce5000 | 5115 |
| Hardhat local | 0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0 | 31337 |
Key functions:
| Function | Selector | Purpose |
|---|---|---|
createInstance(string, string[], string[]) | 0x6f794b70 | Deploy a new BINSTProcess instance |
getTemplateInstances(string) | 0xb43bed00 | All instances for a given L1 template inscription ID |
getUserInstances(address) | 0xfceaae17 | All instances created by an address |
getInstanceCount() | 0xae34325c | Total instance count |
allInstances(uint256) | 0x9b0dc489 | Instance address by global index |
Events:
| Event | Topic0 |
|---|---|
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:
| Function | Selector | Purpose |
|---|---|---|
executeStep(uint8, string) | 0xf16e3a23 | Execute the current step with status + evidence |
currentStepIndex() | 0x334f45ec | Read current step index |
completed() | 0x9d9a7fe9 | Check if all steps are done |
totalSteps() | 0x6931b3ae | Total number of steps |
creator() | 0x02d05d3f | Address of the instance creator |
templateInscriptionId() | 0x0270a0b3 | The L1 inscription this instance is anchored to |
Step status enum: 0 = Pending, 1 = Completed, 2 = Rejected
Events:
| Event | Topic0 |
|---|---|
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:
| Stage | Meaning | Verification |
|---|---|---|
| Soft Confirmation | Sequencer has ordered the tx in an L2 block | Transaction receipt |
| Committed | Sequencer commitment inscribed on Bitcoin | citrea_getLastCommittedL2Height |
| Proven | ZK proof of correct execution inscribed on Bitcoin | citrea_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_stdcompatible, 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 (
valuemodule): decodes raw Citrea LE storage values to addresses, uints, bools, Solidity strings, and packedStepStatestructs - Key discovery: Citrea stores EVM slot values in little-endian word order (entire 32-byte word byte-reversed vs. standard Solidity ABI)
- Carries
BitcoinIdentitystruct linking entities across layers - Miniscript vault module (
vaultmodule): compiles BIP 379 spending policies to Taproot descriptors usingrust-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
bitcoin_pubkey— the root of authority (controls inscription UTXO) — requiredinscription_id— permanent identity on Bitcoinmembership_rune_id— membership token on Bitcoinevm_address— current L2 delegate (optional — changes if L2 changes)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:
| Layer | Technology | What is signed | Wallets |
|---|---|---|---|
| L1 Bitcoin | PSBT (BIP 174) | Commit + Reveal transaction pair | UniSat, Xverse, Leather |
| L2 Citrea EVM | eth_sendTransaction | Individual EVM contract call | MetaMask, 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:
- A commit transaction — funds a Taproot output whose script contains the entity JSON inside an Ordinals envelope
- 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/jsonheader- 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:
| Wallet | Detection | Signing method |
|---|---|---|
| UniSat | window.unisat | signPsbt(hex) |
| Xverse | EIP-6963 / sats-connect | sats-connect SignPsbt request |
| Leather | EIP-6963 | request('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:
| Wallet | Method |
|---|---|
| MetaMask | window.ethereum (isMetaMask) |
| Brave Wallet | window.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 state | ParentSource |
|---|---|
| Institution is also in this batch | InBatch { batch_idx } — live reveal UTXO from same run |
| Institution in mempool (unconfirmed) | FetchMempool { txid } — fetch output 0 at sign time |
| Institution confirmed | FetchMempool { txid } — same fetch, always works |
| Unknown parent | None + 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 txinscription_id— Ordinals inscription IDconfirmedflag — 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.
| Action | Contract call | Trigger |
|---|---|---|
| Create Instance | factory.createInstance(inscriptionId, names[], types[]) | "Create Instance on Citrea" button |
| Execute Step | instance.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
| Module | Purpose |
|---|---|
lib.rs | WASM entry point — boots all views |
auth.rs | Authentication state |
bridge.rs | Confirmation polling loop + pipeline panel DOM; resume_pending_polls on connect |
citrea.rs | All L2 EVM/ABI logic: create_instance, execute_step, ABI encoding, finality queries, event log recovery |
create.rs | Create Institution view — form, validates, pushes to stack |
decode.rs | JSON / witness / vault decoder UI |
design.rs | Design Process view — form, fetches parent UTXO, pushes to stack |
dom.rs | DOM helpers, toast notifications, clipboard |
effects.rs | Visual effects (logo tilt, spark effect) |
execute.rs | Execute view — thin UI wiring, delegates to citrea.rs for all on-chain calls; finality status display |
fetch.rs | Shared HTTP fetch helpers (Mempool.space API) |
inscribe.rs | Full serverless pipeline: fetch UTXOs → build PSBTs → sign → broadcast; fetch_tx_output |
institution.rs | Institution card rendering and navigation |
l2_queue.rs | Pure Rust L2 action queue data model (not wired to UI — kept for tests only) |
nav.rs | View routing and bottom nav, 4 tests |
process.rs | Process view + instance creation via factory flow |
search.rs | Institution search against Ordiscan API + localStorage, 4 tests |
stack.rs | Pure Rust L1 inscription stack — StackEntry, ordering, validation, 21 tests |
stack_plan.rs | Pure-Rust execution planner — institution grouping, InstitutionState, ParentSource, 12 tests |
stack_ui.rs | Stack panel UI — institution-grouped render, plan-driven on_sign_inscribe |
storage.rs | localStorage registry: inscriptions, templates, L2 instance tx hashes, 12 tests |
txbuilder.rs | Commit+reveal PSBT construction; parent UTXO as second reveal input, 9 tests |
wallet.rs | L1 BTC wallet — EIP-6963 + manual detection, connect/disconnect, pubkey extraction |
wallet_picker.rs | Wallet 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:
| Module | Tests |
|---|---|
stack | 21 |
l2_queue | 13 |
storage | 12 |
stack_plan | 12 |
txbuilder | 9 |
decode | 9 |
auth | 7 |
dom | 6 |
search | 4 |
nav | 4 |
institution | 3 |
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 / function | Used for |
|---|---|
bitcoin::Transaction | Constructing commit and reveal transactions |
bitcoin::psbt::Psbt | Building partially-signed transactions for wallet signing |
bitcoin::taproot::TaprootSpendInfo | Computing the Taproot output key from the inscription script leaf |
bitcoin::key::XOnlyPublicKey | Validating the admin public key before inscription |
bitcoin::Address | Deriving commit output and change addresses from the connected pubkey |
bitcoin::ScriptBuf | Building Tapscript inscription envelopes (OP_FALSE OP_IF … OP_ENDIF) |
bitcoin::Amount / bitcoin::OutPoint | UTXO representation for fee calculation |
bitcoin::Network | Switching 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— enablesSerialize/Deserializeon Bitcoin types, used when passing inscription data across the WASM boundary as JSONbase64— 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:
| Feature | Purpose |
|---|---|
std (default) | Standard library — for CLI and native tests |
wasm | Enables 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:
| Responsibility | Crate |
|---|---|
| Parse inscription JSON from Bitcoin witness | binst-inscription (no rust-bitcoin dep) |
| Decode Citrea DA state diffs | citrea-decoder / binst-decoder (no rust-bitcoin dep) |
| Build commit+reveal PSBTs for wallet signing | binst-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.
| Script | Purpose | Status |
|---|---|---|
demo-flow.ts | End-to-end: deploy → institution → members → process → execute all steps | Active |
inscribe-binst.ts | Generate ord commands to inscribe BINST entities on Bitcoin testnet4 | Active |
taproot-vault.ts | Build Taproot leaf scripts for inscription UTXO safety (NUMS + CSV + multisig) | Deprecated — replaced by binst-decoder::vault (Rust miniscript) |
psbt-transfer.ts | Generate PSBT commands for atomic vault transfers | Deprecated — replaced by wallet-native descriptor signing |
bitcoin-awareness.ts | Read Bitcoin Light Client, query finality RPCs | Active |
finality-monitor.ts | Poll Citrea RPCs until a watched L2 block is committed / ZK-proven | Active |
test-protocol.ts | Query live deployed contracts on Citrea testnet | Active |
Note:
taproot-vault.tsandpsbt-transfer.tsare 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
| Component | Details |
|---|---|
| Dev machine (macOS) | Node.js 22+, Rust 1.94, Hardhat 3.2, ord 0.27 |
| Bitcoin Core testnet4 | Remote node via SSH tunnel, rpc:48332, fully synced |
ord index | Local, syncing against testnet4 node |
| Citrea testnet | Public RPC https://rpc.testnet.citrea.xyz, chain 5115 |
Citrea Testnet Configuration
| Setting | Value |
|---|---|
| RPC | https://rpc.testnet.citrea.xyz |
| Chain ID | 5115 |
| EVM | Shanghai (no Cancun) |
| Currency | cBTC |
| Faucet | Citrea Discord #faucet |
| Explorer | explorer.testnet.citrea.xyz |
Citrea System Contracts
| Contract | Address |
|---|---|
| Bitcoin Light Client | 0x3100000000000000000000000000000000000001 |
| Clementine Bridge | 0x3100000000000000000000000000000000000002 |
| Schnorr Precompile (BIP-340) | 0x0000000000000000000000000000000000000200 |
BINST Deployed Contracts
| Contract | Address | Network |
|---|---|---|
| BINSTProcessFactory | 0x6a1d2adbac8682773ed6700d2118c709c8ce5000 | Citrea testnet |
| BINSTProcessFactory | 0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0 | Hardhat 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)
| # | Test | What it verifies |
|---|---|---|
| 1 | Deploy factory | Factory deploys successfully |
| 2 | Create instance | createInstance() deploys BINSTProcess with correct templateInscriptionId |
| 3 | Instance step names | Step names match creation args |
| 4 | Execute step | executeStep(Completed, "") advances currentStepIndex |
| 5 | Complete all steps | Sequential execution through all steps marks completed = true |
| 6 | Get user instances | getUserInstances() returns correct addresses |
| 7 | Get template instances | getTemplateInstances() returns instances by inscription ID |
| 8 | Step state tracking | getStepState() returns correct status per step |
| 9 | Access control | Non-creator calls revert |
| 10 | Instance count | getInstanceCount() tracks total instances |
Extended lifecycle tests (14 tests)
| # | Test | What it verifies |
|---|---|---|
| 11 | Deploy factory (variant) | Alternate deploy scenario |
| 12 | Create institution entity | Institution creation emits event, stores name/admin |
| 13 | Set Inscription ID | Admin can bind Bitcoin inscription |
| 14 | Set Rune ID | Admin can bind Rune for membership |
| 15 | Add Member | Admin adds member, membership confirmed |
| 16 | Remove Member | Admin removes member, membership revoked |
| 17 | Create Process Template | Template created with correct step names |
| 18 | Create Process Instance | Instance linked to template |
| 19 | Execute Step | First step executed, state updated |
| 20 | Execute All Steps | Sequential execution through all steps |
| 21 | Complete Instance | Final step marks instance as completed |
| 22 | Access Control | Non-admin calls revert with correct error |
| 23 | Set admin pubkey | Admin can set Bitcoin x-only pubkey |
| 24 | Admin pubkey validation | Rejects 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)
| Test | What it verifies |
|---|---|
extract_binst_inscription | Parses binst metaprotocol envelope from witness |
extract_with_parent_tag | Handles parent inscription tag |
no_envelope_in_random_data | Rejects non-envelope witness data |
ignore_non_binst_inscription | Skips non-binst metaprotocol |
handle_pushdata1 | Handles OP_PUSHDATA1 encoding |
parse_institution | Parses institution JSON body |
parse_process_template | Parses template JSON body |
parse_step_execution | Parses step execution JSON body |
parse_state_digest | Parses state digest body |
reject_unknown_type | Rejects unknown entity type |
binst-decoder (27 unit + 14 value + 11 vault + 5 e2e = 57 tests)
| Test | What it verifies |
|---|---|
registry_build_lookup_populates_table | Forward-hash lookup table is populated |
registry_lookup | Registry resolves address to contract type |
registry_resolves_known_slot | Known slot hash maps to BINST field |
map_state_diff_finds_binst_entries | State diff entries matched to BINST |
map_state_diff_array_element | Array slot elements decoded correctly |
decode_deployer_elements | Deployer storage slots decoded |
decode_institution_simple_slots | Institution simple fields decoded |
decode_institution_members_array_element | Institution members array decoded |
decode_instance_completed | Instance completion flag decoded |
decode_instance_step_state | Instance step state decoded |
slot_to_u64_simple/large | U256 → u64 conversion |
sub_words_basic/underflow | Word subtraction arithmetic |
evm_storage_hash_* | JMT key computation |
parse_evm_storage/header/account/index | JMT key prefix parsing |
summarize_mixed_diff | JMT diff categorization |
keccak256_known_vector | Keccak256 matches known output |
array_base/element_slot | Array storage layout |
mapping_slot_address | Mapping storage layout |
add_word_offset | U256 word offset addition |
full_pipeline_* (5 e2e) | End-to-end: proof → registry → BINST changes |
Vault module (vault — 11 tests)
| Test | What it verifies |
|---|---|
compile_produces_descriptor | Policy compiles to tr(NUMS, {…}) descriptor |
testnet_address_starts_with_tb1p | Testnet address is valid Taproot bech32m |
mainnet_address_starts_with_bc1p | Mainnet address is valid Taproot bech32m |
csv_delay_one_compiles | Minimum valid CSV delay compiles |
csv_delay_zero_rejected | CSV delay = 0 rejected (miniscript minimum is 1) |
analyze_returns_two_paths | Two spending paths: admin + committee |
admin_path_has_timelock | Admin path has CSV = 144, requires 1 key |
committee_path_is_immediate | Committee path has no timelock, requires 3 keys |
witness_sizes_are_reasonable | All paths < 200 vbytes |
descriptor_round_trips | parse(format(descriptor)) == descriptor |
address_accessor_works | Network-specific address accessor returns correct prefix |
Value decoding (value module — 14 tests)
| Test | What it verifies |
|---|---|
decode_address_from_word | Address extracted from last 20 bytes of BE word |
decode_uint256_small | Small integer decoded from big-endian bytes |
decode_uint256_zero | Zero value decoded correctly |
decode_uint256_timestamp | Timestamp-sized integer decoded |
decode_bool_true | Non-zero byte → true |
decode_bool_false | Zero bytes → false |
decode_bytes32_pubkey | Full 32-byte hex preserved |
decode_short_string | Inline Solidity string (≤31 chars) decoded |
decode_short_string_empty | Empty string slot decoded |
decode_long_string | Long string length marker detected |
decode_step_state_completed | Packed StepState: status + actor extracted |
decode_value_deleted | None raw value → DELETED |
decode_value_from_hex | Full pipeline: Citrea LE hex → BE → decoded address |
field_type_coverage | Every FieldChange variant has a type mapping |
citrea-decoder (7 tests)
| Test | What it verifies |
|---|---|
parse_real_sequencer_commitment | Parses real commitment from witness data |
reject_too_short | Rejects truncated input |
batch_proof_output_roundtrip | Proof output serialize/deserialize |
heuristic_finds_embedded_journal | Heuristic journal extraction |
decompress_empty_fails | Brotli rejects empty input |
decompress_garbage_fails | Brotli rejects random data |
brotli_roundtrip | Brotli compress → decompress roundtrip |
cli (5 tests)
| Test | What it verifies |
|---|---|
state_diff_from_rpc_basic | RPC state diff hex map conversion |
state_diff_from_rpc_no_prefix | Handles keys without 0x prefix |
decode_address_array_empty | ABI decodes empty address[] |
decode_address_array_two_addrs | ABI decodes two-element address[] |
decode_address_array_too_short | Rejects 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):
| Module | Count | What is tested |
|---|---|---|
stack | 21 | Stack ordering, parent refs, reorder, validate, summary |
l2_queue | 13 | L2Queue push/remove/clear/validate/summary, L2ActionKind labels |
storage | 12 | localStorage save/load/confirm/clear, JSON round-trips |
stack_plan | 12 | build_plan — all institution states: Root, InBatch, InMempool, Confirmed, External; ParentSource routing; sibling deferral |
txbuilder | 9 | Commit+reveal PSBT construction, fee calculation, parent UTXO input |
decode | 9 | JSON/witness/vault decoder helpers |
auth | 7 | Authentication state transitions |
dom | 6 | HTML escaping, toast variants, DOM helpers |
search | 4 | Institution card rendering, HTML escaping, source badges |
nav | 4 | View routing, URL hash parsing |
institution | 3 | Institution 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:
- Deploy an institution with a named admin
- Bind a Bitcoin inscription ID to the institution
- Add members to the institution
- Create a process template with defined steps
- Instantiate the process
- Execute each step sequentially, recording actor and timestamp
- Complete the process instance
This generic flow maps to any domain where who did what, when, and in what order matters.
Example mappings
| Domain | Institution | Process Template | Steps |
|---|---|---|---|
| Public admin | Municipal office | Permit application | Submit → Review → Approve/Reject |
| Private sector | Company HR | Hiring workflow | Post → Screen → Interview → Offer |
| Legal | Arbitration body | Dispute resolution | File → Assign mediator → Hear → Decide |
| Governance | Community org | Proposal lifecycle | Draft → Discuss → Vote → Execute |
| Supply chain | Manufacturer | Quality inspection | Sample → 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
- Pilot: github.com/Bitcoin-Institutions/binst-pilot
- Documentation: github.com/Bitcoin-Institutions/binst-pilot-docs
Areas of contribution
| Area | Stack | Description |
|---|---|---|
| Smart contracts | Solidity 0.8.24, Hardhat 3 | Extend or improve the four core contracts |
| BINST Protocol | Rust, no_std | Improve Bitcoin transaction parsing, add crates |
| Scripts & tooling | TypeScript, Viem | New protocol demonstrations, CLI tools |
| Inscription tooling | Rust, ord | Improve binst metaprotocol parsing and indexing |
| Webapp | Rust, WASM, Trunk | Improve the pilot web UI |
| Documentation | Markdown, mdbook | Improve or translate this book |
| Testing | TypeScript, Rust | Expand 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
| Topic | Resource |
|---|---|
| BINST Pilot | github.com/Bitcoin-Institutions/binst-pilot |
| DeBu Studio (Origin) | github.com/diegobianqui/DeBu_studio |
| Ordinals Protocol | docs.ordinals.com |
| Runes Protocol | docs.ordinals.com/runes |
| Citrea | citrea.xyz |
| Citrea Docs | docs.citrea.xyz |
| LayerZero V2 | layerzero.network |
| LayerZero Supported Chains | layerzero.network/developers |
| Hardhat 3 | hardhat.org |
| Viem | viem.sh |
| BIP-340 (Schnorr) | github.com/bitcoin/bips/blob/master/bip-0340.mediawiki |
| BIP-341 (Taproot) | github.com/bitcoin/bips/blob/master/bip-0341.mediawiki |
| BIP-342 (Tapscript) | github.com/bitcoin/bips/blob/master/bip-0342.mediawiki |
| River — What Is Taproot? | river.com/learn/what-is-taproot |
| River — BIP 341 Taproot | river.com/learn/terms/b/bip-341-taproot |
| River — BIP 342 Tapscript | river.com/learn/terms/b/bip-342-tapscript |
| River — BIP 340 Schnorr | river.com/learn/terms/b/bip-340-schnorr-signatures |
| JSON Schema 2020-12 | json-schema.org |
| Borsh Serialization | borsh.io |
| BitVM | bitvm.org |
| Bitcoin Covenants | bitcoincovenants.com |