From c9687b03dca37c70cd2502312416b33278a197b7 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Thu, 9 Jul 2026 13:55:37 -0500 Subject: [PATCH] sysio.msgch: enforce inbound envelope chaining (complete SEC-107) apply_consensus verified the per-outpost envelope chain but STAGED the empty-previous-hash case (accepted + logged, because not every outpost chained yet) and carried an INTERIM SVM cross-stream fallback that bridged the pre-SEC-114 Solana outpost. Both outposts now self-chain per stream -- EVM via OPP.prevEpochHash, SVM via previous_outbound_epoch_hash (wire-solana #364) -- so once a chain tip is recorded for an outpost, an empty or non-continuing previous_envelope_hash is a chain break: the envelope is dropped before dispatch, consensus record, or cleanup, exactly as a non-matching hash already was. An empty prev-hash remains valid only at genesis (before any tip), and an outpost's first accepted envelope still bootstraps. to_checksum256_exact already rejects any non-32-byte field, so wrong-length and all-zero values are breaks too, matching the outposts' canonical-form enforcement. Both the staged empty-accept branch and the SVM cross-stream fallback are removed, collapsing the check to a single "must equal the recorded tip." Recompiles sysio.msgch.wasm; ABI unchanged. --- contracts/sysio.msgch/src/sysio.msgch.cpp | 56 +++++----------- contracts/sysio.msgch/sysio.msgch.wasm | Bin 150394 -> 149407 bytes contracts/tests/sysio.msgch_chain_tests.cpp | 68 +++++++++++--------- 3 files changed, 54 insertions(+), 70 deletions(-) diff --git a/contracts/sysio.msgch/src/sysio.msgch.cpp b/contracts/sysio.msgch/src/sysio.msgch.cpp index 271f5407ee..1ad421661b 100644 --- a/contracts/sysio.msgch/src/sysio.msgch.cpp +++ b/contracts/sysio.msgch/src/sysio.msgch.cpp @@ -903,25 +903,23 @@ void apply_consensus(name self, uint64_t chain_code, uint32_t epoch_index, auto decode_result = in(envelope); check(decode_result == zpp::bits::errc{}, "failed to decode inbound OPP Envelope"); - // Inbound chain verification (staged). Outposts self-chain their outbound envelopes: + // Inbound chain verification (ENFORCED). Outposts self-chain their outbound envelopes: // `previous_envelope_hash` must continue from the canonical epoch digest of the envelope // accepted before it from the same outpost (`outpcons.envelope_digest`). The digest is // recomputed from the DECODED envelope (a defensive re-encode, exactly like the outposts' // `OPPCommon.epochHash`), so a byte-mutated but semantically identical delivery canonicalises - // to the same digest. Staging mirrors the Ethereum OPPInbound: an EMPTY prev-hash is accepted - // and logged, because not every outpost chains yet; a NON-empty prev-hash that does not - // continue the recorded tip is a chain break and the envelope is dropped before any dispatch, + // to the same digest. Both outposts now self-chain per stream — the EVM outpost via + // `OPP.prevEpochHash`, the SVM outpost via `previous_outbound_epoch_hash` (wire-solana + // SEC-114) — so once a chain tip is recorded for an outpost, a NON-continuing + // `previous_envelope_hash` is a chain break and the envelope is dropped before any dispatch, // consensus record, or cleanup (delivery rows stay intact for audit/dispute), per the - // never-throw handler convention. - // - // INTERIM, remove when the Solana outpost adopts per-stream chaining: the Solana outpost - // stamps its outbound `previous_envelope_hash` from its single `previous_epoch_hash` slot, - // which only its `epoch_in` advances, so its envelopes link to the digest of the last DEPOT - // envelope it accepted rather than to its own previous emit. That is still an exact digest - // binding the depot can verify locally: the linked digest is this contract's own last - // outbound emit for the outpost (the surviving one-deep `outenvelopes` row). Accept that - // link as an alternate continuation for SVM outposts ONLY; EVM outposts chain per-stream - // and get no fallback. + // never-throw handler convention. An EMPTY `previous_envelope_hash` is valid only at genesis, + // when the recorded tip itself is still empty; a non-empty value must be exactly the 32-byte + // tip (`to_checksum256_exact` rejects any other length, so a wrong-sized or all-zero field is + // a break, matching the outposts' canonical-form enforcement). The previous STAGED acceptance + // of an empty prev-hash for non-genesis epochs, and the INTERIM SVM cross-stream fallback that + // bridged the pre-SEC-114 Solana outpost, are both removed now that per-stream chaining is + // live on both chains. // // The source chain row also feeds the audit-log endpoints projection below; `deliver` // validated it at delivery time, and `resolvedisp` re-enters through the same registry. @@ -936,32 +934,12 @@ void apply_consensus(name self, uint64_t chain_code, uint32_t epoch_index, const checksum256 chain_tip = opcons.contains(opc_pk) ? opcons.get(opc_pk).envelope_digest : checksum256{}; if (chain_tip != checksum256{}) { - if (envelope.previous_envelope_hash.empty()) { - sysio::print_f("apply_consensus: chain_code=%llu epoch=%u accepted with empty " - "previous_envelope_hash (staged chain enforcement)\n", + const auto prev = to_checksum256_exact(envelope.previous_envelope_hash); + if (!prev.has_value() || *prev != chain_tip) { + sysio::print_f("DROP envelope chain break: chain_code=%llu epoch=%u " + "previous_envelope_hash does not continue the accepted chain\n", static_cast(chain_code), epoch_index); - } else { - const auto prev = to_checksum256_exact(envelope.previous_envelope_hash); - bool continues_chain = prev.has_value() && *prev == chain_tip; - if (!continues_chain && prev.has_value() && - op_row.kind == ChainKind::CHAIN_KIND_SVM) { - msgch::outenvelopes_t outenvs(self); - auto by_outpost = outenvs.get_index<"byoutpost"_n>(); - auto out_it = by_outpost.lower_bound(chain_code); - if (out_it != by_outpost.end() && out_it->chain_code == chain_code && - *prev == out_it->envelope_hash) { - continues_chain = true; - sysio::print_f("apply_consensus: chain_code=%llu epoch=%u accepted via " - "cross-stream link to the depot's last outbound emit\n", - static_cast(chain_code), epoch_index); - } - } - if (!continues_chain) { - sysio::print_f("DROP envelope chain break: chain_code=%llu epoch=%u " - "previous_envelope_hash does not continue the accepted chain\n", - static_cast(chain_code), epoch_index); - return; - } + return; } } } diff --git a/contracts/sysio.msgch/sysio.msgch.wasm b/contracts/sysio.msgch/sysio.msgch.wasm index 483ba9ff216e4063a16447029f9c4eca4f70a59c..f08f12c05dd31422482e7a7b22b398877da1d92a 100755 GIT binary patch delta 19158 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°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 empty field BOOST_REQUIRE_EQUAL(success(), deliver(ETH_OUTPOST_ID, n4)); produce_blocks(); opc = get_outpcons(ETH_OUTPOST_ID); - BOOST_REQUIRE_EQUAL(opc["epoch_index"].as(), staged_epoch); - BOOST_REQUIRE_EQUAL(opc["envelope_digest"].as_string(), n4_digest.str()); - BOOST_REQUIRE_EQUAL(attestation_count(ETH_OUTPOST_ID, staged_epoch), 1u); + BOOST_REQUIRE_EQUAL(opc["epoch_index"].as(), epoch); // still E+1 + BOOST_REQUIRE_EQUAL(opc["envelope_digest"].as_string(), n2_digest.str()); // tip unchanged + BOOST_REQUIRE_EQUAL(attestation_count(ETH_OUTPOST_ID, empty_epoch), 0u); // nothing dispatched // Epoch E+4: a populated envelope_hash field is blanked out of the digest: the envelope is // byte-different but canonicalises to the same digest as its blank-hash twin, and a correctly - // chained prev keeps the chain moving. + // chained prev (from the still-current tip n2, since the empty E+3 delivery was dropped) keeps + // the chain moving. const uint32_t blank_epoch = advance_one_epoch(); - auto n5 = encode_delivery(blank_epoch, "echo", oracle::digest_bytes(n4_digest), + auto n5 = encode_delivery(blank_epoch, "echo", oracle::digest_bytes(n2_digest), /*env_hash=*/std::string(32, '\x22')); auto n5_env = decode_envelope(n5); BOOST_REQUIRE_EQUAL(n5_env.envelope_hash().size(), 32u); @@ -706,21 +709,24 @@ BOOST_FIXTURE_TEST_CASE(inbound_chain_tip_recorded_and_verified, sysio_msgch_cha BOOST_REQUIRE_EQUAL(opc["envelope_digest"].as_string(), n5_digest.str()); } FC_LOG_AND_RETHROW() } -/// INTERIM Solana cross-stream semantics: the Solana outpost stamps its outbound -/// `previous_envelope_hash` from its single inbound tip slot, so its envelopes link to the digest -/// of the last DEPOT envelope it accepted (the depot's own outbound emit) instead of its own -/// previous emit. The depot accepts that link as an alternate continuation for SVM outposts only; -/// remove this case when the outpost adopts per-stream chaining. -BOOST_FIXTURE_TEST_CASE(inbound_accepts_cross_stream_link_for_svm_outposts, +/// SEC-107 completion: the SVM cross-stream alternate is REMOVED. The Solana outpost now +/// self-chains per stream (wire-solana SEC-114 `previous_outbound_epoch_hash`), so an envelope +/// linking to the depot's own outbound emit digest instead of the outpost's inbound tip is a +/// chain break for SVM exactly as it is for EVM — the interim fallback no longer accepts it. +BOOST_FIXTURE_TEST_CASE(inbound_rejects_cross_stream_link_for_svm_outposts, sysio_msgch_chain_tester) { try { bootstrap(); - // Establish an inbound tip for SOL (bootstrap accept), so the alternate link is what gets + // Establish an inbound tip for SOL (bootstrap accept), so the cross-stream link is what gets // exercised on the next delivery rather than the no-tip bootstrap rule. uint32_t epoch = current_epoch(); - BOOST_REQUIRE_EQUAL(success(), deliver(SOL_OUTPOST_ID, encode_delivery(epoch, "xstream-a"))); + auto n1 = encode_delivery(epoch, "xstream-a"); + const auto n1_digest = oracle::epoch_digest(decode_envelope(n1)); + BOOST_REQUIRE_EQUAL(success(), deliver(SOL_OUTPOST_ID, n1)); produce_blocks(); + const uint32_t tip_epoch = epoch; + // Next epoch: emit a depot outbound envelope, then deliver an inbound envelope whose prev is // that emit's digest (NOT the inbound tip). Building the outbound envelope in the delivery // epoch pins the one-deep outenvelopes row so no epoch-advance emission replaces it. @@ -735,20 +741,20 @@ BOOST_FIXTURE_TEST_CASE(inbound_accepts_cross_stream_link_for_svm_outposts, outbound_digest_bytes.data(), outbound_digest_bytes.size())); - auto n2 = encode_delivery(epoch, "xstream-b", outbound_digest_bytes); - const auto n2_digest = oracle::epoch_digest(decode_envelope(n2)); - BOOST_REQUIRE_EQUAL(success(), deliver(SOL_OUTPOST_ID, n2)); + // Cross-stream link (prev = depot's outbound emit digest) is now a chain break: dropped + // without throwing, tip does not move, nothing dispatched. + BOOST_REQUIRE_EQUAL(success(), + deliver(SOL_OUTPOST_ID, encode_delivery(epoch, "xstream-b", outbound_digest_bytes))); produce_blocks(); auto opc = get_outpcons(SOL_OUTPOST_ID); - BOOST_REQUIRE_EQUAL(opc["epoch_index"].as(), epoch); - BOOST_REQUIRE_EQUAL(opc["envelope_digest"].as_string(), n2_digest.str()); - BOOST_REQUIRE_EQUAL(attestation_count(SOL_OUTPOST_ID, epoch), 1u); + BOOST_REQUIRE_EQUAL(opc["epoch_index"].as(), tip_epoch); // not advanced + BOOST_REQUIRE_EQUAL(opc["envelope_digest"].as_string(), n1_digest.str()); // tip unchanged + BOOST_REQUIRE_EQUAL(attestation_count(SOL_OUTPOST_ID, epoch), 0u); // nothing dispatched } FC_LOG_AND_RETHROW() } -/// The cross-stream alternate is SVM-scoped: an EVM outpost chains per-stream, so an envelope -/// linking to the depot's outbound emit digest instead of the inbound tip is a chain break and -/// drops. +/// EVM outposts chain per-stream, so an envelope linking to the depot's outbound emit digest +/// instead of the inbound tip is a chain break and drops (same rule as SVM now). BOOST_FIXTURE_TEST_CASE(inbound_rejects_cross_stream_link_for_evm_outposts, sysio_msgch_chain_tester) { try { bootstrap();