Skip to content

Validator: compare computed end state against expected in JIT and Arbitrator#4563

Open
pmikolajczyk41 wants to merge 12 commits intomasterfrom
pmikolajczyk/nit-4248-expected-validation-result
Open

Validator: compare computed end state against expected in JIT and Arbitrator#4563
pmikolajczyk41 wants to merge 12 commits intomasterfrom
pmikolajczyk/nit-4248-expected-validation-result

Conversation

@pmikolajczyk41
Copy link
Copy Markdown
Member

Adds an optional ExpectedEndState field to InputJSON. When present, both the JIT and Arbitrator prover binaries compare the computed GoGlobalState against it after execution and exit non-zero on mismatch.

ValidationInputsAt now populates this field from entry.End, so block input JSON files exported via the debug API carry the ground-truth end state. CI is updated to pass a testdata file containing ExpectedEndState and assert that both binaries print "Computed state matches the expected one".

The field is intentionally not added to the Rust ValidationRequest struct — that struct is used in contexts (e.g. the validation server, machine construction) where the field is meaningless and would only cause confusion. Instead, each binary performs a lightweight partial parse of the JSON at startup to extract ExpectedEndState before the normal machine-initialization path, keeping the deeper layers unmodified.

This catches divergence between the Go execution path and the Rust/WASM execution path — the core correctness property of the validator.


closes NIT-4248

return server_api.InputJSON{}, err
}
return *server_api.ValidationInputToJson(input), nil
jason := server_api.ValidationInputToJson(input)
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.

to avoid shadowing global package name

@pmikolajczyk41 pmikolajczyk41 marked this pull request as ready for review March 26, 2026 12:33
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 26, 2026

Codecov Report

❌ Patch coverage is 0% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 34.29%. Comparing base (3dd8e2b) to head (9717c70).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #4563      +/-   ##
==========================================
- Coverage   36.05%   34.29%   -1.76%     
==========================================
  Files         498      498              
  Lines       58976    58978       +2     
==========================================
- Hits        21264    20228    -1036     
- Misses      33889    35176    +1287     
+ Partials     3823     3574     -249     

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 26, 2026

❌ 12 Tests Failed:

Tests completed Failed Passed Skipped
4670 12 4658 0
View the top 3 failed tests by shortest run time
TestAliasingFlaky
Stack Traces | -0.000s run time
=== RUN   TestAliasingFlaky
=== PAUSE TestAliasingFlaky
=== CONT  TestAliasingFlaky
    common_test.go:768: BuildL1 deployConfig: DeployBold=true, DeployReferenceDAContracts=false
INFO [03-26|16:58:10.920] Submitted contract creation              hash=0x399184f9977f0f6ea1e1f807698efdd88b4fcbb85106214486ecbfdf28b60683 from=0x57Ff0F473737a1c161bfF9efDF016F7991585088 nonce=0  contract=0xA46C59ce2FCaF445F96f66F0411e06A94D34BF45 value=0
WARN [03-26|16:58:10.920] Served eth_getTransactionReceipt         reqid=7 duration="23.044µs" err="transaction indexing is in progress"  errdata="\"transaction indexing is in progress\""
INFO [03-26|16:58:10.920] Starting work on payload                 id=0x03788da53c109bc6
INFO [03-26|16:58:10.921] Updated payload                          id=0x03788da53c109bc6                      number=1 hash=a9b4ee..742391 txs=1  withdrawals=0 gas=854,353 fees=8.54353e-07 root=50b0c4..5d74a7 elapsed="352.674µs"
INFO [03-26|16:58:10.921] Stopping work on payload                 id=0x03788da53c109bc6                      reason=delivery
INFO [03-26|16:58:10.921] Starting peer-to-peer node               instance=test-stack-name/linux-amd64/go1.25.8
WARN [03-26|16:58:10.921] P2P server will be useless, neither dialing nor listening
INFO [03-26|16:58:10.921] Imported new potential chain segment     number=1 hash=a9b4ee..742391 blocks=1  txs=1  mgas=0.854 elapsed="433.693µs" mgasps=1969.949 triediffs=3.08KiB triedirty=0.00B
INFO [03-26|16:58:10.921] Chain head was updated                   number=1 hash=a9b4ee..742391 root=50b0c4..5d74a7 elapsed="125.936µs"
INFO [03-26|16:58:10.921] Indexed transactions                     blocks=2  txs=1  tail=0 elapsed="78.047µs"
INFO [03-26|16:58:10.922] New local node record                    seq=1,774,544,290,922 id=600e389b6885c217                        ip=127.0.0.1 udp=0 tcp=0
INFO [03-26|16:58:10.922] Started P2P networking                   self=enode://da4330b5b047eab445cd151b928e9ffc9a56ddd08c24f01c0acfc02543e594809608e9d2ac1d90d2c031258f2d9141fd6a3a8dc2a8bd56c17ba56490e0cc8a8d@127.0.0.1:0
INFO [03-26|16:58:10.922] Started log indexer
WARN [03-26|16:58:10.922] Getting file info                        dir= error="stat : no such file or directory"
TestPruningDBSizeReduction
Stack Traces | 0.000s run time
=== RUN   TestPruningDBSizeReduction
INFO [03-26|16:57:08.470] Started P2P networking                   self=enode://411dad4a160ee2152203eea5dc5e0132c6a668406e2aa66caa75ed2b642c17b50e31bd04ecb45aa4b7b7632283e1a816aad1b7e4ccd2b02cebb149b124d7735a@127.0.0.1:0
--- FAIL: TestPruningDBSizeReduction (0.00s)
TestBatchPosterL1SurplusMatchesBatchGasFlaky
Stack Traces | 0.530s run time
... [CONTENT TRUNCATED: Keeping last 20 lines]
panic: runtime error: invalid memory address or nil pointer dereference [recovered, repanicked]
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x2076cf2]

