test(stateless): pin the fail-closed handling of incomplete input - #13788
LukaszRozmej wants to merge 1 commit into
Conversation
Three properties the stateless spec states but nothing here asserted: an empty transaction entry in the payload is rejected, a schema id the decoder does not support reports the zero sentinel rather than a partial result, and a BLOCKHASH ancestor missing from the witness fails the block instead of resolving to zero (the spec reaches the same end by indexing its block-hash list out of range). The transaction mutation goes through the SSZ view on purpose: the payload caches the wrapped transaction array, so a write to the inner byte[][] does not survive the re-encode and the test would pass without exercising anything.
|
Claude finished @LukaszRozmej's task in 1m 28s —— View job Review: PR #13788 (tests only)
Verdict: not mergeable yet (1 Medium open). Counts: 0 Critical, 0 High, 1 Medium, 1 Low. I didn't run the tests; this is a read-through only. Medium: the empty-transaction test doesn't pin what it says (inline)
Low: the BLOCKHASH test checks
What looks fine
|
| body.CopyTo(modified, sizeof(ushort)); | ||
|
|
||
| StatelessValidationResult.Decode(StatelessExecutor.Execute(modified), out StatelessValidationResult result); | ||
| Assert.That(result.IsSuccess, Is.False); |
There was a problem hiding this comment.
Medium: this test can pass even if empty entries stop being rejected. Swapping Transactions[0] changes the reconstructed tx root, but BlockHash stays the same. So if the decoder ever skipped or accepted empty entries, StatelessExecutor.Execute would still fail: either the transactions.Length == publicKeys.Length check or HeaderValidator.ValidateHash would catch it. IsSuccess == false holds either way. That makes this the same case as the existing "transactions" mutation in Encoded_execution_checks_reconstructed_header_hash, and the empty-entry rule isn't actually pinned.
The simplest way to pin it is to assert on the decode step, the same way the first test asserts ValidateHash. NewPayloadRequest.ToBlock throws InvalidOperationException when TryGetBlock returns an error:
Assert.That(() => input.NewPayloadRequest.ToBlock(requestsEnabled: true), Throws.InvalidOperationException);Another option is to recompute BlockHash so the hash check can't be what rejects the block.
Minor: lines 101–104 copy the re-encode lines from Mutate<TPayload> (79–83). A small Reencode<TPayload>(byte[] encoded, StatelessInput<TPayload> input) helper would remove the duplication.
| StatelessBlockTree blockTree = new([parent]); | ||
| BlockhashProvider provider = new(blockTree, Substitute.For<IWorldState>(), NullLogManager.Instance); | ||
|
|
||
| using (Assert.EnterMultipleScope()) | ||
| { | ||
| Assert.That(provider.GetBlockhash(current, 100, Cancun.Instance), Is.EqualTo(parent.Hash)); | ||
| Assert.That(() => provider.GetBlockhash(current, 99, Cancun.Instance), Throws.Exception); |
There was a problem hiding this comment.
Low: this tests BlockhashProvider's default, not the stateless wiring.
- The provider here is built by hand, copying
StatelessBlockProcessingEnv.cs:72. If the env ever passes a permissiveIUnresolvedBlockhashPolicy, this test still passes. The name says "fails the block", but the test only checks that a direct call throws. Throws.Exceptionaccepts any exception, including an NRE from the substitute.Throws.TypeOf<InvalidDataException>()(fromThrowingUnresolvedBlockhashPolicy) would be more exact.- The throw only happens because the stateless path calls
BlockProcessor.ProcessOnedirectly and never callsPrefetch. ThroughBranchProcessor,StatelessBlockTree.Prefetchfills_hashesand leaves missing ancestors asnull.GetBlockhashwould then returnhashes[depth - 1](null, which BLOCKHASH treats as zero) without consulting the policy. That's outside this diff, but it's the scenario this test says it guards against, so it may be worth noting in the remarks or covering.
Changes
Types of changes
What types of changes does your code introduce?
Testing
Requires testing
If yes, did you write tests?
Notes on testing
Nethermind.Consensus.Test390/390. All three properties already hold on master; these tests pin them.Documentation
Requires documentation update
Requires explanation in Release Notes
Remarks
Worth knowing for anyone writing payload mutation tests:
SszExecutionPayload.Transactionscaches the wrapped array on first access, so mutating the innerbyte[][]throughAsExecutionPayload()does not survive the re-encode. The first version of the empty-transaction test did exactly that and passed while exercising nothing; it now mutates through the SSZ view, and a comment says why.The BLOCKHASH case only applies before EIP-2935, since from Prague on the hashes come from state and are witness-bound; the spec reaches the same rejection by indexing its block-hash list out of range.