diff --git a/.cargo/config.toml b/.cargo/config.toml index ed5be05dc..7da4a2c47 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -4,3 +4,4 @@ linker = "aarch64-linux-gnu-gcc" [target.x86_64-unknown-linux-gnu] linker = "x86_64-linux-gnu-gcc" +rustflags = ["-C", "target-cpu=x86-64-v3"] diff --git a/Cargo.lock b/Cargo.lock index c3e1e9c02..c49f15e5c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -103,9 +103,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.99" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arbitrary" @@ -113,6 +113,15 @@ version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +[[package]] +name = "arc-swap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +dependencies = [ + "rustversion", +] + [[package]] name = "ark-bls12-377" version = "0.4.0" @@ -604,9 +613,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" dependencies = [ "allocator-api2", ] @@ -823,9 +832,9 @@ dependencies = [ [[package]] name = "cranelift" -version = "0.123.2" +version = "0.132.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cb56989ef9361e99f5702c97f343bd64d3819f02ac4ffb6aea48af3cb21a634" +checksum = "f2948a18918ec81f4701e0f94d6b1727ec2cf485f8e32d332eddc466886a279d" dependencies = [ "cranelift-codegen", "cranelift-frontend", @@ -837,46 +846,48 @@ dependencies = [ [[package]] name = "cranelift-assembler-x64" -version = "0.123.2" +version = "0.132.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0920ef6863433fa28ece7e53925be4cd39a913adba2dc3738f4edd182f76d168" +checksum = "8c80cf55a351448317210f26c434be761bcb25e7b36116ec92f89540b73e2833" dependencies = [ "cranelift-assembler-x64-meta", ] [[package]] name = "cranelift-assembler-x64-meta" -version = "0.123.2" +version = "0.132.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8990a217e2529a378af1daf4f8afa889f928f07ebbde6ae2f058ae60e40e2c20" +checksum = "07937ca8617b340162fe3a4716be885b5847e9b56d6c7a89abbe4d42340fdc91" dependencies = [ "cranelift-srcgen", ] [[package]] name = "cranelift-bforest" -version = "0.123.2" +version = "0.132.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62225596b687f69a42c038485a28369badc186cb7c74bd9436eeec9f539011b1" +checksum = "88217b08180882436d54c0133274885c590698ae854e352bede1cda041230800" dependencies = [ "cranelift-entity", + "wasmtime-internal-core", ] [[package]] name = "cranelift-bitset" -version = "0.123.2" +version = "0.132.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c23914fc4062558650a6f0d8c1846c97b541215a291fdeabc85f68bdc9bbcca3" +checksum = "d5c3cf7ba29fa56e56040848e34835d4e45988b2760ef212413409af95ffd8c1" dependencies = [ "serde", "serde_derive", + "wasmtime-internal-core", ] [[package]] name = "cranelift-codegen" -version = "0.123.2" +version = "0.132.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a238b2f7e7ec077eb170145fa15fd8b3d0f36cc83d8e354e29ca550f339ca7" +checksum = "ebe1aac2efd4cba2047845fce38a68519935a30e20c8a6294ba7e2f448fe722d" dependencies = [ "bumpalo", "cranelift-assembler-x64", @@ -887,8 +898,9 @@ dependencies = [ "cranelift-control", "cranelift-entity", "cranelift-isle", - "gimli 0.32.3", - "hashbrown 0.15.5", + "gimli 0.33.0", + "hashbrown 0.17.1", + "libm", "log", "postcard", "regalloc2", @@ -898,14 +910,14 @@ dependencies = [ "sha2", "smallvec", "target-lexicon", - "wasmtime-internal-math", + "wasmtime-internal-core", ] [[package]] name = "cranelift-codegen-meta" -version = "0.123.2" +version = "0.132.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9315ddcc2512513a9d66455ec89bb70ae5498cb472f5ed990230536f4cd5c011" +checksum = "0909eaf9d6f18f5bf802d50608cb4368ac340fbd03cc44f2888d1cfcc3faa64e" dependencies = [ "cranelift-assembler-x64-meta", "cranelift-codegen-shared", @@ -915,35 +927,36 @@ dependencies = [ [[package]] name = "cranelift-codegen-shared" -version = "0.123.2" +version = "0.132.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc6acea40ef860f28cb36eaad479e26556c1e538b0a66fc44598cf1b1689393d" +checksum = "c95a8da8be283f49cda7d0ef228c94f10d791e517b27b0c7e282dadd2e79ce45" [[package]] name = "cranelift-control" -version = "0.123.2" +version = "0.132.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b2af895da90761cfda4a4445960554fcec971e637882eda5a87337d993fe1b9" +checksum = "f5b19c81145146da1f7afda2e7f52111842fe6793512e740ad5cf3f5639e6212" dependencies = [ "arbitrary", ] [[package]] name = "cranelift-entity" -version = "0.123.2" +version = "0.132.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e8c542c856feb50d504e4fc0526b3db3a514f882a9f68f956164531517828ab" +checksum = "4a55309b47e6633ab05821304206cb1e92952e845b1224985562bb7ac1e92323" dependencies = [ "cranelift-bitset", "serde", "serde_derive", + "wasmtime-internal-core", ] [[package]] name = "cranelift-frontend" -version = "0.123.2" +version = "0.132.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9996dd9c20929c03360fe0c4edf3594c0cbb94525bdbfa04b6bb639ec14573c7" +checksum = "064d2d3533d9608f1cf44c8899cf2f7f33feb70300b0fb83e687b0d9e7b91147" dependencies = [ "cranelift-codegen", "log", @@ -953,15 +966,15 @@ dependencies = [ [[package]] name = "cranelift-isle" -version = "0.123.2" +version = "0.132.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928b8dccad51b9e0ffe54accbd617da900239439b13d48f0f122ab61105ca6ad" +checksum = "1ac4e0bc095b2dab2212d1e99d7a74b62afc1485db023f1c0cb34a68758f7bd1" [[package]] name = "cranelift-jit" -version = "0.123.2" +version = "0.132.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d332429130ebdd9ee1ed97d2db3684fadbff27556df08d9f346361c22be5972" +checksum = "5b48c2a0720c7d62aadd508c662b9bf666b614a47a888589e553e0511620635e" dependencies = [ "anyhow", "cranelift-codegen", @@ -971,17 +984,18 @@ dependencies = [ "cranelift-native", "libc", "log", + "memmap2 0.2.3", "region", "target-lexicon", "wasmtime-internal-jit-icache-coherence", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "cranelift-module" -version = "0.123.2" +version = "0.132.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fc01757dd26534ecbf14dbd183e7430daf3db2d86558f8419fea8726261beed" +checksum = "28f05d9efce7a4e8c2ceec49c76d26e53f1ee8cb13de822b6ca5118d48f50976" dependencies = [ "anyhow", "cranelift-codegen", @@ -990,9 +1004,9 @@ dependencies = [ [[package]] name = "cranelift-native" -version = "0.123.2" +version = "0.132.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f75ef0a6a2efed3a2a14812318e28dc82c214eab5399c13d70878e2f88947b5" +checksum = "09a40053f5cb925451dd1d57393d14ad3145c8e0786701c27b5415ebb9a3ba4f" dependencies = [ "cranelift-codegen", "libc", @@ -1001,22 +1015,22 @@ dependencies = [ [[package]] name = "cranelift-object" -version = "0.123.2" +version = "0.132.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede9e63983baa4c2530dd2ab7ee25b171c6acf0414c00e1f1899606aa8b26d40" +checksum = "5f7a263727954f7b310796e1b5543e6dfd6afed7e15c62f2454b51b6f38a39e1" dependencies = [ "anyhow", "cranelift-codegen", "cranelift-control", "cranelift-module", "log", - "object 0.37.3", + "object 0.39.1", "target-lexicon", ] [[package]] name = "cranelift-pvm" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" dependencies = [ "anyhow", "cranelift", @@ -1030,9 +1044,9 @@ dependencies = [ [[package]] name = "cranelift-srcgen" -version = "0.123.2" +version = "0.132.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673bd6d1c83cb41d60afb140a1474ef6caf1a3e02f3820fc522aefbc93ac67d6" +checksum = "a3ceab9a53f7d362c89841fbaa8e63e44d47c40e91dc96ee6f777fca5d6b323b" [[package]] name = "crc32fast" @@ -1247,7 +1261,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.0", + "windows-sys 0.61.2", ] [[package]] @@ -1356,12 +1370,6 @@ dependencies = [ "typeid", ] -[[package]] -name = "fallible-iterator" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" - [[package]] name = "fastbloom" version = "0.14.0" @@ -1571,11 +1579,12 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "gimli" -version = "0.32.3" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" dependencies = [ - "fallible-iterator", + "fnv", + "hashbrown 0.16.1", "indexmap", "stable_deref_trait", ] @@ -1702,6 +1711,15 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash 0.2.0", +] + [[package]] name = "heapless" version = "0.7.17" @@ -1950,12 +1968,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.1", ] [[package]] @@ -1977,7 +1995,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.61.0", + "windows-sys 0.61.2", ] [[package]] @@ -2230,9 +2248,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.175" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libloading" @@ -2246,9 +2264,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" @@ -2340,6 +2358,15 @@ version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +[[package]] +name = "memmap2" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "723e3ebdcdc5c023db1df315364573789f8857c11b631a2fdfad7c00f5c046b4" +dependencies = [ + "libc", +] + [[package]] name = "memmap2" version = "0.9.8" @@ -2494,12 +2521,12 @@ dependencies = [ [[package]] name = "object" -version = "0.37.3" +version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" dependencies = [ "crc32fast", - "hashbrown 0.15.5", + "hashbrown 0.17.1", "indexmap", "memchr", ] @@ -2557,7 +2584,7 @@ dependencies = [ "libc", "log", "lz4", - "memmap2", + "memmap2 0.9.8", "parking_lot", "rand 0.9.2", "siphasher", @@ -2731,6 +2758,7 @@ name = "pvm" version = "0.0.0" dependencies = [ "anyhow", + "arc-swap", "hex", "log", "pvm-parser", @@ -2759,7 +2787,7 @@ dependencies = [ [[package]] name = "pvmc" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" dependencies = [ "anyhow", "bindgen", @@ -2772,6 +2800,7 @@ dependencies = [ "libc", "postcard", "pvm", + "rayon", "serde", "serde_json", "spacejam-crypto", @@ -2994,13 +3023,13 @@ dependencies = [ [[package]] name = "regalloc2" -version = "0.12.2" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5216b1837de2149f8bc8e6d5f88a9326b63b8c836ed58ce4a0a29ec736a59734" +checksum = "de2c52737737f8609e94f975dee22854a2d5c125772d4b1cf292120f4d45c186" dependencies = [ "allocator-api2", "bumpalo", - "hashbrown 0.15.5", + "hashbrown 0.17.1", "log", "rustc-hash 2.1.1", "serde", @@ -3238,7 +3267,7 @@ version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.61.0", + "windows-sys 0.61.2", ] [[package]] @@ -3284,9 +3313,9 @@ checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0" [[package]] name = "serde" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6c24dee235d0da097043389623fb913daddf92c76e9f5a1db88607a0bcbd1d" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", @@ -3294,7 +3323,7 @@ dependencies = [ [[package]] name = "serde-jam" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" dependencies = [ "anyhow", "serde", @@ -3302,18 +3331,18 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "659356f9a0cb1e529b24c01e43ad2bdf520ec4ceaf83047b83ddcc2251f96383" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -3462,7 +3491,7 @@ dependencies = [ [[package]] name = "spacejam" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" dependencies = [ "anyhow", "async-trait", @@ -3499,7 +3528,7 @@ dependencies = [ [[package]] name = "spacejam-account" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" dependencies = [ "anyhow", "serde-jam", @@ -3510,7 +3539,7 @@ dependencies = [ [[package]] name = "spacejam-core" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" dependencies = [ "anyhow", "erased-serde", @@ -3565,7 +3594,7 @@ dependencies = [ [[package]] name = "spacejam-network" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" dependencies = [ "anyhow", "clap", @@ -3588,7 +3617,7 @@ dependencies = [ [[package]] name = "spacejam-offchain" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" dependencies = [ "anyhow", "async-trait", @@ -3604,7 +3633,7 @@ dependencies = [ [[package]] name = "spacejam-rpc" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" dependencies = [ "anyhow", "jsonrpsee", @@ -3616,7 +3645,7 @@ dependencies = [ [[package]] name = "spacejam-runtime" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" dependencies = [ "anyhow", "hex", @@ -3637,7 +3666,7 @@ dependencies = [ [[package]] name = "spacejam-service" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" dependencies = [ "anyhow", "blake2b_simd", @@ -3651,14 +3680,14 @@ dependencies = [ [[package]] name = "spacejam-spec" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" dependencies = [ "serde", ] [[package]] name = "spacejam-testing" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" dependencies = [ "anyhow", "hex", @@ -3688,7 +3717,7 @@ dependencies = [ [[package]] name = "spacejam-testnet" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" dependencies = [ "anyhow", "clap", @@ -3699,7 +3728,7 @@ dependencies = [ [[package]] name = "spacejam-worker" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" dependencies = [ "anyhow", "pvm", @@ -3715,7 +3744,7 @@ dependencies = [ [[package]] name = "spacejson" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" dependencies = [ "anyhow", "hex", @@ -3726,7 +3755,7 @@ dependencies = [ [[package]] name = "spacejson-derive" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" dependencies = [ "proc-macro2", "quote", @@ -3735,21 +3764,21 @@ dependencies = [ [[package]] name = "spacevm" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" dependencies = [ "anyhow", "hex", - "lru", "pvm", "pvmc", "pvmi", + "rayon", "tracing", "tracing-subscriber", ] [[package]] name = "spacevm-export" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" dependencies = [ "pvm", "pvmc", @@ -4491,24 +4520,25 @@ dependencies = [ ] [[package]] -name = "wasmtime-internal-jit-icache-coherence" -version = "36.0.2" +name = "wasmtime-internal-core" +version = "45.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71aeb74f9b3fd9225319c723e59832a77a674b0c899ba9795f9b2130a6d1b167" +checksum = "1bdae4b55b15a23d774b15f6e7cd90ae0d0aa17c47c12b4db098b3dd11ba9d58" dependencies = [ - "anyhow", - "cfg-if", - "libc", - "windows-sys 0.60.2", + "hashbrown 0.17.1", + "libm", ] [[package]] -name = "wasmtime-internal-math" -version = "36.0.2" +name = "wasmtime-internal-jit-icache-coherence" +version = "45.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31d5dad8a609c6cc47a5f265f13b52e347e893450a69641af082b8a276043fa7" +checksum = "8a312ba8bb77955dcd44294a223e7f124c3071ff966583d385d3f6a4639c62e3" dependencies = [ - "libm", + "cfg-if", + "libc", + "wasmtime-internal-core", + "windows-sys 0.61.2", ] [[package]] @@ -4571,7 +4601,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.0", + "windows-sys 0.61.2", ] [[package]] @@ -4725,9 +4755,9 @@ dependencies = [ [[package]] name = "windows-sys" -version = "0.61.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link 0.2.1", ] diff --git a/Cargo.toml b/Cargo.toml index d5aaccb65..8a5625135 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,12 +21,15 @@ resolver = "1" [workspace.package] edition = "2024" -version = "0.1.2-pre.8" +version = "0.1.2-pre.9" homepage = "https://spacejam.app" repository = "https://github.com/spacejamapp/jade" license = "GPL-3.0" authors = ["clearloop "] +[workspace.metadata.graypaper] +version = "0.7.2" + [profile.prod] inherits = "release" opt-level = 3 @@ -68,6 +71,7 @@ worker = { path = "crates/offchain/worker", package = "spacejam-worker" } # crates.io anyhow = { version = "1.0.99", default-features = false } +arc-swap = "1.7" ark-bls12-381 = "0.5.0" ark-ec = { version = "0.5", default-features = false } ark-ed-on-bls12-381-bandersnatch = { version = "0.5", default-features = false } @@ -83,8 +87,8 @@ blake3 = "1.8.2" cc = "1.2" clap = "4.5.47" colored = "3.0.0" -cranelift = "0.123.2" -cranelift-codegen = "0.123.2" +cranelift = "0.132.0" +cranelift-codegen = "0.132.0" dhat = "0.3" dirs = "6.0.0" ed25519-zebra = "4.1.0" diff --git a/Makefile b/Makefile index 952f109f8..41d1054a2 100644 --- a/Makefile +++ b/Makefile @@ -53,24 +53,39 @@ linux-amd64: # build linux-amd64 with full-spec constants linux-amd64-full: - cargo b --profile prod --target x86_64-unknown-linux-gnu --no-default-features --features bin,serde,full + cargo b --profile prod --target x86_64-unknown-linux-gnu --no-default-features --features full # build both tiny and full binaries for docker linux-amd64-both: cargo b --profile prod -p spacejam --target x86_64-unknown-linux-gnu cp target/x86_64-unknown-linux-gnu/prod/spacejam target/x86_64-unknown-linux-gnu/prod/spacejam-tiny - cargo b --profile prod -p spacejam --target x86_64-unknown-linux-gnu --no-default-features --features bin,serde,full + cargo b --profile prod -p spacejam --target x86_64-unknown-linux-gnu --no-default-features --features full cp target/x86_64-unknown-linux-gnu/prod/spacejam target/x86_64-unknown-linux-gnu/prod/spacejam-full # build the docker image, tagging both :latest and :$(VERSION) docker: linux-amd64-both docker build --platform=linux/amd64 \ - -f docker/spacejam.Dockerfile \ + -f docker/spacejam.dockerfile \ -t $(DOCKER_IMAGE):latest \ -t $(DOCKER_IMAGE):$(VERSION) \ . +# build the fuzz-paired docker images (regular + interpreter) for AOT-vs-int +# A/B comparison on NUMA hosts. +fuzz: docker + docker build --platform=linux/amd64 \ + --build-arg SPACEJAM_INTERP=1 \ + -f docker/spacejam.dockerfile \ + -t $(DOCKER_IMAGE):int \ + -t $(DOCKER_IMAGE):$(VERSION)-int \ + . + # push images to ghcr dpush: docker push $(DOCKER_IMAGE):latest docker push $(DOCKER_IMAGE):$(VERSION) + +# push fuzz-paired images (regular + interpreter) to ghcr +fpush: dpush + docker push $(DOCKER_IMAGE):int + docker push $(DOCKER_IMAGE):$(VERSION)-int diff --git a/crates/codec/erasure/tests/consistency.rs b/crates/codec/erasure/tests/consistency.rs index d796572ad..0326f38c0 100644 --- a/crates/codec/erasure/tests/consistency.rs +++ b/crates/codec/erasure/tests/consistency.rs @@ -15,7 +15,12 @@ async fn ec_4096() -> anyhow::Result<()> { async fn run_codec(test: &str) -> anyhow::Result<()> { let registry = Registry::new(PathBuf::from("../../../res/jam-test-vectors")); - let test = registry.erasure(specjam::Scale::Tiny)?.test(test)?; + let scale = if cfg!(feature = "full") { + specjam::Scale::Full + } else { + specjam::Scale::Tiny + }; + let test = registry.erasure(scale)?.test(test)?; let mut data = hex::decode(test.input.trim_start_matches("0x"))?; let shards = serde_json::from_str::>(&test.output)? .into_iter() @@ -25,12 +30,19 @@ async fn run_codec(test: &str) -> anyhow::Result<()> { }) .collect::>>()?; + let n = erasure::Config::default().original; + let recovery_pairs = || -> Vec<(usize, Vec)> { + let mut pairs: Vec<_> = (0..n - 1).map(|i| (i, shards[i].clone())).collect(); + pairs.push((n, shards[n].clone())); + pairs + }; + // testing sync { let encoded = erasure::encode_sync(data.clone())?; assert_eq!(encoded, shards); - let decoded = erasure::decode_sync(vec![(0, shards[0].clone()), (2, shards[2].clone())])?; + let decoded = erasure::decode_sync(recovery_pairs())?; data.resize(decoded.len(), 0); assert_eq!(decoded, data); } @@ -40,7 +52,7 @@ async fn run_codec(test: &str) -> anyhow::Result<()> { let encoded = erasure::encode(data.clone()).await?; assert_eq!(encoded, shards); - let decoded = erasure::decode(vec![(0, shards[0].clone()), (2, shards[2].clone())]).await?; + let decoded = erasure::decode(recovery_pairs()).await?; data.resize(decoded.len(), 0); assert_eq!(decoded, data); } diff --git a/crates/core/account/src/registry.rs b/crates/core/account/src/registry.rs index 6da2500d1..dd8bd2812 100644 --- a/crates/core/account/src/registry.rs +++ b/crates/core/account/src/registry.rs @@ -41,8 +41,9 @@ pub trait Accounts: Clone + Send + Sync + 'static { fn accounts(&self) -> &BTreeMap; /// Get the removed accounts from the registry - fn removed(&self) -> BTreeSet { - Default::default() + fn removed(&self) -> &BTreeSet { + static EMPTY: std::sync::OnceLock> = std::sync::OnceLock::new(); + EMPTY.get_or_init(BTreeSet::new) } /// Get the diff of the accounts diff --git a/crates/core/src/extrinsic/ticket.rs b/crates/core/src/extrinsic/ticket.rs index 980b9746a..1b469988b 100644 --- a/crates/core/src/extrinsic/ticket.rs +++ b/crates/core/src/extrinsic/ticket.rs @@ -147,7 +147,7 @@ pub enum TicketsOrKeys { impl TicketsOrKeys { /// Returns the fallback keys for the given ring and entropy. #[cfg(feature = "blake2")] - pub fn fallback(ring: Vec, entropy: OpaqueHash) -> Self { + pub fn fallback(ring: &[BandersnatchPublic], entropy: OpaqueHash) -> Self { let mut keys = EpochKeys::default(); for i in 0..crate::EPOCH_LENGTH { let input = [entropy.as_slice(), &i.to_le_bytes()].concat(); diff --git a/crates/crypto/src/ed25519.rs b/crates/crypto/src/ed25519.rs index 00d773be3..3edeb5a5a 100644 --- a/crates/crypto/src/ed25519.rs +++ b/crates/crypto/src/ed25519.rs @@ -2,8 +2,12 @@ #![cfg(feature = "ed25519")] pub use ed25519_zebra::{batch, Signature, SigningKey, VerificationKey, VerificationKeyBytes}; +use rand::rngs::OsRng; use rayon::{iter::ParallelIterator, slice::ParallelSlice}; +/// Number of signatures per parallel chunk for batch verification. +const BATCH_PAR_CHUNK: usize = 32; + /// Ed25519 key pair. #[derive(Clone)] pub struct KeyPair { @@ -36,30 +40,45 @@ pub fn verify(message: &[u8], signature: [u8; 64], key: [u8; 32]) -> anyhow::Res key.verify(&signature, message).map_err(Into::into) } -/// Number of signatures per parallel chunk for batch verification. -const BATCH_PAR_CHUNK: usize = 32; +/// Owned signature item for deferred verification. +pub struct SigItem { + pub message: Vec, + pub signature: [u8; 64], + pub key: [u8; 32], +} + +impl SigItem { + /// Verify a single signature. + pub fn verify(&self) -> anyhow::Result<()> { + verify(&self.message, self.signature, self.key) + } + + /// Batch verify a slice of items. + pub fn batch_verify(items: &[Self]) -> anyhow::Result<()> { + if items.is_empty() { + return Ok(()); + } + let view: Vec<(&[u8], [u8; 64], [u8; 32])> = items + .iter() + .map(|i| (i.message.as_slice(), i.signature, i.key)) + .collect(); + batch_verify(&view) + } +} /// Batch verify a set of Ed25519 signatures. -/// -/// Uses ZIP-215 batch verification (semantically identical to single-verify) and -/// parallelizes across rayon's pool for batches larger than [`BATCH_PAR_CHUNK`]. -/// -/// Returns `Ok(())` iff every `(message, signature, key)` triple is valid. pub fn batch_verify(items: &[(&[u8], [u8; 64], [u8; 32])]) -> anyhow::Result<()> { if items.is_empty() { return Ok(()); } if items.len() <= BATCH_PAR_CHUNK { - return verify_one_batch(items); + return verify_batch(items); } - items - .par_chunks(BATCH_PAR_CHUNK) - .try_for_each(verify_one_batch) + items.par_chunks(BATCH_PAR_CHUNK).try_for_each(verify_batch) } -fn verify_one_batch(items: &[(&[u8], [u8; 64], [u8; 32])]) -> anyhow::Result<()> { - use rand::rngs::OsRng; +fn verify_batch(items: &[(&[u8], [u8; 64], [u8; 32])]) -> anyhow::Result<()> { let mut batch = batch::Verifier::new(); for (msg, sig, key) in items { batch.queue(( @@ -71,7 +90,6 @@ fn verify_one_batch(items: &[(&[u8], [u8; 64], [u8; 32])]) -> anyhow::Result<()> batch.verify(OsRng).map_err(Into::into) } -#[cfg(feature = "rand")] impl Default for KeyPair { fn default() -> Self { use rand::{rngs::OsRng, Rng}; diff --git a/crates/runtime/src/account/registry.rs b/crates/runtime/src/account/registry.rs index 71bb5ee7a..f36579782 100644 --- a/crates/runtime/src/account/registry.rs +++ b/crates/runtime/src/account/registry.rs @@ -90,8 +90,8 @@ impl account::Accounts for Accounts { &self.accounts } - fn removed(&self) -> BTreeSet { - self.removed.clone() + fn removed(&self) -> &BTreeSet { + &self.removed } fn diff(self) -> (Vec<([u8; 31], Vec)>, Vec<[u8; 31]>) { diff --git a/crates/runtime/src/chain/api.rs b/crates/runtime/src/chain/api.rs index b725624ce..9e7d9cf87 100644 --- a/crates/runtime/src/chain/api.rs +++ b/crates/runtime/src/chain/api.rs @@ -156,7 +156,7 @@ impl Runtime { } else { let validators = self.grid().await.next.bandersnatch(); let entropy = self.entropy().await?; - Ok(TicketsOrKeys::fallback(validators, entropy[1])) + Ok(TicketsOrKeys::fallback(&validators, entropy[1])) } } diff --git a/crates/runtime/src/chain/finalizer.rs b/crates/runtime/src/chain/finalizer.rs index b5886365d..b3bb8b931 100644 --- a/crates/runtime/src/chain/finalizer.rs +++ b/crates/runtime/src/chain/finalizer.rs @@ -45,7 +45,7 @@ impl Chain { let mut finalized = BTreeSet::new(); while let Some((slot, (block, commit))) = chain.blocks.pop_first() { let head = block.header.head(); - self.state.commit(Column::State, commit.clone())?; + self.state.commit(Column::State, &commit)?; // finalize the block in storage let root = self.state.root()?; diff --git a/crates/runtime/src/chain/fork.rs b/crates/runtime/src/chain/fork.rs index 164cfc7e5..cc2a2da51 100644 --- a/crates/runtime/src/chain/fork.rs +++ b/crates/runtime/src/chain/fork.rs @@ -101,7 +101,7 @@ impl Fork { chain.insert(this.header.head()); blocks.insert(*slot, (this.clone(), commit.clone())); - branch.commit(Column::State, commit.clone())?; + branch.commit(Column::State, commit)?; } // import the block @@ -144,7 +144,7 @@ impl Fork { tracing::trace!("transiting block"); let head = block.header.head(); let diff = tx::simulate::(&mut block.clone(), self.state.clone())?; - self.state.commit(Column::State, diff.clone())?; + self.state.commit(Column::State, &diff)?; tracing::info!( "imported block#{}@{}, previous block#{}@{}", block.header.slot, @@ -163,7 +163,7 @@ impl Fork { if epoch > prev_epoch && !self.series.contains_key(&epoch) { let validators = self.state.safrole()?.validators.bandersnatch(); let entropy = self.state.entropy()?; - let series = TicketsOrKeys::fallback(validators, entropy[1]); + let series = TicketsOrKeys::fallback(&validators, entropy[1]); self.series.insert(epoch, series); } @@ -194,7 +194,7 @@ impl Fork { } else { let validators = self.state.safrole()?.validators.bandersnatch(); let entropy = self.state.entropy()?; - let series = TicketsOrKeys::fallback(validators, entropy[1]); + let series = TicketsOrKeys::fallback(&validators, entropy[1]); Ok(series) } } diff --git a/crates/runtime/src/chain/importer.rs b/crates/runtime/src/chain/importer.rs index aec55625b..c64e3db89 100644 --- a/crates/runtime/src/chain/importer.rs +++ b/crates/runtime/src/chain/importer.rs @@ -3,7 +3,7 @@ use crate::{ Chain, Config, chain::fork::Fork, - storage::{Branch, Column, KVStorage, StateStorage, SyncStorage}, + storage::{Branch, Column, Commit, KVStorage, StateStorage, SyncStorage}, }; use anyhow::{Context, Result}; use score::{ @@ -158,7 +158,8 @@ impl Chain { } let root = self.state.root()?; - self.state.commit(Column::State, (kvs, vec![]).into())?; + self.state + .commit(Column::State, &Commit::from((kvs, vec![])))?; self.state.finalize( &Block { header, diff --git a/crates/runtime/src/storage/archive.rs b/crates/runtime/src/storage/archive.rs index de013d707..28fcdf102 100644 --- a/crates/runtime/src/storage/archive.rs +++ b/crates/runtime/src/storage/archive.rs @@ -18,7 +18,7 @@ pub trait ArchiveStorage: KVStorage + Send + Sync + 'static { commit.set(key, value); } - self.commit(Column::Archive, commit)?; + self.commit(Column::Archive, &commit)?; Ok(()) } } @@ -40,7 +40,7 @@ impl Archive { } impl KVStorage for Archive { - fn commit(&self, _column: Column, _commit: Commit>) -> Result<()> { + fn commit(&self, _column: Column, _commit: &Commit>) -> Result<()> { anyhow::bail!("commit is not allowed on archive") } diff --git a/crates/runtime/src/storage/branch.rs b/crates/runtime/src/storage/branch.rs index 7c35bd727..02dea0f48 100644 --- a/crates/runtime/src/storage/branch.rs +++ b/crates/runtime/src/storage/branch.rs @@ -37,14 +37,19 @@ impl Branch { } impl KVStorage for Branch { - fn commit(&self, _column: Column, new_commit: Commit>) -> Result<()> { + fn commit(&self, _column: Column, new_commit: &Commit>) -> Result<()> { let mut commit = self .commit .write() .map_err(|_| anyhow::anyhow!("Failed to acquire commit lock"))?; - // Merge the new commit with the existing one - commit.extend(new_commit); + // Merge the new commit with the existing oneqq + for (k, v) in new_commit.iset() { + commit.set(*k, v.clone()); + } + for k in new_commit.iremoval() { + commit.remove(*k); + } Ok(()) } diff --git a/crates/runtime/src/storage/commit.rs b/crates/runtime/src/storage/commit.rs index 0551d86bc..244bd028e 100644 --- a/crates/runtime/src/storage/commit.rs +++ b/crates/runtime/src/storage/commit.rs @@ -78,6 +78,59 @@ where } } +impl Commit { + /// Sorted, deduplicated union of all keys this commit touches. + pub fn dirty_keys(&self) -> Vec { + let mut keys: Vec = self + .update + .keys() + .copied() + .chain(self.removal.iter().copied()) + .collect(); + keys.sort_unstable(); + keys.dedup(); + keys + } +} + +impl Commit +where + Key: Ord + Copy, + Value: AsRef<[u8]>, +{ + /// Merge-walk `base` ∪ `self.update` in key order, skipping `self.removal`. + pub fn merge_with<'a>(&'a self, base: &'a BTreeMap) -> Vec<(Key, &'a [u8])> { + let mut kvs: Vec<(Key, &[u8])> = Vec::with_capacity(base.len() + self.update.len()); + let mut base_iter = base.iter(); + let mut diff_iter = self.update.iter(); + let mut b = base_iter.next(); + let mut d = diff_iter.next(); + while let Some((key, value)) = match (b, d) { + (Some((bk, _)), Some((dk, dv))) if dk <= bk => { + if dk == bk { + b = base_iter.next(); + } + d = diff_iter.next(); + Some((*dk, dv.as_ref())) + } + (Some((bk, bv)), _) => { + b = base_iter.next(); + Some((*bk, bv.as_ref())) + } + (None, Some((dk, dv))) => { + d = diff_iter.next(); + Some((*dk, dv.as_ref())) + } + (None, None) => None, + } { + if !self.removal.contains(&key) { + kvs.push((key, value)); + } + } + kvs + } +} + impl From<(U, R)> for Commit where U: IntoIterator, diff --git a/crates/runtime/src/storage/kv.rs b/crates/runtime/src/storage/kv.rs index e7692540b..d97c339c4 100644 --- a/crates/runtime/src/storage/kv.rs +++ b/crates/runtime/src/storage/kv.rs @@ -12,7 +12,7 @@ use std::{ /// Key-value storage pub trait KVStorage: Send + Sync + 'static { /// Batch write a set of key-value pairs to the storage - fn commit(&self, column: Column, commit: Commit>) -> Result<()>; + fn commit(&self, column: Column, commit: &Commit>) -> Result<()>; /// Set a key-value pair with column specified fn set(&self, column: Column, key: impl AsRef<[u8]>, value: impl AsRef<[u8]>) -> Result<()>; @@ -67,7 +67,7 @@ impl MemoryDb { } impl KVStorage for MemoryDb { - fn commit(&self, _column: Column, commit: Commit>) -> Result<()> { + fn commit(&self, _column: Column, commit: &Commit>) -> Result<()> { let mut data = self .data .write() diff --git a/crates/runtime/src/tx/assurance/mod.rs b/crates/runtime/src/tx/assurance/mod.rs index 470e2260b..f1546d205 100644 --- a/crates/runtime/src/tx/assurance/mod.rs +++ b/crates/runtime/src/tx/assurance/mod.rs @@ -29,17 +29,21 @@ pub fn reports( reports } -/// (W) Handle assurances input and return newly available reports +/// (W) Handle assurances input and return newly available reports. pub fn available( reports: &AvailabilityAssignments, validators: &[ValidatorData], parent: OpaqueHash, assurances: &[AvailAssurance], -) -> Result<(Vec, [u32; CORES_COUNT])> { +) -> Result<( + Vec, + [u32; CORES_COUNT], + Vec, +)> { // Track assurance count per core let mut core_assurance_counts = [0u32; CORES_COUNT]; - // Check for engaged reports: cheap checks first, then batch verify sigs. + // Semantic checks; sig verification is deferred to the caller's batch. let mut assuror = None; for assurance in assurances.iter() { if assurance.validator_index >= VALIDATORS_COUNT { @@ -73,19 +77,14 @@ pub fn available( } } - let messages: Vec> = assurances.iter().map(|a| a.singing_message()).collect(); - let verify_items: Vec<_> = assurances + let triples: Vec = assurances .iter() - .zip(messages.iter()) - .map(|(a, m)| { - ( - m.as_slice(), - a.signature, - validators[a.validator_index as usize].ed25519, - ) + .map(|a| crypto::ed25519::SigItem { + message: a.singing_message(), + signature: a.signature, + key: validators[a.validator_index as usize].ed25519, }) .collect(); - crypto::ed25519::batch_verify(&verify_items).map_err(|_| Error::BadSignature)?; // Check which cores reached 2/3 majority let mut available = Vec::new(); @@ -97,5 +96,5 @@ pub fn available( } } - Ok((available, core_assurance_counts)) + Ok((available, core_assurance_counts, triples)) } diff --git a/crates/runtime/src/tx/block/header.rs b/crates/runtime/src/tx/block/header.rs index 4cd47f2d9..e7c6fe2d0 100644 --- a/crates/runtime/src/tx/block/header.rs +++ b/crates/runtime/src/tx/block/header.rs @@ -9,9 +9,9 @@ use score::{ }; /// Validate the header -pub fn validate(state: State, header: &Header) -> anyhow::Result<()> { +pub fn validate(state: &State, header: &Header) -> anyhow::Result<()> { let new_epoch = header.slot / score::EPOCH_LENGTH > state.timeslot / score::EPOCH_LENGTH; - self::check(&state, header, new_epoch)?; + self::check(state, header, new_epoch)?; // setup the verifier let slot = (header.slot % score::EPOCH_LENGTH) as usize; @@ -30,7 +30,10 @@ pub fn validate(state: State, header: &Header) -> anyhow::Result<()> { }; // check the ticket mark - if new_epoch && state.safrole.accumulator.len() == score::EPOCH_LENGTH as usize { + if new_epoch + && state.timeslot % score::EPOCH_LENGTH >= score::TICKET_SUBMISSION_PERIOD + && state.safrole.accumulator.len() == score::EPOCH_LENGTH as usize + { let mut tickets = [TicketBody::default(); score::EPOCH_LENGTH as usize]; tickets.copy_from_slice(&TicketBody::sequence(&state.safrole.accumulator)); ticket = Some(tickets[slot]); @@ -52,8 +55,7 @@ pub fn validate(state: State, header: &Header) -> anyhow::Result<()> { }; let key = if new_epoch { - let TicketsOrKeys::Keys(keys) = TicketsOrKeys::fallback(vals.clone(), state.entropy[1]) - else { + let TicketsOrKeys::Keys(keys) = TicketsOrKeys::fallback(&vals, state.entropy[1]) else { anyhow::bail!("invalid series"); }; keys[slot] @@ -182,7 +184,7 @@ pub fn check(state: &State, header: &Header, new_epoch: bool) -> anyhow::Result< // Validate ticket attempts for ticket in tickets_mark { - if ticket.attempt > score::TICKET_ENTRIES_PER_VALIDATOR as u8 { + if ticket.attempt >= score::TICKET_ENTRIES_PER_VALIDATOR as u8 { anyhow::bail!("invalid ticket attempt {}", ticket.attempt); } } diff --git a/crates/runtime/src/tx/block/history.rs b/crates/runtime/src/tx/block/history.rs index 80c750991..4d21e9ae8 100644 --- a/crates/runtime/src/tx/block/history.rs +++ b/crates/runtime/src/tx/block/history.rs @@ -23,7 +23,7 @@ pub fn import( reported, }; - history.history.push(new_block.clone()); + history.history.push(new_block); return; }; diff --git a/crates/runtime/src/tx/block/mod.rs b/crates/runtime/src/tx/block/mod.rs index f0adff446..6882c1beb 100644 --- a/crates/runtime/src/tx/block/mod.rs +++ b/crates/runtime/src/tx/block/mod.rs @@ -6,7 +6,6 @@ use crate::{ tx, }; use anyhow::Result; -use crypto::merkle; use pvm::Pvm; use score::{Block, OpaqueHash, TrieKey, state::StateKeyLike}; use std::{ @@ -14,33 +13,24 @@ use std::{ sync::Arc, }; +pub mod header; +pub mod history; + /// Zero hash sentinel — `current_root` before [`TestChain::init`]. const EMPTY_ROOT: OpaqueHash = [0; 32]; -pub mod header; -pub mod history; +type Fork = Branch; -/// DEVELOPMENT: process the block with given state storage. +/// Process the block with given state storage. pub fn process(block: Block, storage: Arc) -> Result<()> { let state = storage.state()?; - let mut block2 = block.clone(); - let state2 = state.clone(); - let (vresult, sresult) = rayon::join( - || header::validate(state, &block.header), - || tx::simulate_with_state::(&mut block2, state2, storage.clone()), - ); - - match (vresult, sresult) { - (Err(e), _) | (_, Err(e)) => Err(e), - (Ok(()), Ok(diff)) => { - storage.commit(Column::State, diff)?; - Ok(()) - } - } + header::validate(&state, &block.header)?; + let mut block = block; + let diff = tx::simulate_with_state::(&mut block, state, storage.clone())?; + storage.commit(Column::State, &diff)?; + Ok(()) } -type Fork = Branch; - /// DEVELOPMENT: A test chain for processing fuzz blocks. pub struct TestChain { /// The finalized head of the chain. @@ -49,8 +39,9 @@ pub struct TestChain { /// The data of the chain. pub data: Arc, - /// The forks and their states (diff only). - pub forks: HashMap, + /// The forks and their states (diff overlay + the state root computed + /// at import time via [`MultiTree::apply`]). + pub forks: HashMap, /// The state root corresponding to `data`. Tracked incrementally via the /// multitree column so we can skip the O(N log N) full retrie per block. @@ -63,37 +54,40 @@ impl TestChain { self.finalized != [0; 32] } - /// Finalize a parent fork by committing its diff into self.data. - fn finalize_fork(&mut self, parent: OpaqueHash) -> anyhow::Result<()> { - if let Some(fork) = self.forks.remove(&parent) { + /// Compute and persist the post-block state root incrementally. + fn compute_fork_root(&self, commit: &Commit>) -> Result { + let prev = (self.current_root != EMPTY_ROOT).then_some(self.current_root); + let dirty = commit.dirty_keys(); + self.data + .with_data(|base| self.data.apply(prev, &commit.merge_with(base), &dirty))? + } + + /// Commit a fork's diff into `self.data` and release orphan siblings. + fn finalize_fork(&mut self, parent: OpaqueHash) -> Result<()> { + if let Some((fork, parent_root)) = self.forks.remove(&parent) { let commit = fork .commit .read() .map_err(|_| anyhow::anyhow!("lock poisoned"))? .clone(); - let dirty = collect_dirty_keys(&commit); - self.data.commit(Column::State, commit)?; - - let prev = (self.current_root != EMPTY_ROOT).then_some(self.current_root); - let new_root = self.data.with_data(|data| { - let kvs: Vec<(TrieKey, &[u8])> = - data.iter().map(|(k, v)| (*k, v.as_slice())).collect(); - self.data.apply(prev, &kvs, &dirty) - })??; - root::set(parent, new_root); - self.current_root = new_root; + self.data.commit(Column::State, &commit)?; + + // Release sibling trees that won't be finalized. + for (_, sibling_root) in self.forks.values() { + self.data.dereference_tree(*sibling_root)?; + } self.finalized = parent; + self.current_root = parent_root; self.forks.clear(); } Ok(()) } /// Import a new block to the chain. - pub fn import(&mut self, block: Block) -> anyhow::Result { + pub fn import(&mut self, block: Block) -> Result { let head = block.header.hash(); let parent = block.header.parent; - if self.forks.contains_key(&parent) { self.finalize_fork(parent)?; } @@ -102,21 +96,19 @@ impl TestChain { let guard = Arc::new(Branch::checkout(self.data.clone())); self::process::(block, guard.clone())?; - // Compute root from base HashMap + overlay diff (no clone of base) + // Compute the post-block root incrementally from the overlay diff. let state_root = { let commit = guard .commit .read() .map_err(|_| anyhow::anyhow!("lock poisoned"))?; - self.data - .with_data(|data| handle_root_with_diff(head, data, &commit))? + self.compute_fork_root(&commit)? }; + root::set(head, state_root); - // Store Branch (diff only) in forks - self.forks.insert( - head, - Arc::try_unwrap(guard).unwrap_or_else(|arc| (*arc).clone()), - ); + // Store Branch (diff only) + its root in forks + let fork = Arc::try_unwrap(guard).unwrap_or_else(|arc| (*arc).clone()); + self.forks.insert(head, (fork, state_root)); Ok(state_root) } @@ -130,12 +122,19 @@ impl TestChain { } /// Apply the block to the chain. - pub fn apply(&mut self, block: &Block, guard: Arc) { + pub fn apply(&mut self, block: &Block, guard: Arc) -> Result<()> { let head = block.header.hash(); - self.forks.insert( - head, - Arc::try_unwrap(guard).unwrap_or_else(|arc| (*arc).clone()), - ); + let state_root = { + let commit = guard + .commit + .read() + .map_err(|_| anyhow::anyhow!("lock poisoned"))?; + self.compute_fork_root(&commit)? + }; + root::set(head, state_root); + let fork = Arc::try_unwrap(guard).unwrap_or_else(|arc| (*arc).clone()); + self.forks.insert(head, (fork, state_root)); + Ok(()) } /// Initialize the chain with the given block. @@ -174,56 +173,3 @@ impl Default for TestChain { } } } - -/// Sorted, deduplicated union of the keys an overlay commit touches. -fn collect_dirty_keys(commit: &Commit>) -> Vec { - let mut keys: Vec = commit - .update - .keys() - .copied() - .chain(commit.removal.iter().copied()) - .collect(); - keys.sort_unstable(); - keys.dedup(); - keys -} - -/// Compute the state root from base data + overlay diff via merge-walk. -fn handle_root_with_diff( - head: OpaqueHash, - base: &BTreeMap>, - diff: &crate::storage::Commit>, -) -> OpaqueHash { - let mut kvs: Vec<(TrieKey, &[u8])> = Vec::with_capacity(base.len() + diff.update.len()); - let mut base_iter = base.iter(); - let mut diff_iter = diff.update.iter(); - let mut b = base_iter.next(); - let mut d = diff_iter.next(); - - while let Some((key, value)) = match (b, d) { - (Some((bk, _)), Some((dk, dv))) if dk <= bk => { - if dk == bk { - b = base_iter.next(); - } - d = diff_iter.next(); - Some((dk, dv.as_slice())) - } - (Some((bk, bv)), _) => { - b = base_iter.next(); - Some((bk, bv.as_slice())) - } - (None, Some((dk, dv))) => { - d = diff_iter.next(); - Some((dk, dv.as_slice())) - } - (None, None) => None, - } { - if !diff.removal.contains(key) { - kvs.push((*key, value)); - } - } - - let state_root = merkle::trie31(&kvs); - root::set(head, state_root); - state_root -} diff --git a/crates/runtime/src/tx/dispute/mod.rs b/crates/runtime/src/tx/dispute/mod.rs index 890c611d9..4cfb27bdb 100644 --- a/crates/runtime/src/tx/dispute/mod.rs +++ b/crates/runtime/src/tx/dispute/mod.rs @@ -3,6 +3,7 @@ //! 1. update judgements on work-reports and validators (ψ) //! 2. update pending reports (ρ) use super::dispute; +use crypto::ed25519::SigItem; pub use error::{Error, Result}; use score::{ EPOCH_LENGTH, Ed25519Public, OpaqueHash, TimeSlot, VALIDATORS_COUNT, VALIDATORS_SUPER_MAJORITY, @@ -14,16 +15,16 @@ use std::collections::{BTreeMap, HashSet}; pub mod error; -/// (ψ) Update disputes verdicts and offenders +/// (ψ) Update disputes verdicts and offenders. pub fn disputes( timeslot: TimeSlot, kappa: &ValidatorsData, lambda: &ValidatorsData, - psi: &DisputesRecords, + psi: DisputesRecords, extrinsic: &DisputesExtrinsic, -) -> Result<(DisputesRecords, DisputesRecords)> { - let mut next_psi = psi.clone(); - let mut records = dispute::verdicts(timeslot, kappa, lambda, &extrinsic.verdicts)?; +) -> Result<(DisputesRecords, DisputesRecords, Vec)> { + let (mut records, mut triples) = + dispute::verdicts(timeslot, kappa, lambda, &extrinsic.verdicts)?; // get validators for the current slot let validators: HashSet = [kappa.ed25519(), lambda.ed25519()] @@ -32,25 +33,26 @@ pub fn disputes( .collect(); // handle culprits - let offenders = dispute::culprits(&validators, psi, &records.bad, &extrinsic.culprits)?; - records.offenders.extend(&offenders); + let (culprit_offenders, culprit_triples) = + dispute::culprits(&validators, &psi, &records.bad, &extrinsic.culprits)?; + records.offenders.extend(&culprit_offenders); + triples.extend(culprit_triples); // handle faults - let offenders = dispute::faults(&validators, psi, &records.good, &extrinsic.faults)?; - records.offenders.extend(&offenders); - - // update psi - { - next_psi.good.extend(&records.good); - next_psi.wonky.extend(&records.wonky); - next_psi.bad.extend(&records.bad); - - // TODO: make offenders unique - next_psi.offenders.extend(&records.offenders); - next_psi.offenders.sort(); - } - - Ok((next_psi, records)) + let (fault_offenders, fault_triples) = + dispute::faults(&validators, &psi, &records.good, &extrinsic.faults)?; + records.offenders.extend(&fault_offenders); + triples.extend(fault_triples); + + let mut next_psi = psi; + next_psi.good.extend(&records.good); + next_psi.wonky.extend(&records.wonky); + next_psi.bad.extend(&records.bad); + // TODO: make offenders unique + next_psi.offenders.extend(&records.offenders); + next_psi.offenders.sort(); + + Ok((next_psi, records, triples)) } /// (ρ†) Update availability assignments based on verdicts (ψ') @@ -74,14 +76,15 @@ pub fn reports( next_assignments } -// Update goodset, badset, wonkyset based on verdicts +// Update goodset, badset, wonkyset based on verdicts; collect sig triples. fn verdicts( timeslot: TimeSlot, kappa: &ValidatorsData, lambda: &ValidatorsData, verdicts: &[Verdict], -) -> Result { +) -> Result<(DisputesRecords, Vec)> { let mut records = DisputesRecords::default(); + let mut triples: Vec = Vec::new(); let mut last_target: Option = None; for verdict in verdicts { if verdict.votes.len() != VALIDATORS_SUPER_MAJORITY as usize { @@ -107,33 +110,28 @@ fn verdicts( return Err(Error::BadJudgementAge); }; - let mut verify_items = Vec::with_capacity(verdict.votes.len()); for (index, judgement) in verdict.votes.iter().enumerate() { if index != judgement.index as usize { return Err(Error::JudgementsNotSortedUnique); } let message = if judgement.vote { - aye_message.as_slice() + aye_message.clone() } else { - nay_message.as_slice() + nay_message.clone() }; - verify_items.push(( + triples.push(SigItem { message, - judgement.signature, - validators[judgement.index as usize].ed25519, - )); + signature: judgement.signature, + key: validators[judgement.index as usize].ed25519, + }); if judgement.vote { aye += 1; } } - crypto::ed25519::batch_verify(&verify_items) - .inspect_err(|e| tracing::debug!("Invalid verdict signature: {e}")) - .map_err(|_| Error::BadSignature)?; - match aye { aye if aye == VALIDATORS_SUPER_MAJORITY => records.good.push(verdict.target), aye if aye == VALIDATORS_COUNT / 3 => records.wonky.push(verdict.target), @@ -145,31 +143,31 @@ fn verdicts( } } - Ok(records) + Ok((records, triples)) } -/// (ψ) Update offenders based on verdicts +/// (ψ) Update offenders based on culprits; collect sig triples. fn culprits( validators: &HashSet, records: &DisputesRecords, bad: &[OpaqueHash], culprits: &[Culprit], -) -> Result> { +) -> Result<(Vec, Vec)> { let mut last_culprit = None; let mut bad_verdicts = bad.iter().map(|v| (v, 0)).collect::>(); let mut offenders = vec![]; + let mut triples: Vec = Vec::new(); for culprit in culprits { if !validators.contains(&culprit.key) { return Err(Error::BadGuarantorKey); } - if let Err(e) = - crypto::ed25519::verify(&culprit.signature_message(), culprit.signature, culprit.key) - { - tracing::debug!("Invalid signature in culprit: {e}"); - return Err(Error::BadSignature); - } + triples.push(SigItem { + message: culprit.signature_message().to_vec(), + signature: culprit.signature, + key: culprit.key, + }); if records.good.contains(&culprit.target) || records.bad.contains(&culprit.target) @@ -204,19 +202,20 @@ fn culprits( return Err(Error::NotEnoughCulprits); } - Ok(offenders) + Ok((offenders, triples)) } -/// (ψ) Update offenders based on verdicts +/// (ψ) Update offenders based on faults; collect sig triples. fn faults( validators: &HashSet, records: &DisputesRecords, good: &[OpaqueHash], faults: &[Fault], -) -> Result> { +) -> Result<(Vec, Vec)> { let mut last_fault = None; let mut verdicts = good.iter().map(|v| (v, 0)).collect::>(); let mut new_offenders = vec![]; + let mut triples: Vec = Vec::new(); for fault in faults { if !validators.contains(&fault.key) { @@ -234,12 +233,11 @@ fn faults( return Err(Error::OffenderAlreadyReported); } - if let Err(e) = - crypto::ed25519::verify(&fault.singing_message(), fault.signature, fault.key) - { - tracing::debug!("Invalid signature in fault: {e}"); - return Err(Error::BadSignature); - } + triples.push(SigItem { + message: fault.singing_message(), + signature: fault.signature, + key: fault.key, + }); if let Some(last_fault) = last_fault && fault < last_fault @@ -266,5 +264,5 @@ fn faults( return Err(Error::NotEnoughFaults); } - Ok(new_offenders) + Ok((new_offenders, triples)) } diff --git a/crates/runtime/src/tx/executor.rs b/crates/runtime/src/tx/executor.rs new file mode 100644 index 000000000..8e6cc4643 --- /dev/null +++ b/crates/runtime/src/tx/executor.rs @@ -0,0 +1,386 @@ +//! Block state-transition executor. +//! +//! Wraps the four-round STF pipeline into a single struct with one method per +//! round. Guarantee/assurance ed25519 batch verify runs in parallel with the +//! ticket::safrole ring-VRF via `rayon::join`. + +use crate::{ + Storage, + account::Accounts, + storage::Commit, + timing, + tx::{assurance, block, dispute, guarantee, preimage, ticket}, +}; +use account::Accounts as _; +use anyhow::Result; +use pvm::Pvm; +use score::{ + Block, CORES_COUNT, EPOCH_LENGTH, Ed25519Public, OpaqueHash, State, TrieKey, + block::header::{EpochMark, TicketsMark}, + extrinsic::dispute::DisputesRecords, + safrole::{Safrole, ValidatorIter}, + service::{AvailabilityAssignments, ReportedWorkPackage, WorkReport}, +}; +use std::{marker::PhantomData, sync::Arc, thread}; + +/// Four-round block state-transition executor. +pub struct Executor<'a, Vm: Pvm, S: Storage> { + block: &'a mut Block, + state: State, + accounts: Option>, + new_epoch: bool, + + // round-to-round handoff + dispute_records: DisputesRecords, + reports: AvailabilityAssignments, + reported: Vec, + reporters: Vec, + available: Vec, + counts: [u32; CORES_COUNT], + root: OpaqueHash, + _vm: PhantomData, +} + +impl<'a, Vm: Pvm, S: Storage> Executor<'a, Vm, S> { + /// Initialize the executor for a block + prior state. + pub fn new(block: &'a mut Block, state: State, storage: Arc) -> Self { + let new_epoch = block.header.epoch() > (state.timeslot / EPOCH_LENGTH); + Self { + block, + state, + accounts: Some(Accounts::new(storage)), + new_epoch, + dispute_records: DisputesRecords::default(), + reports: AvailabilityAssignments::default(), + reported: vec![], + reporters: vec![], + available: vec![], + counts: [0u32; CORES_COUNT], + root: [0u8; 32], + _vm: PhantomData, + } + } + + /// Run the four-round STF and emit the resulting state diff. + #[tracing::instrument(skip_all, name = "stf")] + pub fn run(mut self) -> Result>> { + self.validate_extrinsics()?; + self.update_reports()?; + self.accumulate()?; + self.finalize() + } + + /// Round 1 — validate the block against prior state. + /// + /// - extrinsic hash check + /// - preimages (E_P) (12.6) + /// - entropy update (η') (6.22) + /// - disputes (ψ') (10.4) + /// - validator rotation (λ', κ') on epoch change (6.13) + /// - last-block state-root patch + /// - `rayon::join`: guarantee/assurance ed25519 batch ∥ ticket::safrole + /// ring-VRF (γ') (12.10) + fn validate_extrinsics(&mut self) -> Result<()> { + if self.block.extrinsic.hash() != self.block.header.extrinsic_hash { + anyhow::bail!("extrinsic hash mismatch"); + } + + // (E_P) Validate preimages against prior state (12.6) + let accounts = self.accounts.as_mut().expect("accounts present"); + preimage::validate(accounts, &self.block.extrinsic.preimages)?; + + // (η') Update entropy (6.22) + let entropy = + crypto::vrf::ietf_output(self.block.header.entropy_source).unwrap_or_default(); + self.state.entropy = ticket::eta(self.new_epoch, &self.state.entropy, entropy); + + // (ψ') Update disputes against prior validator sets (10.4) + self.dispute_records = if self.block.extrinsic.disputes.is_empty() { + if !self.block.header.offenders_mark.is_empty() { + anyhow::bail!("offenders mark is not empty"); + } + DisputesRecords::default() + } else { + let (next_psi, records, triples) = dispute::disputes( + self.state.timeslot, + &self.state.validators.current, + &self.state.validators.previous, + std::mem::take(&mut self.state.disputes), + &self.block.extrinsic.disputes, + )?; + crypto::ed25519::SigItem::batch_verify(&triples)?; + self.state.disputes = next_psi; + self.block.header.offenders_mark = records.offenders.clone(); + records + }; + + // (λ', κ') Update validator state on epoch change (6.13) + if self.new_epoch { + self.state.validators.previous = std::mem::replace( + &mut self.state.validators.current, + self.state.safrole.validators.clone(), + ); + } + + // Patch the parent-state-root field on the last block of history + if let Some(last) = self.state.recent_blocks.history.last_mut() { + last.state_root = self.block.header.parent_state_root; + } + + self.sigs_and_safrole_parallel() + } + + /// Round 2 — apply availability and guarantee outcomes to report assignments. + /// + /// - availability outcomes (ρ‡) (11.17) + /// - new guarantees (ρ') (11.43) + fn update_reports(&mut self) -> Result<()> { + let reports = std::mem::take(&mut self.reports); + let reports = assurance::reports(self.block.header.slot, &self.available, reports); + self.state.reports = guarantee::reports( + self.block.header.slot, + &reports, + &self.block.extrinsic.guarantees, + )?; + Ok(()) + } + + /// Round 3 — statistics and accumulation. + /// + /// - statistics update (π') + /// - accumulate available work reports via the PVM + /// - merge accumulation result into state fields + /// - spawn `ticket::lazy::drawn` warmer for the next safrole candidate + fn accumulate(&mut self) -> Result<()> { + self.state.statistics.update( + self.new_epoch, + self.block.header.author_index, + &self.block.extrinsic, + )?; + self.state + .statistics + .merge_reports(&self.available, &self.counts); + + let _guard = timing::accumulate(); + let available = std::mem::take(&mut self.available); + let accounts = self.accounts.take().expect("accounts present"); + + let accumulation = guarantee::accumulate::( + self.block.header.slot, + self.state.timeslot, + available, + &self.state.queue, + &self.state.history, + &self.state.privileges, + &self.state.validators.drawn, + &self.state.authorization, + accounts, + self.state.entropy, + )?; + + self.state.privileges = accumulation.privileges; + self.state.queue = accumulation.ready_queue; + self.state.history = accumulation.accumulated_queue; + self.state.validators.drawn = accumulation.validators; + self.state.authorization = accumulation.authorization; + + let candidate = self + .state + .safrole + .next(&self.state.validators.drawn, &self.state.disputes.offenders); + thread::spawn(move || ticket::lazy::drawn(&candidate)); + + self.state.statistics.merge_services(accumulation.records); + self.state.logs = accumulation.logs; + self.root = accumulation.root; + self.accounts = Some(accumulation.accounts); + Ok(()) + } + + /// Round 4 — commit block and emit the state diff. + /// + /// - block history (β') + /// - reporter statistics merge + /// - preimage integration (δ') + /// - authorization pools (α') (12.13) + /// - timeslot (τ') + /// - flush state pairs into the final diff + fn finalize(mut self) -> Result>> { + let mut diff = Commit::default(); + + // (β') Update the block history + block::history::import( + &mut self.state.recent_blocks, + self.block.header.hash(), + self.root, + std::mem::take(&mut self.reported), + ); + + if !self.reporters.is_empty() { + self.state + .statistics + .merge_reporters(&self.reporters, &self.state.validators.current.ed25519())?; + } + + // (δ') Integrate preimages into the post-transfer state + let accounts = self.accounts.take().expect("accounts present"); + let accounts = preimage::accounts( + self.block.header.slot, + std::mem::take(&mut self.block.extrinsic.preimages), + accounts, + ); + let (updates, removals) = accounts.diff(); + diff.extend_iter(updates, removals); + + // (α') Update the authorization pools (12.13) + self.state.pools = guarantee::pools( + self.block.header.slot, + &self.state.pools, + &self.state.authorization, + &self.block.extrinsic.guarantees, + ); + + // (τ') Update the timeslot + self.state.timeslot = self.block.header.slot; + + diff.update + .extend(self.state.pairs(self.new_epoch, &self.block.extrinsic)); + Ok(diff) + } + + /// Run guarantee/assurance sig collect + batch_verify in parallel with + /// ticket::safrole ring-VRF. + fn sigs_and_safrole_parallel(&mut self) -> Result<()> { + let new_epoch = self.new_epoch; + let needs_safrole = !self.block.extrinsic.tickets.is_empty() || new_epoch; + let safrole_in = needs_safrole.then(|| std::mem::take(&mut self.state.safrole)); + let state_view: &State = &self.state; + let block_view: &Block = &*self.block; + let accounts_view = self.accounts.as_ref().expect("accounts present"); + let dispute_records_view = &self.dispute_records; + let (sigs_res, safrole_res) = rayon::join( + || { + Self::sigs_branch( + state_view, + accounts_view, + block_view, + dispute_records_view, + new_epoch, + ) + }, + || { + safrole_in + .map(|s| Self::safrole_branch(state_view, block_view, new_epoch, s)) + .transpose() + }, + ); + + let out = sigs_res?; + self.reported = out.reported; + self.reporters = out.reporters; + self.available = out.available; + self.counts = out.counts; + self.reports = out.reports; + + if let Some(s) = safrole_res? { + self.state.safrole = s.safrole; + self.block.header.epoch_mark = s.epoch_mark; + self.block.header.tickets_mark = s.tickets_mark; + } + + Ok(()) + } + + /// Collect guarantee + assurance ed25519 triples and batch-verify them. + fn sigs_branch( + state: &State, + accounts: &Accounts, + block: &Block, + dispute_records: &DisputesRecords, + new_epoch: bool, + ) -> Result { + // (p of β') Collect guarantee triples + let (reported, reporters, mut batch) = if block.extrinsic.guarantees.is_empty() { + (vec![], vec![], vec![]) + } else { + let _guard = timing::guarantees(); + guarantee::report( + state, + block.header.slot, + accounts, + &block.extrinsic.guarantees, + )? + }; + + // (ρ†) Update availability assignments based on verdicts (10.15) + let reports = dispute::reports(dispute_records, &state.reports); + + // (W) Collect assurance triples (11.16) + let (available, counts, a_triples) = assurance::available( + &reports, + if new_epoch { + &state.validators.previous + } else { + &state.validators.current + }, + block.header.parent, + &block.extrinsic.assurances, + )?; + + batch.extend(a_triples); + crypto::ed25519::SigItem::batch_verify(&batch)?; + Ok(SigsOutput { + reported, + reporters, + available, + counts, + reports, + }) + } + + /// Compute next safrole state via ring-VRF and derive header marks. + fn safrole_branch( + state: &State, + block: &Block, + new_epoch: bool, + safrole_in: Safrole, + ) -> Result { + let _guard = timing::safrole(); + let safrole = ticket::safrole( + state.timeslot, + block.header.slot, + state.entropy, + &state.disputes.offenders, + safrole_in, + &state.validators, + &block.extrinsic.tickets, + )?; + let epoch_mark = if new_epoch { + safrole.epoch_mark(&state.entropy) + } else { + None + }; + let tickets_mark = safrole.tickets_mark(state.timeslot, block.header.slot); + Ok(SafroleOutput { + safrole, + epoch_mark, + tickets_mark, + }) + } +} + +/// Output of the guarantee/assurance sigs branch. +struct SigsOutput { + reported: Vec, + reporters: Vec, + available: Vec, + counts: [u32; CORES_COUNT], + reports: AvailabilityAssignments, +} + +/// Output of the ticket::safrole branch. +struct SafroleOutput { + safrole: Safrole, + epoch_mark: Option, + tickets_mark: Option, +} diff --git a/crates/runtime/src/tx/guarantee/exec.rs b/crates/runtime/src/tx/guarantee/exec.rs index 8cd6e8fc4..ba295db5c 100644 --- a/crates/runtime/src/tx/guarantee/exec.rs +++ b/crates/runtime/src/tx/guarantee/exec.rs @@ -189,7 +189,7 @@ pub fn parallel( let mut transfers = Vec::new(); let mut pairings = BTreeSet::new(); for (service_id, result) in results.iter_mut() { - transfers.extend(result.transfers.clone()); + transfers.extend(std::mem::take(&mut result.transfers)); if let Some(hash) = result.hash { pairings.insert((*service_id, hash)); @@ -199,7 +199,7 @@ pub fn parallel( continue; } - for service in result.context.accounts.removed() { + for &service in result.context.accounts.removed() { removed.insert(service); } diff --git a/crates/runtime/src/tx/guarantee/mod.rs b/crates/runtime/src/tx/guarantee/mod.rs index 9f6192de0..33b7d37a5 100644 --- a/crates/runtime/src/tx/guarantee/mod.rs +++ b/crates/runtime/src/tx/guarantee/mod.rs @@ -249,13 +249,17 @@ pub fn pools( new_pools } -/// (p of β') Report the work packages +/// (p of β') Report the work packages. pub fn report( state: &score::State, slot: TimeSlot, services: &impl Accounts, guarantees: &GuaranteesExtrinsic, -) -> Result<(Vec, Vec)> { +) -> Result<( + Vec, + Vec, + Vec, +)> { let mut validator = validator::GuaranteeValidator::new(state, services); validator.validate(slot, guarantees) } diff --git a/crates/runtime/src/tx/guarantee/validator.rs b/crates/runtime/src/tx/guarantee/validator.rs index 851f62972..963166d29 100644 --- a/crates/runtime/src/tx/guarantee/validator.rs +++ b/crates/runtime/src/tx/guarantee/validator.rs @@ -2,6 +2,7 @@ use crate::tx::guarantee::error::{Error, Result}; use account::{Account, Accounts}; +use crypto::ed25519::SigItem; use score::{ CORES_COUNT, CoreIndex, EPOCH_LENGTH, Ed25519Public, Entropy, MAX_DEPENDENCY_COUNT, MAX_WORK_REPORT_OUTPUT_SIZE, OpaqueHash, ROTATION_PERIOD, State, TimeSlot, VALIDATORS_COUNT, @@ -39,26 +40,27 @@ impl<'s, R: Accounts> GuaranteeValidator<'s, R> { } } - /// Validate work reports according to the guarantees extrinsic + /// Validate work reports according to the guarantees extrinsic. #[tracing::instrument(skip_all, name = "guarantee")] pub fn validate( &mut self, slot: TimeSlot, guarantees: &GuaranteesExtrinsic, - ) -> Result<(Vec, Vec)> { + ) -> Result<(Vec, Vec, Vec)> { self.init_deps(guarantees); self.timeslot = slot; // Prepare for reporting let mut reported = Vec::new(); let mut reporters = BTreeSet::new(); + let mut triples = Vec::new(); // Process each guarantee for guarantee in guarantees.iter() { self.validate_results(guarantee)?; self.validate_block(guarantee)?; self.validate_deps(guarantee)?; - let guarantors = self.validate_guarantee(guarantee)?; + let (guarantors, mut g_triples) = self.validate_guarantee(guarantee)?; // Record reported package reported.push(ReportedWorkPackage { @@ -68,11 +70,12 @@ impl<'s, R: Accounts> GuaranteeValidator<'s, R> { // Record reporters (guarantors) reporters.extend(guarantors); + triples.append(&mut g_triples); } // Sort the reported work packages and reporters reported.sort_by_key(|a| a.hash); - Ok((reported, reporters.into_iter().collect())) + Ok((reported, reporters.into_iter().collect(), triples)) } fn init_deps(&mut self, guarantees: &GuaranteesExtrinsic) { @@ -85,7 +88,7 @@ impl<'s, R: Accounts> GuaranteeValidator<'s, R> { .recent_blocks .history .iter() - .flat_map(|b| b.reported.clone()) + .flat_map(|b| b.reported.iter().cloned()) .collect::>(); self.recent = recent; @@ -171,7 +174,10 @@ impl<'s, R: Accounts> GuaranteeValidator<'s, R> { Ok(()) } - fn validate_guarantee(&mut self, guarantee: &ReportGuarantee) -> Result> { + fn validate_guarantee( + &mut self, + guarantee: &ReportGuarantee, + ) -> Result<(Vec, Vec)> { // 1. validate the rotation let guarantors = self.validate_rotation(guarantee)?; @@ -186,10 +192,10 @@ impl<'s, R: Accounts> GuaranteeValidator<'s, R> { return Err(Error::InsufficientGuarantees); } - // 4. validate the signatures: cheap checks first, then batch verify. + // 4. Semantic checks; collect triples for the caller to batch-verify. let message = guarantee.signing_message(); let mut guarantor = None; - let mut to_verify = Vec::with_capacity(guarantee.signatures.len()); + let mut triples = Vec::with_capacity(guarantee.signatures.len()); for sig in guarantee.signatures.iter() { let validator_index = sig.validator_index as usize; if validator_index >= VALIDATORS_COUNT as usize { @@ -211,27 +217,23 @@ impl<'s, R: Accounts> GuaranteeValidator<'s, R> { return Err(Error::BannedValidator); } - to_verify.push((message.as_slice(), sig.signature, *key)); + triples.push(SigItem { + message: message.clone(), + signature: sig.signature, + key: *key, + }); guarantor = Some(validator_index); } - crypto::ed25519::batch_verify(&to_verify) - .inspect_err(|_| { - tracing::debug!( - "failed to verify guarantee signatures for slot={}", - guarantee.slot, - ) - }) - .map_err(|_| Error::BadSignature)?; - self.processed.insert(guarantee.report.core_index); // Return only the validators who actually provided signatures (reporters) - Ok(guarantee + let reporters = guarantee .signatures .iter() .map(|sig| guarantors[&(sig.validator_index as usize)]) - .collect()) + .collect(); + Ok((reporters, triples)) } fn validate_results(&self, guarantee: &ReportGuarantee) -> Result<()> { diff --git a/crates/runtime/src/tx/mod.rs b/crates/runtime/src/tx/mod.rs index 3a7e24cdc..5cb610dae 100644 --- a/crates/runtime/src/tx/mod.rs +++ b/crates/runtime/src/tx/mod.rs @@ -2,19 +2,19 @@ use crate::{ Storage, - account::Accounts, storage::{Column, Commit}, timing, }; -use account::Accounts as _; use anyhow::Result; +pub use executor::Executor; use pvm::Pvm; -use score::{Block, TrieKey, safrole::ValidatorIter}; -use std::{sync::Arc, thread}; +use score::{Block, TrieKey}; +use std::sync::Arc; pub mod assurance; pub mod block; pub mod dispute; +pub mod executor; pub mod guarantee; pub mod preimage; pub mod ticket; @@ -27,7 +27,7 @@ pub fn transit( ) -> Result>> { let diff = self::simulate::(&mut block, storage.clone())?; let _guard = timing::commit(); - storage.commit(Column::State, diff.clone())?; + storage.commit(Column::State, &diff)?; Ok(diff) } @@ -40,7 +40,7 @@ pub fn transit_with_state( ) -> Result>> { let diff = self::simulate_with_state::(&mut block, state, storage.clone())?; let _guard = timing::commit(); - storage.commit(Column::State, diff.clone())?; + storage.commit(Column::State, &diff)?; Ok(diff) } @@ -56,201 +56,8 @@ pub fn simulate( /// Simulate state transition with new block pub fn simulate_with_state( block: &mut Block, - mut state: score::State, + state: score::State, storage: Arc, ) -> Result>> { - let epoch = block.header.epoch(); - let new_epoch: bool = epoch > (state.timeslot / score::EPOCH_LENGTH); - - // validate the extrinsic hash - if block.extrinsic.hash() != block.header.extrinsic_hash { - anyhow::bail!("extrinsic hash mismatch"); - } - - // The first round computation - let mut accounts = Accounts::new(storage); - - // (E_P) Validate preimages against the prior state (12.6) - preimage::validate(&mut accounts, &block.extrinsic.preimages)?; - let (mut reports, reported, reporters) = { - // (η') Update entropy (6.22) - let entropy = crypto::vrf::ietf_output(block.header.entropy_source).unwrap_or_default(); - state.entropy = ticket::eta(new_epoch, &state.entropy, entropy); - - // (ψ') Update disputes against the prior validator sets (10.4) - let marks = if block.extrinsic.disputes.is_empty() { - if !block.header.offenders_mark.is_empty() { - anyhow::bail!("offenders mark is not empty"); - } - Default::default() - } else { - let (disputes, marks) = self::dispute::disputes( - state.timeslot, - &state.validators.current, - &state.validators.previous, - &state.disputes, - &block.extrinsic.disputes, - )?; - - state.disputes = disputes; - block.header.offenders_mark = marks.offenders.clone(); - marks - }; - - if new_epoch { - // (λ', κ') Update validator state (6.13) - state.validators.previous = std::mem::replace( - &mut state.validators.current, - state.safrole.validators.clone(), - ); - } - - // complete the state root of the last block in the history - if let Some(last) = state.recent_blocks.history.last_mut() { - last.state_root = block.header.parent_state_root; - } - - // (p of β') validate the guarantees - let (mut reported, mut reporters) = (vec![], vec![]); - if !block.extrinsic.guarantees.is_empty() { - (reported, reporters) = { - let _guard = timing::guarantees(); - guarantee::report( - &state, - block.header.slot, - &accounts, - &block.extrinsic.guarantees, - )? - } - }; - - // (ρ†) Update availability assignments based on verdicts (V) (10.15) - ( - dispute::reports(&marks, &state.reports), - reported, - reporters, - ) - }; - - // Round 2 computation - let (available, assurances) = { - // (W) the sequence of new available work reports (11.16) - let (available, assurances) = self::assurance::available( - &reports, - if new_epoch { - &state.validators.previous - } else { - &state.validators.current - }, - block.header.parent, - &block.extrinsic.assurances, - )?; - - // (ρ‡) Update availability assignments based on assurances (11.17) - reports = self::assurance::reports(block.header.slot, &available, reports.clone()); - - // (ρ') Update availability assignments based on guarantees (11.43) - state.reports = - guarantee::reports(block.header.slot, &reports, &block.extrinsic.guarantees)?; - (available, assurances) - }; - - // Round 3 computation - let (root, accounts) = { - // (γ') Update the sealing-key series (12.10) - if !block.extrinsic.tickets.is_empty() || new_epoch { - let _guard = timing::safrole(); - state.safrole = ticket::safrole( - state.timeslot, - block.header.slot, - state.entropy, - &state.disputes.offenders, - &state.safrole, - &state.validators, - &block.extrinsic.tickets, - )?; - - { - if new_epoch { - block.header.epoch_mark = state.safrole.epoch_mark(&state.entropy); - } - block.header.tickets_mark = state - .safrole - .tickets_mark(state.timeslot, block.header.slot); - } - } - - // (π') Update the statistic - state - .statistics - .update(new_epoch, block.header.author_index, &block.extrinsic)?; - state.statistics.merge_reports(&available, &assurances); - - // (..., C) Accumulate the available work reports - let _guard = timing::accumulate(); - let accumulation = guarantee::accumulate::( - block.header.slot, - state.timeslot, - available, - &state.queue, - &state.history, - &state.privileges, - &state.validators.drawn, - &state.authorization, - accounts, - state.entropy, - )?; - - // update state fields - state.privileges = accumulation.privileges; - state.queue = accumulation.ready_queue; - state.history = accumulation.accumulated_queue; - state.validators.drawn = accumulation.validators; - state.authorization = accumulation.authorization; - let candidate = state - .safrole - .next(&state.validators.drawn, &state.disputes.offenders); - thread::spawn(move || ticket::lazy::drawn(&candidate)); - - state.statistics.merge_services(accumulation.records); - state.logs = accumulation.logs; - (accumulation.root, accumulation.accounts) - }; - - // Round 4 computation - let mut diff = Commit::default(); - { - // (β') Update the block history - block::history::import( - &mut state.recent_blocks, - block.header.hash(), - root, - reported, - ); - - if !reporters.is_empty() { - state - .statistics - .merge_reporters(&reporters, &state.validators.current.ed25519())?; - } - - // (δ') Integrate preimages into the post-transfer state - let accounts = preimage::accounts(block.header.slot, &block.extrinsic.preimages, accounts); - let (updates, removals) = accounts.diff(); - diff.extend_iter(updates, removals); - - // (α') Update the authorization pools (12.13) - state.pools = guarantee::pools( - block.header.slot, - &state.pools, - &state.authorization, - &block.extrinsic.guarantees, - ); - - // (τ') Update the timeslot - state.timeslot = block.header.slot; - } - - diff.update.extend(state.pairs(new_epoch, &block.extrinsic)); - Ok(diff) + Executor::::new(block, state, storage).run() } diff --git a/crates/runtime/src/tx/preimage.rs b/crates/runtime/src/tx/preimage.rs index f5b6ac7d9..fa2d8957c 100644 --- a/crates/runtime/src/tx/preimage.rs +++ b/crates/runtime/src/tx/preimage.rs @@ -25,7 +25,7 @@ pub fn validate(accounts: &mut A, preimages: &PreimagesExtrinsic) - } /// (δ') Integrate providable preimages into the post-transfer state -pub fn accounts(slot: TimeSlot, preimages: &PreimagesExtrinsic, mut accounts: A) -> A { +pub fn accounts(slot: TimeSlot, preimages: PreimagesExtrinsic, mut accounts: A) -> A { for preimage in preimages { let hash = crypto::blake2b(&preimage.blob); let len = preimage.blob.len() as u32; @@ -33,7 +33,7 @@ pub fn accounts(slot: TimeSlot, preimages: &PreimagesExtrinsic, mut continue; } let account = accounts.get(preimage.requester).expect("just checked"); - account.insert_preimage(hash, preimage.blob.clone()); + account.insert_preimage(hash, preimage.blob); account.insert_lookup(hash, len, vec![slot]); } accounts diff --git a/crates/runtime/src/tx/ticket/mod.rs b/crates/runtime/src/tx/ticket/mod.rs index 09ac521ec..4ce2f7fe5 100644 --- a/crates/runtime/src/tx/ticket/mod.rs +++ b/crates/runtime/src/tx/ticket/mod.rs @@ -51,7 +51,7 @@ pub fn safrole( slot: u32, entropy: [OpaqueHash; 4], offenders: &[Ed25519Public], - safrole: &Safrole, + mut safrole: Safrole, validators: &Validators, tickets: &TicketsExtrinsic, ) -> Result { @@ -72,8 +72,11 @@ pub fn safrole( let epoch = tau / score::EPOCH_LENGTH; let next_epoch = slot / score::EPOCH_LENGTH; let new_epoch: bool = next_epoch > epoch; - let mut safrole = safrole.clone(); - safrole.series = self::sealing_key_series(tau, slot, entropy, &safrole, &validators.current); + if let Some(series) = + self::sealing_key_series(tau, slot, entropy, &safrole, &validators.current) + { + safrole.series = series; + } if new_epoch { let next = safrole.next(&validators.drawn, offenders); if next != safrole.validators { @@ -83,9 +86,10 @@ pub fn safrole( } // Process accumulator and ring commitment in parallel + let acc = std::mem::take(&mut safrole.accumulator); safrole.accumulator = self::accumulator( new_epoch, - &safrole.accumulator, + acc, entropy, &safrole.validators.bandersnatch(), tickets, @@ -99,7 +103,7 @@ pub fn safrole( /// NOTE: gamma_k has already been updated at this point pub fn accumulator( new_epoch: bool, - accumulator: &TicketsAccumulator, + mut accumulator: TicketsAccumulator, entropy: [OpaqueHash; 4], next: &Vec, tickets: &TicketsExtrinsic, @@ -113,7 +117,6 @@ pub fn accumulator( let submitted_ids: Vec = new_tickets.iter().map(|t| t.id).collect(); // update the accumulator - let mut accumulator = accumulator.clone(); if new_epoch { // Clear the accumulator if we're starting a new epoch: 6.34 accumulator.clear(); @@ -146,30 +149,29 @@ pub fn accumulator( } /// (γ_s') Updates the sealing key series according to graypaper formula 6.24. +/// Returns `None` when the series is unchanged (same epoch). pub fn sealing_key_series( tau: u32, slot: u32, entropy: [OpaqueHash; 4], safrole: &Safrole, curr_validators: &[ValidatorData], -) -> TicketsOrKeys { - let mut next = safrole.series.clone(); +) -> Option { let curr_epoch = slot / score::EPOCH_LENGTH; let prev_epoch = tau / score::EPOCH_LENGTH; let prev_slot_phase = tau % score::EPOCH_LENGTH; if curr_epoch == prev_epoch { - return next; + return None; } // FIXME: should be curr_epoch > prev_epoch - if curr_epoch == prev_epoch + 1 + let next = if curr_epoch == prev_epoch + 1 && prev_slot_phase >= score::TICKET_SUBMISSION_PERIOD && safrole.accumulator.len() == score::EPOCH_LENGTH as usize { - next = TicketsOrKeys::Tickets(TicketBody::sequence(&safrole.accumulator)); + TicketsOrKeys::Tickets(TicketBody::sequence(&safrole.accumulator)) } else { - next = TicketsOrKeys::fallback(curr_validators.bandersnatch(), entropy[2]); - } - - next + TicketsOrKeys::fallback(&curr_validators.bandersnatch(), entropy[2]) + }; + Some(next) } diff --git a/crates/spacejam/Cargo.toml b/crates/spacejam/Cargo.toml index 3d572a911..cdebbb8e7 100644 --- a/crates/spacejam/Cargo.toml +++ b/crates/spacejam/Cargo.toml @@ -6,33 +6,32 @@ edition.workspace = true [[bin]] name = "spacejam" path = "bin/spacejam.rs" -required-features = ["node"] [dependencies] account.workspace = true anyhow.workspace = true async-trait.workspace = true -clap = { workspace = true, features = ["derive", "env"], optional = true } +clap = { workspace = true, features = ["derive", "env"] } codec.workspace = true -crypto.workspace = true +crypto = { workspace = true, features = ["bls", "ed25519", "vrf"] } dhat = { workspace = true, optional = true } dirs.workspace = true hex.workspace = true -network.workspace = true +network = { workspace = true, features = ["cmd"] } offchain.workspace = true parity-db.workspace = true spacevm.workspace = true rand.workspace = true rayon.workspace = true runtime.workspace = true -score = { workspace = true } -serde = { workspace = true, optional = true } -serde_json = { workspace = true, optional = true } -spacejson = { workspace = true, optional = true } +score.workspace = true +serde.workspace = true +serde_json.workspace = true +spacejson.workspace = true sysinfo.workspace = true -testing.workspace = true -time = { workspace = true, optional = true, features = ["local-offset"] } -toml = { workspace = true, optional = true } +testing = { workspace = true, optional = true } +time = { workspace = true, features = ["local-offset"] } +toml.workspace = true tokio = { workspace = true, features = [ "rt", "rt-multi-thread", @@ -42,10 +41,7 @@ tokio = { workspace = true, features = [ "signal", ] } tracing.workspace = true -tracing-subscriber = { workspace = true, optional = true, features = [ - "fmt", - "time", -] } +tracing-subscriber = { workspace = true, features = ["fmt", "time"] } [dev-dependencies] temp-dir.workspace = true @@ -54,27 +50,11 @@ temp-dir.workspace = true syn.workspace = true proc-macro2.workspace = true quote.workspace = true +toml.workspace = true [features] -bin = ["node"] -cmd = [ - "clap", - "network/cmd", - "serde", - "spacejson", - "time", - "tracing-subscriber", -] +default = ["tiny"] dhat = ["dep:dhat"] -default = ["bin", "serde", "tiny"] -node = ["cmd"] -serde = [ - "dep:serde", - "serde_json", - "toml", - "crypto/bls", - "crypto/ed25519", - "crypto/vrf", -] tiny = ["score/tiny"] full = ["score/full"] +trace = ["dep:testing"] diff --git a/crates/spacejam/bin/spacejam.rs b/crates/spacejam/bin/spacejam.rs index 5116feabb..2832d4bd6 100644 --- a/crates/spacejam/bin/spacejam.rs +++ b/crates/spacejam/bin/spacejam.rs @@ -2,18 +2,45 @@ use spacejam::cmd::App; -#[cfg(feature = "dhat")] -#[global_allocator] -static ALLOC: dhat::Alloc = dhat::Alloc; - #[tokio::main] async fn main() { + self::init_rayon(); + #[cfg(feature = "dhat")] - { + dhat::init(); + + App::run().await; +} + +/// Cap the rayon global pool at 32 +fn init_rayon() { + let threads = std::env::var("RAYON_NUM_THREADS") + .ok() + .and_then(|s| s.parse::().ok()) + .filter(|n| *n > 0) + .unwrap_or_else(|| { + std::thread::available_parallelism() + .map(|n| n.get().min(32)) + .unwrap_or(8) + }); + + let _ = rayon::ThreadPoolBuilder::new() + .num_threads(threads) + .thread_name(|i| format!("rayon-{i}")) + .build_global(); +} + +#[cfg(feature = "dhat")] +mod dhat { + use std::sync::Mutex; + + #[global_allocator] + static ALLOC: dhat::Alloc = dhat::Alloc; + + pub fn init() { // Leak the profiler so the signal handler task can drop it on Ctrl-C. - let profiler: &'static std::sync::Mutex> = Box::leak(Box::new( - std::sync::Mutex::new(Some(dhat::Profiler::new_heap())), - )); + let profiler: &'static Mutex> = + Box::leak(Box::new(Mutex::new(Some(dhat::Profiler::new_heap())))); tokio::spawn(async move { tokio::signal::ctrl_c().await.ok(); @@ -22,6 +49,4 @@ async fn main() { std::process::exit(0); }); } - - App::run().await; } diff --git a/crates/spacejam/build.rs b/crates/spacejam/build.rs index 70832274b..24d71b601 100644 --- a/crates/spacejam/build.rs +++ b/crates/spacejam/build.rs @@ -1,6 +1,6 @@ //! build script for spacejam -use std::{fs, process::Command}; +use std::{fs, path::Path, process::Command}; const TINY_DEV_SPEC: &str = "https://gist.githubusercontent.com/clearloop/52b9d5c16d3bd2a2d900b756fc64a9d1/raw/fbf84b774254cb68071a8a37cf8faac699bebf48/spec.json"; @@ -12,7 +12,36 @@ fn main() { std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be set"), ); - let dev = root.join("spec/dev"); + self::emit_graypaper_version(&root); + self::fetch_tiny_dev_spec(&root); +} + +/// Read `[workspace.metadata.graypaper] version` from the root Cargo.toml +fn emit_graypaper_version(crate_root: &Path) { + let workspace_manifest = crate_root.join("../../Cargo.toml"); + println!("cargo:rerun-if-changed={}", workspace_manifest.display()); + + let text = fs::read_to_string(&workspace_manifest) + .expect("failed to read workspace Cargo.toml for graypaper version"); + let version = parse_graypaper_version(&text) + .expect("`[workspace.metadata.graypaper] version` missing from Cargo.toml"); + println!("cargo:rustc-env=GRAYPAPER_VERSION={version}"); +} + +fn parse_graypaper_version(manifest: &str) -> Option { + manifest + .parse::() + .ok()? + .get("workspace")? + .get("metadata")? + .get("graypaper")? + .get("version")? + .as_str() + .map(str::to_string) +} + +fn fetch_tiny_dev_spec(crate_root: &Path) { + let dev = crate_root.join("spec/dev"); let target = dev.join("spec.json"); if target.exists() { return; diff --git a/crates/spacejam/src/cmd/fuzz.rs b/crates/spacejam/src/cmd/fuzz.rs index 27ab0bfd1..e05b4ce3e 100644 --- a/crates/spacejam/src/cmd/fuzz.rs +++ b/crates/spacejam/src/cmd/fuzz.rs @@ -1,6 +1,8 @@ //! Fuzz related commands -use crate::fuzz::{self, fuzzer::Fuzzer, target::Target}; +use crate::fuzz::target::Target; +#[cfg(feature = "trace")] +use crate::fuzz::{self, fuzzer::Fuzzer}; use clap::Parser; use std::path::PathBuf; @@ -14,11 +16,12 @@ pub enum Fuzz { socket: PathBuf, /// If use interpreter instead - #[clap(short, long)] + #[clap(short, long, env = "SPACEJAM_INTERP")] interp: bool, }, /// Fuzz with a fuzzer + #[cfg(feature = "trace")] Fuzzer { /// The path to the fuzzer #[clap(default_value = "/tmp/jam_target.sock")] @@ -42,6 +45,7 @@ pub enum Fuzz { }, /// Run trace test via the given trace file + #[cfg(feature = "trace")] Tx { /// The path to the trace file test: PathBuf, @@ -53,6 +57,7 @@ impl Fuzz { pub async fn run(&self) -> anyhow::Result<()> { match self { Self::Target { socket, interp } => Target::serve(socket, *interp).await, + #[cfg(feature = "trace")] Self::Fuzzer { socket, traces, @@ -68,6 +73,7 @@ impl Fuzz { Fuzzer::run(socket, traces, report) } } + #[cfg(feature = "trace")] Self::Tx { test } => fuzz::trace::test(test).await, } } diff --git a/crates/spacejam/src/cmd/mod.rs b/crates/spacejam/src/cmd/mod.rs index 22dcbeeb2..5952fc006 100644 --- a/crates/spacejam/src/cmd/mod.rs +++ b/crates/spacejam/src/cmd/mod.rs @@ -46,7 +46,7 @@ impl App { let app = App::parse(); if app.graypaper { - println!("graypaper: {}", crate::GRAYPAPER); + println!("graypaper: {}", crate::fuzz::message::Version::protocol()); return; } diff --git a/crates/spacejam/src/fuzz/env.rs b/crates/spacejam/src/fuzz/env.rs index c4dde2996..91484b7d2 100644 --- a/crates/spacejam/src/fuzz/env.rs +++ b/crates/spacejam/src/fuzz/env.rs @@ -15,6 +15,7 @@ const JAM_FUZZ_SPEC: &str = "JAM_FUZZ_SPEC"; const JAM_FUZZ_DATA_PATH: &str = "JAM_FUZZ_DATA_PATH"; const JAM_FUZZ_SOCK_PATH: &str = "JAM_FUZZ_SOCK_PATH"; const JAM_FUZZ_LOG_LEVEL: &str = "JAM_FUZZ_LOG_LEVEL"; +const SPACEJAM_INTERP: &str = "SPACEJAM_INTERP"; /// Whether the env-driven fuzz mode is requested. pub fn is_active() -> bool { @@ -26,8 +27,7 @@ pub async fn run() -> Result<()> { init_logger(std::env::var(JAM_FUZZ_LOG_LEVEL).ok().as_deref()); let cfg = Config::from_env()?; log_runtime_env(&cfg); - // Use the compiler on linux; Target::serve falls back to interp on other platforms. - Target::serve(&cfg.socket, /*interp=*/ false).await + Target::serve(&cfg.socket, cfg.interp).await } /// Log target config (spec / data_path / socket) and host hardware (CPU model, @@ -44,8 +44,13 @@ fn log_runtime_env(cfg: &Config) { .unwrap_or_else(|| "unknown".to_string()); let cores = sys.cpus().len(); let ram_gb = sys.total_memory() as f64 / (1024.0 * 1024.0 * 1024.0); + let vm = if cfg.interp { + "interpreter" + } else { + "compiler" + }; tracing::info!( - "fuzz target starting: spec={}, data_path={}, socket={}, vm=compiler, cpu={cpu:?} ({cores} cores), ram={ram_gb:.1} GB", + "fuzz target starting: spec={}, data_path={}, socket={}, vm={vm}, cpu={cpu:?} ({cores} cores), ram={ram_gb:.1} GB", cfg.spec.as_str(), cfg.data_path.display(), cfg.socket.display(), @@ -56,6 +61,7 @@ struct Config { spec: Spec, data_path: PathBuf, socket: PathBuf, + interp: bool, } impl Config { @@ -63,10 +69,15 @@ impl Config { let spec = Spec::from_str(&require_env(JAM_FUZZ_SPEC)?)?; let data_path = PathBuf::from(require_env(JAM_FUZZ_DATA_PATH)?); let socket = PathBuf::from(require_env(JAM_FUZZ_SOCK_PATH)?); + let interp = match std::env::var(SPACEJAM_INTERP).as_deref() { + Ok("" | "0" | "false") | Err(_) => false, + Ok(_) => true, + }; Ok(Self { spec, data_path, socket, + interp, }) } } diff --git a/crates/spacejam/src/fuzz/fuzzer.rs b/crates/spacejam/src/fuzz/fuzzer.rs index 1a899718b..fd07f7579 100644 --- a/crates/spacejam/src/fuzz/fuzzer.rs +++ b/crates/spacejam/src/fuzz/fuzzer.rs @@ -185,10 +185,10 @@ impl Fuzzer { // check the remote peer info tracing::info!("Received peer info: {received:?}"); - if received.jam_version != Version::PROTOCOL { + if received.jam_version != Version::protocol() { anyhow::bail!( "Expected protocol: {:?}, got {:?}", - Version::PROTOCOL, + Version::protocol(), received.jam_version ); } diff --git a/crates/spacejam/src/fuzz/message.rs b/crates/spacejam/src/fuzz/message.rs index 653bfa302..becf658e1 100644 --- a/crates/spacejam/src/fuzz/message.rs +++ b/crates/spacejam/src/fuzz/message.rs @@ -1,8 +1,9 @@ //! Fuzz messages +use anyhow::Context; use score::{Block, OpaqueHash, TimeSlot, TrieKey, block::Header}; use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, fmt::Display}; +use std::{collections::HashMap, fmt::Display, str::FromStr}; /// Messages used in the unix socket communication #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] @@ -83,8 +84,8 @@ impl Default for PeerInfo { fuzz_version: 1, // feature-ancestry (1) | feature-fork (2) — both [M1] mandatory fuzz_features: 3, - jam_version: Version::PROTOCOL, - app_version: Version::SPACEJAM, + jam_version: Version::protocol(), + app_version: Version::spacejam(), app_name: "spacejam".to_string(), } } @@ -104,19 +105,53 @@ pub struct Version { } impl Version { - /// The binary version of spacejam - pub const SPACEJAM: Version = Version { - major: 0, - minor: 1, - patch: 1, - }; - - /// The protocol version of spacejam - pub const PROTOCOL: Version = Version { - major: 0, - minor: 7, - patch: 2, - }; + /// Binary version, derived from `CARGO_PKG_VERSION_*` at compile time. + pub fn spacejam() -> Version { + Version { + major: env!("CARGO_PKG_VERSION_MAJOR") + .parse() + .expect("CARGO_PKG_VERSION_MAJOR not a u8"), + minor: env!("CARGO_PKG_VERSION_MINOR") + .parse() + .expect("CARGO_PKG_VERSION_MINOR not a u8"), + patch: env!("CARGO_PKG_VERSION_PATCH") + .parse() + .expect("CARGO_PKG_VERSION_PATCH not a u8"), + } + } + + /// JAM protocol version, sourced from `[workspace.metadata.graypaper]` + /// in the workspace manifest via the build script. + pub fn protocol() -> Version { + env!("GRAYPAPER_VERSION") + .parse() + .expect("invalid graypaper version") + } +} + +impl Display for Version { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}.{}.{}", self.major, self.minor, self.patch) + } +} + +impl FromStr for Version { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let mut parts = s.split('.'); + let major = parts.next().context("missing major")?.parse()?; + let minor = parts.next().context("missing minor")?.parse()?; + let patch = parts.next().context("missing patch")?.parse()?; + if parts.next().is_some() { + anyhow::bail!("version has too many components"); + } + Ok(Self { + major, + minor, + patch, + }) + } } /// A key-value pair diff --git a/crates/spacejam/src/fuzz/mod.rs b/crates/spacejam/src/fuzz/mod.rs index 7b1bfa861..3793c8e93 100644 --- a/crates/spacejam/src/fuzz/mod.rs +++ b/crates/spacejam/src/fuzz/mod.rs @@ -8,10 +8,13 @@ use std::{ }; pub mod env; -pub mod fuzzer; pub mod init; pub mod message; pub mod target; + +#[cfg(feature = "trace")] +pub mod fuzzer; +#[cfg(feature = "trace")] pub mod trace; /// Extension methods for streams diff --git a/crates/spacejam/src/fuzz/target.rs b/crates/spacejam/src/fuzz/target.rs index 9f6e25b02..e30a69f48 100644 --- a/crates/spacejam/src/fuzz/target.rs +++ b/crates/spacejam/src/fuzz/target.rs @@ -110,7 +110,7 @@ impl Target { /// Received info request pub fn info(&mut self, info: PeerInfo) -> anyhow::Result<()> { let this = PeerInfo::default(); - if info.jam_version != Version::PROTOCOL { + if info.jam_version != Version::protocol() { anyhow::bail!( "protocol version mismatched, remote: {:?}, local: {:?}", info.jam_version, @@ -155,7 +155,7 @@ impl Target { pub fn get_state(&mut self, hash: OpaqueHash) -> Result<()> { let mut state = Vec::new(); let iter: Box, Vec)>>> = - if let Some(fork) = self.chain.forks.get(&hash) { + if let Some((fork, _)) = self.chain.forks.get(&hash) { Box::new(fork.iter(Column::State)?) } else { Box::new(self.chain.data.iter(Column::State)?) diff --git a/crates/spacejam/src/lib.rs b/crates/spacejam/src/lib.rs index 88c537831..a8cbd2c8c 100644 --- a/crates/spacejam/src/lib.rs +++ b/crates/spacejam/src/lib.rs @@ -11,9 +11,6 @@ pub mod storage; mod utils; pub mod validator; -/// The version of matched graypaper -pub const GRAYPAPER: &str = "0.7.1"; - /// The config of development pub struct Development; diff --git a/crates/spacejam/src/node/builder.rs b/crates/spacejam/src/node/builder.rs index cbb27cdf8..34b1168f3 100644 --- a/crates/spacejam/src/node/builder.rs +++ b/crates/spacejam/src/node/builder.rs @@ -8,42 +8,38 @@ use network::Network; use std::{fs, net::SocketAddr, path::PathBuf, sync::Arc}; /// Spacejam node builder -#[derive(Clone)] -#[cfg_attr(feature = "cmd", derive(clap::Parser))] +#[derive(Clone, clap::Parser)] pub struct Builder { /// The genesis path - #[cfg_attr(feature = "cmd", arg(long, env = "CHAIN"))] + #[arg(long, env = "CHAIN")] pub chain: Option, /// The data path - #[cfg_attr(feature = "cmd", arg(short, long, default_value_t = default::data_path(), env = "DATA_PATH"))] + #[arg(short, long, default_value_t = default::data_path(), env = "DATA_PATH")] pub data_path: String, /// Whether running in dev mode - #[cfg_attr(feature = "cmd", arg(long, env = "DEV"))] + #[arg(long, env = "DEV")] pub dev: bool, /// Whether running in light mode - #[cfg_attr(feature = "cmd", arg(long, env = "LIGHT"))] + #[arg(long, env = "LIGHT")] pub light: bool, /// The network configuration - #[cfg_attr(feature = "cmd", command(flatten))] + #[command(flatten)] pub network: network::Config, /// Whether pruning the data directory before running - #[cfg_attr(feature = "cmd", arg(short, long, env = "PRUNE"))] + #[arg(short, long, env = "PRUNE")] pub prune: bool, /// The RPC address - #[cfg_attr( - feature = "cmd", - arg(short, long, default_value = "0.0.0.0:6789", env = "RPC") - )] + #[arg(short, long, default_value = "0.0.0.0:6789", env = "RPC")] pub rpc: SocketAddr, /// The seed of the validator - #[cfg_attr(feature = "cmd", arg(long, env = "VALIDATOR"))] + #[arg(long, env = "VALIDATOR")] pub validator: Option, } diff --git a/crates/spacejam/src/storage/parity.rs b/crates/spacejam/src/storage/parity.rs index 2a21e329f..2014652ba 100644 --- a/crates/spacejam/src/storage/parity.rs +++ b/crates/spacejam/src/storage/parity.rs @@ -5,9 +5,7 @@ use parity_db::{ BTreeIterator, ColumnOptions, Db, NewNode as PdNewNode, NodeRef as PdNodeRef, Operation as Op, Options, }; -use runtime::storage::{ - Column, Commit, KVStorage, MultiTree, NewNode, NodeAddress, NodeRef, Operation, -}; +use runtime::storage::{Column, Commit, KVStorage, MultiTree, NewNode, NodeAddress, NodeRef}; use score::{OpaqueHash, TrieKey}; use std::path::PathBuf; @@ -17,11 +15,14 @@ const TRIE_COL: u8 = Column::TrieNodes as u8; pub struct Parity(Db); impl KVStorage for Parity { - fn commit(&self, column: Column, commit: Commit>) -> Result<()> { - self.0.commit_changes(commit.ops().map(|op| match op { - Operation::Set(k, v) => (column as u8, Op::Set(k.to_vec(), v)), - Operation::Remove(k) => (column as u8, Op::Dereference(k.to_vec())), - }))?; + fn commit(&self, column: Column, commit: &Commit>) -> Result<()> { + let sets = commit + .iset() + .map(|(k, v)| (column as u8, Op::Set(k.to_vec(), v.clone()))); + let removes = commit + .iremoval() + .map(|k| (column as u8, Op::Dereference(k.to_vec()))); + self.0.commit_changes(sets.chain(removes))?; Ok(()) } diff --git a/crates/testing/src/assurances.rs b/crates/testing/src/assurances.rs index 187b3ad0e..ba135271d 100644 --- a/crates/testing/src/assurances.rs +++ b/crates/testing/src/assurances.rs @@ -22,7 +22,11 @@ pub fn run(test: &specjam::Test) -> anyhow::Result<()> { &input.pre_state.curr_validators, input.input.parent, &input.input.assurances, - ); + ) + .and_then(|(available, counts, triples)| { + crypto::ed25519::SigItem::batch_verify(&triples).map_err(|_| Error::BadSignature)?; + Ok((available, counts)) + }); assert_eq!(result.clone().map(|(a, _)| a), output.map(|s| s.reported)); // validate post state diff --git a/crates/testing/src/disputes.rs b/crates/testing/src/disputes.rs index d01bdb91b..2b630cf5e 100644 --- a/crates/testing/src/disputes.rs +++ b/crates/testing/src/disputes.rs @@ -22,9 +22,13 @@ pub fn run(test: &specjam::Test) -> anyhow::Result<()> { input.pre_state.tau, &input.pre_state.kappa, &input.pre_state.lambda, - &input.pre_state.psi, + input.pre_state.psi.clone(), &input.input.disputes, - ); + ) + .and_then(|(next_psi, records, triples)| { + crypto::ed25519::SigItem::batch_verify(&triples).map_err(|_| Error::BadSignature)?; + Ok((next_psi, records)) + }); // check offenders mark assert_eq!( diff --git a/crates/testing/src/preimage.rs b/crates/testing/src/preimage.rs index 2d374658c..f143b46c8 100644 --- a/crates/testing/src/preimage.rs +++ b/crates/testing/src/preimage.rs @@ -22,7 +22,8 @@ pub fn run(test: &specjam::Test) -> anyhow::Result<()> { assert_eq!(input.pre_state, output.post_state); return Ok(()); } - let accounts = tx::preimage::accounts(input.input.slot, &input.input.preimages, accounts); + let accounts = + tx::preimage::accounts(input.input.slot, input.input.preimages.clone(), accounts); assert_eq!( accounts .accounts() diff --git a/crates/testing/src/reports.rs b/crates/testing/src/reports.rs index 8cc19b93a..dc1cca2e7 100644 --- a/crates/testing/src/reports.rs +++ b/crates/testing/src/reports.rs @@ -33,8 +33,11 @@ pub fn run(test: &specjam::Test) -> anyhow::Result<()> { let result = tx::guarantee::reports(input.slot, &pre_state.avail_assignments, &input.guarantees) .and_then(|assignments| { - tx::guarantee::report(&state, input.slot, &state.accounts, &input.guarantees) - .map(|(reported, reporters)| (reported, reporters, assignments)) + let (reported, reporters, triples) = + tx::guarantee::report(&state, input.slot, &state.accounts, &input.guarantees)?; + crypto::ed25519::SigItem::batch_verify(&triples) + .map_err(|_| Error::BadSignature)?; + Ok((reported, reporters, assignments)) }); assert_eq!( diff --git a/crates/testing/src/safrole.rs b/crates/testing/src/safrole.rs index 5f5f0b95c..95b5a1f59 100644 --- a/crates/testing/src/safrole.rs +++ b/crates/testing/src/safrole.rs @@ -133,7 +133,7 @@ impl State { input.slot, self.eta, &self.post_offenders, - &safrole, + safrole.clone(), &validators, &input.extrinsic, ) { diff --git a/crates/testing/src/seq.rs b/crates/testing/src/seq.rs index c29987ef3..022db3a71 100644 --- a/crates/testing/src/seq.rs +++ b/crates/testing/src/seq.rs @@ -32,7 +32,7 @@ impl Processor { }; if is_ok { - self.chain.apply(&block, data); + self.chain.apply(&block, data)?; } Ok(()) } diff --git a/crates/vm/Cargo.toml b/crates/vm/Cargo.toml index 6dec2612f..d80153ce3 100644 --- a/crates/vm/Cargo.toml +++ b/crates/vm/Cargo.toml @@ -7,6 +7,7 @@ description = "Polkadot Virtual Machine" [dependencies] account = { workspace = true, features = ["blake2"] } anyhow.workspace = true +arc-swap.workspace = true codec.workspace = true crypto = { workspace = true, features = ["blake2"] } hex.workspace = true diff --git a/crates/vm/compiler/Cargo.toml b/crates/vm/compiler/Cargo.toml index 59ae18905..df07fc89e 100644 --- a/crates/vm/compiler/Cargo.toml +++ b/crates/vm/compiler/Cargo.toml @@ -10,6 +10,7 @@ dirs.workspace = true hex.workspace = true postcard.workspace = true pvm.workspace = true +rayon.workspace = true translator.workspace = true cranelift = { workspace = true, features = [ "native", diff --git a/crates/vm/compiler/src/compiler.rs b/crates/vm/compiler/src/compiler.rs index a515a7e55..3199d915c 100644 --- a/crates/vm/compiler/src/compiler.rs +++ b/crates/vm/compiler/src/compiler.rs @@ -2,13 +2,15 @@ use crate::{Memory, ModuleLike}; use pvm::{ - Argument, Invocation, Invoked, State, parser, + Argument, Invocation, Invoked, Pvm, State, parser, score::{Gas, OpaqueHash}, }; /// Cranelift JIT module builder pub struct Compiler; +impl Pvm for Compiler {} + impl Invocation for Compiler { fn invoke2( mut ctx: X, diff --git a/crates/vm/compiler/src/engine.rs b/crates/vm/compiler/src/engine.rs index 30ff7f666..dbda75ec3 100644 --- a/crates/vm/compiler/src/engine.rs +++ b/crates/vm/compiler/src/engine.rs @@ -19,7 +19,6 @@ impl Engine { builder.set("enable_incremental_compilation_cache_checks", "false")?; builder.set("unwind_info", "false")?; builder.set("machine_code_cfg_info", "false")?; - builder.set("enable_pcc", "false")?; // Create the ISA builder and finish it with the flags let isa_builder = native::builder().map_err(|e| anyhow::anyhow!("{}", e))?; @@ -38,7 +37,6 @@ impl Engine { builder.set("enable_incremental_compilation_cache_checks", "false")?; builder.set("unwind_info", "false")?; builder.set("machine_code_cfg_info", "false")?; - builder.set("enable_pcc", "false")?; // Create the ISA builder and finish it with the flags let isa_builder = native::builder().map_err(|e| anyhow::anyhow!("{}", e))?; diff --git a/crates/vm/compiler/src/exec.rs b/crates/vm/compiler/src/exec.rs index f51d02351..9e9e07572 100644 --- a/crates/vm/compiler/src/exec.rs +++ b/crates/vm/compiler/src/exec.rs @@ -90,6 +90,12 @@ impl Executable { return Err(anyhow::anyhow!("Failed to allocate memory")); } + // Huge-page hint only; mbind pinned AOT code to one node and hurt + // cross-socket execution. + #[cfg(target_os = "linux")] + unsafe { + libc::madvise(self.memory.cast(), self.size, libc::MADV_HUGEPAGE); + } Ok(()) } diff --git a/crates/vm/compiler/src/module/mod.rs b/crates/vm/compiler/src/module/mod.rs index 1f7c70bc1..3ffce7ef8 100644 --- a/crates/vm/compiler/src/module/mod.rs +++ b/crates/vm/compiler/src/module/mod.rs @@ -28,6 +28,12 @@ pub trait ModuleLike: Sized { /// Compile a program fn compile(self, program: &Program) -> Result; + /// Try to load a previously-saved artifact for `program` without + /// running codegen. + fn try_load(self, _program: &Program) -> Result> { + Ok(None) + } + /// Get the main function fn main(&self) -> Result>; diff --git a/crates/vm/compiler/src/module/object.rs b/crates/vm/compiler/src/module/object.rs index df777ac53..3861857f9 100644 --- a/crates/vm/compiler/src/module/object.rs +++ b/crates/vm/compiler/src/module/object.rs @@ -17,6 +17,19 @@ pub struct ObjectModule { exec: Executable, } +impl ObjectModule { + /// On-disk artifact filename for the AOT object cache. + fn artifact_name(program: &Program) -> String { + let info = program.meta.info(); + format!( + "{}-{}-{}.o", + info.name, + info.version, + &hex::encode(crypto::blake3(program.code.as_ref()))[..6] + ) + } +} + impl ModuleLike for ObjectModule { fn new() -> Result { let isa = Engine::compilation()?; @@ -29,13 +42,7 @@ impl ModuleLike for ObjectModule { } fn compile(mut self, program: &Program) -> Result { - let info = program.meta.info(); - let name = format!( - "{}-{}-{}.o", - info.name, - info.version, - &hex::encode(crypto::blake3(program.code.as_ref()))[..6] - ); + let name = Self::artifact_name(program); if let Some(object) = Artifact::get("lib", &name) { self.exec.load::<()>(&object)?; return Ok(self); @@ -52,6 +59,16 @@ impl ModuleLike for ObjectModule { Ok(self) } + fn try_load(mut self, program: &Program) -> Result> { + match Artifact::get("lib", &Self::artifact_name(program)) { + Some(object) => { + self.exec.load::<()>(&object)?; + Ok(Some(self)) + } + None => Ok(None), + } + } + fn main(&self) -> Result> { let main = self.exec.get("main")?; Ok(unsafe { std::mem::transmute::>(main) }) diff --git a/crates/vm/interpreter/src/pvmi.rs b/crates/vm/interpreter/src/pvmi.rs index 97f40eb6f..72e22194c 100644 --- a/crates/vm/interpreter/src/pvmi.rs +++ b/crates/vm/interpreter/src/pvmi.rs @@ -1,41 +1,24 @@ //! PVM interface implementation use crate::Interpreter; -use lru::LruCache; use parser::{program, reader::Offset, Instruction}; use pvm::{ score::{Gas, OpaqueHash}, - Argument, Invocation, Invoked, + Argument, Cache, Invocation, Invoked, Pvm, }; -use std::{ - num::NonZeroUsize, - sync::{Arc, LazyLock, Mutex}, -}; - -/// The maximum number of cached parsed programs. -const MAX_CACHED_PROGRAMS: usize = 16; +use std::sync::{Arc, LazyLock}; -/// Cached parsed programs (LRU). -pub static CACHED_PROGRAMS: LazyLock>>> = - LazyLock::new(|| { - Mutex::new(LruCache::new( - NonZeroUsize::new(MAX_CACHED_PROGRAMS).expect("MAX_CACHED_PROGRAMS must be non-zero"), - )) - }); +/// Cached parsed programs. +pub static CACHED_PROGRAMS: LazyLock> = LazyLock::new(Default::default); /// Set the parsed program. pub fn set(hash: OpaqueHash, program: ParsedProgram) { - if let Ok(mut cache) = CACHED_PROGRAMS.try_lock() { - cache.put(hash, Arc::new(program)); - } + CACHED_PROGRAMS.put(hash, Arc::new(program)); } /// Get the parsed program. pub fn get(hash: OpaqueHash) -> Option> { - if let Ok(mut cache) = CACHED_PROGRAMS.try_lock() { - return cache.get(&hash).cloned(); - } - None + CACHED_PROGRAMS.get(&hash) } /// The parsed program. @@ -64,3 +47,5 @@ impl Invocation for Interpreter { Self::invoke(program, hash, ctx, gas, pc).expect("fix me later") } } + +impl Pvm for Interpreter {} diff --git a/crates/vm/spacevm/Cargo.toml b/crates/vm/spacevm/Cargo.toml index 7081f7c68..d6e1fd48b 100644 --- a/crates/vm/spacevm/Cargo.toml +++ b/crates/vm/spacevm/Cargo.toml @@ -6,10 +6,10 @@ edition.workspace = true [dependencies] anyhow.workspace = true hex.workspace = true -lru.workspace = true pvm.workspace = true pvmc.workspace = true pvmi.workspace = true +rayon.workspace = true tracing.workspace = true [dev-dependencies] diff --git a/crates/vm/spacevm/src/lib.rs b/crates/vm/spacevm/src/lib.rs index ef5d54075..0f93d15c3 100644 --- a/crates/vm/spacevm/src/lib.rs +++ b/crates/vm/spacevm/src/lib.rs @@ -1,36 +1,17 @@ //! Jastime - JAM virtual machine use anyhow::Result; -use lru::LruCache; pub use pvm; use pvm::{ - Argument, Invocation, Invoked, State, parser, + Argument, Cache, Invocation, Invoked, Pvm, State, parser, score::{Gas, OpaqueHash}, }; pub use pvmc::{Artifact, Compiler, Memory, ModuleLike, SPACEJAM_CACHE_DIR}; pub use pvmi::Interpreter; -use std::{ - collections::BTreeSet, - num::NonZeroUsize, - sync::{Arc, LazyLock, Mutex, RwLock}, - thread, -}; - -/// Maximum number of modules to keep in memory. Evicted modules remain on disk -/// and can be reloaded when needed. -const MAX_CACHED_MODULES: usize = 8; - -/// Cached modules (LRU). Uses Mutex because LruCache::get requires &mut for LRU tracking. -pub static SPACEVM_MODULES: LazyLock>>> = - LazyLock::new(|| { - Mutex::new(LruCache::new( - NonZeroUsize::new(MAX_CACHED_MODULES).expect("MAX_CACHED_MODULES must be non-zero"), - )) - }); +use std::sync::{Arc, LazyLock}; -/// Locks for the Jastime compilation -pub static SPACEVM_LOCKS: LazyLock>> = - LazyLock::new(|| RwLock::new(BTreeSet::new())); +/// Cached AOT modules. +pub static SPACEVM_MODULES: LazyLock> = LazyLock::new(Default::default); /// SpaceVM - JAM virtual machine pub struct SpaceVM; @@ -44,11 +25,20 @@ impl Invocation for SpaceVM { gas: Gas, pc: usize, ) -> Invoked { - if let Ok(true) = SPACEVM_LOCKS.read().map(|lock| !lock.contains(&hash)) - && let Ok(Some(module)) = SPACEVM_MODULES - .lock() - .map(|mut cache| cache.get(&hash).cloned()) + let module = if let Some(module) = SPACEVM_MODULES.get(&hash) { + Some(module) + } else if let Ok(module) = ::new::() + && let Ok(program) = parser::program::preimage(code.clone(), &args) + && let Ok(Some(module)) = ModuleLike::try_load(module, &program) { + let arc = Arc::new(module); + SPACEVM_MODULES.put(hash, arc.clone()); + Some(arc) + } else { + None + }; + + if let Some(module) = module { let program = parser::program::preimage(code, &args).expect("failed to preimage"); let mut context = pvm::Context { registers: program.registers, @@ -86,19 +76,15 @@ impl Invocation for SpaceVM { }; } - // lock the compilation + // Kick off background AOT compile. { - if let Ok(locks) = SPACEVM_LOCKS.read() - && !locks.contains(&hash) - { - let code = code.clone(); - let args = args.clone(); - thread::spawn(move || { - if let Err(e) = self::compile::(code, args, hash, true) { - tracing::debug!("failed to compile program: {e:?}"); - } - }); - } + let code = code.clone(); + let args = args.clone(); + rayon::spawn(move || { + if let Err(e) = self::compile::(code, args, hash, true) { + tracing::debug!("failed to compile program: {e:?}"); + } + }); } // fallback to the interpreter @@ -106,6 +92,8 @@ impl Invocation for SpaceVM { } } +impl Pvm for SpaceVM {} + /// Compile a program pub fn compile( code: Vec, @@ -113,25 +101,17 @@ pub fn compile( hash: OpaqueHash, memcache: bool, ) -> Result<()> { - if let Ok(mut locks) = SPACEVM_LOCKS.write() { - locks.insert(hash); - } - match ::new::()? .compile(&parser::program::preimage(code, &args).expect("failed to preimage")) { Ok(module) => { - if memcache && let Ok(mut cache) = SPACEVM_MODULES.lock() { - cache.put(hash, Arc::new(module)); + if memcache { + SPACEVM_MODULES.put(hash, Arc::new(module)); } } Err(err) => { tracing::debug!("failed to compile program: {:?}", err); } } - - if let Ok(mut locks) = SPACEVM_LOCKS.write() { - locks.remove(&hash); - } Ok(()) } diff --git a/crates/vm/src/cache.rs b/crates/vm/src/cache.rs new file mode 100644 index 000000000..7b296fc72 --- /dev/null +++ b/crates/vm/src/cache.rs @@ -0,0 +1,34 @@ +//! Lock-free read-mostly cache keyed by program hash. + +use arc_swap::ArcSwap; +use score::OpaqueHash; +use std::{collections::HashMap, sync::Arc}; + +/// Lock-free hash-keyed cache holding `Arc`. +pub struct Cache { + inner: ArcSwap>>, +} + +impl Cache { + /// Look up by hash; returns a cheap `Arc` clone on hit. + pub fn get(&self, hash: &OpaqueHash) -> Option> { + self.inner.load().get(hash).cloned() + } + + /// Insert or replace. + pub fn put(&self, hash: OpaqueHash, value: Arc) { + self.inner.rcu(|prev| { + let mut next = (**prev).clone(); + next.insert(hash, value.clone()); + next + }); + } +} + +impl Default for Cache { + fn default() -> Self { + Self { + inner: ArcSwap::from_pointee(HashMap::new()), + } + } +} diff --git a/crates/vm/src/invocation/mod.rs b/crates/vm/src/invocation/mod.rs index c35dc491d..bba65d6ac 100644 --- a/crates/vm/src/invocation/mod.rs +++ b/crates/vm/src/invocation/mod.rs @@ -348,3 +348,4 @@ pub trait Invocation { } impl Invocation for () {} +impl crate::Pvm for () {} diff --git a/crates/vm/src/lib.rs b/crates/vm/src/lib.rs index 024fda7c1..0fb1a616f 100644 --- a/crates/vm/src/lib.rs +++ b/crates/vm/src/lib.rs @@ -5,6 +5,7 @@ pub use parser::{ }; pub use { account::Account, + cache::Cache, codec, context::{check_range, Argument, Context, Executed, Invoked, MemoryLike, State}, invocation::{AccumulateContext, AccumulateState, Accumulated, Invocation}, @@ -22,6 +23,7 @@ macro_rules! bail { }; } +mod cache; mod context; pub mod host; pub mod invocation; @@ -39,5 +41,3 @@ pub const MAX_FUNCTIONS: usize = 512; /// The PVM interface pub trait Pvm: Invocation {} - -impl Pvm for T where T: Invocation {} diff --git a/docker/spacejam.Dockerfile b/docker/spacejam.dockerfile similarity index 75% rename from docker/spacejam.Dockerfile rename to docker/spacejam.dockerfile index 144e3f3f6..64629444a 100644 --- a/docker/spacejam.Dockerfile +++ b/docker/spacejam.dockerfile @@ -11,11 +11,17 @@ # JAM_FUZZ_SOCK_PATH Unix domain socket path for fuzzer communication. # JAM_FUZZ_LOG_LEVEL Optional. error | warn | info | debug | trace. # -# Build via `make docker` (builds both tiny and full first). +# Build args: +# SPACEJAM_INTERP Set to 1 to bake an interpreter-only image (used by +# `make fuzz` for AOT-vs-interpreter A/B on NUMA hosts). +# +# Build via `make docker` (regular) or `make fuzz` (regular + interpreter). FROM debian:bookworm-slim +ARG SPACEJAM_INTERP="" COPY target/x86_64-unknown-linux-gnu/prod/spacejam-tiny /usr/local/bin/spacejam-tiny COPY target/x86_64-unknown-linux-gnu/prod/spacejam-full /usr/local/bin/spacejam-full COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh RUN chmod +x /usr/local/bin/entrypoint.sh +ENV SPACEJAM_INTERP=${SPACEJAM_INTERP} ENTRYPOINT ["entrypoint.sh"]