goroutine 9 [running]:
testing.tRunner.func1.2({0x37d7fe0, 0x61dd9b0})
	/opt/hostedtoolcache/go/1.25.8/x64/src/testing/testing.go:1872 +0x237
testing.tRunner.func1()
	/opt/hostedtoolcache/go/1.25.8/x64/src/testing/testing.go:1875 +0x35b
panic({0x37d7fe0?, 0x61dd9b0?})
	/opt/hostedtoolcache/go/1.25.8/x64/src/runtime/panic.go:783 +0x132
github.com/offchainlabs/nitro/arbnode.(*InboxTracker).GetBatchCount(0x225f900?)
	/home/runner/work/nitro/nitro/arbnode/inbox_tracker.go:210 +0x12
github.com/offchainlabs/nitro/arbnode.(*InboxTracker).FindInboxBatchContainingMessage(0x0, 0x6)
	/home/runner/work/nitro/nitro/arbnode/inbox_tracker.go:225 +0x2f
github.com/offchainlabs/nitro/system_tests.TestBatchPosterL1SurplusMatchesBatchGasFlaky(0xc000395880)
	/home/runner/work/nitro/nitro/system_tests/batch_poster_test.go:838 +0x725
testing.tRunner(0xc000395880, 0x41a79c8)
	/opt/hostedtoolcache/go/1.25.8/x64/src/testing/testing.go:1934 +0xea
created by testing.(*T).Run in goroutine 1
	/opt/hostedtoolcache/go/1.25.8/x64/src/testing/testing.go:1997 +0x465

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

bragaigor
bragaigor previously approved these changes Mar 26, 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 just one question. The other thing is, we don't have any tests or assurances that if we remove this "field" ExpectedEndState it would go unnoticed. But I guess we don't care?

KolbyML
KolbyML previously approved these changes Mar 26, 2026
Copy link
Copy Markdown
Member

@KolbyML KolbyML left a comment

Choose a reason for hiding this comment

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

:shipit: looks good

}

let file = File::open(path)?;
let req: ExpectedState = serde_json::from_reader(BufReader::new(file))?;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
let req: ExpectedState = serde_json::from_reader(BufReader::new(file))?;
let req = serde_json::from_reader::<ExpectedState>(BufReader::new(file))?;

Also is req? a request?

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.

changed to be less confusing, sorry: e657275

Comment on lines +477 to +483
let gs = mach.get_global_state();
let actual = GoGlobalState {
block_hash: gs.bytes32_vals[0],
send_root: gs.bytes32_vals[1],
batch: gs.u64_vals[0],
pos_in_batch: gs.u64_vals[1],
};
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Does it make sense to just implement the From trait for this type conversion.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[repr(C)]
pub struct GlobalState {
    pub bytes32_vals: [Bytes32; GLOBAL_STATE_BYTES32_NUM],
    pub u64_vals: [u64; GLOBAL_STATE_U64_NUM],
}

^ this is gross but maybe it is this way due to the c ffi? This GlobalState code is 4+ years old so definitly not related to this PR, but it is indeed gross, so just thinking out loud this comment doesn't block this pr or anything

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.

yeah, I think it is required for C FFI; anyway, implemented From conversion in e657275

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

if it is required, I wonder if it is possible to use getters and setters and make the fields non-public then, just to avoid people from indexing these throughout the codebase, we can contain them more.

But this isn't related to this PR of course

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.

indexing these fields is a common pain point raised already in other PRs :/ (e.g. #4521 (comment)) - will take care of it at some point, I promise 😅

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

😁

@KolbyML KolbyML assigned pmikolajczyk41 and unassigned KolbyML Mar 26, 2026
KolbyML
KolbyML previously approved these changes Mar 26, 2026
Copy link
Copy Markdown
Member

@KolbyML KolbyML left a comment

Choose a reason for hiding this comment

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

:shipit: looks good

Comment on lines +70 to +84
fn get_expected_state(opts: &Opts) -> Result<Option<GoGlobalState>> {
match &opts.input_mode {
jit::InputMode::Json { inputs } => {
let file = File::open(inputs)?;

// Use a temporary struct with the only interesting field, to avoid parsing all other data.
#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
struct ExpectedState {
#[serde(default)]
pub expected_end_state: Option<GoGlobalState>,
}

let req: ExpectedState = serde_json::from_reader(&mut BufReader::new(file))?;
Ok(req.expected_end_state)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

is it possible to do the same thing to this function as well? anyways this doesn't block this PR

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.

oh, sorry... done in 9717c70

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.

4 participants