Skip to content

Optimize preimage reading in provers#4337

Open
pmikolajczyk41 wants to merge 15 commits intomasterfrom
pmikolajczyk/nit-4372-preimage-read
Open

Optimize preimage reading in provers#4337
pmikolajczyk41 wants to merge 15 commits intomasterfrom
pmikolajczyk/nit-4372-preimage-read

Conversation

@pmikolajczyk41
Copy link
Copy Markdown
Member

@pmikolajczyk41 pmikolajczyk41 commented Feb 5, 2026

Changes

  1. Introduce and use wavmio.readPreimage (described in detail below).
  2. Refactor code to reuse the core logic for both the old and the new API.
  3. Deprecate the old wavmio.resolveTypedPreimage function. However, both provers still support this API in order to handle old replay.wasm binaries. (The same way as we did with wavmio.resolvePreImage).

Previous mechanism for reading preimages

The old wavmio module linked to replay.wasm provided a method wavmio__resolveTypedPreimage, which reads (up to) 32 bytes of a queried preimage at a given offset. Given that replay.wasm cares only about reading preimages in full, this was suboptimal, as most of the preimages are between 80 and 500 bytes (with some exceptions). For every preimage longer than 32 bytes we had to loop multiple times over it, each iteration calling the external module function, that was fetching full preimage.

In particular, in the SP1 execution environment, this brought a significant performance penalty.

Proposed mechanism

Ideally, we'd add a new function wavmio__readPreimage( preimageType uint32, hash unsafe.Pointer, output unsafe.Pointer ) (so without the offset uint32 argument when compared to the old wavmio__resolveTypedPreimage). The problem is that the caller has to allocate enough memory for the output (the image). That's why we do as follows.

The function has the following signature:

func readPreimage(ty uint32, hash unsafe.Pointer, output unsafe.Pointer, preimageOffset uint32, allocatedOutputSpace uint32) uint32
  • ty is the preimage type (as before)
  • hash is the pointer to the 32 bytes of preimage hash (as before)
  • output is the pointer to the buffer for either the preimage prefix of suffix (depending on the following arguments)
  • preimageOffset is the offset in the preimage that we want to start reading from; this will always be either 0 or 512 (a constant)
  • allocatedOutputSpace is the free allocated space at the output pointer

The function always returns the length of the preimage (not the number of bytes read!).

The caller allocates an initial memory buffer of 512 bytes (enough for vast majority of preimages). Then it does the first call to readPreimage:

  • if the preimage length is less than 512, it means that the space provided was sufficient, and thus the whole preimage has been read
  • otherwise, caller allocates more memory (now they know exactly how much space is needed), and does the second call (with updated output, preimageOffset and allocatedOutputSpace arguments) in order to read the preimage suffix.

Thus we always have at most 2 calls to the wavmio module.


closes NIT-4372
Co-authored-by: @wakabat

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 5, 2026

❌ 3 Tests Failed:

Tests completed Failed Passed Skipped
4227 3 4224 0
View the top 3 failed tests by shortest run time
TestNitroNodeVersionAlerter
Stack Traces | 1.100s run time
... [CONTENT TRUNCATED: Keeping last 20 lines]
INFO [02-09|14:56:16.056] Starting work on payload                 id=0x038e245985fc293f
INFO [02-09|14:56:16.057] Submitted contract creation              hash=0xa0075bf4e0d7a1b8ff52d6e06df344499096c56b9409aae44d8b60dce8bd3890 from=0x57Ff0F473737a1c161bfF9efDF016F7991585088 nonce=21  contract=0x5E6d7135B0a5F12bF935A83B7d5b5e4a8BFf112d value=0
INFO [02-09|14:56:16.057] Creating rollup
INFO [02-09|14:56:16.058] Starting work on payload                 id=0x031ce44e5487ba70
INFO [02-09|14:56:16.059] Updated payload                          id=0x038e245985fc293f number=35  hash=7720af..839270 txs=1   withdrawals=0 gas=142,106    fees=7.1053e-06     root=8f9668..1a072b elapsed=2.652ms
INFO [02-09|14:56:16.059] Updated payload                          id=0x031ce44e5487ba70 number=21  hash=d1ef71..5de4a8 txs=1   withdrawals=0 gas=3,088,290  fees=3.08829e-06    root=9bb203..627909 elapsed=1.024ms
INFO [02-09|14:56:16.061] Stopping work on payload                 id=0x031ce44e5487ba70 reason=delivery
INFO [02-09|14:56:16.062] Imported new potential chain segment     number=21  hash=d1ef71..5de4a8 blocks=1  txs=1   mgas=3.088  elapsed=2.683ms      mgasps=1150.882 triediffs=76.96KiB   triedirty=0.00B
INFO [02-09|14:56:16.063] Chain head was updated                   number=21  hash=d1ef71..5de4a8 root=9bb203..627909 elapsed="339.083µs"
INFO [02-09|14:56:16.063] Stopping work on payload                 id=0x038e245985fc293f reason=delivery
INFO [02-09|14:56:16.066] Imported new potential chain segment     number=35  hash=7720af..839270 blocks=1  txs=1   mgas=0.142  elapsed=6.438ms      mgasps=22.070   triediffs=183.95KiB  triedirty=0.00B
INFO [02-09|14:56:16.066] Chain head was updated                   number=35  hash=7720af..839270 root=8f9668..1a072b elapsed="111.298µs"
INFO [02-09|14:56:16.069] Log index head rendering in progress     firstblock=0 lastblock=267 processed=12 remaining=0 elapsed=1.226s
INFO [02-09|14:56:16.069] Log index head rendering finished        firstblock=0 lastblock=267 processed=12 elapsed=1.226s
INFO [02-09|14:56:16.084] Starting peer-to-peer node               instance=test-stack-name/linux-amd64/go1.25.6
WARN [02-09|14:56:16.084] P2P server will be useless, neither dialing nor listening
WARN [02-09|14:56:16.104] empty sequencer message
WARN [02-09|14:56:16.104] reading virtual delayed message segment  delayedMessagesRead=0 afterDelayedMessages=1
WARN [02-09|14:56:16.115] Getting file info                        dir= error="stat : no such file or directory"
--- FAIL: TestNitroNodeVersionAlerter (1.10s)
TestVersion30
Stack Traces | 8.260s run time
... [CONTENT TRUNCATED: Keeping last 20 lines]
        github.com/offchainlabs/nitro/system_tests.TestVersion30(0xc09972fa40?)
        	/home/runner/work/nitro/nitro/system_tests/precompile_inclusion_test.go:67 +0x798
        testing.tRunner(0xc09972fa40, 0x3d19d78)
        	/opt/hostedtoolcache/go/1.25.6/x64/src/testing/testing.go:1934 +0xea
        created by testing.(*T).Run in goroutine 1
        	/opt/hostedtoolcache/go/1.25.6/x64/src/testing/testing.go:1997 +0x465
        
    precompile_inclusion_test.go:94: �[31;1m [] execution aborted (timeout = 5s) �[0;0m
INFO [02-09|14:51:19.021] Submitted transaction                    hash=0x775c87dfcfb674559482afeb19a218ecea6f60a572fef1ebcb53a162e87ba6a2 from=0x26E554a8acF9003b83495c7f45F06edCB803d4e3 nonce=173  recipient=0x0C709F340F0BB2e361229e345B7e26999d0969Ab value=1
INFO [02-09|14:51:19.024] Writing cached state to disk             block=1   hash=cb0527..4bb029 root=c02835..9ffaab
INFO [02-09|14:51:19.024] Persisted trie from memory database      nodes=20   flushnodes=0 size=3.26KiB   flushsize=0.00B time="112.009µs"  flushtime=0s gcnodes=0 gcsize=0.00B gctime="1.814µs"  livenodes=0    livesize=0.00B
INFO [02-09|14:51:19.024] Writing cached state to disk             block=1   hash=cb0527..4bb029 root=c02835..9ffaab
INFO [02-09|14:51:19.024] Persisted trie from memory database      nodes=0    flushnodes=0 size=0.00B     flushsize=0.00B time="1.593µs"    flushtime=0s gcnodes=0 gcsize=0.00B gctime=0s         livenodes=0    livesize=0.00B
INFO [02-09|14:51:19.024] Writing snapshot state to disk           root=92585b..f227c9
INFO [02-09|14:51:19.024] Persisted trie from memory database      nodes=0    flushnodes=0 size=0.00B     flushsize=0.00B time=832ns        flushtime=0s gcnodes=0 gcsize=0.00B gctime=0s         livenodes=0    livesize=0.00B
INFO [02-09|14:51:19.024] Imported new potential chain segment     number=1462 hash=c17d01..26f875 blocks=1   txs=1   mgas=0.143  elapsed=6.817ms      mgasps=20.954   triediffs=731.95KiB  triedirty=664.97KiB
INFO [02-09|14:51:19.025] Blockchain stopped
INFO [02-09|14:51:19.025] Chain head was updated                   number=1462 hash=c17d01..26f875 root=8df9e7..edf7f9 elapsed="151.223µs"
INFO [02-09|14:51:19.025] Stopping work on payload                 id=0x03168dd0bdd75a39 reason=delivery
--- FAIL: TestVersion30 (8.26s)
TestVersion40
Stack Traces | 8.900s run time
... [CONTENT TRUNCATED: Keeping last 20 lines]
INFO [02-09|14:51:10.763] Chain head was updated                   number=615  hash=2383d2..9855af root=327164..706344 elapsed="85.219µs"
    precompile_inclusion_test.go:94: goroutine 618392 [running]:
        runtime/debug.Stack()
        	/opt/hostedtoolcache/go/1.25.6/x64/src/runtime/debug/stack.go:26 +0x5e
        github.com/offchainlabs/nitro/util/testhelpers.RequireImpl({0x40e13d0, 0xc09972fc00}, {0x409dd60, 0xc13d876de0}, {0x0, 0x0, 0x0})
        	/home/runner/work/nitro/nitro/util/testhelpers/testhelpers.go:29 +0x55
        github.com/offchainlabs/nitro/system_tests.Require(0xc09972fc00, {0x409dd60, 0xc13d876de0}, {0x0, 0x0, 0x0})
        	/home/runner/work/nitro/nitro/system_tests/common_test.go:2065 +0x5d
        github.com/offchainlabs/nitro/system_tests.testPrecompiles(0xc09972fc00, 0x28, {0xc0a8491df8, 0x5, 0x39?})
        	/home/runner/work/nitro/nitro/system_tests/precompile_inclusion_test.go:94 +0x371
        github.com/offchainlabs/nitro/system_tests.TestVersion40(0xc09972fc00?)
        	/home/runner/work/nitro/nitro/system_tests/precompile_inclusion_test.go:71 +0x64b
        testing.tRunner(0xc09972fc00, 0x3d19d80)
        	/opt/hostedtoolcache/go/1.25.6/x64/src/testing/testing.go:1934 +0xea
        created by testing.(*T).Run in goroutine 1
        	/opt/hostedtoolcache/go/1.25.6/x64/src/testing/testing.go:1997 +0x465
        
    precompile_inclusion_test.go:94: �[31;1m [] execution aborted (timeout = 5s) �[0;0m
WARN [02-09|14:51:19.023] Served eth_call                          reqid=12  duration=8.210338379s  err="execution aborted (timeout = 5s)"
--- FAIL: TestVersion40 (8.90s)

📣 Thoughts on this report? Let Codecov know! | Powered by Codecov

@pmikolajczyk41 pmikolajczyk41 marked this pull request as ready for review February 5, 2026 16:54
@codecov
Copy link
Copy Markdown

codecov Bot commented Feb 5, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 32.92%. Comparing base (890f0e2) to head (3f77108).
⚠️ Report is 265 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #4337      +/-   ##
==========================================
- Coverage   33.98%   32.92%   -1.07%     
==========================================
  Files         488      488              
  Lines       57783    57783              
==========================================
- Hits        19640    19025     -615     
- Misses      34672    35430     +758     
+ Partials     3471     3328     -143     

bragaigor
bragaigor previously approved these changes Feb 9, 2026
Copy link
Copy Markdown
Contributor

@bragaigor bragaigor left a comment

Choose a reason for hiding this comment

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

just nitpicks. LGTM

Comment thread crates/wasm-libraries/host-io/lib.rs Outdated
Comment thread crates/wasm-libraries/host-io/lib.rs
@bragaigor bragaigor assigned pmikolajczyk41 and unassigned bragaigor Feb 9, 2026
Copy link
Copy Markdown
Contributor

@bragaigor bragaigor left a comment

Choose a reason for hiding this comment

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

LGTM

Copy link
Copy Markdown
Contributor

@tsahee tsahee left a comment

Choose a reason for hiding this comment

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

I still didn't do a deep look at the host-io implementation, which I will need to do.
The general design is great!
Just one comment on the go side in the meantime.
We'll only push this after arbos-60.

Comment thread wavmio/higher.go
preimage := make([]byte, INITIAL_PREIMAGE_ALLOCATION)

// 1. Read the preimage prefix (up to INITIAL_PREIMAGE_ALLOCATION bytes)
preimageLen := readPreimage(uint32(ty), hashUnsafe, unsafe.Pointer(&preimage[0]), 0, INITIAL_PREIMAGE_ALLOCATION)
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.

this should be a for loop that can handle any preimage size.
Blobs are 128Kib, and certificate-DA could be even larger then that.

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.

You can have variation of ResolveTypedPreimage that accepts size per read to use
In /Users/tsahi/src/nitro/cmd/replay/main.go:

  • GetBlobs will always read an image of the same size (1 full blob)
  • getBlockHeaderByHash reads one block header, much smaller
  • (db PreimageDb) Get (in db.go) reads from the triedb and I think that for that the 512 constant works well

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

before I handle the variation with size hint, I wanted to clarify this:

this should be a for loop that can handle any preimage size

I claim that the current code will handle any preimage size with at most 2 calls to readPreimage, so no need for any loop.

@tsahee tsahee added the after-next-version This PR shouldn't be merged until after the next version is released label Feb 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

after-next-version This PR shouldn't be merged until after the next version is released

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants