Skip to content

Conversation

@pmikolajczyk41
Copy link
Member

@pmikolajczyk41 pmikolajczyk41 commented Dec 24, 2025

Foundations of the Rust validator service:

  • add a new binary crate validator
  • add a basic server configuration with defaults (host, port, logging format)
  • start an axum server with the required endpoints (enable implementing ValidationSpawner interface for the validation client)
  • add required data structures for request and response data
  • derive serde for arbutil::PreimageType
  • do required adjustments to CI and Docker building (new crate)

Currently we have no validation logic - once we have https://linear.app/offchain-labs/issue/NIT-4260/jit-enable-socketless-mode-for-jit-validator, we can integrate the jit crate here and go ahead with nitro integration (testing).

part of NIT-4255

@codecov
Copy link

codecov bot commented Dec 24, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 32.94%. Comparing base (9e434ee) to head (227fdf9).
⚠️ Report is 3 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #4173      +/-   ##
==========================================
- Coverage   33.05%   32.94%   -0.11%     
==========================================
  Files         459      459              
  Lines       55830    55830              
==========================================
- Hits        18453    18395      -58     
- Misses      34159    34214      +55     
- Partials     3218     3221       +3     

@github-actions
Copy link

github-actions bot commented Dec 24, 2025

❌ 5 Tests Failed:

Tests completed Failed Passed Skipped
4495 5 4490 0
View the top 3 failed tests by shortest run time
TestVersion30
Stack Traces | 11.420s run time
... [CONTENT TRUNCATED: Keeping last 20 lines]
=== PAUSE TestVersion30
=== CONT  TestVersion30
    precompile_inclusion_test.go:94: goroutine 610317 [running]:
        runtime/debug.Stack()
        	/opt/hostedtoolcache/go/1.25.5/x64/src/runtime/debug/stack.go:26 +0x5e
        github.com/offchainlabs/nitro/util/testhelpers.RequireImpl({0x40fdb70, 0xc07ffaee00}, {0x40bade0, 0xc0d1bea930}, {0x0, 0x0, 0x0})
        	/home/runner/work/nitro/nitro/util/testhelpers/testhelpers.go:29 +0x55
        github.com/offchainlabs/nitro/system_tests.Require(0xc07ffaee00, {0x40bade0, 0xc0d1bea930}, {0x0, 0x0, 0x0})
        	/home/runner/work/nitro/nitro/system_tests/common_test.go:2034 +0x5d
        github.com/offchainlabs/nitro/system_tests.testPrecompiles(0xc07ffaee00, 0x1e, {0xc10d399db0, 0x6, 0x0?})
        	/home/runner/work/nitro/nitro/system_tests/precompile_inclusion_test.go:94 +0x371
        github.com/offchainlabs/nitro/system_tests.TestVersion30(0xc07ffaee00?)
        	/home/runner/work/nitro/nitro/system_tests/precompile_inclusion_test.go:67 +0x798
        testing.tRunner(0xc07ffaee00, 0x3d3c728)
        	/opt/hostedtoolcache/go/1.25.5/x64/src/testing/testing.go:1934 +0xea
        created by testing.(*T).Run in goroutine 1
        	/opt/hostedtoolcache/go/1.25.5/x64/src/testing/testing.go:1997 +0x465
        
    precompile_inclusion_test.go:94: �[31;1m [] execution aborted (timeout = 5s) �[0;0m
--- FAIL: TestVersion30 (11.42s)
TestConsumeNextBid_Direct
Stack Traces | 15.110s run time
... [CONTENT TRUNCATED: Keeping last 20 lines]
INFO [12-29|17:50:31.141] Starting work on payload                 id=0x031d9c704e283ad2
INFO [12-29|17:50:31.141] Updated payload                          id=0x031d9c704e283ad2 number=15 hash=b371ab..9db8ea txs=1 withdrawals=0 gas=979,249   fees=9.79249e-07  root=695024..640bf6 elapsed="763.584µs"
INFO [12-29|17:50:31.142] Stopping work on payload                 id=0x031d9c704e283ad2 reason=delivery
INFO [12-29|17:50:31.143] Imported new potential chain segment     number=15 hash=b371ab..9db8ea blocks=1 txs=1 mgas=0.979 elapsed=1.180ms     mgasps=829.867  triediffs=39.26KiB triedirty=0.00B
INFO [12-29|17:50:31.143] Chain head was updated                   number=15 hash=b371ab..9db8ea root=695024..640bf6 elapsed="82.193µs"
    auctioneer_bid_consumption_test.go:187: 
        	Error Trace:	/home/runner/work/nitro/nitro/timeboost/auctioneer_bid_consumption_test.go:187
        	Error:      	Received unexpected error:
        	            	invalid length, need 256 bits
        	            	opening wallet
        	            	github.com/offchainlabs/nitro/timeboost.NewAuctioneerServer
        	            		/home/runner/work/nitro/nitro/timeboost/auctioneer.go:214
        	            	github.com/offchainlabs/nitro/timeboost.TestConsumeNextBid_Direct
        	            		/home/runner/work/nitro/nitro/timeboost/auctioneer_bid_consumption_test.go:186
        	            	testing.tRunner
        	            		/opt/hostedtoolcache/go/1.25.5/x64/src/testing/testing.go:1934
        	            	runtime.goexit
        	            		/opt/hostedtoolcache/go/1.25.5/x64/src/runtime/asm_amd64.s:1693
        	Test:       	TestConsumeNextBid_Direct
--- FAIL: TestConsumeNextBid_Direct (15.11s)
TestSequencerInboxReader
Stack Traces | 22.690s run time
... [CONTENT TRUNCATED: Keeping last 20 lines]
DEBUG[12-29|17:59:10.086] Journaled pathdb diff layer              root=fb9ce3..4fbb07 parent=c4160d..06209a id=104                block=103
DEBUG[12-29|17:59:10.086] Journaled pathdb diff layer              root=1262fc..ef2037 parent=fb9ce3..4fbb07 id=105                block=104
DEBUG[12-29|17:59:10.086] Journaled pathdb diff layer              root=6bdb48..e22a1d parent=1262fc..ef2037 id=106                block=105
DEBUG[12-29|17:59:10.086] Journaled pathdb diff layer              root=9032ca..bd6632 parent=6bdb48..e22a1d id=107                block=106
DEBUG[12-29|17:59:10.086] Journaled pathdb diff layer              root=0c62f5..21e97c parent=9032ca..bd6632 id=108                block=107
DEBUG[12-29|17:59:10.086] Journaled pathdb diff layer              root=f7bcb0..7bd28e parent=0c62f5..21e97c id=109                block=108
DEBUG[12-29|17:59:10.086] Journaled pathdb diff layer              root=c7d234..4478c8 parent=f7bcb0..7bd28e id=110                block=109
DEBUG[12-29|17:59:10.086] Journaled pathdb diff layer              root=77026a..0eafce parent=c7d234..4478c8 id=111                block=110
DEBUG[12-29|17:59:10.086] Journaled pathdb diff layer              root=30c489..5df634 parent=77026a..0eafce id=112                block=111
DEBUG[12-29|17:59:10.087] Journaled pathdb diff layer              root=db39d1..f62239 parent=30c489..5df634 id=113                block=112
DEBUG[12-29|17:59:10.087] Journaled pathdb diff layer              root=21d4b9..a2d325 parent=db39d1..f62239 id=114                block=113
DEBUG[12-29|17:59:10.087] Journaled pathdb diff layer              root=ac961d..089301 parent=21d4b9..a2d325 id=115                block=114
DEBUG[12-29|17:59:10.087] Journaled pathdb diff layer              root=a56365..c829a9 parent=ac961d..089301 id=116                block=115
DEBUG[12-29|17:59:10.087] Journaled pathdb diff layer              root=86d763..cb9137 parent=a56365..c829a9 id=117                block=116
DEBUG[12-29|17:59:10.087] Journaled pathdb diff layer              root=4a2b96..191dae parent=86d763..cb9137 id=118                block=117
DEBUG[12-29|17:59:10.087] Journaled pathdb diff layer              root=b059bb..a869b3 parent=4a2b96..191dae id=119                block=118
INFO [12-29|17:59:10.088] Persisted dirty state to disk            size=536.51KiB elapsed=27.049ms
INFO [12-29|17:59:10.088] Blockchain stopped
TRACE[12-29|17:59:10.089] P2P networking is spinning down
--- FAIL: TestSequencerInboxReader (22.69s)

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


/// Initialize `tracing` logging based on the specified format. By default, the logging level is set
/// to "info" unless overridden by the `RUST_LOG` environment variable.
fn init_logging(format: LoggingFormat) -> Result<()> {
Copy link
Contributor

Choose a reason for hiding this comment

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

I know right now we have a simple server, but eventually this logic will grow, so wondering if it would be better to create a init.rs file (or telemetry.rs) and move this there. Also that way we keep main.rs simple

Copy link
Member Author

Choose a reason for hiding this comment

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

I really like the comment - personally I have tendency to create multiple smaller, granular modules, but I didn't want to push it too far; but with your support, I extracted this function to logging.rs (I was afraid that init.rs might be too general for now)

.route(
&format!("{BASE_NAMESPACE}_wasmModuleRoots"),
get(spawner_endpoints::wasm_module_roots),
);
Copy link
Contributor

Choose a reason for hiding this comment

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

same for this, would probably move to it's own function and probably to config.rs or router.rs? That way we can also move const BASE_NAMESPACE: &str = "/validation"; there as well? That should also make our lives easier for testing.

Copy link
Member Author

Choose a reason for hiding this comment

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

done ✅

bragaigor
bragaigor previously approved these changes Dec 29, 2025
Copy link
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
Contributor

@MishkaRogachev MishkaRogachev left a comment

Choose a reason for hiding this comment

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

A few minor questions

pub struct ServerConfig {
/// Host where the server should be run.
#[clap(long, default_value = "0.0.0.0")]
pub host: String,
Copy link
Contributor

Choose a reason for hiding this comment

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

I may be worth using SocketAddr to handle invalid host strings

Copy link
Member Author

Choose a reason for hiding this comment

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

good one! added in 8279a86

Json,
}

impl FromStr for LoggingFormat {
Copy link
Contributor

Choose a reason for hiding this comment

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

you can add #[serde(rename_all = "lowercase")] to LoggingFormat and fully remove impl FromStr for LoggingFormat

Copy link
Member Author

Choose a reason for hiding this comment

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

oh, I didn't know that trick; nice one! 217bd80

MishkaRogachev
MishkaRogachev previously approved these changes Dec 29, 2025
KolbyML
KolbyML previously approved these changes Dec 29, 2025
Copy link
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.

Overall looks good, I have a few nits, but they aren't blockers by any means. Good work 🔥

pub struct ValidationRequest {
id: u64,
has_delayed_msg: bool,
delayed_msg_nr: u64,
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
delayed_msg_nr: u64,
#[serde(rename = "DelayedMsgNr")]
delayed_message_number: u64,

What does nr mean? Maybe we can use #[serde(rename so we have clearer field names, I assume it means number, but that is a crazy shorthand

Copy link
Member Author

Choose a reason for hiding this comment

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

it's pretty funny, because I was sure 'nr' is a commonly used abbreviation, but I just checked it and it turns out it's popular rather in central and eastern Europe 🙃

I followed the suggestion in 227fdf9


/// Counterpart for Go struct `validator.BatchInfo`.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "PascalCase")] // Match Go struct field names
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
#[serde(rename_all = "PascalCase")] // Match Go struct field names
#[serde(rename_all = "PascalCase")]

// Match Go struct field names for all these feels a bit redundant, maybe we can put that information in the files doc comment

Copy link
Member Author

Choose a reason for hiding this comment

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

done in 227fdf9

Comment on lines +33 to +39
// TODO: Implement actual validation logic
serde_json::to_string(&request.start_state)
.map_err(|e| format!("Failed to serialize state: {e}",))
}

pub async fn wasm_module_roots() -> impl IntoResponse {
"[]" // TODO: Figure this out from local replay.wasm
Copy link
Member

Choose a reason for hiding this comment

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

are we going to cross post these todo's and inline issue tracker links like was discussed a week or 2 ago?

Copy link
Member Author

Choose a reason for hiding this comment

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

I created one ticket: https://linear.app/offchain-labs/issue/NIT-4265/rust-validator-figure-out-max-number-of-workers-optionally-make-it and the other todos are going to be tackled in the next steps (hopefully pretty soon)

Comment on lines +10 to +13
pub fn create_router() -> Router {
Router::new()
.route(
&format!("{BASE_NAMESPACE}_capacity"),
Copy link
Member

@KolbyML KolbyML Dec 29, 2025

Choose a reason for hiding this comment

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

Suggested change
pub fn create_router() -> Router {
Router::new()
.route(
&format!("{BASE_NAMESPACE}_capacity"),
pub fn create_router() -> Router {
Router::new()
.route(
concat!(BASE_NAMESPACE, "_capacity"),

This is a little more directed/concise I think. But doesn't matter either way

I wish the endpoints were

/validator/capacity but I guess we are conforming to the golang validator

Copy link
Member

Choose a reason for hiding this comment

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

It might actually be cleaner to put the _ in BASE_NAMESPACE now that I think about it as well

Copy link
Member Author

Choose a reason for hiding this comment

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

unfortunately concat doesn't work with constants nor variables, so I couldn't use BASE_NAMESPACE; because of that, I left the code as is, to avoid "{BASE_NAMESPACE}capacity"

Copy link
Member

Choose a reason for hiding this comment

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

Sounds good

Copy link
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

Comment on lines +45 to +79
type Hash = Bytes32;

/// Counterpart for Go struct `validator.ValidationInput`.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "PascalCase")]
pub struct ValidationRequest {
id: u64,
has_delayed_msg: bool,
#[serde(rename = "DelayedMsgNr")]
delayed_msg_number: u64,
preimages: HashMap<PreimageType, Hash>,
user_wasms: HashMap<String, HashMap<Hash, Vec<u8>>>,
batch_info: Vec<BatchInfo>,
delayed_msg: Vec<u8>,
start_state: GlobalState,
debug_chain: bool,
}

/// Counterpart for Go struct `validator.BatchInfo`.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "PascalCase")]
pub struct BatchInfo {
number: u64,
data: Vec<u8>,
}

/// Counterpart for Go struct `validator.GoGlobalState`.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "PascalCase")]
pub struct GlobalState {
block_hash: Hash,
send_root: Hash,
batch: u64,
pos_in_batch: u64,
}
Copy link
Member

Choose a reason for hiding this comment

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

Also shouldn't we avoid type alias's like type Hash = Bytes32;, you can already infer it is a hash by the field's name. Being able to read that the actual type is Bytes32 is very nice.

I think using type alias's for primitives like B256,u64 etc makes the code harder to read. Because then I need to check what the type alias is.

For more complex type definitions like some type with generics it can be nice to use a type alias. If we want to keep the type alias it would be nice if it was instead below the imports above the structs/enums/impl/funcs at least that is where I expect to see them or

use arbutil::{Bytes32 as Hash, PreimageType};
^ do this

But I think this type alias makes the code harder to read in this case, so we should remove it, but up to you.

Copy link
Member

Choose a reason for hiding this comment

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

B256/Bytes32 is almost always hashes as well, I can't think of an example at the time of my head where you would need it other then hashes

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah I agree with only type aliasing complex types.

@joshuacolvin0 joshuacolvin0 added this pull request to the merge queue Dec 29, 2025
Merged via the queue into master with commit 416e512 Dec 30, 2025
40 of 41 checks passed
@joshuacolvin0 joshuacolvin0 deleted the pmikolajczyk/nit-4255-rust-jitdator branch December 30, 2025 00:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants