From 78524414b562633f56b76badc0ff145ecc37736c Mon Sep 17 00:00:00 2001 From: Kevin Heifner Date: Mon, 9 Nov 2026 22:39:35 -0500 Subject: [PATCH] Record the envelope epoch on each desyndication return Each sysio.synd returns row records sent_epoch, the depot epoch of the outbound envelope that carried its DESYNDICATE_LIQ, so a cleanup can expire the row once sysio.msgch's outpcons epoch for the outpost reaches it. sysio.msgch::buildenv sends one inline sysio.synd::retsent with the envelope's epoch and the request ids it shipped; sent_epoch stays 0 while the attestation is queued. retsent runs inside sysio.epoch::advance and never aborts: an id with no row, a row of another outpost and a row already stamped are dropped with a diagnostic. Change-Id: Iafa9710093510e9ed967a51a2d5f3ecc7e60d603 --- .../include/sysio.msgch/sysio.msgch.hpp | 2 + contracts/sysio.msgch/src/sysio.msgch.cpp | 28 ++- contracts/sysio.msgch/sysio.msgch.wasm | Bin 157765 -> 161562 bytes .../include/sysio.synd/sysio.synd.hpp | 32 ++- contracts/sysio.synd/src/sysio.synd.cpp | 37 +++- contracts/sysio.synd/sysio.synd.abi | 27 +++ contracts/sysio.synd/sysio.synd.wasm | Bin 121084 -> 122955 bytes contracts/tests/sysio.synd_tests.cpp | 195 ++++++++++++++++++ 8 files changed, 309 insertions(+), 12 deletions(-) diff --git a/contracts/sysio.msgch/include/sysio.msgch/sysio.msgch.hpp b/contracts/sysio.msgch/include/sysio.msgch/sysio.msgch.hpp index e11d93ed00..e2e13b4bf5 100644 --- a/contracts/sysio.msgch/include/sysio.msgch/sysio.msgch.hpp +++ b/contracts/sysio.msgch/include/sysio.msgch/sysio.msgch.hpp @@ -166,6 +166,8 @@ namespace sysio { /// destination's `sysio.chains` `{kind, external_chain_id}`) so the /// receiving outpost can reject an envelope a misconfigured relay /// delivered to the wrong same-kind chain. + /// When the envelope carries DESYNDICATE_LIQ attestations, sends one inline + /// `sysio.synd::retsent` with the envelope's epoch and their request ids. [[sysio::action]] void buildenv(uint64_t chain_code); diff --git a/contracts/sysio.msgch/src/sysio.msgch.cpp b/contracts/sysio.msgch/src/sysio.msgch.cpp index b70b4d250e..5a08492e37 100644 --- a/contracts/sysio.msgch/src/sysio.msgch.cpp +++ b/contracts/sysio.msgch/src/sysio.msgch.cpp @@ -55,6 +55,9 @@ constexpr auto action_onsynd = "onsynd"_n; constexpr auto action_onyield = "onyield"_n; /// Sent once after the envelope's last attestation, only when one of them reached `sysio.synd`. constexpr auto action_closeenv = "closeenv"_n; +/// Sent once per outbound envelope that carried a DESYNDICATE_LIQ, with the envelope's epoch and the +/// request ids it carried. +constexpr auto action_retsent = "retsent"_n; } // namespace synd // System-owned rows bill to the sysio RAM pool, not this contract account (privileged-contract @@ -469,6 +472,17 @@ bool dispatch_syndicate_liq(name self, const std::vector& data, uint64_t c return true; } +/// The request id of the DESYNDICATE_LIQ attestation `data`, or `std::nullopt` when the bytes do not decode +/// or carry no id (0, which the outpost reads as "no id"). `sysio.synd` queues every DESYNDICATE_LIQ with a +/// nonzero id. Anything else is skipped rather than aborting, because `buildenv` runs inside +/// `sysio.epoch::advance`. +std::optional desyndication_request_id(const std::vector& data) { + opp::attestations::DesyndicateLIQ desyndication; + auto in = zpp::bits::in{std::span{data.data(), data.size()}, zpp::bits::no_size{}}; + if (in(desyndication) != zpp::bits::errc{} || desyndication.request_id == 0) return std::nullopt; + return desyndication.request_id; +} + /// LIQ_YIELD: the outpost claimed yield for its syndicated pool since its last report. After the /// same checks as a syndication it goes to `sysio.synd::onyield` with the envelope's identity, /// which holds the amount as a number until the envelope is underwritten; nothing is minted on this @@ -1707,9 +1721,15 @@ void msgch::buildenv(uint64_t chain_code) { // The shipped rows' bytes are in `packed` now: erase them, and count the general-lane ones out of the // outpost's queue. Rows the walk stopped before, or the trim loop popped, stay READY for the next // epoch's `buildenv` call. - uint32_t general_shipped = 0; + uint32_t general_shipped = 0; + std::vector shipped_returns; for (size_t i = 0; i < included_ids.size(); ++i) { if (lane_of(entries[i].type) == queue_lane::general) ++general_shipped; + if (entries[i].type == AttestationType::ATTESTATION_TYPE_DESYNDICATE_LIQ) { + if (const auto request_id = desyndication_request_id(entries[i].data)) { + shipped_returns.push_back(*request_id); + } + } atts.erase(id_key{included_ids[i]}); } if (general_shipped > 0) { @@ -1719,6 +1739,12 @@ void msgch::buildenv(uint64_t chain_code) { queue.queued -= std::min(queue.queued, general_shipped); queues.set(ram_payer, outbound_queue_key{chain_code}, queue); } + // sysio.synd records this envelope's epoch on each shipped desyndication's `returns` row. A row can wait + // in the queue past several envelopes, so only this build knows which envelope carried it. + if (!shipped_returns.empty()) { + action(permission_level{get_self(), "active"_n}, synd::account, synd::action_retsent, + std::make_tuple(sysio::slug_name{chain_code}, epoch, shipped_returns)).send(); + } // This envelope's canonical epoch digest: keccak256 over the packed bytes, which equal the // digest preimage because `envelope_hash` is empty on the wire. This is the value the receiving diff --git a/contracts/sysio.msgch/sysio.msgch.wasm b/contracts/sysio.msgch/sysio.msgch.wasm index b1f354585464c398aa016531d9eec2b45cd95662..54412faed211008273811b5781254318c491356d 100755 GIT binary patch delta 15892 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 K+09~V+J6JJANuqF delta 12304 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 request_ids); + // ----------------------------------------------------------------------- // Launch ingestion (privileged caller, epoch-0 bootstrap window) // ----------------------------------------------------------------------- @@ -658,7 +672,11 @@ namespace sysio { opp::types::ChainKind chain_kind; ///< destination key family std::vector pubkey; ///< destination captured when queued uint64_t amount = 0; ///< exact net burned amount; the fee is not refundable here - SYSLIB_SERIALIZE(return_row, (request_id)(holder)(chain_code)(token_code)(chain_kind)(pubkey)(amount)) + /// Depot epoch of the outbound envelope that carried the DESYNDICATE_LIQ, from `retsent`; 0 until + /// that envelope is built. No envelope is built for epoch 0. + uint32_t sent_epoch = 0; + SYSLIB_SERIALIZE(return_row, + (request_id)(holder)(chain_code)(token_code)(chain_kind)(pubkey)(amount)(sent_epoch)) }; /// Outstanding returns only; completed/refunded rows are erased atomically. using returns_t = kv::table<"returns"_n, return_key, return_row>; diff --git a/contracts/sysio.synd/src/sysio.synd.cpp b/contracts/sysio.synd/src/sysio.synd.cpp index 111824e9f8..60aea87ca4 100644 --- a/contracts/sysio.synd/src/sysio.synd.cpp +++ b/contracts/sysio.synd/src/sysio.synd.cpp @@ -128,10 +128,11 @@ constexpr std::string_view desynd_net_message = "desyndication net of the fee constexpr std::string_view desynd_fee_flat_message = "flat desyndication fee must fit an asset and be below the minimum desyndication"; -/// Diagnostic path names of the intake actions. -constexpr std::string_view onsynd_path = "onsynd"; -constexpr std::string_view onyield_path = "onyield"; -constexpr std::string_view closeenv_path = "closeenv"; +/// Diagnostic path names of the intake actions and of `retsent`. +constexpr std::string_view onsynd_path = "onsynd"; +constexpr std::string_view onyield_path = "onyield"; +constexpr std::string_view closeenv_path = "closeenv"; +constexpr std::string_view retsent_path = "retsent"; /// Drop reasons, one per rule an intake message can fail. constexpr std::string_view pubkey_misfit_reason = "pubkey does not fit the chain family"; @@ -147,6 +148,11 @@ constexpr std::string_view envelope_closed_reason = "envelope already closed or constexpr std::string_view replay_reason = "replayed sequence"; constexpr std::string_view digest_reason = "envelope digest does not match; left OPEN"; +/// Why `retsent` left a request id's row as it was. +constexpr std::string_view return_resolved_reason = "no outstanding return for the request id"; +constexpr std::string_view return_outpost_reason = "the return belongs to another outpost"; +constexpr std::string_view return_stamped_reason = "the return already records the envelope that carried it"; + /// Why one step of the queue left an envelope or an item where it was. constexpr std::string_view no_bond_reason = "sysio.bond is not deployed; the envelope stays WAITING"; constexpr std::string_view bond_token_reason = "sysio.bond cannot bond the token; the envelope stays WAITING"; @@ -1589,6 +1595,29 @@ void synd::desyndicate(name holder, asset quantity, uint64_t max_fee) { .send(); } +void synd::retsent(sysio::slug_name chain_code, uint32_t epoch_index, std::vector request_ids) { + require_auth(MSGCH_ACCOUNT); + returns_t pending(get_self()); + for (const uint64_t request_id : request_ids) { + const return_key key{request_id}; + auto row = pending.try_get(key); + if (!row.has_value()) { + drop(retsent_path, return_resolved_reason); + continue; + } + if (row->chain_code != chain_code) { + drop(retsent_path, return_outpost_reason); + continue; + } + if (row->sent_epoch != 0) { + drop(retsent_path, return_stamped_reason); + continue; + } + row->sent_epoch = epoch_index; + pending.set(ram_payer, key, *row); + } +} + // --------------------------------------------------------------------------- // Launch ingestion // --------------------------------------------------------------------------- diff --git a/contracts/sysio.synd/sysio.synd.abi b/contracts/sysio.synd/sysio.synd.abi index 347ec4af6c..f0125400a1 100644 --- a/contracts/sysio.synd/sysio.synd.abi +++ b/contracts/sysio.synd/sysio.synd.abi @@ -906,6 +906,24 @@ } ] }, + { + "name": "retsent", + "base": "", + "fields": [ + { + "name": "chain_code", + "type": "slug_name" + }, + { + "name": "epoch_index", + "type": "uint32" + }, + { + "name": "request_ids", + "type": "uint64[]" + } + ] + }, { "name": "return_key", "base": "", @@ -947,6 +965,10 @@ { "name": "amount", "type": "uint64" + }, + { + "name": "sent_epoch", + "type": "uint32" } ] }, @@ -1313,6 +1335,11 @@ "type": "refundreturn", "ricardian_contract": "" }, + { + "name": "retsent", + "type": "retsent", + "ricardian_contract": "" + }, { "name": "setconfig", "type": "setconfig", diff --git a/contracts/sysio.synd/sysio.synd.wasm b/contracts/sysio.synd/sysio.synd.wasm index c29fccfb486c3c94d01de5b7abfa4761a2818c08..19fc19df6e5bcd2fcdb171f96bdc865481d24c41 100755 GIT binary patch delta 16164 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 delta 15020 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%0A^d)esvvEH>E6aJR4mkXlfq&0tG^&T8Ve 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 F{{bij+#>)0 diff --git a/contracts/tests/sysio.synd_tests.cpp b/contracts/tests/sysio.synd_tests.cpp index 8078a5645c..3e8ce2c715 100644 --- a/contracts/tests/sysio.synd_tests.cpp +++ b/contracts/tests/sysio.synd_tests.cpp @@ -431,6 +431,17 @@ class sysio_synd_tester : public tester { return push(SYND_ACCOUNT, synd_abi_ser, signer == name{} ? holder : signer, "desyndicate"_n, mvo() ("holder", holder)("quantity", asset(static_cast(amount), sym))("max_fee", max_fee)); } + /// The `sysio.msgch::buildenv` notice that `chain_code`'s envelope of `epoch` carried `request_ids`. + action_result retsent(std::string_view chain_code, uint32_t epoch, const std::vector& request_ids, + name signer = MSGCH_ACCOUNT) { + return push(SYND_ACCOUNT, synd_abi_ser, signer, "retsent"_n, mvo() + ("chain_code", codename_mvo(chain_code))("epoch_index", epoch)("request_ids", request_ids)); + } + /// Build `chain_code`'s outbound envelope at the depot's current epoch, as `sysio.epoch::advance` does. + action_result buildenv(std::string_view chain_code) { + return push(MSGCH_ACCOUNT, msgch_abi_ser, EPOCH_ACCOUNT, "buildenv"_n, + mvo()("chain_code", slug_value(chain_code))); + } /// Query the fee path and retain its return trace, including refusal diagnostics. action_result quotedesynd(std::string_view chain, std::string_view token, uint64_t quantity) { return push(SYND_ACCOUNT, synd_abi_ser, SYND_ACCOUNT, "quotedesynd"_n, mvo() @@ -819,6 +830,12 @@ class sysio_synd_tester : public tester { fc::variant return_row(uint64_t request_id) { return decode(synd_abi_ser, "return_row", get_row_by_id(SYND_ACCOUNT, SYND_ACCOUNT, "returns"_n, request_id)); } + /// The `sent_epoch` of `request_id`'s `returns` row: the epoch of the envelope that carried it, 0 until then. + uint32_t sent_epoch(uint64_t request_id) { + const auto row = return_row(request_id); + BOOST_REQUIRE(!row.is_null()); + return row["sent_epoch"].as(); + } fc::variant pool_row(std::string_view token_code) { return decode(synd_abi_ser, "pool_row", get_row_by_id(SYND_ACCOUNT, SYND_ACCOUNT, "yieldpool"_n, slug_value(token_code))); @@ -1597,6 +1614,7 @@ BOOST_FIXTURE_TEST_CASE(outstanding_returns_record_the_original_obligation, sysi BOOST_REQUIRE_EQUAL(amount, row["amount"].as_uint64()); BOOST_REQUIRE_EQUAL("alice", row["holder"].as_string()); BOOST_REQUIRE_EQUAL(32u, row["pubkey"].as>().size()); + BOOST_REQUIRE_EQUAL(0u, row["sent_epoch"].as()); // no envelope built yet } }; check_log(1, LIQSOL, 10 * UNIT); @@ -1652,6 +1670,183 @@ BOOST_FIXTURE_TEST_CASE(return_refund_failure_preserves_obligation, sysio_synd_t BOOST_REQUIRE_EQUAL(0, liq_balance("alice"_n)); } FC_LOG_AND_RETHROW() +/// The request ids of the `retsent` notices the last pushed transaction ran, in order. +std::vector> retsent_notices(sysio_synd_tester& t) { + std::vector> notices; + if (!t.last_trace) return notices; + for (const auto& at : t.last_trace->action_traces) { + if (at.receiver != sysio_synd_tester::SYND_ACCOUNT || at.act.account != sysio_synd_tester::SYND_ACCOUNT || + at.act.name != "retsent"_n) + continue; + const auto data = t.synd_abi_ser.binary_to_variant("retsent", at.act.data, + sysio_synd_tester::fixture_abi_yield_function()); + notices.push_back(data["request_ids"].as>()); + } + return notices; +} + +/// A `returns` row records the epoch of the outbound envelope that carried its DESYNDICATE_LIQ: 0 while the +/// attestation is queued, then the epoch `buildenv` built the envelope at. Desyndications that ride one +/// envelope share its epoch; a later envelope's epoch leaves earlier rows as they were. +BOOST_FIXTURE_TEST_CASE(a_return_records_the_epoch_of_the_envelope_that_carried_it, sysio_synd_tester) try { + BOOST_REQUIRE_EQUAL(success(), liq_mint("alice"_n, LIQSOL, 100 * UNIT)); + BOOST_REQUIRE_EQUAL(success(), link_svm("alice"_n, ed_signer())); + BOOST_REQUIRE_EQUAL(success(), setconfig(SOLANA, LIQSOL, {})); + BOOST_REQUIRE_EQUAL(success(), set_epoch(3)); + BOOST_REQUIRE_EQUAL(success(), desyndicate("alice"_n, 10 * UNIT)); + BOOST_REQUIRE_EQUAL(success(), desyndicate("alice"_n, 7 * UNIT)); + BOOST_REQUIRE_EQUAL(0u, sent_epoch(1)); + BOOST_REQUIRE_EQUAL(0u, sent_epoch(2)); + + // ETH's envelope ships ETH's own row and none of SOLANA's: no notice, nothing stamped. + BOOST_REQUIRE_EQUAL(success(), push(MSGCH_ACCOUNT, msgch_abi_ser, MSGCH_ACCOUNT, "queueout"_n, mvo() + ("chain_code", slug_value(ETH)) + ("attest_type", static_cast(sysio::opp::types::ATTESTATION_TYPE_OPERATOR_ACTION)) + ("data", std::vector{'\x01'}))); + BOOST_REQUIRE(!attestation_row(3).is_null()); + BOOST_REQUIRE_EQUAL(success(), set_epoch(4)); + BOOST_REQUIRE_EQUAL(success(), buildenv(ETH)); + BOOST_REQUIRE(attestation_row(3).is_null()); + BOOST_REQUIRE(retsent_notices(*this).empty()); + BOOST_REQUIRE_EQUAL(0u, sent_epoch(1)); + + // `advance` builds SOLANA's envelope at the epoch it just started: one notice for both rows. + BOOST_REQUIRE_EQUAL(success(), buildenv(SOLANA)); + BOOST_REQUIRE(attestation_row(1).is_null()); + BOOST_REQUIRE(attestation_row(2).is_null()); + BOOST_REQUIRE((retsent_notices(*this) == std::vector>{{1, 2}})); + BOOST_REQUIRE_EQUAL(4u, sent_epoch(1)); + BOOST_REQUIRE_EQUAL(4u, sent_epoch(2)); + + // A desyndication in epoch 4 rides the envelope built at epoch 5. + BOOST_REQUIRE_EQUAL(success(), desyndicate("alice"_n, 5 * UNIT)); + BOOST_REQUIRE_EQUAL(0u, sent_epoch(3)); + BOOST_REQUIRE_EQUAL(success(), set_epoch(5)); + BOOST_REQUIRE_EQUAL(success(), buildenv(SOLANA)); + BOOST_REQUIRE((retsent_notices(*this) == std::vector>{{3}})); + BOOST_REQUIRE_EQUAL(4u, sent_epoch(1)); + BOOST_REQUIRE_EQUAL(4u, sent_epoch(2)); + BOOST_REQUIRE_EQUAL(5u, sent_epoch(3)); +} FC_LOG_AND_RETHROW() + +/// Desyndications all queued in epoch 1 spill across three envelopes, and each row records the epoch of the +/// envelope it actually shipped in. Two desyndications queued back to back land in different envelopes when +/// the first fills one. The rows between them carry DESYNDICATE_LIQ bytes with no request id: `buildenv` +/// skips them, and an envelope that carries only such rows sends no notice. +BOOST_FIXTURE_TEST_CASE(returns_spilled_across_envelopes_record_the_envelope_each_shipped_in, + sysio_synd_tester) try { + constexpr uint32_t MAX_ENVELOPE_ATTESTATIONS = 64; // mirrors sysio.msgch.hpp + constexpr uint32_t QUEUEOUTS_PER_BLOCK = 20; + BOOST_REQUIRE_EQUAL(success(), liq_mint("alice"_n, LIQSOL, 100 * UNIT)); + BOOST_REQUIRE_EQUAL(success(), setconfig(SOLANA, LIQSOL, {})); + BOOST_REQUIRE_EQUAL(success(), link_svm("alice"_n, ed_signer())); + BOOST_REQUIRE_EQUAL(success(), set_epoch(1)); + + // `next_attestation` is the id the next queued attestation takes. + uint64_t next_attestation = 1; + // Queue `count` rows with no request id. + const auto queue_filler = [&](uint32_t count) { + for (uint32_t i = 0; i < count; ++i) { + BOOST_REQUIRE_EQUAL(success(), push(MSGCH_ACCOUNT, msgch_abi_ser, MSGCH_ACCOUNT, "queueout"_n, mvo() + ("chain_code", slug_value(SOLANA)) + ("attest_type", static_cast(sysio::opp::types::ATTESTATION_TYPE_DESYNDICATE_LIQ)) + ("data", std::vector{static_cast(i >> 8), static_cast(i)}))); + ++next_attestation; + if ((i + 1) % QUEUEOUTS_PER_BLOCK == 0) produce_block(); + } + produce_block(); + }; + const auto queue_desyndication = [&](uint64_t amount) { + BOOST_REQUIRE_EQUAL(success(), desyndicate("alice"_n, amount)); + BOOST_REQUIRE_EQUAL("ATTESTATION_TYPE_DESYNDICATE_LIQ", + attestation_row(next_attestation)["type"].as_string()); + ++next_attestation; + }; + + // Attestations 1-64 are filler; requests 1 and 2 take 65 and 66; filler takes 67-127; requests 3 and 4 + // take 128 and 129. + queue_filler(MAX_ENVELOPE_ATTESTATIONS); + queue_desyndication(10 * UNIT); + queue_desyndication(5 * UNIT); + queue_filler(MAX_ENVELOPE_ATTESTATIONS - 3); + queue_desyndication(4 * UNIT); + queue_desyndication(3 * UNIT); + BOOST_REQUIRE_EQUAL(2 * MAX_ENVELOPE_ATTESTATIONS + 2, next_attestation); + + // The epoch-2 envelope carries only filler. + BOOST_REQUIRE_EQUAL(success(), set_epoch(2)); + BOOST_REQUIRE_EQUAL(success(), buildenv(SOLANA)); + BOOST_REQUIRE(!executed(SYND_ACCOUNT, "retsent"_n)); + for (uint64_t id = 1; id <= 4; ++id) BOOST_REQUIRE_EQUAL(0u, sent_epoch(id)); + + // The epoch-3 envelope carries requests 1, 2 and 3 and fills before request 4. + BOOST_REQUIRE_EQUAL(success(), set_epoch(3)); + BOOST_REQUIRE_EQUAL(success(), buildenv(SOLANA)); + BOOST_REQUIRE((retsent_notices(*this) == std::vector>{{1, 2, 3}})); + BOOST_REQUIRE_EQUAL(3u, sent_epoch(1)); + BOOST_REQUIRE_EQUAL(3u, sent_epoch(2)); + BOOST_REQUIRE_EQUAL(3u, sent_epoch(3)); + BOOST_REQUIRE_EQUAL(0u, sent_epoch(4)); + BOOST_REQUIRE(!attestation_row(2 * MAX_ENVELOPE_ATTESTATIONS + 1).is_null()); + + BOOST_REQUIRE_EQUAL(success(), set_epoch(4)); + BOOST_REQUIRE_EQUAL(success(), buildenv(SOLANA)); + BOOST_REQUIRE((retsent_notices(*this) == std::vector>{{4}})); + BOOST_REQUIRE(attestation_row(2 * MAX_ENVELOPE_ATTESTATIONS + 1).is_null()); + BOOST_REQUIRE_EQUAL(3u, sent_epoch(3)); + BOOST_REQUIRE_EQUAL(4u, sent_epoch(4)); +} FC_LOG_AND_RETHROW() + +/// A return governance resolves before its envelope is built leaves the build whole: the notice for its +/// request id finds no row and is dropped with a diagnostic. +BOOST_FIXTURE_TEST_CASE(a_return_resolved_before_its_envelope_is_built_leaves_the_build_whole, + sysio_synd_tester) try { + BOOST_REQUIRE_EQUAL(success(), liq_mint("alice"_n, LIQSOL, 100 * UNIT)); + BOOST_REQUIRE_EQUAL(success(), link_svm("alice"_n, ed_signer())); + BOOST_REQUIRE_EQUAL(success(), setconfig(SOLANA, LIQSOL, {})); + BOOST_REQUIRE_EQUAL(success(), desyndicate("alice"_n, 10 * UNIT)); + BOOST_REQUIRE_EQUAL(success(), desyndicate("alice"_n, 5 * UNIT)); + BOOST_REQUIRE_EQUAL(success(), + push(SYND_ACCOUNT, synd_abi_ser, SYSIO_ACCOUNT, "finishreturn"_n, mvo()("request_id", 1))); + + BOOST_REQUIRE_EQUAL(success(), set_epoch(1)); + BOOST_REQUIRE_EQUAL(success(), buildenv(SOLANA)); + BOOST_REQUIRE((retsent_notices(*this) == std::vector>{{1, 2}})); + BOOST_REQUIRE(console_has("sysio.synd::retsent: DROP -- no outstanding return for the request id")); + BOOST_REQUIRE(return_row(1).is_null()); + BOOST_REQUIRE_EQUAL(1u, sent_epoch(2)); +} FC_LOG_AND_RETHROW() + +/// `retsent` is sysio.msgch's alone. It stamps a row once, only for the outpost whose envelope carried +/// it, and drops every id it cannot stamp without aborting. +BOOST_FIXTURE_TEST_CASE(retsent_stamps_each_return_once_for_its_own_outpost, sysio_synd_tester) try { + BOOST_REQUIRE_EQUAL(success(), liq_mint("alice"_n, LIQSOL, 100 * UNIT)); + BOOST_REQUIRE_EQUAL(success(), link_svm("alice"_n, ed_signer())); + BOOST_REQUIRE_EQUAL(success(), setconfig(SOLANA, LIQSOL, {})); + BOOST_REQUIRE_EQUAL(success(), desyndicate("alice"_n, 10 * UNIT)); + + BOOST_REQUIRE(mentions(retsent(SOLANA, 7, {1}, "alice"_n), "missing authority of sysio.msgch")); + BOOST_REQUIRE(mentions(retsent(SOLANA, 7, {1}, SYND_ACCOUNT), "missing authority of sysio.msgch")); + BOOST_REQUIRE_EQUAL(0u, sent_epoch(1)); + + BOOST_REQUIRE_EQUAL(success(), retsent(SOLANA, 7, {})); + BOOST_REQUIRE_EQUAL(0u, sent_epoch(1)); + + BOOST_REQUIRE_EQUAL(success(), retsent(ETH, 7, {1})); + BOOST_REQUIRE(console_has("sysio.synd::retsent: DROP -- the return belongs to another outpost")); + BOOST_REQUIRE_EQUAL(0u, sent_epoch(1)); + + BOOST_REQUIRE_EQUAL(success(), retsent(SOLANA, 7, {99, 1})); + BOOST_REQUIRE(console_has("sysio.synd::retsent: DROP -- no outstanding return for the request id")); + BOOST_REQUIRE_EQUAL(7u, sent_epoch(1)); + BOOST_REQUIRE(return_row(99).is_null()); + + BOOST_REQUIRE_EQUAL(success(), retsent(SOLANA, 9, {1})); + BOOST_REQUIRE( + console_has("sysio.synd::retsent: DROP -- the return already records the envelope that carried it")); + BOOST_REQUIRE_EQUAL(7u, sent_epoch(1)); +} FC_LOG_AND_RETHROW() + /// The DESYNDICATE_LIQ attestation `id` queued, decoded. sysio::opp::attestations::DesyndicateLIQ queued_desyndication(sysio_synd_tester& t, uint64_t id) { const auto att = t.attestation_row(id);