BINST Pilot
A proof-of-concept for Bitcoin-sovereign institutional processes.
This pilot implements a three-layer architecture for institutional operations where the Bitcoin key is the root of authority, Ordinals inscriptions carry institutional identity, and an EVM-compatible L2 executes operational logic as a portable delegate.
The problem
Current approaches to on-chain institutional operations either:
- Run everything on an L2, with no Bitcoin anchor — identity is L2-dependent
- Use Bitcoin only for settlement, with no institutional semantics
- Lock users into a single L2 with no portability
The pilot's approach
| Concern | How the pilot handles it |
|---|---|
| Institutional identity | Inscribed on Bitcoin L1 via Ordinals — permanent, sovereign |
| Membership | Represented by Runes tokens on Bitcoin L1 |
| Operational logic | Runs on an L2 (Citrea) as a portable delegate |
| Authority | The Bitcoin key holder controls everything; the L2 contract obeys |
| L2 portability | Switching L2s means redeploying contracts bound to the same inscription |
| UTXO safety | Taproot script tree (NUMS + CSV + multisig) protects inscription sats |
| Bitcoin verification | Taproot Reader decodes L2 state directly from Bitcoin DA transactions |
If the L2 disappears, the inscription remains. If the L2 is replaced, the same Bitcoin identity binds to the new contracts.
What the pilot implements
- 4 smart contracts deployed and verified on Citrea testnet — factory, institution, process template, process instance
- 4 Rust crates (Taproot Reader) — 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
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
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.
┌─────────────────────────────────────────────────────────────┐
│ 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) │
│ │
│ Institution.sol ─────── On-chain institution definition │
│ ProcessTemplate.sol ─── Immutable workflow logic │
│ ProcessInstance.sol ─── Running execution + state │
│ BINSTDeployer.sol ───── Factory & registry │
│ │
│ Cross-chain: LayerZero V2 mirrors identity to other L2s │
│ Execution state verified trustlessly via Bitcoin DA proofs │
└──────────────────────────┬──────────────────────────────────┘
│ future
┌──────────────────────────▼──────────────────────────────────┐
│ VERIFICATION LAYER (Future) │
│ │
│ BitVM ──────── Fraud-proof verification on BTC │
│ BitVMX ─────── RISC-V execution verification │
│ Covenants ──── Native BTC spending constraints │
│ SNARK ──────── ZK proof verification on BTC │
└─────────────────────────────────────────────────────────────┘
Each layer serves a distinct purpose:
- Bitcoin L1 — permanent identity, membership, and the root of authority
- L2 Processing — complex logic execution as a delegate of the Bitcoin key holder
- Verification (future) — trust-minimized verification between L2 and Bitcoin
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)
Complex institutional logic executes on L2 smart contracts as a delegate of the Bitcoin key holder:
- Multi-step workflow execution with validation rules
- Payment processing
- Cross-contract calls and event emission
- State management (current step, completion, timestamps)
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 contracts at the same inscription ID. The identity stays on Bitcoin.
Why Citrea?
| Feature | Why it matters |
|---|---|
| Fully EVM-compatible | Solidity contracts 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 redeploying contracts and pointing them at the same inscription.
Layer 3: Verification (Future)
As Bitcoin's scripting capabilities evolve, BINST will add a trust-minimized verification layer:
- BitVM / BitVM2 / BitVM3 — fraud-proof verification of L2 state transitions
- BitVMX — RISC-V program execution verification on Bitcoin
- Covenants (OP_CTV, OP_CAT) — native spending constraints for enhanced vaults and trustless bridges
- SNARK verification — ZK proof verification within Bitcoin Script
This layer doesn't exist yet — it depends on Bitcoin protocol evolution. See the Technology Landscape section for details.
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 contract | Processing delegate executes logic | Bitcoin consensus + ZK math | L2 down → redeploy 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 contracts are replaceable processing delegates. Losing an L2 is graceful — redeploy 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 deploy a new contract, point it at the same inscription ID, 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 contract | Processing logic (workflows, payments) | Yes — redeploy to any L2 |
| Mirror contracts | Read-only identity/membership on other L2s | 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:
- LayerZero V2 syncs identity and membership across L2s in real-time
- Bitcoin DA provides trustless execution state verification via ZK batch proofs
Mirror contracts on other L2s 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 | Redeploy contracts on another L2; identity survives on Bitcoin |
| Inscription UTXO lost | Serious | Re-inscribe as child of original + deploy new L2 contract |
| 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: btcPubkey
The Institution contract stores a bytes32 btcPubkey field — the
32-byte x-only public key (BIP-340) of the institution admin's Bitcoin key.
This closes the trust gap between Bitcoin identity and L2 contracts. Without it, the link between an inscription and a contract is informational (a stored string). With it, the binding is verifiable:
- Read the institution's
btcPubkeyfrom the L2 contract - Read the inscription UTXO's owner from Bitcoin
- Verify they match — no oracle, no trust
Citrea's Schnorr precompile (0x0000000000000000000000000000000000000200)
enables BIP-340 signature verification on-chain, which is the foundation
for this binding. The Rust BitcoinIdentity struct requires
bitcoin_pubkey — the L2 contract stores the same key.
Institution Anchoring Lifecycle
An institution progresses through three anchoring states:
The Three States
State 1: UNANCHORED (L2-only)
→ Contract deployed on Citrea (or any L2)
→ Functional on L2: members, processes work
→ Batch proofs reach Bitcoin DA (orphan proofs — see below)
→ No inscription, no rune
→ Status: DRAFT
State 2: BINDING (partially anchored)
→ Contract deployed + inscription created
→ But not yet fully linked (setInscriptionId not called,
or rune not yet etched)
→ Status: BINDING
State 3: ANCHORED (fully sovereign)
→ Contract deployed + inscription bound + rune etched + rune bound
→ L2 state is provably linked to Bitcoin identity
→ Status: ANCHORED
Design Decision: Progressive Anchoring
Anchoring is not deployment. An institution exists on the L2 from the moment its contract is deployed. It becomes Bitcoin-anchored when its Ordinals inscription is created and bound to the contract.
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 deploys Institution.sol on Citrea but never inscribes the ordinal identity, 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 an Institution contract, but inscriptionId and runeId would be empty strings.
Orphan proofs are not harmful:
- They are noise the indexer filters with a simple check:
if inscriptionId == "" → 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 (deploy + bind) | Create institution |
| L2-only creation | No | Yes (deploy) | Unanchored institution, process template |
| Event registration | No | Yes (EVM tx) | Execute step, add member |
| 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 contract deployment 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
─────────────────────────────────────────────────────────────
Create Institution → deploy contract (nothing yet)
→ state change on L2
... L2 batches state ...
→ batch proof inscribed ← Bitcoin DA write
(automatic, not
user-initiated)
Inscribe Identity (nothing on L2) ← ordinal inscription
(user-initiated,
separate Bitcoin tx)
Bind Inscription → setInscriptionId() (nothing)
to Contract → now L2 knows its Bitcoin anchor
Write Phases (User-Initiated Transactions)
| Action | Where | Who pays / signs |
|---|---|---|
| Inscribe identity | Bitcoin (ordinal) | User, BTC wallet |
| Etch membership Rune | Bitcoin (rune) | User, BTC wallet |
| Deploy Institution contract | Citrea (EVM) | User, EVM wallet |
| Bind inscription to contract | Citrea (EVM) | User, EVM wallet |
| Add member | Citrea (EVM) | Admin, EVM wallet |
| Send Rune to member | Bitcoin (rune) | Admin, BTC wallet |
| Create process template | Citrea (EVM) | Admin, EVM wallet |
| Create process instance | Citrea (EVM) | Admin, EVM wallet |
| Execute step | Citrea (EVM) | Member, 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 |
| Cross-chain identity query | LayerZero / Bitcoin DA | Relay fee or free |
Wallet UX
Current: Two Wallets
- Bitcoin wallet (Xverse, Unisat) — for inscriptions and runes
- EVM wallet (MetaMask) — 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. Admin deploys Institution.sol on an L2 (currently Citrea)
→ constructor gets: name, admin address
→ admin calls setInscriptionId() and setRuneId() to bind the contract
→ the L2 contract is now a DELEGATE of the Bitcoin key holder
5. L2 state reaches Bitcoin via batch proof
→ institution is now represented THREE ways on Bitcoin:
a) Ordinal inscription (identity — AUTHORITATIVE)
b) Rune (membership token)
c) State diff in batch proof (computational state)
Note: Step 4 can be repeated on any L2. The inscription ID and Rune ID stay the same. Only the L2 contract address changes.
Note: Steps 2–3 (Bitcoin) and step 4 (L2) are independent — they can happen in any order. If step 4 happens first without steps 2–3, 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 |
| Deploy contract | Citrea | EVM contract creation | ~cBTC gas |
| Bind inscription | Citrea | EVM function call | ~cBTC gas |
| Bind Rune | Citrea | EVM function call | ~cBTC gas |
| 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 5 ← metadata tag
OP_PUSH <CBOR-encoded metadata> ← structured metadata
OP_PUSH 3 ← parent tag
OP_PUSH <binst-root-inscription-id> ← provenance chain
OP_PUSH 0 ← body separator
OP_PUSH '{
"type": "institution",
"name": "Acme Financial",
"admin_btc_pubkey": "a3f4...x-only-32-bytes",
"citrea_contract": "0x1234...5678",
"created_btc_height": 127600,
"members": ["pubkey1...", "pubkey2..."]
}'
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 (Xverse, Unisat).
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. Admin calls addMember(memberAddress) on the L2 contract
→ L2 contract updates member list
→ (optional: contract verifies Rune balance via bridge)
3. L2 state diff reaches Bitcoin via batch proof
→ member addition is now 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) | isMember[address] mapping in Institution.sol | EVM view call |
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 that run on L2.
Concepts
- ProcessTemplate — an immutable blueprint defining a sequence of steps (name, description, action type, configuration)
- ProcessInstance — a running execution of a template, tracking which steps have been completed, by whom, and when
Executing a Step
1. Member calls executeStep() on L2 ProcessInstance
→ complex validation, payment, state transitions happen on L2
→ event emitted: StepExecuted(who, stepIndex, timestamp)
2. (Optional) Member inscribes step execution as child of instance
→ permanent, discoverable record on Bitcoin
→ not required for protocol correctness (batch proof handles that)
→ makes it human-readable on explorers
3. L2 batch proof writes state diff to Bitcoin
→ step execution is ZK-proven
Entity-to-Primitive Mapping
| Entity | Nature | Bitcoin Primitive | Reasoning |
|---|---|---|---|
| Institution | Unique, one-of-one | Ordinal inscription | Needs metadata, provenance, UTXO-based ownership |
| Process Template | Unique, immutable | Ordinal inscription (child of institution) | Unique artifact with structured content |
| Process Instance | Unique, mutable state | Ordinal inscription (child of template) | State updates via batch proofs |
| Step Execution | Immutable event record | Ordinal inscription (child of instance) | Permanent discoverable record |
| Membership | Fungible relationship | Rune balance | "Hold ≥1 token = member" |
| Governance vote | Fungible weight | Rune balance (separate) | Transferable, weighted voting power |
Cross-Chain Execution Safety
A ProcessInstance has exactly one home chain. Steps execute only on that chain. This is enforced by design:
- Mirror chains provide read-only identity and membership verification
- Mirror contracts (
InstitutionMirror.sol) have noexecuteStep()function - 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.
See Cross-Chain Synchronization 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. Admin deploys new Institution contract on the new L2
→ binds it to the SAME inscription ID and rune ID
3. The Bitcoin-layer identity is unchanged:
→ same inscription, same UTXO, same admin key
→ same membership Rune, same member balances
→ provenance chain is intact
4. The old L2 contract becomes historical — its batch proofs
remain on Bitcoin as a permanent record of past operations
5. New operations flow through the new L2 contract
→ the institution continues seamlessly
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 contract | 🆕 New address, bound to same inscription |
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.
This is analogous to moving a company's operations from one country to another — the company's identity (registration, brand, ownership) doesn't change. Only the operational jurisdiction does.
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
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, members, inscriptionId, runeId | 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, runeId, admin, name | LayerZero (fast) | Home chain only |
| Membership | members[], isMember | LayerZero (fast) | Home chain only |
| Execution | stepStates[], process progress | Bitcoin DA (trustless) | Home chain only |
Single-Writer Rule
Critical invariant: A ProcessInstance 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 (
InstitutionMirror.sol) expose only view functions:isMember(),getAdmin(),getInscriptionId() - No
executeStep(), nocreateProcess(), 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.sol— an OApp contract that listens for institution events on the home L2 and broadcasts identity state to registered mirror chainsInstitutionMirror.sol— a read-only contract on non-home chains that receives and exposes institution identity/membership 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_btc_pubkey": "a3f4b2c1d5e6f7890123456789abcdef0123456789abcdef0123456789abcdef",
"created": "2026-03-15T12:00:00Z"
}
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.
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 (Xverse, Unisat) — shows as an asset
- Self-hosted
ordindexer — trustless, complete access - No custom BINST software needed for basic discovery
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 (Xverse, Unisat), 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 Contract (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 deploys a new contract on another L2
- New contract references the same inscription ID and rune ID
- 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 contracts continue to function short-term
- Admin re-inscribes a recovery record (child of original)
- L2 contracts are updated to reference the new inscription
- 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 deploys new L2 contracts from the new key
- This is the hardest recovery — the committee backstop is the last resort
The Hierarchy
L2 contract lost → redeploy elsewhere, identity survives on Bitcoin
Inscription UTXO lost → re-inscribe + update L2 (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
btcPubkeybindings on the L2 contracts. ThesetBtcPubkey()function is currently set-once. A PQ migration would require either a new setter or a contract upgrade to rebind to a PQ public key. -
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 setBtcPubkey v2 — allow admin-initiated key rotation with timelocked delay | 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 |
|---|---|---|
| Soft Confirmation | Sequencer signs the L2 block | Transaction receipt |
| Committed | Sequencer commitment inscribed on Bitcoin | citrea_getLastCommittedL2Height |
| ZK-Proven | ZK batch proof inscribed on Bitcoin | citrea_getLastProvenL2Height |
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
taproot-readercan reconstruct which steps were executed, by whom, at what timestamp - Cross-chain execution verification is trustless — it goes through Bitcoin, not through any relay
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 with auto-discovery of BINST contracts
cargo run --bin citrea-scanner -- \
--citrea-rpc https://rpc.testnet.citrea.xyz \
--discover \
--deployer 0xd0abca83bd52949fcf741d6da0289c5ec7235aaf \
--block 127848
# Manual contract addresses (skip discovery)
cargo run --bin citrea-scanner -- \
--citrea-rpc https://rpc.testnet.citrea.xyz \
--deployer 0x... --template 0x... --instance 0x... \
--from 127840 --to 127850
The --discover flag crawls the deployer contract on-chain:
deployer.getInstitutions() → institution.getProcesses() → template.getAllInstances().
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:
ProcessTemplate.name = "KYC Verification"
ProcessInstance.creator = 0x8cf6fe5cd0905b6bfb81643b0dcda64af32fd762
ProcessInstance.stepStates[0] = Completed by 0x8cf6fe5cd0905b6bfb81643b0dcda64af32fd762
ProcessInstance.currentStepIndex = 4
ProcessInstance.completed = true
ProcessTemplate.steps.length = 4
BINSTDeployer.institutions[0] = 0x3a6a07c5d2c420331f68dd407aafff92f3275a86
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 taproot-reader/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? | L2 view call (inscriptionId != "") | ❌ No |
| Was step execution valid? | ZK batch proof decode | ✅ Yes |
| Full trustless verification? | taproot-reader | ✅ Yes |
| Which L2 is processing? | Inscription metadata | ❌ No |
| Is this person a member? (cross-chain) | InstitutionMirror on any L2 | ❌ 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 + taproot-reader
→ 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 deployer contract to find
all BINST contracts registered on-chain:
cargo run --bin citrea-scanner -- \
--citrea-rpc https://rpc.testnet.citrea.xyz \
--discover \
--deployer 0xd0abca83bd52949fcf741d6da0289c5ec7235aaf \
--block 127848
Discovery chain:
deployer.getInstitutions()→ all institution addressesinstitution.getProcesses()→ all template addressesdeployer.getDeployedProcesses()→ standalone templatestemplate.getAllInstances()→ all instance addresses
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
Four Solidity contracts deployed and verified on Citrea Testnet (Chain 5115, Shanghai EVM, Solidity 0.8.24).
Contract Overview
| Contract | Purpose |
|---|---|
BINSTDeployer | Factory/registry — creates institutions and deploys process templates |
Institution | Institution entity — members, admin, Bitcoin identity (inscriptionId, runeId) |
ProcessTemplate | Immutable workflow blueprint with named steps |
ProcessInstance | Running execution with step-by-step state tracking |
BINSTDeployer
The factory and registry for all BINST entities. Creates institutions and process templates, maintains a global registry.
Key functions:
createInstitution(name, admin)→ deploys a newInstitutioncontractcreateProcessTemplate(institutionAddr, steps[])→ deploys an immutable templatecreateProcessInstance(templateAddr)→ creates a running instance
Institution
Defines an institution with Bitcoin anchoring.
State:
name— institution nameadmin— current admin address (EVM)inscriptionId— Ordinals inscription ID (Bitcoin anchor)runeId— Rune ID for membershipmembers[]— member addressesisMembermapping — O(1) membership checkprocesses[]— deployed process contracts
Key functions:
setInscriptionId(id)/setRuneId(id)— bind to Bitcoin identityaddMember(addr)/removeMember(addr)— manage membershiptransferAdmin(newAdmin)— transfer control
ProcessTemplate
Immutable blueprint defining a sequence of steps.
Step struct:
struct Step {
string name;
string description;
string actionType;
string config;
}
Steps are set at deployment and cannot be modified — the template is a permanent record.
ProcessInstance
A running execution of a template, tracking step-by-step progress.
StepState struct:
struct StepState {
StepStatus status; // Pending, Completed, Skipped
address actor; // who executed it
string data; // execution data
uint256 timestamp; // when it was executed
}
Key function:
executeStep(stepIndex, data)— complete a step (only callable once per step)
EVM Configuration
Citrea does not support Cancun. Target Shanghai:
solidity: {
version: "0.8.24",
settings: { evmVersion: "shanghai", optimizer: { enabled: true, runs: 200 } }
}
Taproot Reader (Rust)
A Rust workspace that decodes BINST data directly from Bitcoin. Four crates, 79 tests.
Crate Architecture
taproot-reader/
crates/
citrea-decoder/ ← Citrea DA inscription parser (no_std, WASM-ready)
binst-decoder/ ← Storage slots → protocol entities + miniscript vault module
binst-inscription/ ← Ordinals envelope parser for binst metaprotocol
cli/ ← citrea-scanner binary (Bitcoin Core RPC + Citrea RPC)
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
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.
Scripts & Tooling
Six TypeScript scripts demonstrate the protocol end-to-end.
| 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 taproot-reader && 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 |
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 # 14 Solidity tests
cd taproot-reader && cargo test # 68 Rust tests
npx hardhat run scripts/demo-flow.ts # local demo
Test Suite
Solidity Tests (Hardhat 3, node:test)
14 tests in test/BINSTPilot.ts covering the full contract lifecycle:
| # | Test | What it verifies |
|---|---|---|
| 1 | Deploy BINSTDeployer | Factory deploys, owner set correctly |
| 2 | Create Institution | createInstitution() emits event, stores name/admin |
| 3 | Set Inscription ID | Admin can bind Bitcoin inscription |
| 4 | Set Rune ID | Admin can bind Rune for membership |
| 5 | Add Member | Admin adds member, isMember returns true |
| 6 | Remove Member | Admin removes member, isMember returns false |
| 7 | Create Process Template | Template created with correct step names |
| 8 | Create Process Instance | Instance linked to template and institution |
| 9 | Execute Step | First step executed, state updated |
| 10 | Execute All Steps | Sequential execution through all steps |
| 11 | Complete Instance | Final step marks instance as completed |
| 12 | Access Control | Non-admin calls revert with correct error |
| 13 | Set btcPubkey | Admin can set Bitcoin x-only pubkey |
| 14 | btcPubkey validation | Rejects zero pubkey and non-admin calls |
npx hardhat test # runs all 14
Rust Tests (cargo test)
79 tests across 4 crates in taproot-reader/:
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 taproot-reader && cargo test # runs all 79
Running Everything
# From project root
npx hardhat test && cd taproot-reader && cargo test
# 14 Solidity + 79 Rust = 93 total tests
No external services required — all tests run against local state.
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 |
| Taproot Reader | 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 |
| 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 taproot-reader
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 |