To ensure complete commitment verification on Bitcoin, we’ve adopted Blake3 hashing, a crucial upgrade from the previous system, where incomplete commitment verification left the bridge vulnerable to BTC theft by malicious operators. Fiamma has solved this critical vulnerability in the current BitVM2 bridge implementations.
In this article, we’ll explore how it works.
What does the complete Groth16 verifier mean, and why is it important?
Groth16 is a renowned pairing-based zk-SNARK system. Its principal function is to allow a prover to validate the accuracy of a statement to a verifier, without revealing any additional data. A distinct feature of Groth16 is its succinctness; the proofs generated are concise, occupying minimal storage and transmission space. This characteristic is pivotal for platforms like blockchain systems or privacy-focused protocols, where proof size is paramount.
At its core, the Groth16 verifier checks whether a multivariable polynomial. t satisfies the condition t(τ, π) = 0, which serves as the fundamental verification test.

This equation validates the relationship between the proof [A, B, C]
, the inputs [a₁, a₂, ... aₘ]
, and the verifying key VK = [α, β, γ, δ, ...]

The term,

represents the MSM (Multi-Scalar Multiplication) computation. Since elliptic curve operations are computationally expensive on the verifier’s side, an optimization is to reduce their cost by replacing the full inputs with a hash. This allows the value of $$m$$ to be treated as a constant — an approach commonly adopted in many ZKEVM implementations on Ethereum.

Ensuring Input Integrity with Digest Verification
When the value of m is treated as constant, the actual inputs are designated as private inputs, while the corresponding inputs_digest is exposed as a public input. However, for verification to remain secure, the verifier must ensure that this digest actually corresponds to the private inputs used in the Proof check.
It should be noted that if the verifier doesn’t check the inputs_digest, the prover could cheat the verifier. This is because the verifier only receives the inputs_digest and the Proof
— without knowing what the inputs_digest actually represents. For example, the prover may claim to know an input x such that:

Clearly, the public input in this case would be [1,4]. But if the verifier only sees a Digest and doesn’t validate that the inputs_digest maps to [1,4], the prover could instead generate a proof for any arbitrary computation. As long as the structure appears valid, the verifier would wrongly accept the proof — compromising correctness.
Why is it important for BitVM2 Bridge
The completed verifier on Bitcoin could prevent the operator from stealing BTC from the bridge. If we only use digest as input of proof, we can’t ensure that the digest corresponds to the pre-image that it should be. Such as the operator should use transaction Burn A and PEG-OUT A in the claim. However, the operator uses proof of any computation to kick off the Claim transaction. Because on the Bitcoin side, it only checks whether the proof could be passed with the digest, but does not check what the digest is at all.

