Skip to content
Merged
4 changes: 2 additions & 2 deletions contracts/sysio.authex/include/sysio.authex/sysio.authex.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -285,8 +285,8 @@ namespace sysio {
* A malformed or mismatched address skips only the sweep, not link insertion. A missing or
* non-privileged sysio.dclaim deployment likewise skips the sweep. An identical recordlink can
* retry before node-owner registration completes; after registration, nodeownreg's duplicate
* gate returns before this action, so remediation requires an operator-pushed recordlink or
* linkswept action.
* gate returns before this action, so remediation requires governance to push recordlink or
* linkswept as sysio.authex@active.
*
* Unlike createlink, this does NOT enforce a unique `pub_key`: one external wallet may hold
* several WireNodes NFTs and back several Wire accounts, so one ETH key -> many accounts is
Expand Down
2 changes: 1 addition & 1 deletion contracts/sysio.authex/src/sysio.authex.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ void send_linked_rewards_sweep(const name self, const name account,
* Best-effort sweep for the trusted inbound path.
*
* A missing or non-privileged DClaim deployment cannot safely bill new rows to sysio. Preserve the
* recorded link so an identical operator-authorized recordlink can retry after bootstrap.
* recorded link so governance can retry with an identical recordlink after bootstrap.
*/
void try_send_linked_rewards_sweep(const name self, const name account,
const opp::types::ChainKind chain_kind,
Expand Down
200 changes: 0 additions & 200 deletions contracts/sysio.dclaim/tools/convert_import.py

This file was deleted.

31 changes: 31 additions & 0 deletions contracts/tests/sysio.authex_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ using mvo = fc::mutable_variant_object;
using ChainKind = sysio::opp::types::ChainKind;

using sysio_system::test_support::build_link_message;
using sysio_system::test_support::sign_createlink;

// ——— Tester class ———
class sysio_authex_tester : public tester {
Expand Down Expand Up @@ -342,6 +343,36 @@ BOOST_FIXTURE_TEST_CASE( createlink_preserves_old_prelink_dclaim_rewards,
BOOST_REQUIRE_EQUAL(pending["balance"].as<asset>().get_amount(), 5000);
} FC_LOG_AND_RETHROW()

BOOST_FIXTURE_TEST_CASE( createlink_svm_sweeps_prelink_dclaim_rewards, sysio_authex_tester ) try {
deploy_dclaim();
const auto private_key = fc::crypto::private_key::generate(fc::crypto::private_key::key_type::ed);
const auto public_key = private_key.get_public_key();
const auto raw_key = public_key.get<fc::crypto::ed::public_key_shim>().serialize();
const std::vector<char> native_address(raw_key.begin(), raw_key.end());
// Same first 8 bytes, hence the same `bychainad` key: the sweep must match the full address.
std::vector<char> same_prefix_address = native_address;
same_prefix_address.back() ^= char{0x01};

BOOST_REQUIRE_EQUAL(success(), import_credit(
same_prefix_address, 3000, ChainKind::CHAIN_KIND_SVM));
BOOST_REQUIRE_EQUAL(success(), import_credit(
native_address, 8000, ChainKind::CHAIN_KIND_SVM));

const auto nonce = now_ms();
const auto sig = sign_createlink(private_key, "carol", ChainKind::CHAIN_KIND_SVM, nonce);
BOOST_REQUIRE_EQUAL(success(), createlink(
"carol"_n, ChainKind::CHAIN_KIND_SVM, "carol", sig, public_key, nonce));
produce_blocks();

BOOST_REQUIRE(get_dclaim_row("unmapped"_n, "unmapped_token", 2).is_null());
const auto same_prefix = get_dclaim_row("unmapped"_n, "unmapped_token", 1);
BOOST_REQUIRE(!same_prefix.is_null());
BOOST_REQUIRE_EQUAL(same_prefix["balance"].as<asset>().get_amount(), 3000);
const auto pending = get_dclaim_row("pclaims"_n, "pending_claim", "carol"_n.to_uint64_t());
BOOST_REQUIRE(!pending.is_null());
BOOST_REQUIRE_EQUAL(pending["balance"].as<asset>().get_amount(), 8000);
} FC_LOG_AND_RETHROW()

BOOST_FIXTURE_TEST_CASE( recordlink_records_link_when_dclaim_is_missing, sysio_authex_tester ) try {
const auto public_key = fc::crypto::private_key::generate(
fc::crypto::private_key::key_type::em).get_public_key();
Expand Down
23 changes: 22 additions & 1 deletion docs/contract-upgrade-order.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ WIRE-352 adds separate inline edges whose failure behavior depends on the caller
| `sysio.msgch` node-owner dispatch | `sysio.roa::nodeownreg` | Deploy `sysio.msgch` and `sysio.roa` as one compatibility-coupled set. | WIRE-352 adds the required `eth_address` field. An old caller underflows the new action decoder; the inverse pairing is also unsupported. Node-owner delivery can abort. |
| `sysio.roa::nodeownreg` | `sysio.authex::recordlink` | Deploy `sysio.roa` and `sysio.authex` as the same compatibility-coupled set. | WIRE-352 adds the required `native_address` field. An old caller underflows the new action decoder; the inverse pairing is also unsupported. Node-owner delivery can abort. |
| `sysio.authex::createlink` | `sysio.dclaim::linkswept` | Deploy `sysio.dclaim` before the first user-created external-key link. | A missing or non-privileged callee aborts before link insertion. Any inline failure rolls the transaction back, so the user can submit a fresh retry. |
| `sysio.authex::recordlink` | `sysio.dclaim::linkswept` | Prefer deploying `sysio.dclaim` before trusted node-owner dispatch begins. | A missing or non-privileged callee skips the sweep but preserves the trusted link; an identical operator-authorized `recordlink` can retry it after bootstrap. |
| `sysio.authex::recordlink` | `sysio.dclaim::linkswept` | Prefer deploying `sysio.dclaim` before trusted node-owner dispatch begins. | A missing or non-privileged callee skips the sweep but preserves the trusted link; governance can retry it after bootstrap with an identical `recordlink` submitted as `sysio.authex@active`. |
| `sysio.msgch` inbound envelope dispatch | `sysio.synd::onsynd`, `sysio.synd::onyield`, `sysio.synd::closeenv` | Deploy `sysio.msgch`, `sysio.synd`, `sysio.liq`, `sysio.bond` and `sysio.authex` as one compatibility-coupled set, and deploy `sysio.synd` before any liq token is activated. | The actions run inside the consensus transaction that accepts the envelope. A missing `sysio.synd`, or one without these actions, asserts in its dispatcher and aborts that transaction, so no envelope of the outpost is accepted until the set is whole. |
| `sysio.synd` intake and queue | `sysio.liq::mint`, `sysio.liq::burn`, `sysio.liq::mintyield` | The same set. | `sysio.synd` verifies every condition `mint` checks before sending it, so the current `sysio.liq` never refuses it. A `sysio.liq` without these actions, or one that does not accept `sysio.synd` as their only signer, aborts the intake and with it the inbound envelope. |
| `sysio.authex::createlink` | `sysio.synd::linkswept` | None. Nothing is parked before `sysio.synd` is deployed, and it deploys privileged through `setsyscode`. | A missing or non-privileged callee skips the sweep and keeps the link; the permissionless `sysio.synd::sweep` delivers the parked shadow later. The delivery's `sysio.liq::transfer` notifies the linking account, so a contract account whose transfer handler throws aborts its own `createlink` and cannot link while shadow is parked for its key. |
Expand All @@ -155,6 +155,27 @@ Production deployment through `sysio.roa::setsyscode` privileges `sysio.dclaim`
as part of the deploy, so there is no separate privilege step. The durable
precondition is ordering: deploy DClaim before any link action can execute.

On a fresh deployment, finish the DClaim import before the first `createlink` or
`recordlink`: fund `sysio.dclaim` with the total of the credits, push every
`importseed` batch, reconcile the `unmapped` rows against the export, and only
then seal the import with `importdone`. The LCO import in the cluster bootstrap
(`LcoImportSteps` in wire-tools-ts) runs these steps in the epoch-0 window and
stops before the seal when a row differs. A link sweeps only the credits already
in `unmapped`, and its holder cannot trigger the sweep again: `createlink`
accepts one link per account and chain, and only `sysio.authex` may send
`linkswept`. `linkswept` moves the `unmapped` row matching the supplied chain and
address to the supplied account without consulting the link, so recovering a
credit imported after its key was linked is for governance, through `sysio.msig`
as `sysio.authex@active`. The `links` row keeps only the account, chain and
public key, not the native address, so governance derives the address from the
stored key (SVM: the raw 32-byte ED key; EVM: the canonical 20-byte address of
the stored EM key, the trailing 20 bytes of `keccak256(X || Y)` over the 64-byte
coordinates, without the leading SEC1 `0x04` byte, as
`opp::evm_address_from_uncompressed_key` computes it) and checks the link and the
exact target `unmapped` row before proposing `linkswept`. The same derivation and
check come first when governance replays the identical `recordlink` for a
node-owner link.

The two added address fields are intentionally not a staged compatibility
protocol. Activate WIRE-352 in a quiesced maintenance window:

Expand Down
Loading