Skip to content

System: defer global singleton writes so view actions run read-only - #562

Merged
heifner merged 8 commits into
masterfrom
fix/system-readonly-view-actions
Aug 18, 2026
Merged

System: defer global singleton writes so view actions run read-only#562
heifner merged 8 commits into
masterfrom
fix/system-readonly-view-actions

Conversation

@heifner

@heifner heifner commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Summary

system_contract and trx_priority each cached their global row in a member and wrote it back from a destructor. The generated dispatcher destroys the contract as soon as the action returns, so every action issued a kv_set -- including the view actions that exist to be called through send_read_only_transaction, which the chain refuses with cannot store a KV record when executing a readonly transaction.

The original report looked self-contradictory, and this is why: viewnodedist against a non-node-owner account reported account is not a node owner correctly, while the same action against a valid account died. sysio::check traps before the destructor runs, so only the success path ever reached the offending write.

Both singletons move to kv::cached_global, which defers the write to the end of the action and performs it only when something actually mutated. The _gstate members and both destructors are gone: readers use _global.get(), mutators _global.modify(). No contract in the tree has a destructor any more.

Defaults belong to the handle, not to the constructor

An earlier revision of this branch seeded the global from the constructor. That charges every action for a singleton most actions never touch: seeding reads the row, and get_default_parameters() passed as an argument runs its get_blockchain_parameters host call whether or not the action goes on to use the global. Only five functions do -- onblock, update_ranked_producers, setram, setparams and payepoch -- so roughly forty actions were paying for a singleton they never read.

The defaults now ride on the singleton type via kv::cached_global's new MakeDefault parameter (Wire-Network/wire-cdt#107). The constructor body is empty and everything defers to first use, so an action that never touches the global neither reads the row nor computes the defaults, and one that only reads it still owes no write. get_default_parameters moves out of system_contract as a free default_global_state, because the alias names it as a template argument and has to exist before the class is declared.

sysio.system grows 4380 bytes. That is the price of materializing lazily rather than in the constructor, not of the defaults themselves: with a trivial default it is 4196, and routing every access through a single materialization point recovers only 367. Seeding eagerly let the optimizer prove the cache loaded once and strip the checks everywhere downstream, which is precisely what deferring gives up.

trx_priority::touch_last_update uses modify_or_create (the cached wrapper's creating form, renamed from upsert in the CDT PR to keep it distinct from kv::table::upsert, which stores its default verbatim and skips the updater on insert). The distinction matters here: the blank default is only a seed and the lambda runs on the create path too, which is what stamps the current time on the very first addtrxp/deltrxp rather than slot zero.

Rebuilt contract binaries

Eight binaries are affected, not one. The kv::global size check added in Wire-Network/wire-cdt#106 changes generated code for every contract that uses the type: chalg, dclaim, epoch, msgch, opreg, reserv, system and uwrit. PR builds run with BUILD_SYSTEM_CONTRACTS=OFF and exercise the committed binaries, while a release tag rebuilds from source -- so leaving any of them stale would mean CI testing one artifact and a tag shipping another, from identical source.

Merging master moved four of them. opreg now takes the guard from master itself, which rebuilt it independently; epoch and reserv are rebuilt here because master's copies still predate wire-cdt#106; and system is rebuilt because it combines this branch's cached_global handle with master's claimable-payout emissions work. A sweep of all seventeen contracts on the merged tree, built against a from-scratch wire-cdt master (adad4b290) toolchain, reproduces every other one byte for byte -- chalg, dclaim, msgch and uwrit among them, so the rebuilds this branch carries for those stand as committed. None of them reference kv::global.

uwrit was already stale for an unrelated reason: its committed artifact came from a different CDT install than the rest of the tree and sat 27KB away from what this toolchain produces from the same source. This corrects it.

test_contracts/sendinline is deliberately untouched -- it references none of the changed headers, and its output differs across rebuilds at an identical size, so committing a fresh snapshot would be churn rather than a fix.

No ABI changed.

Notes for review

payepoch previously decremented total_unpaid_blocks inside the producer reset loop. It now accumulates the reclaimed count across the loop and applies it in a single modify, so the reset costs one deferred write regardless of producer count.

Only one handle exists per singleton ("global"_n and "trxpglobal"_n), so the deferred-write visibility rule that comes with caching does not apply -- there is no second handle that could observe a stale row mid-action. That also matters for the one nested call: onblock reads the global three times and mutates it twice, then calls update_ranked_producers, which mutates it twice more. Because the handle is a contract member rather than a local, those five mutations collapse into a single kv_set. The payer for every deferred write is get_self(), unchanged from the destructor it replaces.

On a handle carrying defaults, exists() reports that a value is available rather than that a row is stored. Nothing here asks that question, and the idiom it affects -- create the row if exists() is false -- is the one this change exists to retire.

Dependency

Requires kv_cached.hpp and the kv::global hardening from Wire-Network/wire-cdt#106, plus the MakeDefault parameter from Wire-Network/wire-cdt#107. PR CI does not build system contracts, so this stands on its own here, but a release tag rebuilds them against the latest published wire-cdt release -- that release has to ship before the next wire-sysio tag or sysio.system will not compile.

Regression coverage

emissions_tests gains a helper that pushes a system action as a read-only transaction, plus two cases: a view action must execute and return the same payload it returns normally, and viewnodedist must still report its own error read-only. Swapping the pre-change wasm back into the build directory reproduces the original chain error in the first case and leaves the second passing, which is the asymmetry described above.

Those two cannot reach the absent-row path, because their fixture has produced post-deploy blocks and onblock has already persisted the global. sysio_fresh_deploy_tester deploys sysio.system and stops, so the row has never been written, and a third case walks that path: a succeeding action that does not touch the global must not create the row, a read-only query must run with it still missing, the values served must be the declared defaults rather than a zero-initialized struct, and the first genuine mutation must store defaults plus that change. Reverting the constructor to the pre-fix seeding leaves both existing read-only cases passing while that case fails.

huangminghuang
huangminghuang previously approved these changes Aug 12, 2026

@huangminghuang huangminghuang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved after review.

system_contract and trx_priority each cached their global row in a member and wrote it back from a destructor. The dispatcher destroys the contract as soon as the action returns, so every action issued a kv_set -- including the view actions that exist to be called through a read-only transaction, which the chain refuses with "cannot store a KV record when executing a readonly transaction". The report looked contradictory because viewnodedist reported its own "account is not a node owner" error correctly: check() traps before the destructor runs, so only the success path died.

Both singletons move to kv::cached_global, which defers the write to the end of the action and performs it only when something actually mutated. The _gstate members and the destructors are gone; readers use _global.get() and mutators _global.modify(), and trx_priority stamps its update time through upsert() so the row is created on first write rather than on first read.

payepoch accumulates the reclaimed unpaid-block count across the producer reset loop and applies it in a single modify, so the reset costs one deferred write regardless of producer count.

Requires the kv_cached header from wire-cdt; a release build of the system contracts needs a wire-cdt release that carries it.
…affected contracts

trx_priority follows the cached_value rename to modify_or_create, and its comment now states the property the first-ever addtrxp/deltrxp depends on: the blank default is only a seed, and the lambda runs on the create path too, so the stamp is the current time rather than slot zero.

system_contract's constructor uses the new seed_if_absent instead of set(). On a chain where the global row had never been written, set() left every action owing a kv_set -- including the read-only view actions this change exists to enable -- so the comment claiming a reading action issues none was false there. Seeding leaves the handle clean and the defaults reach storage the first time an action genuinely mutates the global.

Rebuilds every contract binary whose generated code the kv::global size check changes: chalg, dclaim, epoch, msgch, opreg, reserv, system and uwrit. Leaving them stale would put the tree in the state where CI tests one binary and a release tag builds another from the same source, since PR builds run with BUILD_SYSTEM_CONTRACTS=OFF and a tag build rebuilds from source. That is the state uwrit was already in for an unrelated reason -- its committed artifact came from a different CDT install than the rest of the tree, 27KB apart from what this toolchain produces -- and it is corrected here as well.

test_contracts/sendinline is deliberately left alone: it references none of the changed headers, and its output differs across rebuilds at an identical size, so committing a fresh snapshot would be churn rather than a fix.

Change-Id: Ib7a52fa553b26963f27e1ecbb5c0ff7f6a887ab4

@huangminghuang huangminghuang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Commit the remaining rebuilt contract artifacts

I rebuilt this exact head against the wire-cdt#106 headers with the same CDT toolchain. The build reproduces the committed sysio.system.wasm and sysio.msgch.wasm byte-for-byte, but sysio.{chalg,dclaim,epoch,opreg,reserv,uwrit}.wasm all differ. Each rebuilt binary contains the new kv::global: stored value size guard; the committed binary does not.

Because PR CI runs with BUILD_SYSTEM_CONTRACTS=OFF, it is currently testing these stale tracked blobs, while a tag build will rebuild and ship different artifacts—the mismatch this PR description says it prevents. Please commit those six rebuilt WASMs before merging.

/// With kv::cached_global the write happens only when an action actually mutated the global, so a
/// query issues none.
BOOST_FIXTURE_TEST_CASE( view_actions_execute_in_readonly_transaction, sysio_emissions_tester ) try {
auto trace = push_system_action_readonly( "viewemitcfg"_n, mvo() );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Exercise the missing-row path

sysio_emissions_tester has already produced post-deploy blocks, so onblock has dirtied and persisted global before this call. The earlier constructor that used set(defaults) would therefore pass this test whenever the row exists, leaving the seed_if_absent() regression from the second commit uncovered.

Please add a fresh-deploy/no-post-deploy-block case that runs the read-only view while global is absent, verifies the query does not create the row, then runs a real mutator and confirms the seeded defaults plus mutation persist.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You were right, and I confirmed it by measurement rather than argument: reverting the constructor to the pre-fix seeding leaves both existing read-only cases passing while the new one fails.

sysio_fresh_deploy_tester deploys sysio.system and stops, so no block has run against that code and the row has never been written. The case then walks the path you described -- a succeeding action that does not touch the global must not create the row, the read-only view runs with it still missing and must not create it either, and the first genuine mutation stores defaults plus that change. For "the seeded defaults are what the contract reads" I leaned on setram's increase-only check: a decrease is rejected at the 64GiB default, which a zero-initialized cache would accept.

One thing that shaped it -- base_tester::push_action calls produce_block() once the transaction lands, and the next onblock stamps last_pervote_bucket_fill and creates the row itself. Without a no-block push path the case silently measured nothing.

Fixed in ec2db0d. Note the constructor this covers is gone as of fbed694: seeding moved onto the singleton type in Wire-Network/wire-cdt#107.

@heifner

heifner commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

e2e gate: GREEN — all 14 flows

Run 31842724389 · head 082487e055 · Release

Branch combination — only wire-sysio was overridden; every other repo floated to its manifest default (wire-ethereum / wire-solananext, the rest → master). This branch needed no companion PR: it changes sysio.system sources and the wasm, but no ABI, so SysioContractTypes is unaffected.

ref
wire-sysio fix/system-readonly-view-actions
everything else manifest default
✅ batch-operator-slashing (426s)     ✅ swap-from-wire (723s)
✅ batch-operator-termination (1038s) ✅ swap-non-native-tokens (2205s)
✅ emissions-soak (2177s)             ✅ swap-private-reserves (1952s)
✅ node-owner-nft (558s)              ✅ swap-to-wire (688s)
✅ operator-collateral-deposit (823s) ✅ swap-variance-revert (468s)
✅ reserve-lifecycle (917s)           ✅ swap-with-underwriting (1037s)
✅ underwriter-slashing (931s)        ✅ yield-distribution (731s)

Worth noting for this PR specifically: emissions-soak (2177s) and the five swap flows exercise payepoch and the read-only view actions across many epoch boundaries, which is the surface the deferred-singleton-write change touches.

Only sysio.system and sysio.msgch were rebuilt on this branch; a rebase dropped the rest. Rebuilding this head against the wire-cdt#106 headers reproduces those two byte for byte and produces different binaries for chalg, dclaim, epoch, opreg, reserv and uwrit, each carrying the kv::global stored-value-size guard the committed one lacks.

PR builds run with BUILD_SYSTEM_CONTRACTS=OFF and exercise the committed binaries, while a release tag rebuilds from source, so leaving those six stale is exactly the split this branch set out to close: CI testing one artifact and a tag shipping another, from identical source.

A sweep of all seventeen contracts confirms these six are the complete set. Every other contract rebuilds byte-identical, and none of them reference kv::global. No ABI changed.
The constructor seeded the global with seed_if_absent(get_default_parameters()). Both halves cost something on every action: seeding reads the row, and get_default_parameters() is an ordinary argument, so its get_blockchain_parameters host call ran whether or not the action went on to touch the global. Only five functions do -- onblock, update_ranked_producers, setram, setparams and payepoch -- so roughly forty actions were paying for a singleton they never read.

The defaults now ride on the type (Wire-Network/wire-cdt#107), which leaves the constructor body empty and defers everything to first use: an action that never touches the global neither reads the row nor computes the defaults. get_default_parameters moves out of system_contract as a free default_global_state, because the alias names it as a template argument and has to exist before the class is declared.

sysio.system grows 4380 bytes, and that is the price of materializing lazily rather than in the constructor, not of the defaults themselves. With a trivial default it is 4196, and routing every access through a single materialization point recovers only 367. Seeding eagerly let the optimizer prove the cache loaded once and strip the checks everywhere downstream, which is precisely what deferring gives up.

One behavioural note that comes with the type: on a handle carrying defaults, exists() reports that a value is available rather than that a row is stored. Nothing here asks that question, and the idiom it affects -- create the row if exists() is false -- is the one this change exists to retire.
view_actions_execute_in_readonly_transaction runs against a fixture that has produced post-deploy blocks, so onblock has already persisted the global. With the row present, every way of materializing defaults looks alike and the read-only query passes either way, which left the absent-row path uncovered: reverting the constructor to the pre-fix "create it if exists() is false" seeding leaves both existing read-only cases passing while the new one fails.

sysio_fresh_deploy_tester deploys sysio.system and stops, so no block has been produced against that code and the row has never been written. The case then walks the path the change exists for -- an action that succeeds without touching the global must not bring the row into existence, a read-only query must run with the row still missing and must not create it either, the values served must be the declared defaults rather than a zero-initialized struct (a max_ram_size decrease is rejected at the 64GiB default), and the first genuine mutation must store the defaults plus that change.

Ordering is load-bearing. Each action constructs its own system_contract and flushes on return, so an action that persisted the row early would mask everything after it. Every push therefore goes through push_system_action_no_block: base_tester::push_action produces a block once the transaction lands, and the next onblock stamps last_pervote_bucket_fill and creates the row itself.
@heifner

heifner commented Aug 15, 2026

Copy link
Copy Markdown
Contributor Author

Confirmed and fixed in 745df70. Rebuilding this head against the wire-cdt#106 headers reproduces sysio.system and sysio.msgch byte for byte and produces different binaries for the six you named, each carrying the guard the committed one lacks -- a rebase had dropped them.

I also swept all seventeen contracts to check for a seventh: there isn't one. Every other contract rebuilds byte-identical and none reference kv::global. No ABI changed.

Two things worth flagging since they weren't in your review. Chasing this surfaced that the constructor was charging every action for the global -- it read the row and ran get_blockchain_parameters whether or not the action touched it, and only five functions do. That moves onto the singleton type via Wire-Network/wire-cdt#107 (fbed694), which costs sysio.system 4380 bytes; the description explains why that is the price of deferring rather than of the defaults. The description is rewritten rather than appended to, since it claimed eight binaries were rebuilt when six were not.

@heifner

heifner commented Aug 15, 2026

Copy link
Copy Markdown
Contributor Author

e2e gate: GREEN — all 14 flows

Run 31885409167 · wire-sysio @ ec2db0dc4c · Release

ref
wire-sysio fix/system-readonly-view-actions @ ec2db0dc4c
wire-cdt feature/kv-cached-lazy-defaults (wire-cdt#107)
everything else manifest default
✅ batch-operator-slashing (460s)      ✅ swap-non-native-tokens (2265s)
✅ batch-operator-termination (953s)   ✅ swap-private-reserves (1815s)
✅ emissions-soak (2164s)              ✅ swap-to-wire (688s)
✅ node-owner-nft (590s)               ✅ swap-variance-revert (482s)
✅ operator-collateral-deposit (806s)  ✅ swap-with-underwriting (1049s)
✅ reserve-lifecycle (947s)            ✅ underwriter-slashing (918s)
✅ swap-from-wire (675s)               ✅ yield-distribution (733s)

This branch needs wire-cdt#107 — the gate cannot pass without it

Worth stating explicitly, because nothing on this PR declares it. fbed6942f0 introduces

using global_state_singleton =
   sysio::kv::cached_global< "global"_n, sysio_global_state, &default_global_state >;

and the three-argument cached_global only exists on wire-cdt#107. Against wire-cdt master the alias still takes two parameters, so sysio.system does not compile:

sysio.system.hpp:245:18: error: too many template arguments for alias template 'cached_global'
sysio.system.hpp:267:10: error: unknown type name 'global_state_singleton'

A first run without the CDT override (31884995426) died there in 7 minutes, before reaching any flow. wire-cdt#107 has to merge before this one, and any gate run in between needs BRANCH_WIRE_CDT=feature/kv-cached-lazy-defaults.

Note also that this result pins wire-cdt#107 at its current head. Two review findings are open against it — remove() recording an erase debt for a row that was never stored, and modify_or_create's explicit default being silently discarded on a defaulted handle. Neither is reachable from sysio.system, which uses only get() and modify() on _global, so the flows above are unaffected; but if the fix changes cached_value's layout, this run describes the pre-fix header.

…-view-actions

Change-Id: I6ddae95caa5464de56bdd344c92f584c03a8cefb
No contract source changed. The binary moves because wire-cdt#107 merged
(adad4b29) and cached_value is a header-only template embedded in
system_contract: the committed artifact was built in 745df70 against that
PR's FIRST commit, 878b88f4, and five further commits reworked kv_cached.hpp
before it landed -- remove() keying on stored-row presence, removal tracked
apart from the pending erase, the read/presence state folded into one enum,
flush() refused mid-mutation with the default-taking calls constrained away, and
the mutation mark moved onto the stack. Compiling each sysio.system translation
unit against both CDT states, five of seven differ, so what is committed is not
reproducible from this source and the CDT that will build it -- and CI deploys
the checked-in artifact.

Only sysio.system is affected. Rebuilding all seventeen contracts against the
merged CDT reproduces sixteen byte-for-byte and moves this one alone
(177,928 -> 178,263), which is the expected result given kv_cached.hpp is the
only functional change between the two CDT states. Every ABI is unchanged, so
nothing downstream needs regenerating.

contracts_unit_test --sys-vm: 636 test cases, no errors, run against this exact
binary.

Change-Id: I6e3fa131abf13275ad70b765f272a8743e014d6c
@heifner

heifner commented Aug 15, 2026

Copy link
Copy Markdown
Contributor Author

e2e gate: GREEN — all 14 flows

Run 31910302404 · wire-sysio @ 01a6ad1881 · Release

No BRANCH_WIRE_CDT override — wire-cdt#107 is merged (adad4b29), so the manifest default carries it.

✅ batch-operator-slashing (424s)      ✅ swap-non-native-tokens (2219s)
✅ batch-operator-termination (1006s)  ✅ swap-private-reserves (1967s)
✅ emissions-soak (2190s)              ✅ swap-to-wire (688s)
✅ node-owner-nft (554s)               ✅ swap-variance-revert (480s)
✅ operator-collateral-deposit (777s)  ✅ swap-with-underwriting (1082s)
✅ reserve-lifecycle (932s)            ✅ underwriter-slashing (918s)
✅ swap-from-wire (690s)               ✅ yield-distribution (735s)

Includes 01a6ad1881, which rebuilds sysio.system.wasm against the merged CDT — the previously committed binary was built against wire-cdt#107's first commit, before five further kv_cached.hpp changes landed. Rebuilding all seventeen contracts reproduced sixteen byte-for-byte and moved only this one; ABIs unchanged; contracts_unit_test --sys-vm 636 cases, no errors.

@heifner
heifner requested a review from huangminghuang August 16, 2026 00:40

@huangminghuang huangminghuang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Rebuild sysio.chalg and sysio.uwrit against the final merged CDT headers

On this exact head, a clean 17-contract sweep against wire-cdt master / PR #107 merge adad4b29 reproduces 15 committed WASMs byte-for-byte, including the new sysio.system.wasm, but two still differ:

  • sysio.chalg.wasm: committed 67,555 bytes / b0089c83…; rebuilt 67,565 / d36d39ed…
  • sysio.uwrit.wasm: committed 157,214 bytes / 2e3bc8c9…; rebuilt 157,236 / 2889580d…

A second fresh configure/build reproduced both rebuilt hashes, and every ABI is unchanged. Since PR CI consumes the checked-in blobs while tag builds compile from source, these two still leave CI and release artifacts split. Please sync current master (the PR presently has binary conflicts), run the full contract rebuild from the combined tree, commit the resulting artifacts, and correct the claim that sixteen binaries reproduce.

…-view-actions

Only the four contract wasms conflicted; every source hunk merged on its own.
sysio.epoch, sysio.opreg, sysio.reserv and sysio.system were rebuilt from the
merged sources rather than resolved to either side: master's copies of the first
three predate wire-cdt#106's kv::global guard, and sysio.system combines this
branch's cached_global handle with master's claimable-payout emissions work.

Sweeping all seventeen contracts against wire-cdt master (adad4b290) reproduces
the other thirteen byte for byte, chalg and uwrit included. No ABI changed.
@heifner

heifner commented Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

Merged master and rebuilt from the combined tree. Four wasms conflicted -- epoch, opreg, reserv and system -- while every source hunk merged on its own. Rebuilt sizes: epoch 75,691, opreg 88,451, reserv 84,138, system 182,477. opreg comes out at exactly master's committed bytes, since master rebuilt it independently; epoch and reserv move because master's copies still predate the wire-cdt#106 guard. No ABI changed, and the description's sweep claim is restated for the merged tree.

On chalg and uwrit I cannot reproduce your result, and I went to a from-scratch toolchain to be sure of that rather than argue from an incremental build tree.

I deleted the 27 GB CDT build directory, moved the worktree to wire-cdt master adad4b290, and rebuilt the whole toolchain from scratch. The rebuilt cdt-cpp, cdt-ld and cdt-codegen came out byte-identical to the incremental tree they replaced, so that tree was not stale. Against that clean toolchain, a seventeen-contract sweep of the merged tree reproduces every system contract byte for byte -- chalg at 67,555 / b0089c83... and uwrit at 157,214 / 2e3bc8c9..., which is what is committed, not the 67,565 / d36d39ed... and 157,236 / 2889580d... you measured.

uwrit is the stronger of the two cases independently of the sweep: three separately produced builds agree on 157,214 -- master's, this branch's, and this one. Both branches reached those bytes on their own, which is why uwrit.wasm did not conflict during the merge at all.

So the +10 and +22 track the environment the CDT itself was compiled in rather than the CDT commit or the contract source. This tree has seen that effect before at far larger magnitude on identical source, and unlike a genuinely stale artifact it does not leave CI and a tag build compiled from different source. That said, if your toolchain is the one a release tag will use, I would rather match it than argue it: tell me which wire-cdt build produced yours -- local, or the CI container -- and I will rebuild against that instead.

One correction in the other direction, since it is the same kind of claim. The description says test_contracts/sendinline "differs across rebuilds at an identical size". Two independent from-scratch builds of it in separate worktrees agreed exactly (4de77cc9...), and both differ from the committed snapshot at the same 4,346 bytes. So what is demonstrated is that the committed blob predates the current toolchain, not that the output is unstable between rebuilds. It stays untouched here either way.

@heifner
heifner requested a review from huangminghuang August 17, 2026 16:29

@huangminghuang huangminghuang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed the merged head and found no remaining issues. I rebuilt the top-level contracts_project using the official Linux CDT CI package from wire-cdt commit adad4b29; all 17 tracked production WASMs and all 18 ABIs reproduce byte-for-byte. In particular, chalg and uwrit match, so I am withdrawing my earlier artifact-reproducibility finding—the prior mismatch was caused by combining the final headers with a different compiler binary. The ASan and assertions configurations have passed; the remaining CI matrix jobs are still in progress.

@heifner
heifner merged commit db732eb into master Aug 18, 2026
28 of 36 checks passed
@heifner
heifner deleted the fix/system-readonly-view-actions branch August 18, 2026 12:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants