From One‑Off Genesis to Live Governance: Treating Role Addresses as State in the Bitcoin Indexer
We've moved Via's Bitcoin stack from a one-time "genesis import" to a live governance-driven state.
System wallets (sequencer, verifiers, bridge, governance) are no longer hardcoded facts learned at startup. They're tracked as state, validated against on-chain inscriptions, and updated atomically at runtime.
Safe key rotations without restarts
Genesis boostrapping is now a dedicated step that constructs a canonical state snapshot and validates it against the chain.
Correct address kids per role, a strict-majority vote from the complete verifier set, anchored transaction IDs, and pinned code hashes.
The indexer is simplified to parsing and routing. It consumes the current SytemWallets from storage, and the parser authenticates messages by checking the sender against the active roles.
Governance messages don't come from the proper or authorized addresses and are dropped by design.
Address rotation is now a governed state transition. Bootstrap is a one-shot validated import, and the index's job is deterministic message extraction.
The result is faster incident response, lower blast radius, and a cleaner upgrade path.
TL;DR
- Bootstrap is now separated from indexing. It produces a single validated snapshot anchored to L1.
- System wallets are state, not constants. Rotations apply atomically and propagate without restarts.
- The parser enforces role‑based authorization by verifying that their currently authorized Bitcoin addresses sign governance, bridge, sequencer, and verifier actions. Any message from an unauthorized address is discarded.
- Indexer now focuses on extraction. Verification and state management move to dedicated components.
- Governance messages follow a stable envelope so different implementations agree on how to find and parse them.

What's this upgrade patch about?
Before this patch, our Bitcoin indexer bundled its “bootstrap” logic into the same code path. At start-up, it replayed the genesis transactions, built an in-memory list of sequencer/verifier/bridge/governance addresses, and then kept using those values forever.
What actually changed here is that system wallets stopped being something the verifier learned only once at genesis and became treated as something that can be updated and validated continuously.
If a key rotated later, the verifier never saw the update, as it kept checking signatures against the old address set, so the verifier refused to accept them and froze at the last batch produced under the old key.
Separates bootstrap from indexing: Genesis processing ViaBootstrap It is now a standalone module that runs once and writes validated system wallets to the database. TheViaIndexer no longer replays Genesis on startup as it loads the current wallet state from storage
Treats wallet rotations as state transitions: The inscription parser MessageParser Now, when a valid rotation is detected, the parser treats wallet update messages as operations, updates the store's system wallets immediately, and downstream verifiers see the change without having to restart.
Simplied the index: It no longer tries to bootstrap itself; it takes a SystemWallets snapshot from storage at construction time, uses that for signature validation when parsing messages, and exposes an update_system_wallets method to atomically replace that snapshot when governance messages approve a change.
The parser has been extended to accept the current SystemWallets and to drop any control-plane message (sequencer proposals, bridge proposals, governance upgrades, validator attestations) that isn't signed by the expected signer for that role
Adds operational tooling: There’s also a new inscription builder for proposing a new bridge address, along with examples of a simulated bridge withdrawal and wallet migration.
The result: verifiers automatically pick up key rotations without restarts. Before, every verifier had to update with the new address and restart manually.
The verifier sees rotation messages on-chain and validates them against the current governance rules (e.g., signed by the governance multsig and approved by the verifier quorum).
Bootstrap State: making the start deterministic
When we initially thought about bootstrapping the network, things looked straightforward. Set the sequencer, define the bridge, register governance wallets, etc.
Early on, every note had its own notion of readiness. One had the sequencer key but no bootstrap transaction. Another knew verifiers but hadn't seen all the votes yet. It worked until it did not.
A single missing vote or mismatched address key type could silently fork the view of who was in charge. We realized we needed a canonical state, one bject that described without ambiguity what it means for the network to be "born".
This led us to BoostrapState which is a small struct that has the entire definition of genesis, which wallets play which roles.
Which Bitcoin transaction cements the sequencer proposal and bootstrap event?
Which verifiers signed off, and which versions (bootloader, abstract account) must every node trust?
We gave it a validate() method because description without enforcement will obviously never be enough. The system must check itself.
- The sequencer must be
P2WPKH - The bridge must be Taproot
- Governance must be
P2WSH - The starting block must be non-zero
- A majority of verifiers must have voted
- All expected votes must be there.
Only when everything lines up does the bootstrap pass validation.
We created a new via_bootstrap.rs This file defines the data model for the entire bootstrapping phase, with enforced validation.
The imports bring in the Bitcoin address, transaction IDs, as well as H256 for code hashes, and the existing SystemWallets type that maps system roles to addresses. The struct's operational field shows the current state of bootstrap processing, including the key transaction IDsbinds that bind the sequencer proposal and the overall bootstrap transaction to the Bitcoin chain.
The vote map uses addresses as keys and boolean values. The valid() The method enforces the correct script types per role. Taproot for the bridge, P2WSH for governance, and P2WPKH for other keys. A non-zero starting block verified by the majority of expected verifiers.
In other words, or maybe more understandable. Before the system starts, there are several things that have to be decided and confirmed on-chain.
- Which wallets control which roles for the sequencer, bridge, governance, and verifiers?
- Which Bitcoin transaction defines the sequencer proposal
- Which code versions (bootloader, abstract account) should the network trust?
- Which verifiers have voted yes for the configuration?

