@@ -1179,27 +1179,31 @@ pub fn verify_continuation_with_roots(
11791179 decode_commitment : Option < Commitment > ,
11801180 page_genesis_commitments : Option < & [ ( u64 , Commitment ) ] > ,
11811181) -> Result < Option < Vec < u8 > > , Error > {
1182- verify_continuation_view (
1182+ let result = verify_continuation_view (
11831183 ContinuationProofView :: Owned ( bundle) ,
11841184 elf_bytes,
11851185 opts,
11861186 decode_commitment,
11871187 page_genesis_commitments,
1188- )
1188+ ) ?;
1189+ Ok ( result. map ( |( public_output, _entry_point) | public_output) )
11891190}
11901191
11911192/// [`verify_continuation_with_roots`]'s zero-copy counterpart, for the
11921193/// recursion `continuation` guest: reads every per-epoch/global proof in
11931194/// place via [`ContinuationProofView::Archived`] instead of deserializing an
11941195/// owned [`MultiProof`]. Only small per-epoch metadata is materialized. Roots
11951196/// are always supplied here (the guest never recomputes from the ELF in-VM).
1197+ ///
1198+ /// Also returns `entry_point` so callers can fold a `program_id` via
1199+ /// [`crate::recursion::program_id_from_digest`] without a second `Elf::load`.
11961200pub ( crate ) fn verify_continuation_archived (
11971201 archived : & ArchivedContinuationProof ,
11981202 elf_bytes : & [ u8 ] ,
11991203 opts : & ProofOptions ,
12001204 decode_commitment : Commitment ,
12011205 page_genesis_commitments : & [ ( u64 , Commitment ) ] ,
1202- ) -> Result < Option < Vec < u8 > > , Error > {
1206+ ) -> Result < Option < ( Vec < u8 > , u64 ) > , Error > {
12031207 verify_continuation_view (
12041208 ContinuationProofView :: Archived ( archived) ,
12051209 elf_bytes,
@@ -1213,13 +1217,14 @@ pub(crate) fn verify_continuation_archived(
12131217/// [`verify_continuation_archived`] (archived), operating on a
12141218/// [`ContinuationProofView`] rather than either's concrete type — the same
12151219/// split [`crate::verify_recursion_blob`] uses for the monolithic path.
1220+ /// Returns the public output plus `entry_point` (see [`verify_continuation_archived`]).
12161221fn verify_continuation_view (
12171222 bundle : ContinuationProofView < ' _ > ,
12181223 elf_bytes : & [ u8 ] ,
12191224 opts : & ProofOptions ,
12201225 decode_commitment : Option < Commitment > ,
12211226 page_genesis_commitments : Option < & [ ( u64 , Commitment ) ] > ,
1222- ) -> Result < Option < Vec < u8 > > , Error > {
1227+ ) -> Result < Option < ( Vec < u8 > , u64 ) > , Error > {
12231228 // Bound the claimed private-input page count before using it to size/allocate AIRs
12241229 // (mirrors `verify_with_options`). The count is also bound into the global proof's
12251230 // Fiat-Shamir statement (`absorb_continuation_global_statement`), so any wrong value
@@ -1342,7 +1347,7 @@ fn verify_continuation_view(
13421347 return Ok ( None ) ;
13431348 }
13441349
1345- Ok ( Some ( public_output) )
1350+ Ok ( Some ( ( public_output, elf . entry_point ) ) )
13461351}
13471352
13481353/// Precompute the ELF-derived roots [`verify_continuation_with_roots`] accepts:
@@ -2153,10 +2158,11 @@ mod tests {
21532158 ) ;
21542159 }
21552160
2156- // Negative: corrupting an epoch's claimed L2G table root must be rejected —
2157- // `verify_l2g_commitment_binding_view` compares each epoch's `l2g_root` against the
2158- // corresponding sub-proof root in the global proof, so a mismatched root causes
2159- // the binding to fail. Guards the L2G root↔global commitment binding.
2161+ // Negative: corrupting an epoch's claimed L2G table root must be rejected. This
2162+ // tamper is caught by `verify_epoch`'s own root-consistency check (the epoch's
2163+ // claimed `l2g_root` no longer matches what its own proof committed) before the
2164+ // cross-epoch `verify_l2g_commitment_binding_view` ever runs — see
2165+ // `test_split_verify_rejects_global_proof_from_a_different_run` for that.
21602166 #[ test]
21612167 fn test_split_verify_rejects_tampered_l2g_root ( ) {
21622168 let _ = env_logger:: builder ( ) . is_test ( true ) . try_init ( ) ;
@@ -2177,9 +2183,8 @@ mod tests {
21772183
21782184 // Same tamper as `test_split_verify_rejects_tampered_l2g_root`, but through the
21792185 // zero-copy blob path (`verify_continuation_and_attest`) rather than
2180- // `verify_continuation`. Guards `verify_continuation_archived`'s view-based
2181- // `verify_l2g_commitment_binding_view` against the same corruption its owned
2182- // counterpart already catches.
2186+ // `verify_continuation`. Guards the archived path's per-epoch root check against
2187+ // the same corruption the owned path already catches.
21832188 #[ test]
21842189 fn test_continuation_blob_rejects_tampered_l2g_root ( ) {
21852190 let _ = env_logger:: builder ( ) . is_test ( true ) . try_init ( ) ;
@@ -2209,4 +2214,91 @@ mod tests {
22092214 "a tampered l2g_root must be rejected over the archived blob path too"
22102215 ) ;
22112216 }
2217+
2218+ // Negative: `verify_l2g_commitment_binding_view`'s own reject branch, which the two
2219+ // tests above don't reach (they're caught earlier by `verify_epoch`'s per-epoch root
2220+ // check). Two bundles proved from the same ELF/epoch size with different
2221+ // same-length private inputs share every shape value (`n`, `table_counts`,
2222+ // `touched_page_bases`, `num_private_input_pages`) but commit different actual L2G
2223+ // data, so splicing one's `global` proof onto the other's epochs leaves every
2224+ // per-epoch check and `verify_global`'s own `multi_verify` passing (each half is
2225+ // independently valid for that exact shape) while the per-epoch claimed roots no
2226+ // longer match what the spliced-in global proof's L2G sub-tables actually commit.
2227+ #[ test]
2228+ fn test_split_verify_rejects_global_proof_from_a_different_run ( ) {
2229+ let _ = env_logger:: builder ( ) . is_test ( true ) . try_init ( ) ;
2230+ let elf_bytes = asm_elf_bytes ( "test_private_input_xpage" ) ;
2231+ let opts = ProofOptions :: default_test_options ( ) ;
2232+
2233+ let input_a: Vec < u8 > = ( 0u8 ..16 ) . collect ( ) ;
2234+ let input_b: Vec < u8 > = ( 0u8 ..16 ) . map ( |b| b ^ 0xFF ) . collect ( ) ;
2235+
2236+ let mut bundle_a = prove_continuation ( & elf_bytes, & input_a, 2 , & opts) . unwrap ( ) ;
2237+ let bundle_b = prove_continuation ( & elf_bytes, & input_b, 2 , & opts) . unwrap ( ) ;
2238+ assert ! (
2239+ verify_continuation( & elf_bytes, & bundle_a, & opts)
2240+ . unwrap( )
2241+ . is_some( ) ,
2242+ "bundle_a must verify standalone before splicing"
2243+ ) ;
2244+ assert ! (
2245+ verify_continuation( & elf_bytes, & bundle_b, & opts)
2246+ . unwrap( )
2247+ . is_some( ) ,
2248+ "bundle_b must verify standalone before splicing"
2249+ ) ;
2250+ assert_eq ! (
2251+ bundle_a. epochs. len( ) ,
2252+ bundle_b. epochs. len( ) ,
2253+ "same ELF/epoch size/input length must yield the same epoch split"
2254+ ) ;
2255+ assert_eq ! (
2256+ bundle_a. touched_page_bases, bundle_b. touched_page_bases,
2257+ "same-length private inputs must touch the same pages"
2258+ ) ;
2259+ assert_ne ! (
2260+ bundle_a. epochs[ 0 ] . l2g_root, bundle_b. epochs[ 0 ] . l2g_root,
2261+ "different private-input bytes must commit different L2G data"
2262+ ) ;
2263+
2264+ bundle_a. global = bundle_b. global ;
2265+
2266+ assert ! (
2267+ verify_continuation( & elf_bytes, & bundle_a, & opts)
2268+ . unwrap( )
2269+ . is_none( ) ,
2270+ "a global proof spliced in from a different run must be rejected"
2271+ ) ;
2272+ }
2273+
2274+ // Same construction as `test_split_verify_rejects_global_proof_from_a_different_run`,
2275+ // but through the zero-copy blob path — guards
2276+ // `verify_l2g_commitment_binding_view`'s archived call site.
2277+ #[ test]
2278+ fn test_continuation_blob_rejects_global_proof_from_a_different_run ( ) {
2279+ let _ = env_logger:: builder ( ) . is_test ( true ) . try_init ( ) ;
2280+ let elf_bytes = asm_elf_bytes ( "test_private_input_xpage" ) ;
2281+ let opts = crate :: recursion:: MIN_PROOF_OPTIONS ;
2282+
2283+ let input_a: Vec < u8 > = ( 0u8 ..16 ) . collect ( ) ;
2284+ let input_b: Vec < u8 > = ( 0u8 ..16 ) . map ( |b| b ^ 0xFF ) . collect ( ) ;
2285+
2286+ let mut bundle_a = prove_continuation ( & elf_bytes, & input_a, 2 , & opts) . unwrap ( ) ;
2287+ let bundle_b = prove_continuation ( & elf_bytes, & input_b, 2 , & opts) . unwrap ( ) ;
2288+ assert_eq ! ( bundle_a. epochs. len( ) , bundle_b. epochs. len( ) ) ;
2289+ assert_eq ! ( bundle_a. touched_page_bases, bundle_b. touched_page_bases) ;
2290+ assert_ne ! ( bundle_a. epochs[ 0 ] . l2g_root, bundle_b. epochs[ 0 ] . l2g_root) ;
2291+
2292+ bundle_a. global = bundle_b. global ;
2293+
2294+ let blob =
2295+ crate :: recursion:: encode_continuation_guest_input ( bundle_a, & elf_bytes, & opts)
2296+ . expect ( "encode_continuation_guest_input failed" ) ;
2297+ let result = crate :: recursion:: verify_continuation_and_attest ( & blob, & opts)
2298+ . expect ( "verify_continuation_and_attest errored" ) ;
2299+ assert ! (
2300+ result. is_none( ) ,
2301+ "a global proof spliced in from a different run must be rejected over the archived blob path too"
2302+ ) ;
2303+ }
22122304}
0 commit comments