diff --git a/Cargo.lock b/Cargo.lock index df3a74b..58eb415 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,18 +17,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" -[[package]] -name = "ahash" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy 0.7.35", -] - [[package]] name = "aho-corasick" version = "1.1.3" @@ -1077,7 +1065,6 @@ dependencies = [ "bincode", "bollard", "http", - "mockall", "reqwest", "serde", "serde_json", @@ -1096,6 +1083,7 @@ version = "0.1.5" dependencies = [ "alloy 0.11.1", "serde", + "serde_json", ] [[package]] @@ -1112,37 +1100,25 @@ dependencies = [ "bollard", "clap", "directories", - "ecdsa", "futures", "futures-util", "hex", "http-body-util", "hyper", "hyper-util", - "k256", "metrics", - "metrics-exporter-prometheus 0.16.2", - "mockall", - "once_cell", - "rand 0.8.5", - "regex", "reqwest", "rust-tracing", + "semver 1.0.26", "serde", "serde_json", - "simd-json", "sled", "tempfile", "thiserror 2.0.12", "tokio", - "tokio-stream", "tokio-util", "tracing", - "tracing-subscriber", - "util", "uuid", - "wiremock", - "zerocopy 0.7.35", ] [[package]] @@ -1951,12 +1927,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562" -[[package]] -name = "downcast" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" - [[package]] name = "dunce" version = "1.0.5" @@ -1982,7 +1952,6 @@ dependencies = [ "digest 0.10.7", "elliptic-curve", "rfc6979", - "serdect", "signature", "spki", ] @@ -2011,7 +1980,6 @@ dependencies = [ "pkcs8", "rand_core 0.6.4", "sec1", - "serdect", "subtle", "zeroize", ] @@ -2097,15 +2065,6 @@ dependencies = [ "static_assertions", ] -[[package]] -name = "float-cmp" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" -dependencies = [ - "num-traits", -] - [[package]] name = "fnv" version = "1.0.7" @@ -2142,12 +2101,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fragile" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" - [[package]] name = "fs2" version = "0.4.3" @@ -2351,16 +2304,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "halfbrown" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8588661a8607108a5ca69cab034063441a0413a0b041c13618a7dd348021ef6f" -dependencies = [ - "hashbrown 0.14.5", - "serde", -] - [[package]] name = "hashbrown" version = "0.12.3" @@ -2372,10 +2315,6 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", - "allocator-api2", -] [[package]] name = "hashbrown" @@ -2901,7 +2840,6 @@ dependencies = [ "elliptic-curve", "once_cell", "sha2", - "signature", ] [[package]] @@ -2935,70 +2873,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" -[[package]] -name = "lexical-core" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b765c31809609075565a70b4b71402281283aeda7ecaf4818ac14a7b2ade8958" -dependencies = [ - "lexical-parse-float", - "lexical-parse-integer", - "lexical-util", - "lexical-write-float", - "lexical-write-integer", -] - -[[package]] -name = "lexical-parse-float" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de6f9cb01fb0b08060209a057c048fcbab8717b4c1ecd2eac66ebfe39a65b0f2" -dependencies = [ - "lexical-parse-integer", - "lexical-util", - "static_assertions", -] - -[[package]] -name = "lexical-parse-integer" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72207aae22fc0a121ba7b6d479e42cbfea549af1479c3f3a4f12c70dd66df12e" -dependencies = [ - "lexical-util", - "static_assertions", -] - -[[package]] -name = "lexical-util" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a82e24bf537fd24c177ffbbdc6ebcc8d54732c35b50a3f28cc3f4e4c949a0b3" -dependencies = [ - "static_assertions", -] - -[[package]] -name = "lexical-write-float" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5afc668a27f460fb45a81a757b6bf2f43c2d7e30cb5a2dcd3abf294c78d62bd" -dependencies = [ - "lexical-util", - "lexical-write-integer", - "static_assertions", -] - -[[package]] -name = "lexical-write-integer" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "629ddff1a914a836fb245616a7888b62903aae58fa771e1d83943035efa0f978" -dependencies = [ - "lexical-util", - "static_assertions", -] - [[package]] name = "libc" version = "0.2.171" @@ -3108,33 +2982,12 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "metrics" -version = "0.24.1" +version = "0.24.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a7deb012b3b2767169ff203fadb4c6b0b82b947512e5eb9e0b78c2e186ad9e3" +checksum = "89550ee9f79e88fef3119de263694973a8adb26c21d75322164fb8c493039fe2" dependencies = [ - "ahash", "portable-atomic", -] - -[[package]] -name = "metrics-exporter-prometheus" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd7399781913e5393588a8d8c6a2867bf85fb38eaf2502fdce465aad2dc6f034" -dependencies = [ - "base64", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "indexmap 2.8.0", - "ipnet", - "metrics", - "metrics-util", - "quanta", - "thiserror 1.0.69", - "tokio", - "tracing", + "rapidhash", ] [[package]] @@ -3206,32 +3059,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "mockall" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39a6bfcc6c8c7eed5ee98b9c3e33adc726054389233e201c95dab2d41a3839d2" -dependencies = [ - "cfg-if", - "downcast", - "fragile", - "mockall_derive", - "predicates", - "predicates-tree", -] - -[[package]] -name = "mockall_derive" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ca3004c2efe9011bd4e461bd8256445052b9615405b4f7ea43fc8ca5c20898" -dependencies = [ - "cfg-if", - "proc-macro2", - "quote", - "syn 2.0.100", -] - [[package]] name = "native-tls" version = "0.2.14" @@ -3656,33 +3483,7 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy 0.8.23", -] - -[[package]] -name = "predicates" -version = "3.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" -dependencies = [ - "anstyle", - "predicates-core", -] - -[[package]] -name = "predicates-core" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" - -[[package]] -name = "predicates-tree" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" -dependencies = [ - "predicates-core", - "termtree", + "zerocopy", ] [[package]] @@ -3919,7 +3720,7 @@ checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", - "zerocopy 0.8.23", + "zerocopy", ] [[package]] @@ -3993,6 +3794,15 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rapidhash" +version = "4.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5da7e78a036ce858e8d55b7e7dc8ba3a88b78350fd2155d3591bbd966b58589e" +dependencies = [ + "rustversion", +] + [[package]] name = "raw-cpuid" version = "11.5.0" @@ -4057,26 +3867,6 @@ dependencies = [ "thiserror 2.0.12", ] -[[package]] -name = "ref-cast" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", -] - [[package]] name = "regex" version = "1.11.1" @@ -4255,7 +4045,7 @@ dependencies = [ "alloy 0.12.6", "init4-from-env-derive", "metrics", - "metrics-exporter-prometheus 0.17.0", + "metrics-exporter-prometheus", "opentelemetry", "opentelemetry-otlp", "opentelemetry-semantic-conventions", @@ -4443,7 +4233,6 @@ dependencies = [ "der", "generic-array", "pkcs8", - "serdect", "subtle", "zeroize", ] @@ -4599,16 +4388,6 @@ dependencies = [ "syn 2.0.100", ] -[[package]] -name = "serdect" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" -dependencies = [ - "base16ct", - "serde", -] - [[package]] name = "sha1" version = "0.10.6" @@ -4694,28 +4473,6 @@ dependencies = [ "rand_core 0.6.4", ] -[[package]] -name = "simd-json" -version = "0.13.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0228a564470f81724e30996bbc2b171713b37b15254a6440c7e2d5449b95691" -dependencies = [ - "getrandom 0.2.15", - "halfbrown", - "lexical-core", - "ref-cast", - "serde", - "serde_json", - "simdutf8", - "value-trait", -] - -[[package]] -name = "simdutf8" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" - [[package]] name = "sketches-ddsketch" version = "0.3.0" @@ -4968,12 +4725,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "termtree" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" - [[package]] name = "thiserror" version = "1.0.69" @@ -5453,12 +5204,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1883f788ebb25ad22fe882ea46046b57f481cac322ef7da7f52dc2fff51bed09" - [[package]] name = "uuid" version = "1.16.0" @@ -5474,18 +5219,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" -[[package]] -name = "value-trait" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dad8db98c1e677797df21ba03fca7d3bf9bec3ca38db930954e4fe6e1ea27eb4" -dependencies = [ - "float-cmp", - "halfbrown", - "itoa", - "ryu", -] - [[package]] name = "vcpkg" version = "0.2.15" @@ -5981,34 +5714,13 @@ dependencies = [ "synstructure", ] -[[package]] -name = "zerocopy" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "byteorder", - "zerocopy-derive 0.7.35", -] - [[package]] name = "zerocopy" version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" dependencies = [ - "zerocopy-derive 0.8.23", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", + "zerocopy-derive", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index d48e645..6592e6d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,27 +17,18 @@ futures = "0.3.30" reqwest = { version = "0.12.5", features = ["json"] } serde = "1.0.204" serde_json = "1.0.121" +semver = "1.0" tokio = { version = "1.39.2", features = ["full"] } tokio-util = "0.7.15" -zerocopy = "0.7.35" hyper = { version = "1.4.1", features = ["full"] } -util = "0.1.3" http-body-util = "0.1.2" -simd-json = "0.13.10" -rand = "0.8.5" hyper-util = "0.1.6" bincode = "1.3.3" -tokio-stream = "0.1.15" sled = { version = "1.0.0-alpha.122" } -ecdsa = { version = "0.16.9", features = ["serde", "signing"] } -k256 = "0.13.4" hex = "0.4.3" tempfile = { version = "3.12.0" } -mockall = { version = "0.13.0" } wiremock = "0.6.1" directories = "6.0.0" bollard = "0.18" tracing = "0.1.41" -tracing-subscriber = {version = "0.3", features = ["std", "env-filter"]} - diff --git a/README.md b/README.md index e6f774e..dfd8243 100644 --- a/README.md +++ b/README.md @@ -176,14 +176,23 @@ Parameters: "result": { "solidity_source": "contract MyAssertion { ... }", "bytecode": "0x...", - "signature": "0x..." + "prover_signature": "0x...", "encoded_constructor_args": "", - "constructor_abi_signature": "constructor()" + "constructor_abi_signature": "constructor()", + "assertion_contract_name": "MyAssertion", + "compiler_version": "0.8.17", + "abi": [{ "type": "function", "name": "check", "inputs": [] }] }, "id": 1 } ``` +`abi` is the exact JSON ABI emitted by the selected Solidity compiler and is +stored when the assertion is submitted. Existing records are recompiled and +backfilled on first read after verifying that the compiler bytecode matches the +stored deployment bytecode. Compiler metadata is omitted only for raw-bytecode +submissions or legacy records that cannot be safely reconstructed. + ### Error Codes The API uses the following error codes: @@ -227,9 +236,12 @@ curl -X POST -H "Content-Type: application/json" http://localhost:5001 -d '{ { "solidity_source": "contract MyAssertion { ... }", "bytecode": "0x...", - "signature": "0x..." + "prover_signature": "0x...", "encoded_constructor_args": "0x0000000000000000000000000000000000000000000000000000000000000005", - "constructor_abi_signature": "constructor(uint256)" + "constructor_abi_signature": "constructor(uint256)", + "assertion_contract_name": "MyAssertion", + "compiler_version": "0.8.17", + "abi": [] }, ], "id": 1 @@ -238,6 +250,6 @@ curl -X POST -H "Content-Type: application/json" http://localhost:5001 -d '{ ## Limitations -- Only one contract per Solidity source file is supported +- The contract selected by `assertion_contract_name` is returned when a source file defines multiple contracts - Compilation is performed using Docker containers - Supported Solidity versions depend on available [ethereum/solc](https://hub.docker.com/r/ethereum/solc) images diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 6bfbe1c..1e76bc0 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -25,6 +25,5 @@ alloy.workspace = true bincode.workspace = true sled.workspace = true tempfile.workspace = true -mockall.workspace = true wiremock.workspace = true bollard.workspace = true diff --git a/crates/client/src/lib.rs b/crates/client/src/lib.rs index 77afee4..972738b 100644 --- a/crates/client/src/lib.rs +++ b/crates/client/src/lib.rs @@ -76,6 +76,10 @@ struct JsonRpcError { impl DaClient { /// Create a new DA client + /// + /// # Errors + /// + /// Returns an error when `da_url` is invalid or the HTTP client cannot be built. pub fn new(da_url: &str) -> Result { let base_url = Url::parse(da_url)?; let client = Client::builder().use_rustls_tls().build()?; @@ -88,6 +92,11 @@ impl DaClient { } /// Create a new DA client with authentication + /// + /// # Errors + /// + /// Returns an error when the URL or authorization header is invalid, or when the HTTP client + /// cannot be built. pub fn new_with_auth(da_url: &str, auth: &str) -> Result { let base_url = Url::parse(da_url)?; let mut headers = header::HeaderMap::new(); @@ -174,6 +183,10 @@ impl DaClient { } /// Fetch the bytecode and signature for the given assertion id from the DA layer + /// + /// # Errors + /// + /// Returns an error when the request fails or the DA returns an invalid JSON-RPC response. pub async fn fetch_assertion( &self, assertion_id: B256, @@ -183,6 +196,10 @@ impl DaClient { } /// Submit the assertion bytecode to the DA layer + /// + /// # Errors + /// + /// Returns an error when the request fails or the DA returns an invalid JSON-RPC response. pub async fn submit_assertion( &self, assertion_contract_name: String, @@ -200,6 +217,10 @@ impl DaClient { } /// Submit the assertion bytecode with constructor args to the DA layer + /// + /// # Errors + /// + /// Returns an error when the request fails or the DA returns an invalid JSON-RPC response. pub async fn submit_assertion_with_args( &self, assertion_contract_name: String, @@ -292,7 +313,7 @@ mod tests { // Start the database listener tokio::spawn(async move { - listen_for_db(db_receiver, db, CancellationToken::new()) + Box::pin(listen_for_db(db_receiver, db, CancellationToken::new())) .await .unwrap(); }); @@ -305,9 +326,9 @@ mod tests { #[tokio::test] async fn test_client_submit_solidity_assertion() { - let (_temp_dir, _db_sender, _signer, client) = setup_test_env().await; + let (_temp_dir, _db_sender, _signer, client) = Box::pin(setup_test_env()).await; - let source_code = r#" + let source_code = r" // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; @@ -322,7 +343,7 @@ mod tests { return value; } } - "#; + "; let response = client .submit_assertion( @@ -339,7 +360,7 @@ mod tests { #[tokio::test] async fn test_client_get_assertion() { - let (_temp_dir, db_sender, signer, client) = setup_test_env().await; + let (_temp_dir, db_sender, signer, client) = Box::pin(setup_test_env()).await; // First submit an assertion directly to DB let source_code = "contract Test { }"; @@ -348,7 +369,7 @@ mod tests { bytecode: vec![1, 2, 3, 4], prover_signature: signer.sign_hash(&keccak256([1, 2, 3, 4])).await.unwrap(), assertion_contract_name: "Test".to_string(), - compiler_version: "0.8.17".to_string(), + compiler_version: "legacy".to_string(), constructor_abi_signature: "constructor()".to_string(), encoded_constructor_args: Bytes::new(), }; @@ -376,7 +397,7 @@ mod tests { #[tokio::test] async fn test_get_nonexistent_assertion() { - let (_temp_dir, _db_sender, _signer, client) = setup_test_env().await; + let (_temp_dir, _db_sender, _signer, client) = Box::pin(setup_test_env()).await; let nonexistent_id = B256::ZERO; let result = client.fetch_assertion(nonexistent_id).await; @@ -425,9 +446,9 @@ mod tests { #[tokio::test] async fn test_client_submit_solidity_assertion_with_args() { - let (_temp_dir, _db_sender, _signer, client) = setup_test_env().await; + let (_temp_dir, _db_sender, _signer, client) = Box::pin(setup_test_env()).await; - let source_code = r#" + let source_code = r" // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; @@ -442,7 +463,7 @@ mod tests { return value; } } - "#; + "; let response = client .submit_assertion_with_args( @@ -461,7 +482,7 @@ mod tests { #[tokio::test] async fn test_invalid_solidity_submission() { - let (_temp_dir, _db_sender, _signer, client) = setup_test_env().await; + let (_temp_dir, _db_sender, _signer, client) = Box::pin(setup_test_env()).await; // Invalid Solidity code (missing semicolon) let invalid_source = "contract Test { uint256 x = 5 }"; @@ -509,7 +530,7 @@ mod tests { #[tokio::test] async fn test_response_content_validation() { - let (_temp_dir, db_sender, signer, client) = setup_test_env().await; + let (_temp_dir, db_sender, signer, client) = Box::pin(setup_test_env()).await; // Create a test contract with specific bytecode let source_code = "contract Test { uint256 value; }"; @@ -523,7 +544,7 @@ mod tests { bytecode: bytecode.clone(), prover_signature: signature, assertion_contract_name: "Test".to_string(), - compiler_version: "0.8.17".to_string(), + compiler_version: "legacy".to_string(), constructor_abi_signature: "constructor()".to_string(), encoded_constructor_args: Bytes::new(), }; @@ -583,7 +604,7 @@ mod tests { .mount(&mock_server) .await; - let result = client.fetch_assertion(Default::default()).await; + let result = client.fetch_assertion(B256::default()).await; assert!(result.is_err()); match result.unwrap_err() { DaClientError::InvalidResponse(msg) => { @@ -614,7 +635,7 @@ mod tests { .mount(&mock_server) .await; - let result = client.fetch_assertion(Default::default()).await; + let result = client.fetch_assertion(B256::default()).await; assert!(result.is_err()); match result.unwrap_err() { DaClientError::InvalidResponse(msg) => { diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index e115fa2..070b435 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -5,4 +5,5 @@ edition = "2024" [dependencies] serde.workspace = true +serde_json.workspace = true alloy.workspace = true diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index e0d9f73..a217bb0 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -1,3 +1,4 @@ +use alloy::json_abi::JsonAbi; use alloy::primitives::{ B256, Bytes, @@ -25,11 +26,41 @@ pub struct DaSubmissionResponse { } ///The response from the DA layer when fetching an assertion -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, Clone)] pub struct DaFetchResponse { pub solidity_source: String, pub bytecode: Bytes, pub prover_signature: Bytes, pub encoded_constructor_args: Bytes, pub constructor_abi_signature: String, + /// Producer-selected assertion contract. Absent on older servers. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub assertion_contract_name: Option, + /// Compiler used for the stored artifact. Absent on older servers. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub compiler_version: Option, + /// Exact compiler-produced ABI. Absent only for raw or unrecoverable legacy artifacts. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub abi: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn deserializes_legacy_fetch_response_without_compiler_metadata() { + let response: DaFetchResponse = serde_json::from_value(serde_json::json!({ + "solidity_source": "contract Assertion {}", + "bytecode": "0x6000", + "prover_signature": "0x", + "encoded_constructor_args": "0x", + "constructor_abi_signature": "constructor()" + })) + .unwrap(); + + assert!(response.assertion_contract_name.is_none()); + assert!(response.compiler_version.is_none()); + assert!(response.abi.is_none()); + } } diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index a19b8cb..a1e5559 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -19,19 +19,13 @@ futures.workspace = true reqwest.workspace = true serde.workspace = true serde_json.workspace = true +semver.workspace = true tokio.workspace = true -zerocopy.workspace = true hyper.workspace = true -util.workspace = true http-body-util.workspace = true -simd-json.workspace = true -rand.workspace = true hyper-util.workspace = true bincode.workspace = true -tokio-stream.workspace = true sled.workspace = true -ecdsa.workspace = true -k256.workspace = true hex.workspace = true directories.workspace = true bollard.workspace = true @@ -40,21 +34,15 @@ tokio-util.workspace = true tracing.workspace = true rust-tracing.workspace = true -tracing-subscriber.workspace = true futures-util = "0.3" uuid = { version = "1.15", features = ["v4"] } tempfile = "3.18" -metrics-exporter-prometheus = "0.16" -metrics = "0.24" +metrics = "0.24.6" thiserror = "2" -regex = "1" assertion-da-core = { path = "../core" } [dev-dependencies] tempfile.workspace = true -mockall.workspace = true -wiremock.workspace = true -once_cell = "1.21" assertion-da-client = { path = "../client" } diff --git a/crates/server/src/api/compiler_abi.rs b/crates/server/src/api/compiler_abi.rs new file mode 100644 index 0000000..59f1598 --- /dev/null +++ b/crates/server/src/api/compiler_abi.rs @@ -0,0 +1,272 @@ +use std::sync::Arc; + +use alloy::{ + json_abi::JsonAbi, + primitives::B256, +}; +use anyhow::Result; +use bollard::Docker; +use serde::{ + Deserialize, + Serialize, +}; +use tracing::{ + info, + warn, +}; + +use crate::api::{ + db, + source_compilation::{ + compile_solidity, + is_supported_compiler_version, + MAX_COMPILER_ABI_BYTES, + }, + types::DbRequestSender, +}; + +const STORAGE_KEY_PREFIX: &[u8] = b"assertion-compiler-abi:"; +const SCHEMA_VERSION: u8 = 1; + +#[derive(Debug, Deserialize, Serialize)] +struct StoredCompilerAbi { + schema_version: u8, + abi: JsonAbi, +} + +enum CompilerAbiLookup { + Missing, + Available(JsonAbi), + Unsupported, +} + +/// Producer data required to recover compiler metadata for a legacy assertion. +pub struct LegacyCompilerArtifact<'a> { + pub contract_name: &'a str, + pub compiler_version: &'a str, + pub source: &'a str, + pub deployment_bytecode: &'a [u8], +} + +/// Stores the compiler ABI in an additive sidecar, leaving the legacy assertion +/// record unchanged for old binaries and rollback deployments. +pub async fn store(db: &DbRequestSender, id: B256, abi: JsonAbi) -> Result<()> { + db::insert_metadata(db, storage_key(id), serialize(abi)?).await +} + +/// Resolves a stored ABI or lazily reconstructs a missing legacy sidecar. +/// Unknown future sidecar versions are never overwritten. +pub async fn resolve( + db: &DbRequestSender, + id: B256, + legacy: LegacyCompilerArtifact<'_>, + docker: Arc, +) -> Result> { + match lookup(db, id).await? { + CompilerAbiLookup::Available(abi) => Ok(Some(abi)), + CompilerAbiLookup::Unsupported => Ok(None), + CompilerAbiLookup::Missing => Ok(backfill(db, id, legacy, docker).await), + } +} + +async fn lookup(db: &DbRequestSender, id: B256) -> Result { + let Some(value) = db::get_value(db, storage_key(id)).await? else { + return Ok(CompilerAbiLookup::Missing); + }; + + let stored: StoredCompilerAbi = match serde_json::from_slice(&value) { + Ok(stored) => stored, + Err(error) => { + warn!(?id, %error, "Ignoring invalid compiler ABI sidecar"); + return Ok(CompilerAbiLookup::Unsupported); + } + }; + if stored.schema_version != SCHEMA_VERSION { + warn!( + ?id, + schema_version = stored.schema_version, + "Ignoring unsupported compiler ABI sidecar" + ); + return Ok(CompilerAbiLookup::Unsupported); + } + if serde_json::to_vec(&stored.abi)?.len() > MAX_COMPILER_ABI_BYTES { + warn!(?id, "Ignoring oversized compiler ABI sidecar"); + return Ok(CompilerAbiLookup::Unsupported); + } + Ok(CompilerAbiLookup::Available(stored.abi)) +} + +async fn backfill( + db: &DbRequestSender, + id: B256, + legacy: LegacyCompilerArtifact<'_>, + docker: Arc, +) -> Option { + if legacy.source.is_empty() || !is_supported_compiler_version(legacy.compiler_version) { + return None; + } + + let artifact = match compile_solidity( + legacy.contract_name, + legacy.source, + legacy.compiler_version, + docker, + ) + .await + { + Ok(artifact) => artifact, + Err(error) => { + warn!(?id, %error, "Could not reconstruct compiler ABI for legacy assertion"); + return None; + } + }; + + if !deployment_matches_compilation(legacy.deployment_bytecode, &artifact.bytecode) { + warn!( + ?id, + "Refusing compiler ABI backfill because bytecode does not match stored assertion" + ); + return None; + } + + let serialized = match serialize(artifact.abi.clone()) { + Ok(serialized) => serialized, + Err(error) => { + warn!(?id, %error, "Could not serialize compiler ABI backfill"); + return None; + } + }; + if let Err(error) = db::insert_metadata(db, storage_key(id), serialized).await { + warn!(?id, %error, "Could not persist compiler ABI backfill"); + } else { + info!(?id, "Persisted compiler ABI sidecar for legacy assertion"); + } + + Some(artifact.abi) +} + +fn storage_key(id: B256) -> Vec { + let mut key = Vec::with_capacity(STORAGE_KEY_PREFIX.len() + id.len()); + key.extend_from_slice(STORAGE_KEY_PREFIX); + key.extend_from_slice(id.as_slice()); + key +} + +fn serialize(abi: JsonAbi) -> Result> { + let abi_size = serde_json::to_vec(&abi)?.len(); + anyhow::ensure!( + abi_size <= MAX_COMPILER_ABI_BYTES, + "compiler ABI exceeds {MAX_COMPILER_ABI_BYTES} bytes" + ); + Ok(serde_json::to_vec(&StoredCompilerAbi { + schema_version: SCHEMA_VERSION, + abi, + })?) +} + +fn deployment_matches_compilation(deployment: &[u8], compiled_bytecode: &[u8]) -> bool { + !compiled_bytecode.is_empty() && deployment.starts_with(compiled_bytecode) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + use tokio::sync::mpsc; + use tokio_util::sync::CancellationToken; + + fn test_abi() -> JsonAbi { + serde_json::from_value(json!([ + { + "type": "function", + "name": "check", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + } + ])) + .unwrap() + } + + #[test] + fn sidecar_is_versioned_and_uses_alloy_json_abi() { + let serialized = serialize(test_abi()).unwrap(); + let stored: StoredCompilerAbi = serde_json::from_slice(&serialized).unwrap(); + + assert_eq!(stored.schema_version, SCHEMA_VERSION); + assert!(stored.abi.functions.contains_key("check")); + } + + #[test] + fn rejects_oversized_abi() { + let oversized: JsonAbi = serde_json::from_value(json!([ + { + "type": "function", + "name": "x".repeat(MAX_COMPILER_ABI_BYTES), + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + } + ])) + .unwrap(); + + assert!(serialize(oversized).is_err()); + } + + #[test] + fn backfill_requires_matching_nonempty_bytecode_prefix() { + assert!(deployment_matches_compilation( + &[0x60, 0x01, 0xaa, 0xbb], + &[0x60, 0x01] + )); + assert!(deployment_matches_compilation(&[0x60, 0x01], &[0x60, 0x01])); + assert!(!deployment_matches_compilation( + &[0x60, 0x02], + &[0x60, 0x01] + )); + assert!(!deployment_matches_compilation(&[0x60, 0x01], &[])); + } + + #[tokio::test] + async fn stores_and_loads_sidecar_through_db_port() { + let database = sled::Config::tmp().unwrap().open().unwrap(); + let (sender, receiver) = mpsc::unbounded_channel(); + let cancel = CancellationToken::new(); + let listener = tokio::spawn(db::listen_for_db(receiver, database, cancel.clone())); + let id = B256::repeat_byte(0x11); + + store(&sender, id, test_abi()).await.unwrap(); + let result = lookup(&sender, id).await.unwrap(); + + assert!(matches!( + result, + CompilerAbiLookup::Available(abi) if abi.functions.contains_key("check") + )); + cancel.cancel(); + listener.await.unwrap().unwrap(); + } + + #[tokio::test] + async fn preserves_unknown_future_sidecar_version() { + let database = sled::Config::tmp().unwrap().open().unwrap(); + let (sender, receiver) = mpsc::unbounded_channel(); + let cancel = CancellationToken::new(); + let listener = tokio::spawn(db::listen_for_db(receiver, database, cancel.clone())); + let id = B256::repeat_byte(0x22); + let future_sidecar = serde_json::to_vec(&json!({ + "schema_version": SCHEMA_VERSION + 1, + "abi": [] + })) + .unwrap(); + db::insert_metadata(&sender, storage_key(id), future_sidecar) + .await + .unwrap(); + + assert!(matches!( + lookup(&sender, id).await.unwrap(), + CompilerAbiLookup::Unsupported + )); + cancel.cancel(); + listener.await.unwrap().unwrap(); + } +} diff --git a/crates/server/src/api/db.rs b/crates/server/src/api/db.rs index e890ae6..6b13a2b 100644 --- a/crates/server/src/api/db.rs +++ b/crates/server/src/api/db.rs @@ -2,6 +2,7 @@ use crate::{ api::types::{ DbOperation, DbRequest, + DbRequestSender, DbResponse, }, LEAF_FANOUT, @@ -10,11 +11,18 @@ use crate::{ use sled::Db; use anyhow::Result; -use tokio::sync::mpsc; +use tokio::sync::{ + mpsc, + oneshot, +}; use tokio_util::sync::CancellationToken; /// Listens to a mpsc channel for database events and responds /// accordingly. +/// +/// # Errors +/// +/// Returns an error when a database operation fails. pub async fn listen_for_db( mut rx: mpsc::UnboundedReceiver, db: Db<{ LEAF_FANOUT }>, @@ -22,7 +30,7 @@ pub async fn listen_for_db( ) -> Result<()> { loop { tokio::select! { - _ = cancel_token.cancelled() => { + () = cancel_token.cancelled() => { tracing::info!("Database listener received cancellation signal, shutting down..."); break; } @@ -30,7 +38,11 @@ pub async fn listen_for_db( let res = match req.request { DbOperation::Get(key) => db_get(&db, &key)?, DbOperation::Insert(key, value) => { - db_insert(&db, &key, &value)?; + db_insert(&db, &key, &value, true)?; + None + } + DbOperation::InsertMetadata(key, value) => { + db_insert(&db, &key, &value, false)?; None } }; @@ -42,28 +54,75 @@ pub async fn listen_for_db( Ok(()) } -fn db_get(db: &Db<{ LEAF_FANOUT }>, key: &Vec) -> Result> { - let rax = db.get(key)?; - if rax.is_none() { - return Ok(None); - } - let rax = rax.unwrap().to_vec(); - Ok(Some(DbResponse::Value(rax))) +fn db_get(db: &Db<{ LEAF_FANOUT }>, key: &[u8]) -> Result> { + Ok(db.get(key)?.map(|value| DbResponse::Value(value.to_vec()))) } -fn db_insert(db: &Db<{ LEAF_FANOUT }>, key: &Vec, value: &Vec) -> Result<()> { +fn db_insert( + db: &Db<{ LEAF_FANOUT }>, + key: &[u8], + value: &[u8], + count_assertion: bool, +) -> Result<()> { db.insert(key, value.to_owned())?; let db_size = db.size_on_disk()? / (1024 * 1024); - metrics::gauge!("db_size_mb").set(db_size as f64); - metrics::gauge!("database_assertions_sum").increment(1); + metrics::gauge!("db_size_mb").set(u32::try_from(db_size).unwrap_or(u32::MAX)); + if count_assertion { + metrics::gauge!("database_assertions_sum").increment(1); + } + Ok(()) +} + +/// Reads a raw value through the database task. +/// +/// # Errors +/// +/// Returns an error when the request cannot be sent or its response channel closes. +pub async fn get_value(db: &DbRequestSender, key: Vec) -> Result>> { + let (response, receiver) = oneshot::channel(); + db.send(DbRequest { + request: DbOperation::Get(key), + response, + })?; + Ok(receiver.await?.map(|response| { + match response { + DbResponse::Value(value) => value, + } + })) +} + +/// Inserts an assertion and updates assertion-count metrics. +/// +/// # Errors +/// +/// Returns an error when the request cannot be sent or its response channel closes. +pub async fn insert_assertion(db: &DbRequestSender, key: Vec, value: Vec) -> Result<()> { + insert(db, DbOperation::Insert(key, value)).await +} + +/// Inserts auxiliary metadata without changing assertion-count metrics. +/// +/// # Errors +/// +/// Returns an error when the request cannot be sent or its response channel closes. +pub async fn insert_metadata(db: &DbRequestSender, key: Vec, value: Vec) -> Result<()> { + insert(db, DbOperation::InsertMetadata(key, value)).await +} + +async fn insert(db: &DbRequestSender, operation: DbOperation) -> Result<()> { + let (response, receiver) = oneshot::channel(); + db.send(DbRequest { + request: operation, + response, + })?; + receiver.await?; Ok(()) } #[cfg(test)] mod tests { use super::*; - use tokio::sync::oneshot; #[tokio::test] async fn test_db_operations() { @@ -72,7 +131,7 @@ mod tests { // Test insert let key = vec![1, 2, 3]; let value = vec![4, 5, 6]; - db_insert(&db, &key, &value).unwrap(); + db_insert(&db, &key, &value, true).unwrap(); // Test get let result = db_get(&db, &key).unwrap(); diff --git a/crates/server/src/api/mod.rs b/crates/server/src/api/mod.rs index cf93489..001a7c8 100644 --- a/crates/server/src/api/mod.rs +++ b/crates/server/src/api/mod.rs @@ -97,6 +97,7 @@ //! - -32603: Internal error pub mod accept; +mod compiler_abi; pub mod db; pub mod process_request; pub mod source_compilation; @@ -118,11 +119,18 @@ use tokio::{ sync::mpsc, }; -use anyhow::Result; +use anyhow::{ + Context, + Result, +}; use alloy::signers::local::PrivateKeySigner; /// Start the API server +/// +/// # Errors +/// +/// Returns an error when the private key is invalid or the listener fails. pub async fn serve( listener: TcpListener, db_tx: mpsc::UnboundedSender, @@ -130,21 +138,21 @@ pub async fn serve( pk_str: String, cancel_token: CancellationToken, ) -> Result<()> { - let pk_bytes = pk_str.parse().expect("Invalid Private Key"); + let pk_bytes = pk_str.parse().context("invalid private key")?; let signer = - PrivateKeySigner::from_bytes(&pk_bytes).expect("Failed to create private key signer"); + PrivateKeySigner::from_bytes(&pk_bytes).context("failed to create private key signer")?; // We start a loop to continuously accept incoming connections loop { tokio::select! { - _ = cancel_token.cancelled() => { + () = cancel_token.cancelled() => { tracing::info!("Api received cancellation signal, shutting down..."); break; } res = listener.accept() => { match res { Ok((stream, socketaddr)) => { - serve_connection(socketaddr, db_tx.clone(), docker.clone(), signer.clone(), stream); + serve_connection(socketaddr, &db_tx, &docker, signer.clone(), stream); } Err(err) => { tracing::error!(?err, "Error accepting connection"); @@ -159,8 +167,8 @@ pub async fn serve( fn serve_connection( socketaddr: SocketAddr, - db_tx: mpsc::UnboundedSender, - docker: Arc, + db_tx: &mpsc::UnboundedSender, + docker: &Arc, signer: PrivateKeySigner, stream: TcpStream, ) { diff --git a/crates/server/src/api/process_request.rs b/crates/server/src/api/process_request.rs index 69749fc..13bda08 100644 --- a/crates/server/src/api/process_request.rs +++ b/crates/server/src/api/process_request.rs @@ -6,13 +6,13 @@ use std::{ use crate::{ api::{ - source_compilation::compile_solidity, - types::{ - DbOperation, - DbRequest, - DbRequestSender, - DbResponse, + compiler_abi::{ + self, + LegacyCompilerArtifact, }, + db, + source_compilation::compile_solidity, + types::DbRequestSender, }, encode_args::encode_constructor_args, }; @@ -24,6 +24,7 @@ use assertion_da_core::{ }; use alloy::{ + json_abi::JsonAbi, primitives::{ keccak256, Bytes, @@ -51,7 +52,6 @@ use serde_json::{ json, Value, }; -use tokio::sync::oneshot; use uuid::Uuid; use http_body_util::BodyExt; @@ -175,7 +175,10 @@ where let res = process_add_assertion( id, - stored_assertion, + StoredAssertionArtifact { + assertion: stored_assertion, + compiler_abi: None, + }, db, &json_rpc, request_id, @@ -219,7 +222,7 @@ where debug!(target: "json_rpc", compiler_version = da_submission.compiler_version, da_submission.solidity_source , "Compiling solidity source"); - let bytecode = match compile_solidity( + let compilation = match compile_solidity( da_submission.assertion_contract_name.as_str(), da_submission.solidity_source.as_str(), da_submission.compiler_version.as_str(), @@ -227,7 +230,7 @@ where ) .await { - Ok(bytecode) => bytecode, + Ok(compilation) => compilation, Err(err) => { warn!(target: "json_rpc", method = "da_submit_solidity_assertion", %request_id, %client_ip, json_rpc_id = %json_rpc_id, error = %err, compiler_version = da_submission.compiler_version, contract_name = da_submission.assertion_contract_name, "Solidity compilation failed"); return Ok(rpc_error_with_request_id( @@ -255,7 +258,7 @@ where } }; - let mut deployment_data = bytecode.clone(); + let mut deployment_data = compilation.bytecode; deployment_data.extend_from_slice(&encoded_constructor_args); // Hash to get ID @@ -286,7 +289,10 @@ where let res = process_add_assertion( id, - stored_assertion, + StoredAssertionArtifact { + assertion: stored_assertion, + compiler_abi: Some(compilation.abi), + }, db, &json_rpc, request_id, @@ -296,10 +302,10 @@ where .await; if let Ok(ref response) = res { - if !response.contains("\"error\"") { - info!(target: "json_rpc", method = "da_submit_solidity_assertion", %request_id, %client_ip, json_rpc_id = %json_rpc_id, ?id, contract_name = da_submission.assertion_contract_name, compiler_version = da_submission.compiler_version, "Successfully compiled Solidity assertion"); - } else { + if response.contains("\"error\"") { warn!(target: "json_rpc", method = "da_submit_solidity_assertion", %request_id, %client_ip, json_rpc_id = %json_rpc_id, ?id, contract_name = da_submission.assertion_contract_name, compiler_version = da_submission.compiler_version, "Failed to process Solidity assertion"); + } else { + info!(target: "json_rpc", method = "da_submit_solidity_assertion", %request_id, %client_ip, json_rpc_id = %json_rpc_id, ?id, contract_name = da_submission.assertion_contract_name, compiler_version = da_submission.compiler_version, "Successfully compiled Solidity assertion"); } } @@ -312,38 +318,40 @@ where res } "da_get_assertion" => { - let id = match json_rpc["params"][0].as_str() { - Some(id) => id, - None => { - warn!(target: "json_rpc", method = "da_get_assertion", %request_id, %client_ip, json_rpc_id = %json_rpc_id, "Invalid params: missing id parameter"); - return Ok(rpc_error_with_request_id( - &json_rpc, - -32602, - "Invalid params: Didn't find id", - &request_id, - )); - } + let Some(id) = json_rpc["params"][0].as_str() else { + warn!(target: "json_rpc", method = "da_get_assertion", %request_id, %client_ip, json_rpc_id = %json_rpc_id, "Invalid params: missing id parameter"); + return Ok(rpc_error_with_request_id( + &json_rpc, + -32602, + "Invalid params: Didn't find id", + &request_id, + )); }; // Validate hex input - let id: B256 = match id.trim_start_matches("0x").parse() { - Ok(id) => id, - _ => { - warn!(target: "json_rpc", method = "da_get_assertion", %request_id, %client_ip, json_rpc_id = %json_rpc_id, id = id, "Failed to decode hex ID"); - return Ok(rpc_error_with_request_id( - &json_rpc, - -32605, - "Internal Error: Failed to decode hex of id", - &request_id, - )); - } + let Ok(decoded_id) = id.trim_start_matches("0x").parse() else { + warn!(target: "json_rpc", method = "da_get_assertion", %request_id, %client_ip, json_rpc_id = %json_rpc_id, id = id, "Failed to decode hex ID"); + return Ok(rpc_error_with_request_id( + &json_rpc, + -32605, + "Internal Error: Failed to decode hex of id", + &request_id, + )); }; + let id: B256 = decoded_id; debug!(target: "json_rpc", ?id, "Getting assertion"); - let res = - process_get_assertion(id, db, &json_rpc, request_id, &client_ip, &json_rpc_id) - .await; + let res = process_get_assertion( + id, + db, + docker, + &json_rpc, + request_id, + &client_ip, + &json_rpc_id, + ) + .await; // Log success for get_assertion if not an error response if let Ok(ref response) = res { @@ -413,6 +421,11 @@ pub struct StoredAssertion { pub encoded_constructor_args: Bytes, } +struct StoredAssertionArtifact { + assertion: StoredAssertion, + compiler_abi: Option, +} + impl StoredAssertion { pub fn new( assertion_contract_name: impl Into, @@ -437,16 +450,17 @@ impl StoredAssertion { async fn process_add_assertion( id: B256, - stored_assertion: StoredAssertion, + artifact: StoredAssertionArtifact, db: &DbRequestSender, json_rpc: &Value, request_id: Uuid, client_ip: &str, json_rpc_id: &Value, ) -> Result { - // Store in database - let (tx, rx) = oneshot::channel(); - + let StoredAssertionArtifact { + assertion: stored_assertion, + compiler_abi, + } = artifact; let ser_assertion = match bincode::serialize(&stored_assertion) { Ok(ser) => ser, Err(err) => { @@ -460,76 +474,69 @@ async fn process_add_assertion( } }; - let req = DbRequest { - request: DbOperation::Insert(id.to_vec(), ser_assertion), - response: tx, - }; + if let Some(abi) = compiler_abi { + compiler_abi::store(db, id, abi).await?; + } - db.send(req)?; + db::insert_assertion(db, id.to_vec(), ser_assertion).await?; let result = DaSubmissionResponse { id, prover_signature: stored_assertion.prover_signature.as_bytes().into(), }; - match rx.await { - Ok(_) => { - debug!(target: "json_rpc", %request_id, %client_ip, json_rpc_id = %json_rpc_id, ?id, "Successfully stored assertion in database"); - Ok(rpc_response(json_rpc, result)) - } - Err(err) => { - debug!(target: "json_rpc", %request_id, %client_ip, json_rpc_id = %json_rpc_id, error = %err, "Database operation failed for assertion storage"); - Ok(rpc_error_with_request_id( - json_rpc, - -32603, - "Internal error c", - &request_id, - )) - } - } + debug!(target: "json_rpc", %request_id, %client_ip, json_rpc_id = %json_rpc_id, ?id, "Successfully stored assertion in database"); + Ok(rpc_response(json_rpc, result)) } async fn process_get_assertion( id: B256, db: &DbRequestSender, + docker: Arc, json_rpc: &Value, request_id: Uuid, client_ip: &str, json_rpc_id: &Value, ) -> Result { - let (tx, rx) = oneshot::channel(); - let req = DbRequest { - request: DbOperation::Get(id.to_vec()), - response: tx, + let res = db::get_value(db, id.to_vec()).await?; + + let Some(val) = res else { + warn!(target: "json_rpc", %request_id, %client_ip, json_rpc_id = %json_rpc_id, ?id, "Assertion not found in database"); + return Ok(rpc_error_with_request_id( + json_rpc, + 404, + "Assertion not found", + &request_id, + )); + }; + let stored_assertion: StoredAssertion = bincode::deserialize(&val)?; + let compiler_abi = compiler_abi::resolve( + db, + id, + LegacyCompilerArtifact { + contract_name: &stored_assertion.assertion_contract_name, + compiler_version: &stored_assertion.compiler_version, + source: &stored_assertion.solidity_source, + deployment_bytecode: &stored_assertion.bytecode, + }, + docker, + ) + .await?; + let has_compiler_metadata = !stored_assertion.solidity_source.is_empty(); + + let result = DaFetchResponse { + solidity_source: stored_assertion.solidity_source, + bytecode: stored_assertion.bytecode.into(), + prover_signature: stored_assertion.prover_signature.as_bytes().into(), + encoded_constructor_args: stored_assertion.encoded_constructor_args, + constructor_abi_signature: stored_assertion.constructor_abi_signature, + assertion_contract_name: has_compiler_metadata + .then_some(stored_assertion.assertion_contract_name), + compiler_version: has_compiler_metadata.then_some(stored_assertion.compiler_version), + abi: compiler_abi, }; - db.send(req)?; - let res = rx.await?; - - match res { - Some(DbResponse::Value(val)) => { - let stored_assertion: StoredAssertion = bincode::deserialize(&val)?; - - let result = DaFetchResponse { - solidity_source: stored_assertion.solidity_source, - bytecode: stored_assertion.bytecode.into(), - prover_signature: stored_assertion.prover_signature.as_bytes().into(), - encoded_constructor_args: stored_assertion.encoded_constructor_args, - constructor_abi_signature: stored_assertion.constructor_abi_signature, - }; - - Ok(rpc_response(json_rpc, result)) - } - None => { - warn!(target: "json_rpc", %request_id, %client_ip, json_rpc_id = %json_rpc_id, ?id, "Assertion not found in database"); - Ok(rpc_error_with_request_id( - json_rpc, - 404, - "Assertion not found", - &request_id, - )) - } - } + Ok(rpc_response(json_rpc, result)) } fn rpc_response(request: &Value, result: T) -> String { @@ -580,13 +587,20 @@ mod tests { use crate::api::{ db::listen_for_db, serve, + types::{ + DbOperation, + DbRequest, + }, }; use alloy::primitives::hex; use sled::Config as DbConfig; use tempfile::TempDir; use tokio::{ net::TcpListener, - sync::mpsc, + sync::{ + mpsc, + oneshot, + }, }; use tokio_util::sync::CancellationToken; @@ -628,7 +642,7 @@ mod tests { // Start the database listener tokio::spawn(async move { - listen_for_db(db_receiver, db, CancellationToken::new()) + Box::pin(listen_for_db(db_receiver, db, CancellationToken::new())) .await .unwrap(); }); @@ -639,7 +653,7 @@ mod tests { #[tokio::test] #[cfg(feature = "debug_assertions")] async fn test_submit_assertion() { - let (_temp_dir, _db_sender, _signer, server_url) = setup_test_env().await; + let (_temp_dir, _db_sender, _signer, server_url) = Box::pin(setup_test_env()).await; // Bytecode for a simple contract let bytecode = "608060405234801561001057600080fd5b50610150806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80632e64cec11461003b5780636057361d14610059575b600080fd5b610043610075565b60405161005091906100a1565b60405180910390f35b610073600480360381019061006e91906100ed565b61007e565b005b60008054905090565b8060008190555050565b6000819050919050565b61009b81610088565b82525050565b60006020820190506100b66000830184610092565b92915050565b600080fd5b6100ca81610088565b81146100d557600080fd5b50565b6000813590506100e7816100c1565b92915050565b600060208284031215610103576101026100bc565b5b6000610111848285016100d8565b9150509291505056fea2646970667358221220ec5ef0b1d98f01639556c6ea2467c504e5f0d7d7c6eb9b622bb6d8ac93d5fed464736f6c63430008110033"; @@ -673,9 +687,9 @@ mod tests { #[tokio::test] async fn test_submit_solidity_assertion() { - let (_temp_dir, _db_sender, _signer, server_url) = setup_test_env().await; + let (_temp_dir, _db_sender, _signer, server_url) = Box::pin(setup_test_env()).await; - let source_code = r#" + let source_code = r" // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; @@ -690,7 +704,7 @@ mod tests { return value; } } - "#; + "; let request_body = json!({ "jsonrpc": "2.0", @@ -725,6 +739,29 @@ mod tests { assert!(id.is_string()); assert!(response_json["result"]["prover_signature"].is_string()); + let fetched: Value = client + .post(&server_url) + .json(&json!({ + "jsonrpc": "2.0", + "method": "da_get_assertion", + "params": [id], + "id": 3 + })) + .send() + .await + .unwrap() + .json() + .await + .unwrap(); + assert_eq!( + fetched["result"]["assertion_contract_name"], + "SimpleStorage" + ); + assert_eq!(fetched["result"]["compiler_version"], "0.8.17"); + assert!(fetched["result"]["abi"] + .as_array() + .is_some_and(|abi| abi.iter().any(|item| item["name"] == "set"))); + let request_with_args = json!({ "jsonrpc": "2.0", "method": "da_submit_solidity_assertion", @@ -759,13 +796,13 @@ mod tests { #[tokio::test] async fn test_get_assertion() { - let (_temp_dir, db_sender, signer, server_url) = setup_test_env().await; + let (_temp_dir, db_sender, signer, server_url) = Box::pin(setup_test_env()).await; // First submit an assertion let source_code = "contract Test { }"; let stored_assertion = StoredAssertion { assertion_contract_name: "Test".to_string(), - compiler_version: "0.8.17".to_string(), + compiler_version: "legacy".to_string(), solidity_source: source_code.to_string(), bytecode: vec![1, 2, 3, 4], prover_signature: signer.sign_hash(&keccak256([1, 2, 3, 4])).await.unwrap(), @@ -807,6 +844,9 @@ mod tests { let result = response_json["result"].as_object().unwrap(); assert_eq!(result["solidity_source"].as_str().unwrap(), source_code); + assert_eq!(result["assertion_contract_name"], "Test"); + assert_eq!(result["compiler_version"], "legacy"); + assert!(result.get("abi").is_none()); assert_eq!( hex::decode( result["bytecode"] @@ -821,7 +861,7 @@ mod tests { #[tokio::test] async fn test_get_nonexistent_assertion() { - let (_temp_dir, _db_sender, _signer, server_url) = setup_test_env().await; + let (_temp_dir, _db_sender, _signer, server_url) = Box::pin(setup_test_env()).await; let request_body = json!({ "jsonrpc": "2.0", @@ -849,7 +889,7 @@ mod tests { #[tokio::test] async fn test_invalid_method() { - let (_temp_dir, _db_sender, _signer, server_url) = setup_test_env().await; + let (_temp_dir, _db_sender, _signer, server_url) = Box::pin(setup_test_env()).await; let request_body = json!({ "jsonrpc": "2.0", diff --git a/crates/server/src/api/source_compilation.rs b/crates/server/src/api/source_compilation.rs index 8881a85..e7f883e 100644 --- a/crates/server/src/api/source_compilation.rs +++ b/crates/server/src/api/source_compilation.rs @@ -1,3 +1,4 @@ +use alloy::json_abi::JsonAbi; use anyhow::Result; use bollard::container::{ Config, @@ -12,7 +13,6 @@ use bollard::Docker; use futures::TryStreamExt; use futures_util::stream::StreamExt; use metrics; -use regex::Regex; use serde_json::Value; use std::sync::Arc; use tempfile::TempDir; @@ -28,6 +28,15 @@ use tracing::{ }; use uuid::Uuid; +/// Upper bound for the compiler ABI persisted and returned with an assertion. +pub const MAX_COMPILER_ABI_BYTES: usize = 1024 * 1024; + +#[derive(Debug, Clone)] +pub struct CompilationArtifact { + pub bytecode: Vec, + pub abi: JsonAbi, +} + /// Maximum number of attempts to ensure image availability const MAX_ATTEMPTS: u32 = 3; /// Initial delay of the exponential backoff for image availability check @@ -46,10 +55,10 @@ pub struct SolcArgs { impl Default for SolcArgs { fn default() -> Self { Self { - output_type: "bin".to_string(), + output_type: "bin,abi".to_string(), metadata_hash: "none".to_string(), combined_json: true, - file_path: "".to_string(), + file_path: String::new(), base_path: None, } } @@ -66,6 +75,7 @@ impl SolcArgs { self } + #[must_use] pub fn to_command_args(&self) -> Vec { let mut args = Vec::new(); @@ -82,7 +92,7 @@ impl SolcArgs { args.push(base_path.clone()); } - args.push(self.file_path.to_string()); + args.push(self.file_path.clone()); args } @@ -94,6 +104,7 @@ pub struct DockerImageManager { } impl DockerImageManager { + #[must_use] pub fn new(docker: Arc) -> Self { #[cfg(target_arch = "aarch64")] debug!( @@ -105,6 +116,10 @@ impl DockerImageManager { } /// Ensures an image is available locally, pulling it if necessary + /// + /// # Errors + /// + /// Returns an error when Docker cannot inspect or pull the compiler image. pub async fn ensure_image_available(&self, image_name: &str) -> Result<(), CompilationError> { let mut attempts = 0; let mut delay = INITIAL_DELAY; @@ -112,7 +127,7 @@ impl DockerImageManager { loop { attempts += 1; match self.try_ensure_image_available(image_name).await { - Ok(_) => return Ok(()), + Ok(()) => return Ok(()), Err(e) => { if attempts >= MAX_ATTEMPTS { return Err(e); @@ -141,8 +156,10 @@ impl DockerImageManager { .iter() .any(|img| img.repo_tags.contains(&image_name.to_string())); - // Only pull the image if it doesn't exist locally - if !image_exists { + // Only pull the image if it does not exist locally. + if image_exists { + debug!(target: "solidity_compilation", image_name, "Solc image exists locally"); + } else { debug!(target: "solidity_compilation", "Pulling image: {}", image_name); self.docker .create_image( @@ -156,8 +173,6 @@ impl DockerImageManager { ) .try_collect::>() .await?; - } else { - debug!(target: "solidity_compilation", image_name, "Solc image exists locally"); } Ok(()) } @@ -173,6 +188,7 @@ pub struct ContainerManager { impl ContainerManager { /// Create a new container manager + #[must_use] pub fn new(docker: Arc, name_prefix: &str) -> Self { let container_name = format!("{}_{}", name_prefix, Uuid::new_v4()); Self { @@ -184,6 +200,10 @@ impl ContainerManager { } /// Create and start a container with the given configuration + /// + /// # Errors + /// + /// Returns an error when Docker cannot create or start the container. pub async fn create_and_start( &mut self, image: &str, @@ -218,6 +238,10 @@ impl ContainerManager { .await?; self.container_id = Some(container.id); + let container_id = self + .container_id + .as_deref() + .ok_or(CompilationError::NoContainerCreated)?; debug!( target: "solidity_compilation", @@ -226,16 +250,18 @@ impl ContainerManager { ); self.docker - .start_container( - self.container_id.as_ref().unwrap(), - None::>, - ) + .start_container(container_id, None::>) .await?; Ok(()) } /// Wait for the container to finish and return its exit code + /// + /// # Errors + /// + /// Returns an error when no container exists, Docker fails, the wait stream ends early, or + /// compilation fails. pub async fn wait_for_exit(&self) -> Result { let container_id = self .container_id @@ -272,6 +298,10 @@ impl ContainerManager { } /// Get container logs + /// + /// # Errors + /// + /// Returns an error when no container exists or Docker cannot read its logs. pub async fn get_logs(&self, stdout: bool, stderr: bool) -> Result { let container_id = self .container_id @@ -308,17 +338,24 @@ impl ContainerManager { } /// Get the container name + #[must_use] pub fn name(&self) -> &str { &self.container_name } /// Explicitly clean up the container + /// + /// # Errors + /// + /// Returns an error when Docker cannot remove the container. pub async fn cleanup(&mut self) -> Result<(), CompilationError> { - if self.is_cleaned_up || self.container_id.is_none() { + if self.is_cleaned_up { return Ok(()); } - let container_id = self.container_id.as_ref().unwrap(); + let Some(container_id) = self.container_id.as_ref() else { + return Ok(()); + }; debug!( target: "solidity_compilation", @@ -350,6 +387,10 @@ pub struct SoliditySourceFile { impl SoliditySourceFile { /// Create a new source file with the given content + /// + /// # Errors + /// + /// Returns an error when the temporary directory or source file cannot be created. pub fn new(source_code: &str) -> Result { let temp_dir = TempDir::new()?; let file_name = format!("{}.sol", Uuid::new_v4()); @@ -364,16 +405,22 @@ impl SoliditySourceFile { } /// Get the file name + #[must_use] pub fn file_name(&self) -> &str { &self.file_name } /// Get the file path + #[must_use] pub fn file_path(&self) -> &std::path::Path { &self.file_path } /// Get the container path configuration + /// + /// # Errors + /// + /// Returns an error when the temporary directory path is not valid UTF-8. pub fn container_paths(&self) -> Result<(String, String, Vec), CompilationError> { let container_root_path = "/code".to_string(); let container_file_path = format!("{}/{}", container_root_path, self.file_name); @@ -401,24 +448,13 @@ pub struct CompilationConfig { } impl CompilationConfig { + #[must_use] pub fn new(assertion_contract_name: &str, source_code: &str, compiler_version: &str) -> Self { - // Validate compiler version format (should be like 0.8.17) - // This regex handles both exact versions (0.8.17) and complex version requirements (=0.8.28 ^0.8.13) - let version_regex = - Regex::new(r"^\d+\.\d+\.\d+$|^=\d+\.\d+\.\d+\s+\^\d+\.\d+\.\d+$").unwrap(); - if !version_regex.is_match(compiler_version) { - debug!( - target: "solidity_compilation", - compiler_version = compiler_version, - "Invalid compiler version format" - ); - } - Self { assertion_contract_name: assertion_contract_name.to_string(), source_code: source_code.to_string(), compiler_version: compiler_version.to_string(), - output_type: "bin".to_string(), + output_type: "bin,abi".to_string(), metadata_hash: "none".to_string(), } } @@ -430,18 +466,19 @@ pub struct CompilationMetrics { } impl CompilationMetrics { - pub fn new(compiler_version: &str) -> Self { - Self { + #[must_use] + pub fn start(compiler_version: &str) -> Self { + let metrics = Self { labels: [("compiler_version", compiler_version.to_string())], - } - } - - pub fn start_compilation(&self) { - metrics::counter!("assertion_compilations_total", &self.labels).increment(1); - metrics::gauge!("assertion_compilations_running", &self.labels).increment(1); + }; + metrics::counter!("assertion_compilations_total", &metrics.labels).increment(1); + metrics::gauge!("assertion_compilations_running", &metrics.labels).increment(1); + metrics } +} - pub fn end_compilation(&self) { +impl Drop for CompilationMetrics { + fn drop(&mut self) { metrics::gauge!("assertion_compilations_running", &self.labels).decrement(1); } } @@ -452,21 +489,27 @@ pub struct SolidityCompiler { } impl SolidityCompiler { + #[must_use] pub fn new(docker: Arc) -> Self { Self { docker } } - /// Compiles a Solidity contract and returns the bytecode - pub async fn compile(&self, config: CompilationConfig) -> Result, CompilationError> { - let metrics = CompilationMetrics::new(&config.compiler_version); - metrics.start_compilation(); - - // Ensure the contract exists in the source code - if !config.source_code.contains(&config.assertion_contract_name) { - return Err(CompilationError::ContractNotFound( - config.assertion_contract_name, + /// Compiles a Solidity contract and returns its deployment bytecode and ABI. + /// + /// # Errors + /// + /// Returns an error for unsupported compiler versions, Docker failures, source-file failures, + /// compilation failures, or invalid compiler output. + pub async fn compile( + &self, + config: CompilationConfig, + ) -> Result { + if !is_supported_compiler_version(&config.compiler_version) { + return Err(CompilationError::InvalidCompilerVersion( + config.compiler_version, )); } + let _metrics = CompilationMetrics::start(&config.compiler_version); // Set up the image let image_name = format!("ethereum/solc:{}", config.compiler_version); @@ -489,24 +532,18 @@ impl SolidityCompiler { solc_args.metadata_hash = config.metadata_hash; // Execute the compilation - let result = self - .execute_compilation( - &image_name, - solc_args.to_command_args(), - mount_binds, - &config.assertion_contract_name, - source_file.file_name(), - ) - .instrument(tracing::info_span!("execute_compilation")) - .await; - - // Always decrement the metrics counter - metrics.end_compilation(); - - result + self.execute_compilation( + &image_name, + solc_args.to_command_args(), + mount_binds, + &config.assertion_contract_name, + source_file.file_name(), + ) + .instrument(tracing::info_span!("execute_compilation")) + .await } - /// Executes the compilation in a container and extracts the bytecode + /// Executes the compilation in a container and extracts the compiler artifact. #[instrument(name = "execute_compilation", skip(self, mount_binds))] async fn execute_compilation( &self, @@ -515,7 +552,7 @@ impl SolidityCompiler { mount_binds: Vec, contract_name: &str, file_name: &str, - ) -> Result, CompilationError> { + ) -> Result { // Create and run the container let mut container = ContainerManager::new(self.docker.clone(), contract_name); // Use a defer-like pattern with Result to ensure cleanup happens @@ -536,10 +573,7 @@ impl SolidityCompiler { // Get the output logs let log_messages = container.get_logs(true, false).await?; - // Parse output and extract bytecode - let bytecode = self - .extract_bytecode(&log_messages, file_name, contract_name) - .await?; + let artifact = extract_compilation_artifact(&log_messages, file_name, contract_name)?; debug!( target: "solidity_compilation", @@ -547,7 +581,7 @@ impl SolidityCompiler { "Finished Solidity compilation" ); - Ok(bytecode) + Ok(artifact) } .await; @@ -563,59 +597,86 @@ impl SolidityCompiler { result } +} - /// Parse the JSON output and extract the bytecode - async fn extract_bytecode( - &self, - json_output: &str, - file_name: &str, - contract_name: &str, - ) -> Result, CompilationError> { - // Parse the JSON output from solc - let output: Value = serde_json::from_str(json_output)?; - - // Extract the bytecode from the assertion contract - let contracts = output["contracts"] - .as_object() - .ok_or_else(|| CompilationError::InvalidJsonOutput)?; - - let contract_key = format!("{file_name}:{contract_name}"); - let bytecode = contracts[&contract_key]["bin"] - .as_str() - .ok_or_else(|| CompilationError::MissingBytecode)?; - - // Convert hex bytecode to bytes - let bytecode_bytes = hex::decode(bytecode)?; - - Ok(bytecode_bytes) +fn extract_compilation_artifact( + json_output: &str, + file_name: &str, + contract_name: &str, +) -> Result { + let output: Value = serde_json::from_str(json_output)?; + let contracts = output["contracts"] + .as_object() + .ok_or(CompilationError::InvalidJsonOutput)?; + let contract_key = format!("{file_name}:{contract_name}"); + let contract = contracts + .get(&contract_key) + .ok_or_else(|| CompilationError::ContractOutputNotFound(contract_key.clone()))?; + let bytecode = contract["bin"] + .as_str() + .ok_or(CompilationError::MissingBytecode)?; + let abi_value = contract + .get("abi") + .filter(|value| value.is_array()) + .cloned() + .ok_or(CompilationError::MissingAbi)?; + let abi: JsonAbi = serde_json::from_value(abi_value)?; + let abi_bytes = serde_json::to_vec(&abi)?.len(); + if abi_bytes > MAX_COMPILER_ABI_BYTES { + return Err(CompilationError::AbiTooLarge { + actual: abi_bytes, + maximum: MAX_COMPILER_ABI_BYTES, + }); } + Ok(CompilationArtifact { + bytecode: hex::decode(bytecode)?, + abi, + }) } /// Compiles Solidity source code using a Docker container with the specified compiler version +/// +/// # Errors +/// +/// Returns an error for unsupported compiler versions, Docker failures, compilation failures, or +/// invalid compiler output. pub async fn compile_solidity( assertion_contract_name: &str, source_code: &str, compiler_version: &str, docker: Arc, -) -> Result> { +) -> Result { let config = CompilationConfig::new(assertion_contract_name, source_code, compiler_version); let compiler = SolidityCompiler::new(docker); - compiler.compile(config).await.map_err(Into::into) + compiler.compile(config).await +} + +#[must_use] +pub fn is_supported_compiler_version(value: &str) -> bool { + semver::Version::parse(value).is_ok_and(|version| { + version.pre.is_empty() && version.build.is_empty() && version.to_string() == value + }) } #[derive(Debug, thiserror::Error)] pub enum CompilationError { - #[error("Contract '{0}' not found in source code")] - ContractNotFound(String), - #[error("Compilation failed: {0}")] CompilationFailed(String), #[error("Missing bytecode in compiler output")] MissingBytecode, - #[error("Invalid compiler output: {0}")] - InvalidOutput(String), + #[error("Missing ABI in compiler output")] + MissingAbi, + + #[error("Compiler output did not contain contract '{0}'")] + ContractOutputNotFound(String), + + #[error("Compiler ABI is {actual} bytes; maximum is {maximum} bytes")] + AbiTooLarge { actual: usize, maximum: usize }, + + #[error("Invalid compiler version '{0}'")] + InvalidCompilerVersion(String), #[error("Docker error {0}")] DockerError(#[from] bollard::errors::Error), @@ -642,11 +703,11 @@ pub enum CompilationError { #[cfg(test)] mod tests { use super::*; - use once_cell::sync::Lazy; + use std::sync::LazyLock; // Shared Docker client for all tests - static DOCKER: Lazy> = - Lazy::new(|| Arc::new(Docker::connect_with_local_defaults().unwrap())); + static DOCKER: LazyLock> = + LazyLock::new(|| Arc::new(Docker::connect_with_local_defaults().unwrap())); fn setup_docker() -> Arc { DOCKER.clone() @@ -655,7 +716,7 @@ mod tests { #[tokio::test] async fn test_successful_compilation() { let docker = setup_docker(); - let source_code = r#" + let source_code = r" // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; @@ -670,12 +731,16 @@ mod tests { return value; } } - "#; + "; let result = compile_solidity("SimpleStorage", source_code, "0.8.17", docker).await; assert!(result.is_ok(), "Compilation should succeed"); - let bytecode = result.unwrap(); - assert!(!bytecode.is_empty(), "Bytecode should not be empty"); + let artifact = result.unwrap(); + assert!( + !artifact.bytecode.is_empty(), + "Bytecode should not be empty" + ); + assert!(artifact.abi.functions.contains_key("set")); } #[tokio::test] @@ -690,7 +755,7 @@ mod tests { #[tokio::test] async fn test_multiple_contracts() { let docker = setup_docker(); - let source_code = r#" + let source_code = r" // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; @@ -701,7 +766,7 @@ mod tests { contract Second { string private name; } - "#; + "; let result = compile_solidity("First", source_code, "0.8.17", docker).await; assert!(result.is_ok(), "Should succeed with multiple contracts"); @@ -710,11 +775,11 @@ mod tests { #[tokio::test] async fn test_syntax_error() { let docker = setup_docker(); - let source_code = r#" + let source_code = r" contract BrokenContract { This is not valid Solidity; } - "#; + "; let result = compile_solidity("BrokenContract", source_code, "0.8.17", docker).await; assert!(result.is_err(), "Should fail with syntax error"); @@ -730,14 +795,14 @@ mod tests { #[tokio::test] async fn test_different_compiler_versions() { let docker = setup_docker(); - let source_code = r#" + let source_code = r" // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract VersionTest { uint256 private value; } - "#; + "; // Test multiple compiler versions let versions = vec!["0.8.17", "0.8.20", "0.8.24"]; @@ -749,7 +814,10 @@ mod tests { result.is_ok(), "Compilation should succeed with version {version}" ); - assert!(!result.unwrap().is_empty(), "Bytecode should not be empty"); + assert!( + !result.unwrap().bytecode.is_empty(), + "Bytecode should not be empty" + ); } } @@ -764,7 +832,7 @@ mod tests { "Complex contract compilation should succeed" ); assert!( - !result.unwrap().is_empty(), + !result.unwrap().bytecode.is_empty(), "Complex contract bytecode should not be empty" ); } @@ -772,14 +840,14 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn test_concurrent_compilations() { let docker = setup_docker(); - let source_code = r#" + let source_code = r" // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Simple { uint256 private value; } - "#; + "; let mut handles = vec![]; @@ -797,10 +865,60 @@ mod tests { for handle in handles { let result = handle.await.unwrap(); assert!(result.is_ok(), "Concurrent compilation should succeed"); - assert!(!result.unwrap().is_empty(), "Bytecode should not be empty"); + assert!( + !result.unwrap().bytecode.is_empty(), + "Bytecode should not be empty" + ); } } + #[test] + fn extracts_exact_contract_artifact() { + let output = serde_json::json!({ + "contracts": { + "Assertion.sol:First": { + "bin": "6001", + "abi": [{"type": "function", "name": "first", "inputs": [], "outputs": []}] + }, + "Assertion.sol:Second": { + "bin": "6002", + "abi": [{"type": "function", "name": "second", "inputs": [], "outputs": []}] + } + } + }); + + let artifact = + extract_compilation_artifact(&output.to_string(), "Assertion.sol", "Second").unwrap(); + + assert_eq!(artifact.bytecode, hex::decode("6002").unwrap()); + assert!(artifact.abi.functions.contains_key("second")); + } + + #[test] + fn rejects_missing_or_non_array_abi() { + for abi in [Value::Null, Value::String("[]".to_string())] { + let output = serde_json::json!({ + "contracts": { + "Assertion.sol:Assertion": {"bin": "6001", "abi": abi} + } + }); + + assert!(matches!( + extract_compilation_artifact(&output.to_string(), "Assertion.sol", "Assertion"), + Err(CompilationError::MissingAbi) + )); + } + } + + #[test] + fn rejects_invalid_compiler_version_before_docker() { + assert!(!is_supported_compiler_version("latest")); + assert!(!is_supported_compiler_version("0.8")); + assert!(!is_supported_compiler_version("0.8.28-nightly")); + assert!(!is_supported_compiler_version("01.8.28")); + assert!(is_supported_compiler_version("0.8.28")); + } + #[tokio::test] async fn test_docker_image_manager() { let docker = setup_docker(); @@ -825,7 +943,9 @@ mod tests { let source_file = SoliditySourceFile::new(source_code).unwrap(); assert!( - source_file.file_name().ends_with(".sol"), + std::path::Path::new(source_file.file_name()) + .extension() + .is_some_and(|extension| extension.eq_ignore_ascii_case("sol")), "File should have .sol extension" ); assert!( @@ -925,14 +1045,14 @@ mod tests { #[tokio::test] async fn test_container_cleanup_after_compilation() { let docker = setup_docker(); - let source_code = r#" + let source_code = r" // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract CleanupTest { uint256 private value; } - "#; + "; let result = compile_solidity("CleanupTest", source_code, "0.8.17", docker.clone()).await; assert!(result.is_ok(), "Compilation should succeed"); @@ -989,23 +1109,21 @@ mod tests { .await .expect("Failed to list images"); - let image_exists = images - .iter() - .any(|img| img.repo_tags.contains(&image_name.to_string())); + let image_exists = images.iter().any(|img| img.repo_tags.contains(&image_name)); if image_exists { println!("Warning: Could not remove image for testing, test may be less reliable"); } // Use a simple contract with a pragma that works with our specific version - let source_code = r#" + let source_code = r" // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract TestPull { uint256 private value; } - "#; + "; // Run compilation which should trigger a pull let result = compile_solidity( @@ -1030,7 +1148,7 @@ mod tests { let image_exists_after = images_after .iter() - .any(|img| img.repo_tags.contains(&image_name.to_string())); + .any(|img| img.repo_tags.contains(&image_name)); assert!(image_exists_after, "Image should exist after compilation"); } diff --git a/crates/server/src/api/types.rs b/crates/server/src/api/types.rs index ef7bad1..4b2c91e 100644 --- a/crates/server/src/api/types.rs +++ b/crates/server/src/api/types.rs @@ -13,6 +13,8 @@ pub enum DbOperation { Get(Vec), /// Inserts into Db Insert(Vec, Vec), + /// Inserts auxiliary metadata without changing assertion-count metrics. + InsertMetadata(Vec, Vec), } #[derive(Debug, Clone)] diff --git a/crates/server/src/config.rs b/crates/server/src/config.rs index ac671ee..2385611 100644 --- a/crates/server/src/config.rs +++ b/crates/server/src/config.rs @@ -8,6 +8,7 @@ use std::{ sync::Arc, }; +use anyhow::Context; use bollard::Docker; use clap::Parser; use tokio::net::TcpListener; @@ -39,33 +40,36 @@ pub struct Config { } impl Config { /// Build the assertion DA Server + /// + /// # Errors + /// + /// Returns an error when the listener, data directory, database, or Docker connection cannot + /// be initialized. pub async fn build(self) -> anyhow::Result { // Bind to an address let listener = TcpListener::bind(&self.listen_addr).await?; tracing::info!(listen_addr = ?self.listen_addr, "Listening on address"); // Get the database path - let root_dir = - directories::ProjectDirs::from("com", "phylaxsystems", "assertion-da").unwrap(); - let db_path = if let Some(db_path) = &self.db_path { - db_path - } else { - &root_dir.data_dir().join("db") - }; + let root_dir = directories::ProjectDirs::from("com", "phylaxsystems", "assertion-da") + .context("failed to resolve assertion-da data directory")?; + let db_path = self + .db_path + .unwrap_or_else(|| root_dir.data_dir().join("db")); // Try to open the sled db let db: Db<{ crate::LEAF_FANOUT }> = DbConfig::new() - .path(db_path.clone()) + .path(&db_path) .cache_capacity_bytes(self.cache_size) .open()?; let db_size = db.size_on_disk()?; tracing::info!( database_size_mbs = db_size, - database_path = db_path.to_str().unwrap(), + database_path = %db_path.display(), "Opened database" ); - metrics::gauge!("db_size_mb").set(db_size as u32); + metrics::gauge!("db_size_mb").set(u32::try_from(db_size).unwrap_or(u32::MAX)); // Connect to Docker daemon let docker = Arc::new(Docker::connect_with_local_defaults()?); @@ -75,7 +79,7 @@ impl Config { listener, db, docker, - private_key: self.private_key.clone(), + private_key: self.private_key, }; Ok(server) @@ -116,15 +120,16 @@ mod tests { let cancel_token_clone = cancel_token.clone(); let task_handle = tokio::task::spawn(async move { - server.run(cancel_token_clone).await.unwrap(); + Box::pin(server.run(cancel_token_clone)).await.unwrap(); }); std::thread::sleep(std::time::Duration::from_secs(1)); let da_client = assertion_da_client::DaClient::new(&format!("http://{listen_addr}")).unwrap(); - if let Err(DaClientError::JsonRpcError { code, message }) = - da_client.fetch_assertion(Default::default()).await + if let Err(DaClientError::JsonRpcError { code, message }) = da_client + .fetch_assertion(alloy::primitives::B256::default()) + .await { assert_eq!(code, 404); assert_eq!(message, "Assertion not found"); @@ -146,7 +151,7 @@ mod tests { .unwrap(); // Check default values - assert_eq!(config.cache_size, 1000000); + assert_eq!(config.cache_size, 1_000_000); assert_eq!(config.listen_addr, "0.0.0.0:5001".parse().unwrap()); assert_eq!(config.log_level, LevelFilter::INFO); assert_eq!(config.metrics_addr.to_string(), "0.0.0.0:9002"); @@ -171,7 +176,7 @@ mod tests { ]) .unwrap(); - assert_eq!(config.cache_size, 2000000); + assert_eq!(config.cache_size, 2_000_000); assert_eq!(config.listen_addr, "127.0.0.1:8080".parse().unwrap()); assert_eq!(config.log_level, LevelFilter::DEBUG); assert_eq!(config.db_path, Some(PathBuf::from("/tmp/test-db"))); diff --git a/crates/server/src/encode_args.rs b/crates/server/src/encode_args.rs index c9d0516..627c26d 100644 --- a/crates/server/src/encode_args.rs +++ b/crates/server/src/encode_args.rs @@ -48,7 +48,7 @@ where .collect() } -/// Helper function to coerce a value to a [DynSolValue] given a type string +/// Helper function to coerce a value to a [`DynSolValue`] given a type string pub fn coerce_value(ty: &str, arg: &str) -> Result { let ty = DynSolType::parse(ty)?; Ok(DynSolType::coerce_str(&ty, arg)?) diff --git a/crates/server/src/lib.rs b/crates/server/src/lib.rs index eb908eb..1fb98f0 100644 --- a/crates/server/src/lib.rs +++ b/crates/server/src/lib.rs @@ -1,3 +1,5 @@ +#![recursion_limit = "256"] + pub mod api; mod config; mod encode_args; diff --git a/crates/server/src/server.rs b/crates/server/src/server.rs index d9144e1..7f9a4ed 100644 --- a/crates/server/src/server.rs +++ b/crates/server/src/server.rs @@ -42,6 +42,11 @@ impl DaServer { (Box::pin(api_handle), Box::pin(db_handle)) } /// Run the server until the cancellation token is cancelled. + /// + /// # Errors + /// + /// Returns an error when either the API server or database listener fails while the other task + /// is shutting down. pub async fn run(self, cancel_token: CancellationToken) -> Result<()> { let (mut api_handle, mut db_handle) = self.start(cancel_token.clone()); loop { @@ -101,8 +106,8 @@ mod tests { // Create server instance let server = DaServer { - db, listener, + db, docker, private_key, }; @@ -113,7 +118,7 @@ mod tests { // Run server in background task let server_handle = tokio::spawn(async move { - server.run(cancel_token).await.unwrap(); + Box::pin(server.run(cancel_token)).await.unwrap(); }); // Wait briefly to ensure server is running