So, it’s imperative for us to add additional checks between the digest and the true pre-image. It means that when the operator uses other data which doesn’t correspond to the digest, the operator should be slashed as well.
The First Complete Groth16 Verifier on Bitcoin
Thanks to the latest upgrade of SP1, we’ve achieved the first complete implementation of a Groth16 verifier on Bitcoin. This advancement makes it possible to verify SP1-generated zero-knowledge proofs directly using Fiamma’s BitVM2 verifier
To demonstrate this, the following code illustrates the actual script-level implementation used to perform a full verification. It includes:
- Blake3-based input hashing
- MSM preparation logic
- Pairing checks for proof validation
These scripts show how all components — from hash verification to pairing logic — are integrated to form a trust-minimized and complete Groth16 verifier, executable on Bitcoin via BitVM2.
1. The pseudocode of the Groth16 verification script implementation with Blake3
pub fn split_verify_proof_with_blake3(
pvk: &PreparedVerifyingKey<Bn254>,
precompute_lines: &Vec<G2Prepared>,
) -> (Vec<Script>, usize) {
let blake3_hash_scripts = hash::blake3_u32::split_blake3();
let msm_scripts = Self::split_prepare_inputs_optimization_split(pvk);
let check_pairing_scripts = Self::split_check_pairing_bucket_split(precompute_lines);
(
[
blake3_hash_scripts.clone(),
msm_scripts.clone(),
check_pairing_scripts,
]
.concat(),
blake3_hash_scripts.len(),
)
}
2. The pseudocode of the Groth16 verification script witness implementation with Blake3
pub fn verify_proof_with_blake3_split(
pvk: &PreparedVerifyingKey<Bn254>,
proof: &Proof<Bn254>,
public_inputs: &[Fr],
native_pub_inputs: &[u8],
) -> (bool, Vec<ScriptBigUintContext>) {
let mut script_contexts = vec![];
let mut sript_inputs = native_pub_inputs.clone().to_owned().to_vec();
sript_inputs.reverse();
// check 1: pub_hash = Blake3(native_pub_inputs);
let (res, witness) = blake3_public_hash_reduce_fr(&native_pub_inputs, &sript_inputs);
// check 2: public_inputs[1] == Fr::from_be_bytes_mod_order(pub_hash)
assert_eq!(Fr::from_be_bytes_mod_order(&res), public_inputs[1]);
script_contexts.extend(witness);
let (res, witness) = Self::prepare_inputs_split(pvk, public_inputs);
script_contexts.extend(witness);
let (res, witness) =
Self::verify_proof_with_prepared_inputs_bucket_split(pvk, proof, &res.into_group());
script_contexts.extend(witness);
(res, script_contexts)
}
3. The E2E test example
We wrote a Fibonacci instance to test the whole process. The test example:
#[test]
//Checking https://github.com/fiamma-chain/sp1-bitvm-blake3/ for details
fn test_groth16() {
let fixture_path =
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../contracts/src/fixtures");
let fixture_path = fixture_path.join("groth16-fixture.json");
let fixture = std::fs::read_to_string(&fixture_path).unwrap();
let fixture: SP1FibonacciProofFixture = serde_json::from_str(&fixture).unwrap();
let proof = hex::decode(&fixture.proof[2..]).unwrap();
let public_inputs = hex::decode(&fixture.public_values[2..]).unwrap();
let ark_proof = load_ark_proof_from_bytes(&proof[4..]).unwrap();
let ark_vkey = load_ark_groth16_verifying_key_from_bytes(&GROTH16_VK_BYTES).unwrap();
let ark_public_inputs = load_ark_public_inputs_from_bytes(
&decode_sp1_vkey_hash(&fixture.vkey).unwrap(),
&hash_public_inputs_with_fn(&public_inputs, blake3_hash), // blake3
);
// ....Verify proof locally
Groth16::<Bn254>::verify_proof(&ark_vkey.clone().into(), &ark_proof, &ark_public_inputs)
.unwrap();
// ...Verify proof in BitVM2 Verifier
bitvm::groth16::test_tools::subscript_lt_400KB::test_split_script_groth16_verifier_subscript_check_with_blake3_lt_400KB(
ark_public_inputs.into(), ark_vkey, ark_proof,&public_inputs
);
}
The following section highlights some key information:
// Checking https://github.com/fiamma-chain/sp1-bitvm-blake3/tree/main/data for details
vk: VerifyingKey { alpha_g1: (20491192805390485299153009773594534940189261866228447918068658471970481763042, 9383485363053290200918347156157836566562967994039712273449902621266178545958), beta_g2: (QuadExtField(6375614351688725206403948262868962793625744043794305715222011528459656738731 + 4252822878758300859123897981450591353533073413197771768651442665752259397132 * u), QuadExtField(10505242626370262277552901082094356697409835680220590971873171140371331206856 + 21847035105528745403288232691147584728191162732299865338377159692350059136679 * u)), gamma_g2: (QuadExtField(10857046999023057135944570762232829481370756359578518086990519993285655852781 + 11559732032986387107991004021392285783925812861821192530917403151452391805634 * u), QuadExtField(8495653923123431417604973247489272438418190587263600148770280649306958101930 + 4082367875863433681332203403145435568316851327593401208105741076214120093531 * u)), delta_g2: (QuadExtField(19629295988673812457237747993086053613709181874324227239033635205670891327060 + 17270349666695681994109533429817368669497520119106681015856196115021033411091 * u), QuadExtField(7606452412506804803120568203841502737122216991812443942893981403980455929856 + 9671211007953687162466560247240578603884908824862104008754446926069546380089 * u)), gamma_abc_g1: [(6712036353136249806951869451908368653566549662781372756321174254690599374583, 18149145036868871064182651529802275370638950642742152190925800889169295968585), (12384021290558951773126140100379496012525836638155233096890881157449062205923, 16530732960917040406371332977337573092100509754908292717547628595948196259098), (18749839173537272836199384751191600551090725238737491530604969678014545165197, 1828450848853234449784725988911172793808451038026258152543319358376349553777)] }
proof: Proof { a: (17757616286716265014907054195283507789878982270674072626866207773207292138096, 19858877562716308546494927946695717921085897663102371031171615544173707138429), b: (QuadExtField(16303356530394076424879343181474124406422425147274245545524407172356548774729 + 6386312428882894434341180845506592827580655629766259705878349136187769496682 * u), QuadExtField(2503015776023087686495886374740149237115497565780397567085084229496483568976 + 21863620051757457899730673946306128916915850889511753328806851867881512571113 * u)), c: (7412412751397267725059539974243720891206689881407746287876365489943010772952, 4749626842206354936057137191670727674590575345747365445525739916512900832447) }
public input: [BigInt([2081010597440243391, 8933620997411816115, 14244448838099968857, 876476526593916]), BigInt([6568716051497370742, 7772415922033085877, 8208084356351775240, 1563921272962275877])]
native public input: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 26, 109, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 42, 194]
...
blake3 script len: 2
...
blake3 contains 2 sub_scripts
groth16::split_verify_proof contains 14820 sub_scripts,
msm contains 510 sub_scripts
...
witness_15331, witness_len:25 input_len:24, output_len:1
subscript[15331] size = 2275 bytes
subscript[15331] runs successfully!
...
max_nb_stack_items = 614
max_input_len:12, min_input_len:2
max_output_len:6, min_output_len:0
max_witness_len:14, min_witness_len: 4
...
total chunks size: 15332
About Succinct
Succinct is building a protocol on Ethereum that coordinates a distributed network of provers to generate zero knowledge proofs for any piece of software. This protocol creates a two-sided marketplace between provers and requesters, enabling anyone to receive proofs for applications such as blockchains, bridges, AI agents, games, and more.
About Fiamma
Fiamma is unlocking real-world use cases for Bitcoin, transforming it into a dynamic asset and the foundation for a decentralized internet and financial system. Backed by Lightspeed Faction and L2IV, Fiamma leads innovation with the first BitVM2-powered infrastructure , including a trust-minimized Bitcoin bridge, Bitcoin settlement layer, and a non-custodial, seamless “one-click” experience for both end users and institutions to utilize native Bitcoin.
With deep integration and strategic support from Babylon, BOB, Arbitrum, Satlayer, and RiscZero, Fiamma is building the ultimate Bitcoin gateway and shaping the future of decentralized systems.