Configurable Bridge Deployment, Taproot Dual-Path Custody, and Multi-Proposal Governance

November 28, 2025
Development
VIA Team

Via is a Bitcoin Layer 2 sovereign validity-proof zk-rollup. Anchored by commitments with a zkEVM execution layer using Celestia for data availability, and a distributed verifier network for proof validation.

Delivering full EVM programmability with high throughput, cost efficiency, Bitcoin-anchored settlement with reorg-bounded finality, and end-user verifiability.

The Via Network allows users to bridge their bitcoins to the network. The bridge holds user deposits while they interact with the L2. When users want to exit, the bridge releases funds back to them on Bitcoin's main chain.

Other Bitcoin L2 bridges have serious tradeoffs:

  • Single custodian: Fast but centralized, as one entity controls all funds
  • Standard Bitcoin multisig (e.g., 3-of-5): Decentralized but creates large, expensive transactions with every signature visible on-chain
  • Smart contract custody: Requires trusted execution environments or elaborate verification.

Federated multisig sidechains that call themselves Bitcoin L2s, users basically send BTC to an address controlled by the federation. Those keys mint "wrapped BTC" on their L2. The problem is that the BTC is custodied by a multisig. The bitcoins are lost if the signers collude or get hacked.

Merge-minded, such as Rootstock, Bitcoin miners validate or secure the L2 chain. If miner participation is low, there's a chance of a 51% attack. Other Validium chains' zk-proofs may verify execution, but the L2 shared security is not the same as Bitcoin's shared security. Settlement is on their chain, not Bitcoin.

Then there are the Bitcoin L2s using BitVM, which is still experimental, promising, but incredibly inefficient at this moment, not ready for high throughput, with exit games being complex and slow.

Via uses a Musig2 threshold bridge governed by on-chain governance. Keys are rotated through Bitcoin inscriptions, not trusted parties. Withdrawals require a validity proof and a finalized batch. No individual or federation can move funds. Settlement is on Bitcoin itself. Via uses stand ZK proofs verified off-chain and does not run logic inside Bitcoin script. Via MusSig2 bridge is practical and scalable today.

Via's MuSig2 bridge

  • Privacy: On-chain, a MuSig2 withdrawal looks like a single-key spend, with no Script showing "3-of-5 multisig" or required operators. Bitcoin observers can't determine the number of operators, whether custody is shared, or whether aggregation happened. This improves security and makes the bridge indistinguishable from normal Bitcoin activity.
  • Efficiency: Traditional Bitcoin multisig needs one signature per signer. A 3-of-5 scheme embeds three ECDSA signatures (210+ bytes) directly in the transaction. MuSig2 combines all signatures into a single 64-byte Schnorr signature. Whether two or twenty operators, the signature size, transaction weight, and fee stay the same. This scales custody without increasing costs.
  • Native Bitcoin: MuSig2, verified by Bitcoin's Taproot, is pure cryptography. It involves no oracles, trusted hardware, sidechains, or extra security assumptions beyond Bitcoin's base layer.

Via Network MuSig2 bridge can now be configured at runtime, secured with independent operational and governance control paths, and managed through a unified governance interface.

Operators no longer need to modify code to change bridge addresses, governance no longer depends on operational keys for custody rotation, and the CLI no longer assumes a single governance action type.

TL;DR

  • Bridge addresses are now CLI parameters, not hardcoded constants. Test multiple environments without recompiling
  • Bridge custody now uses Taproot with two independent spending paths: MuSig2 key-path for daily withdrawals, script-path for governance actions
  • Governance CLI generalized from upgrade-only to multi-proposal with type-specific commands and universal signing
  • Patch 227 is now active. Operators can manage bridge configurations via CLI parameters, custody rotation is no longer dependent on operational keys, and governance proposals are executed through a single, unified interface.

See patch 227 at Github here.

After months of development, our MuSig2 bridge was near complete. Then comes the part of finding a potential partner. Everything works perfectly on testnet, but at one point, a deposit failed. Our deposit flows leaned on a single, hardcoded regtest bridge address in the deposit examples and the CLI wrappers. That was convenient for a first bootstrapping pass, but wouldn't work forever.

Every time we switched environments, rotated keys, or wanted to upgrade the bridge, someone had to dig deep in the source code again to find that string of text. Our CLI wrapper had no parameter to specify a different bridge.

The bridge address isn't a constant since our latest upgrades. Its runtime configuration changes with network upgrades, key rotations, and governance decisions.

Decoupling Deposits: Making VIA’s Bridge Address a CLI Parameter

In the CLI layer, we added a required --bridge-address parameter. No more defaults, no more hidden assumptions. When you deposit, you say which bridge you're depositing to

.requiredOption('--bridge-address <bridgeAddress>', 'The bridge address')

The new parameter flows down to our Rust examples, which now read the bridge address from the command-line arguments and validate it against the active network before using it

The examples now use the same bridge value that the operator typed and verify that the address belongs to the current network before using it.

There was one more security measure to take. The validation code checked whether the provided address appeared in the list of verifier addresses, which was both permissive and conceptually wrong. This fix switches the check to struct equality against the recoded bridge address, making it impossible to substitute a verifier P2WPKH for the Taproot bridge without being noticed.

The change improves operational hygiene, tests, and narrows attack surface. A small change but results in safer procedures and fewer builds.

Building a MuSig2 Taproot Bridge with Script-Path Governance

Our bridge uses a MuSig2-aggregated key, requiring all operators to sign withdrawals.

That's great, but what happens when the operator loses their key? What if there's an emergency and the keys protecting the funds need to be replaced?

Our bridge was simple and efficient for routine transactions, but inflexible when it came to changing custody.

The solution: single address with dual spending paths

Our solution comes from understanding Taproot. Taproot outputs have two spending paths. A key path (fast and private) and a script path (flexible, auditable, and condition-based).

We could keep our MuSig2 key path for normal operations while adding a completely separate, independent governance script path.

We needed a second, independent way to authorize transfers, one that coexists with the key path but responds to a different set of signers.

By keeping the MuSig2 aggregated key as the internal key, while preserving the fast-key spending for normal withdrawals. Then, appending a separate lead that encodes an M-of-N Schnorr policy for governance, the result is a single address with dual spending paths.

Operators use the key path for daily withdrawals.

Governance uses the script path when custody needs to change or an emergency response is required. The result is a single address with dual spending paths.

The key path for operations and a script path for governance and recovery, both bound into one tweaked output key.

We built 2 example programs. The first computes the dual-path wallet by

  1. Aggregating the operation keys: using MuSig2 to create the internal key
  2. Building the governance script: using Taproot's native M-of-N with OP_CHECKSIGADD
  3. Combining them into a Taproot tree, creating a single address with two spending methods
  4. Export everything needed to spend via either path: The address, control blocks Merkle root

Here's the wallet computation:

Every governance key adds to a counter, and we check if the counter equals the threshold with pure Taproot.
See our complete implementation here ⬇️

The governance script uses a pattern. Each governance key adds to a counter via OP_CHECKSIGADD, and we check if the counter equals our threshold with OP_NUMEQUAL. Binding the governance script and the aggregated key into a single output key and emits a JSON.

Both pands End-to-End

When we need to rate custody or respond to an mergency, we spend a UTXO using 2/3 governance keys

Script-spend (governance)

The witness stack contains the signatures, the script, and cryptographic proof confirming that this script is part of the Taproot tree.
Full implementation can be found here ⬇️

Key-path spend (operations)
For normal withdrawals, use MuSig2. The signer coordinates to produce nonces, exchange them, create a partial signature, and aggregate them into a single 64-byte Schnorr signature that looks like any other Taproot key-path spend-private and indistinguishable from a single key spend.

When creating MuSig2 signers, they have to know about the script tree's merkle root because it tweaks the aggregated key. The binding prevents attacks where the signatures for one configuration could be replayed against another.

We also use a fee strategy object during transaction building. That keeps the fee policy decoupled from the transaction structure.

Via bridge operates like a hot wallet, but governs like a MultiSig.

The bridge wallet acts like an operational wallet on fast paths and like a governance lock when we need to rotate custody.

Normal withdrawals are fast and cheap. Emergency responses and custody upgrades use the governance path without depending on the very keys being replaced. It improves security by separating tasks and giving us a clean history for upgrades that don't depend on the very keys we try to replace

We've used Taproot's native primitives to encode policy without extra dependencies, with examples as tests and as templates for operators.

Governance‑Grade CLI and

Our CLI was built around a single use case case such as protocol upgrades. The commands were literally named sing-upgrade-tx and finialize-upgrade-tx That worked fine. Until we needed to update the sequencer, rotate the bridge, or modify the governance parameters.

Suddenly, the names were wrong, and a validation bug allowed any verifier address to masquerade as the bridge, undermining the precise security checks we needed.

We had to go back to the drawing board and think more about our CLI around the transaction builder that handles common PSBT construction and add clear type-specific commands for each governance action create-update-sequencer, create-update-bridge, create-update-gov. The signing and finalization commands with a universal verbs-sign-tx and finalize-tx that works no matter which proposal we started with.

A single helper created unsigned PSBTs carrying the right OP_RETURN prefix and payload, and specialized wrappers pass in the data to each proposal type

The constants that brand the OP_RETURN payloads moved into a shared module.

Here's the validation. By comparing the candidate directly to the recorded bridge, we prevent confusion between very different address classes.

To close the "how to build" to "how to operate," we added an example of that proposal. A new bridge address as a formal on-chain message, and then verified it by parsing the reveal transaction.

The documentation was upgraded to match the tools. The “upgrade” guide now uses the new command names, adds end‑to‑end procedures for updating the sequencer and the bridge, and shows deposits that explicitly name the bridge they are targeting. The edits also fix placeholders and environment variable names so operators can copy and paste and succeed.

Operators can build the right unsigned PSBT for the job, sign it with multiple participants using consistent verbs, and finalize it without worrying about which type of proposal they started with. The transactions are easier to recognize, too.

Taproot Implementation in VIA: Single Address, Dual Spending Paths

1. The Internal Key (MuSig2 Aggregation)

Multiple operator public keys aggregated into a single key using MuSig2.

Code Reference: via_verifier/lib/via_musig2/examples/compute_musig2.rs

// Aggregate MuSig2 key
let musig_key_agg_cache = KeyAggContext::new(musig_pubkeys)?;
let agg_pubkey = musig_key_agg_cache.aggregated_pubkey::<secp256k1_musig2::PublicKey>();
let (xonly_agg_key, _) = agg_pubkey.x_only_public_key();
let internal_key = XOnlyPublicKey::from_slice(&xonly_agg_key.serialize())?;

This creates the internal key - the base public key before any Taproot tweaking.

2. The Script Path (Governance Script)

An M-of-N multisig script using Taproot-native opcodes for governance.

Code Reference: via_verifier/lib/via_musig2/examples/compute_musig2.rs

// Build generic M-of-N Taproot Schnorr multisig script
let mut builder = Builder::new();
for (i, key) in gov_xonly.iter().enumerate() {
    if i == 0 {
        builder = builder.push_x_only_key(key).push_opcode(OP_CHECKSIG);
    } else {
        builder = builder.push_x_only_key(key).push_opcode(OP_CHECKSIGADD);
    }
}
let multisig_script = builder.push_int(m as i64).push_opcode(OP_NUMEQUAL).into_script();

This script exists as a leaf in the Taproot tree.

3. Combining into a Taproot Tree

The internal key and script(s) are combined into a Merkle tree structure.

Code Reference: via_verifier/lib/via_musig2/examples/compute_musig2.rs

let spend_info = bitcoin::taproot::TaprootBuilder::new()
    .add_leaf(0, ScriptBuf::from(multisig_script.clone()))?  // Add script as a leaf
    .finalize(&secp, internal_key)                           // Finalize with internal key
    .unwrap();

The finalize() function:

  1. Computes the merkle root of all script leaves
  2. Tweaks the internal key with this merkle root
  3. Produces the final tweaked output key

4. The Taproot Output Key & Address

The final public key that appears on-chain - a single address with two spending methods.

Code Reference: via_verifier/lib/via_musig2/examples/compute_musig2.rs