BootstrapState collects all of that. Once everything is known, the validate() function is called. Only if everything is correct, such as wallet script types, valid TXIDs, majority verifier approach, non-zero starting block, and defined code hashes. Only then is the bootstrap legitimate.

Runtime validation and ownership
This change adds a bridge address validation helper and simplifies how transaction identifiers are threaded through wallet details. The helper defers correctness checks to runtime by returning an error when a bridge address is missing.

The old txid: seq_txid.clone() calls to Clone trait and results in a 32-byte copy to the stack. The new txid: *tx.id dereferences the reference.

Avoiding the function call overhead of .clone. Basically, a small micro optimization.
Separating Bootstrap from indexing
Before, the indexer had too much responsibility as it didn't just parse inscriptions but also handled bootstrapping, relayed historical transactions, and tried to infer the system's wallet configuration on its own.
So here we move Bootstrap into its own module that builds a single, validated snapshot instead of letting the indexer discoverer do so.
It reads the configured bootstrap txids, parses their inscriptions, and constructs the system wallets (sequencer/bridge/governance/verifiers), the starting L1 block, code hashes, and verifier attestations tied to the sequencer.
Now the bootstrap process has been pulled out into its own module, leaving the indexer to do one thing: Extract data from the chain.
Instead of reconstructing the system wallet state from on-chain messages, the indexer now receives a live snapshot of the network's current wallets through an injected Arc<SystemWallets.
Instead of reconstructing the system wallet state from on-chain messages, the indexer now receives the current wallets through an injected Arc<SystemWallets>.
This means that who is authorized to sign is no longer something the indexer discovers, as it is now provided, enabling atomic updates when roles change.

All governance and system operations, such as sequencer proposals, attestations, upgrades, and bridge configuration, are now in a SystemTransactions bundle.
Every parsed message is now paired with its exact outpoint on the Bitcoin chain. Each system event includes a verifiable location, which makes duplication, replay protection, and audit easier.
The indexer no longer interprets or validates the meaning or legitimacy of system messages, as it simply collects and forwards them now. Validation now happens inside the parser (for message origin) and the bootstrap module (for initial state consistency).


Upgrading the parser to enforce signature validation
Before this change, the perser only validated what an inscription said, not who it said. Any transaction that looked like an upgrade, bridge proposal, or attestation would be emitted for downstream checks.
The parser now also verifies the sender. It can optionally take a snapshot of the system wallets (trusted Bitcoin addresses for governance, bridge, sequencer, and verifiers). For each inscription, it decodes the message and checks the sender against the expected role.
- Governance can issue upgrades and propose a new bridge
- Verifiers and submit attestations
- User activity (L1 -> L2 deposits, withdrawals) as well as sequencer proposals and boostrapping remain unconditional.
- If a control message doesn't come from the right wallet, the parser drops it.

When the wallet snapshot is None, the parser drops governance/verifiers control messages rather than emitting unauthenticated data.
This integrates with the indexer, which maintains an in-memory wallet snapshot and updates atomically when on-chain wallet updates are observed.
The indexer can reproccess the affected block window so parsing with the new snapshot yields a consistent authorized view of history.
Our broader pipeline also associates tx/oupoint with each emitted message and with order system inscriptions to ensure wallet updates take effect before dependent messages.

Via Inscription Builders for Bridge Operations on Bitcoin
When we wanted to propose a bridge change, the "what" was clear. Rotate a new key and point to a new script path. The "how" to do that on Bitcoin was anything but.
We would stuff bytes into a transaction, but without a stable pattern, different indexers would disagree on how to find them. How to interpret them and if they were meant for us at all.
Each new feature meant another encoding, another parser tweak, and more risks for a long tail of edge cases.
In a few months, we will end up with a museum of "almost right" formats that make upgrades risky.
So we started thinking:
What a good governance message looks like on Bitcoin?
- It should be committed on-chain but not executed.
- It should be easy to discover, even if we don't know the exact transaction. The inder should be able to filter for "our" messages.
- It should be self-descriptive, so once found, we know which action it is and how to parse the data.
- It should be portable across languages and safe against classic off-by-one endian mistakes.
- It should scale. Today, a bridge proposal, tomorrow other governance actions, without reinventing the wheel each time.
This led us to a simple envelope pattern.
A non-executing script branch that carries three things in order. A protocol tag (our VIA namespace), an action code (what the message is), and a payload.
Because the branch doesn't execute, Bitcoin validation ignores it, but the data is still committed to the chain. Indexers can scan for our namespace, read the action, and parse the payload deterministically.
We wrote a small builder so no one has to construct scripts anymore, and we added the first concrete action. Propose a new bridge. It carries 2 pieces of information.
The new key's hash and the script path's hash are encoded in a straightforward JSON payload.
JSON isn't the most compact, but it's readable, ubiquitous across systems, and minimizes integration friction.
If fee pressure ever makes the bytes matter, we can switch to a more compact encoding without changing the overall pattern.
When someone proposes a bridge change, they use the builder, include the script in a transaction, and broadcast. The indexers that understand our namespaces spot it right away, see the propose_new_bridge action, parse those 32-byte hashes, and hand it off to the governance, which knows how to validate and gate it.

More technical
The inscription module defines a fixed protocol tag string via and provides builders that produce small Bitcoin scripts carrying Via messages. We get a consistent script envelope plus a specific message type for proposing a new bridge.
The script is wrapped in OP_FALSE OP_IF ... OP_ENDIF. Because it starts with OP_FALSE, the IF body never executes during validation. However, all bytes inside are still committed on-chain.
That's the standard pattern for committing tagged data in a non-executing branch so off-chain indexers can find and parse it without affecting spendability.
What goes inside the envelope:
- Protocol tag:
"via"(a namespace so indexers can filter for Via messages). - Opcode: a short action string, here
"propose_new_bridge". - Payload: the action’s data, here two 32‑byte hashes (
key_hashandscript_path_hash) serialized to JSON.
This matters because on Bitcoin, you generally can't "run" complex governance logic on-chain.
Instead, we publish structured data that expresses intent. Indexers that understand the Via protocol scan for this pattern (protocol tag + opcode), parse the payload, and then enforce business rules off-chain or in higher-level components.
This is how a governance intent becomes a discoverable, routable on-chain message.
Each part
Protocol tag ("via"): Namespace our messages. It prevents collisions with other systems using similar patterns and lets indexers ignore unrelated inscriptions.
Opcode ("propose_new_bridge"): Tells the parser what the payload means here, like "This is a bridge-proposal message"
Payload (key_hash, script_path_hash): supplies the exact parameters the downstream logic needs. Both are 32-byte identifiers that are carried as JSON for portability.
Non-executing IF branch: ensures the transaction remains standard/valid while embedding the data. The data is committed to blockchain, and indexers can rely on it, but Bitcoin itself doesn't try to execute it.

Dedicated script builder

About Via
Via Network is a modular sovereign, validity-proof zk-rollup for Bitcoin with a zkEVM execution layer using Celestia DA that scales throughput without introducing new L1 programmability or custodial trust.
🌐 Website
🧑💻 Testnet Bridge
🐦 (X) Twitter
👋 Discord
🙋♂️ Telegram
📖 Blog
🗞️ Docs
💻 Github