let taproot_output_key = spend_info.output_key();  // The tweaked key
let taproot_address = Address::p2tr_tweaked(taproot_output_key, net);

This single address commits to BOTH:

  • The internal key (for key-path spending)
  • The script tree (for script-path spending)

5. The Tweak Mechanism

What it is: The mathematical binding that commits the internal key to the script tree.

Code Reference: via_verifier/lib/via_musig2/src/lib.rs

// Calculate taproot tweak = hash(internal_key || merkle_root)
let tap_tweak = TapTweakHash::from_key_and_tweak(internal_key, merkle_root);
let tweak = tap_tweak.to_scalar();
let tweak_bytes = tweak.to_be_bytes();
let musig2_compatible_tweak = secp256k1_musig2::Scalar::from_be_bytes(tweak_bytes).unwrap();

// Apply tweak to the key aggregation context
musig_key_agg_cache = musig_key_agg_cache.with_xonly_tweak(musig2_compatible_tweak)?;

Formula:
tweaked_key = internal_key + hash(internal_key || merkle_root) * G

This is crucial because:

  • MuSig2 signers MUST know the merkle root to sign correctly
  • The tweak binds the key to the specific script tree
  • Prevents "wrong tree" attacks

6. Two Ways to Spend

A. Key-Path Spending (Normal Operations)

Code Reference: via_verifier/lib/via_musig2/src/transaction_builder.rs

// Key-path sighash (no script knowledge needed on-chain)
let sighash = sighash_cache.taproot_key_spend_signature_hash(
    i, 
    &Prevouts::All(&txout_list), 
    sighash_type
)?;

Witness: Just a single 64-byte Schnorr signature
Privacy: Doesn't reveal the script tree exists

B. Script-Path Spending (Governance)

Code Reference: via_verifier/lib/via_musig2/examples/musig2_with_script_path.rs

// Script-path sighash (commits to the specific script leaf)
let leaf_hash = TapLeafHash::from_script(&multisig_script, LeafVersion::TapScript);
let sighash = sighash_cache.taproot_script_spend_signature_hash(
    0,
    &Prevouts::All(&[prevout.clone()]),
    leaf_hash,  // Commits to THIS specific script
    TapSighashType::All,
)?;

Witness Stack: [empty, sig2, sig1, script, control_block]

The control block proves the script belongs to the tree:

Code Reference: via_verifier/lib/via_musig2/examples/compute_musig2.rs

let control_block = spend_info
    .control_block(&(multisig_script.clone(), LeafVersion::TapScript))
    .unwrap();

The control block contains:

  1. The internal key
  2. The parity bit
  3. The merkle proof path

7. Exported Wallet Data

Code Reference: via_verifier/lib/via_musig2/examples/compute_musig2.rs

let data = ExportData {
    public_keys,                              // Original governance keys
    aggregated_internal_key: internal_key.to_string(),  // MuSig2 aggregated key
    governance_script_hex: multisig_script.to_hex_string(),  // The script
    taproot_output_key: taproot_output_key.to_string(),  // Final tweaked key
    merkle_root: spend_info.merkle_root().map(|h| h.to_string()),  // Tree root
    taproot_address: taproot_address.to_string(),  // The address
    control_block,  // Proof for script-path spending
    threshold: m,
    total_governance_keys: n,
};

Single Taproot Address = Address::p2tr_tweaked(output_key, network)

Key Path = MuSig2 aggregated operators sign with tweaked context → produces single Schnorr signature

Script Path = M-of-N governance keys sign → include script + control block in witness

One address, two completely independent spending conditions, bound together cryptographically through the merkle root tweak.

About Via

VIA Network is a sovereign, Bitcoin-secured zkEVM chain that bringsnative yield to bridged assets. VIA serves as the central meeting point where Bitcoin and Ethereum users converge to unlock the full potential of DeFi.

🌐 Website‍
🧑‍💻 Testnet Bridge
🐦 (X) Twitter‍
👋 Discord‍
🙋‍♂️ Telegram
📖 Blog
🗞️ Docs
💻 Github
🛠️ VIA SDK
🧭 Explorer