diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b3572020c..edd0ed447 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,7 +48,7 @@ Use log levels consistently so logs are easier to read and easier to filter by t - `info` - Use for important events that explain the node's current state or major transitions. - - Someone reading only `info` logs should still understand whether the node is starting, syncing, progressing, changing epoch/session, proposing blocks, or hitting notable conditions. + - Someone reading only `info` logs should still understand whether the node is starting, syncing, progressing, changing epoch, proposing blocks, or hitting notable conditions. - `debug` - Use for details that explain why the node behaved a certain way. diff --git a/Cargo.lock b/Cargo.lock index 3051a30f9..c13c0f390 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -464,9 +464,9 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "asn1-rs" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" dependencies = [ "asn1-rs-derive", "asn1-rs-impl", @@ -671,16 +671,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" dependencies = [ "base64 0.22.1", - "http 1.4.0", + "http 1.4.1", "log", "url", ] [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "axum" @@ -692,7 +692,7 @@ dependencies = [ "axum-core", "bytes", "futures-util", - "http 1.4.0", + "http 1.4.1", "http-body", "http-body-util", "hyper", @@ -724,7 +724,7 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http 1.4.0", + "http 1.4.1", "http-body", "http-body-util", "mime", @@ -824,7 +824,7 @@ dependencies = [ "quote", "regex", "rustc-hash 2.1.2", - "shlex", + "shlex 1.3.0", "syn 2.0.117", ] @@ -888,9 +888,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytecount" @@ -937,9 +937,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cbindgen" -version = "0.29.2" +version = "0.29.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "befbfd072a8e81c02f8c507aefce431fe5e7d051f83d48a23ffc9b9fe5a11799" +checksum = "c95537b45400390270fae69ac098d057c8f5399001cde9d04f700c105ddfff2d" dependencies = [ "heck", "indexmap 2.14.0", @@ -955,14 +955,14 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.61" +version = "1.2.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ "find-msvc-tools", "jobserver", "libc", - "shlex", + "shlex 2.0.1", ] [[package]] @@ -1243,9 +1243,9 @@ dependencies = [ [[package]] name = "const-hex" -version = "1.19.0" +version = "1.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20d9a563d167a9cce0f94153382b33cb6eded6dfabff03c69ad65a28ea1514e0" +checksum = "33e2a781ebdf4467d1428dc4593067825fb646f6871475098d8577421af73558" dependencies = [ "cfg-if", "cpufeatures 0.2.17", @@ -1450,7 +1450,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" dependencies = [ "dispatch2", - "nix 0.31.2", + "nix 0.31.3", "windows-sys 0.61.2", ] @@ -1632,7 +1632,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090" dependencies = [ "data-encoding", - "syn 1.0.109", + "syn 2.0.117", ] [[package]] @@ -1786,7 +1786,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1803,9 +1803,9 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", @@ -1908,9 +1908,9 @@ dependencies = [ [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "elliptic-curve" @@ -2050,7 +2050,7 @@ checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if", "rustix 1.1.4", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -2269,9 +2269,9 @@ checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-timer" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" [[package]] name = "futures-util" @@ -2303,9 +2303,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "1.4.1" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dab9e9188e97a93276e1fe7b56401b851e2b45a46d045ca658100c1303ada649" +checksum = "c2e55f16dcf0e9c00efbe2e655ffe45fc98e7066b52bc92f8a79e64060a79351" dependencies = [ "rustversion", "serde_core", @@ -2440,7 +2440,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http 1.4.0", + "http 1.4.1", "indexmap 2.14.0", "slab", "tokio", @@ -2507,9 +2507,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "hasher" @@ -2672,9 +2672,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" dependencies = [ "bytes", "itoa", @@ -2687,7 +2687,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.4.0", + "http 1.4.1", ] [[package]] @@ -2698,7 +2698,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.4.0", + "http 1.4.1", "http-body", "pin-project-lite", ] @@ -2732,16 +2732,16 @@ checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[package]] name = "hyper" -version = "1.9.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" dependencies = [ "atomic-waker", "bytes", "futures-channel", "futures-core", "h2", - "http 1.4.0", + "http 1.4.1", "http-body", "httparse", "httpdate", @@ -2758,7 +2758,7 @@ version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ - "http 1.4.0", + "http 1.4.1", "hyper", "hyper-util", "log", @@ -2809,14 +2809,14 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.4.0", + "http 1.4.1", "http-body", "hyper", "ipnet", "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.3", + "socket2 0.6.4", "tokio", "tower-service", "tracing", @@ -3004,7 +3004,7 @@ dependencies = [ "attohttpc 0.24.1", "bytes", "futures", - "http 1.4.0", + "http 1.4.1", "http-body-util", "hyper", "hyper-util", @@ -3025,7 +3025,7 @@ dependencies = [ "attohttpc 0.30.1", "bytes", "futures", - "http 1.4.0", + "http 1.4.1", "http-body-util", "hyper", "hyper-util", @@ -3075,7 +3075,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -3128,7 +3128,7 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" dependencies = [ - "socket2 0.6.3", + "socket2 0.6.4", "widestring", "windows-registry", "windows-result 0.4.1", @@ -3218,9 +3218,9 @@ dependencies = [ [[package]] name = "jiff" -version = "0.2.24" +version = "0.2.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" +checksum = "4603d3033e49e2b0e31229fcab20a5d40089c607d975cd9c80551dc69eed9102" dependencies = [ "jiff-static", "log", @@ -3231,9 +3231,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.24" +version = "0.2.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" +checksum = "782d32378dddf207193ac91cefb848ad41abb58195c95168e1291227a0832b47" dependencies = [ "proc-macro2", "quote", @@ -3252,9 +3252,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.98" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" dependencies = [ "cfg-if", "futures-util", @@ -3387,7 +3387,7 @@ dependencies = [ "bytes", "either", "futures", - "http 1.4.0", + "http 1.4.1", "http-body", "http-body-util", "hyper", @@ -3420,7 +3420,7 @@ checksum = "f126d2db7a8b532ec1d839ece2a71e2485dc3bbca6cc3c3f929becaa810e719e" dependencies = [ "derive_more", "form_urlencoded", - "http 1.4.0", + "http 1.4.1", "jiff", "json-patch", "k8s-openapi", @@ -3668,9 +3668,9 @@ dependencies = [ [[package]] name = "libp2p-identity" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c7892c221730ba55f7196e98b0b8ba5e04b4155651736036628e9f73ed6fc3" +checksum = "9525f3831544f7ae497bde79adf114ef127b0fbbb97edbbf692a80408636421c" dependencies = [ "asn1_der", "bs58", @@ -3678,7 +3678,7 @@ dependencies = [ "hkdf", "k256", "multihash", - "quick-protobuf", + "prost 0.14.3", "rand 0.8.6", "serde", "sha2", @@ -3941,9 +3941,9 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" dependencies = [ "libc", ] @@ -4029,9 +4029,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" [[package]] name = "logos-blockchain-api-service" @@ -4098,12 +4098,12 @@ dependencies = [ "logos-blockchain-blend-crypto", "logos-blockchain-blend-proofs", "logos-blockchain-core", + "logos-blockchain-cryptarchia-engine", "logos-blockchain-groth16", "logos-blockchain-key-management-system-keys", "logos-blockchain-log-targets", "logos-blockchain-utils", "serde", - "serde-big-array", "serde_with", "test-log", "thiserror 2.0.18", @@ -4126,7 +4126,7 @@ dependencies = [ "logos-blockchain-blend-message", "logos-blockchain-blend-proofs", "logos-blockchain-blend-scheduling", - "logos-blockchain-core", + "logos-blockchain-cryptarchia-engine", "logos-blockchain-key-management-system-keys", "logos-blockchain-libp2p", "logos-blockchain-log-targets", @@ -4142,7 +4142,7 @@ version = "0.1.2" dependencies = [ "const-hex", "ed25519-dalek", - "generic-array 1.4.1", + "generic-array 1.4.3", "hex", "logos-blockchain-blend-crypto", "logos-blockchain-core", @@ -4198,7 +4198,6 @@ dependencies = [ "libp2p-stream", "libp2p-swarm-test", "logos-blockchain-blend", - "logos-blockchain-chain-broadcast-service", "logos-blockchain-chain-service", "logos-blockchain-core", "logos-blockchain-cryptarchia-engine", @@ -4278,12 +4277,10 @@ version = "0.1.2" dependencies = [ "async-trait", "derivative", - "futures", "logos-blockchain-core", "overwatch", "serde", "tokio", - "tokio-stream", "tracing", ] @@ -4372,7 +4369,6 @@ dependencies = [ "rand 0.8.6", "serde", "serde_with", - "strum", "tempfile", "thiserror 2.0.18", "time", @@ -4392,8 +4388,8 @@ dependencies = [ [[package]] name = "logos-blockchain-circuits-build" -version = "0.5.0" -source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?rev=e6a8495dd39ed6fe94b071e8c60faa4dcdf0a619#e6a8495dd39ed6fe94b071e8c60faa4dcdf0a619" +version = "0.5.1" +source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?tag=v0.5.1#523091e17c8f2280ecf5d598b5563f3465393ced" dependencies = [ "dirs", "fd-lock", @@ -4404,16 +4400,16 @@ dependencies = [ [[package]] name = "logos-blockchain-circuits-common" -version = "0.5.0" -source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?rev=e6a8495dd39ed6fe94b071e8c60faa4dcdf0a619#e6a8495dd39ed6fe94b071e8c60faa4dcdf0a619" +version = "0.5.1" +source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?tag=v0.5.1#523091e17c8f2280ecf5d598b5563f3465393ced" dependencies = [ "logos-blockchain-circuits-types", ] [[package]] name = "logos-blockchain-circuits-poc-sys" -version = "0.5.0" -source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?rev=e6a8495dd39ed6fe94b071e8c60faa4dcdf0a619#e6a8495dd39ed6fe94b071e8c60faa4dcdf0a619" +version = "0.5.1" +source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?tag=v0.5.1#523091e17c8f2280ecf5d598b5563f3465393ced" dependencies = [ "logos-blockchain-circuits-build", "logos-blockchain-circuits-common", @@ -4422,8 +4418,8 @@ dependencies = [ [[package]] name = "logos-blockchain-circuits-pol-sys" -version = "0.5.0" -source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?rev=e6a8495dd39ed6fe94b071e8c60faa4dcdf0a619#e6a8495dd39ed6fe94b071e8c60faa4dcdf0a619" +version = "0.5.1" +source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?tag=v0.5.1#523091e17c8f2280ecf5d598b5563f3465393ced" dependencies = [ "logos-blockchain-circuits-build", "logos-blockchain-circuits-common", @@ -4432,8 +4428,8 @@ dependencies = [ [[package]] name = "logos-blockchain-circuits-poq-sys" -version = "0.5.0" -source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?rev=e6a8495dd39ed6fe94b071e8c60faa4dcdf0a619#e6a8495dd39ed6fe94b071e8c60faa4dcdf0a619" +version = "0.5.1" +source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?tag=v0.5.1#523091e17c8f2280ecf5d598b5563f3465393ced" dependencies = [ "logos-blockchain-circuits-build", "logos-blockchain-circuits-common", @@ -4450,16 +4446,16 @@ dependencies = [ [[package]] name = "logos-blockchain-circuits-rapidsnark-sys" -version = "0.5.0" -source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?rev=e6a8495dd39ed6fe94b071e8c60faa4dcdf0a619#e6a8495dd39ed6fe94b071e8c60faa4dcdf0a619" +version = "0.5.1" +source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?tag=v0.5.1#523091e17c8f2280ecf5d598b5563f3465393ced" dependencies = [ "logos-blockchain-circuits-build", ] [[package]] name = "logos-blockchain-circuits-signature-sys" -version = "0.5.0" -source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?rev=e6a8495dd39ed6fe94b071e8c60faa4dcdf0a619#e6a8495dd39ed6fe94b071e8c60faa4dcdf0a619" +version = "0.5.1" +source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?tag=v0.5.1#523091e17c8f2280ecf5d598b5563f3465393ced" dependencies = [ "logos-blockchain-circuits-build", "logos-blockchain-circuits-common", @@ -4468,8 +4464,8 @@ dependencies = [ [[package]] name = "logos-blockchain-circuits-types" -version = "0.5.0" -source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?rev=e6a8495dd39ed6fe94b071e8c60faa4dcdf0a619#e6a8495dd39ed6fe94b071e8c60faa4dcdf0a619" +version = "0.5.1" +source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?tag=v0.5.1#523091e17c8f2280ecf5d598b5563f3465393ced" dependencies = [ "bytes", "libc", @@ -4511,6 +4507,7 @@ version = "0.1.2" dependencies = [ "hex", "logos-blockchain-core", + "logos-blockchain-cryptarchia-engine", "logos-blockchain-groth16", "logos-blockchain-key-management-system-service", "logos-blockchain-libp2p", @@ -4668,7 +4665,7 @@ dependencies = [ "ark-groth16", "ark-serialize 0.4.2", "bincode", - "generic-array 1.4.1", + "generic-array 1.4.3", "hex", "num-bigint", "serde", @@ -4705,7 +4702,7 @@ dependencies = [ "async-trait", "bytes", "ed25519-dalek", - "generic-array 1.4.1", + "generic-array 1.4.3", "hex", "logos-blockchain-groth16", "logos-blockchain-key-management-system-macros", @@ -4893,7 +4890,7 @@ dependencies = [ "dhat", "futures", "hex", - "http 1.4.0", + "http 1.4.1", "libp2p", "logos-blockchain-api-service", "logos-blockchain-blend", @@ -5485,9 +5482,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "memmap2" @@ -5538,9 +5535,9 @@ checksum = "c505b3e17ed6b70a7ed2e67fbb2c560ee327353556120d6e72f5232b6880d536" [[package]] name = "mio" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "wasi", @@ -5835,9 +5832,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.31.2" +version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ "bitflags 2.11.1", "cfg-if", @@ -5920,9 +5917,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-format" @@ -6081,9 +6078,9 @@ checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "openssl" -version = "0.10.79" +version = "0.10.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf0b434746ee2832f4f0baf10137e1cabb18cbe6912c69e2e33263c45250f542" +checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" dependencies = [ "bitflags 2.11.1", "cfg-if", @@ -6112,9 +6109,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" -version = "0.9.115" +version = "0.9.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781" +checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" dependencies = [ "cc", "libc", @@ -6155,7 +6152,7 @@ checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d" dependencies = [ "async-trait", "bytes", - "http 1.4.0", + "http 1.4.1", "opentelemetry", "reqwest", ] @@ -6166,7 +6163,7 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f69cd6acbb9af919df949cd1ec9e5e7fdc2ef15d234b6b795aaa525cc02f71f" dependencies = [ - "http 1.4.0", + "http 1.4.1", "opentelemetry", "opentelemetry-http", "opentelemetry-proto", @@ -6432,18 +6429,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.12" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbf0d9e68100b3a7989b4901972f265cd542e560a3a8a724e1e20322f4d06ce9" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.12" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a990e22f43e84855daf260dded30524ef4a9021cc7541c26540500a50b624389" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", @@ -6905,7 +6902,7 @@ dependencies = [ "quinn-udp", "rustc-hash 2.1.2", "rustls", - "socket2 0.6.3", + "socket2 0.6.4", "thiserror 2.0.18", "tokio", "tracing", @@ -6942,7 +6939,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.3", + "socket2 0.6.4", "tracing", "windows-sys 0.60.2", ] @@ -7198,7 +7195,7 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http 1.4.0", + "http 1.4.1", "http-body", "http-body-util", "hyper", @@ -7283,12 +7280,13 @@ dependencies = [ [[package]] name = "rpds" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e75f485e819d4d3015e6c0d55d02a4fd3db47c1993d9e603e0361fba2bffb34" +checksum = "e025feb26210bc196b908e72deb063b1b4000754304341cbc168a1e72c857ebc" dependencies = [ "archery", "serde", + "smallvec", ] [[package]] @@ -7621,15 +7619,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde-big-array" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11fc7cc2c76d73e0f27ee52abbd64eec84d46f370c88371120433196934e4b7f" -dependencies = [ - "serde", -] - [[package]] name = "serde-value" version = "0.7.0" @@ -7681,9 +7670,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -7726,11 +7715,12 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.19.0" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05839ce67618e14a09b286535c0d9c94e85ef25469b0e13cb4f844e5593eb19" +checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" dependencies = [ "base64 0.22.1", + "bs58", "chrono", "hex", "serde_core", @@ -7741,9 +7731,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.19.0" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf2ebbe86054f9b45bc3881e865683ccfaccce97b9b4cb53f3039d67f355a334" +checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" dependencies = [ "darling 0.23.0", "proc-macro2", @@ -7828,6 +7818,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -7939,9 +7935,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", "windows-sys 0.61.2", @@ -7993,9 +7989,9 @@ dependencies = [ [[package]] name = "str_stack" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9091b6114800a5f2141aee1d1b9d6ca3592ac062dc5decb3764ec5895a47b4eb" +checksum = "7f446288b699d66d0fd2e30d1cfe7869194312524b3b9252594868ed26ef056a" [[package]] name = "strength_reduce" @@ -8202,9 +8198,9 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" [[package]] name = "tar" -version = "0.4.45" +version = "0.4.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" dependencies = [ "filetime", "libc", @@ -8585,9 +8581,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.2" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -8595,7 +8591,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.3", + "socket2 0.6.4", "tokio-macros", "tracing", "windows-sys 0.61.2", @@ -8704,14 +8700,14 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.11+spec-1.1.0" +version = "0.25.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" dependencies = [ "indexmap 2.14.0", "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow 1.0.2", + "winnow 1.0.3", ] [[package]] @@ -8720,7 +8716,7 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.2", + "winnow 1.0.3", ] [[package]] @@ -8741,7 +8737,7 @@ dependencies = [ "base64 0.22.1", "bytes", "h2", - "http 1.4.0", + "http 1.4.1", "http-body", "http-body-util", "hyper", @@ -8768,7 +8764,7 @@ dependencies = [ "async-trait", "base64 0.22.1", "bytes", - "http 1.4.0", + "http 1.4.1", "http-body", "http-body-util", "hyper", @@ -8837,15 +8833,15 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.10" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "base64 0.22.1", "bitflags 2.11.1", "bytes", "futures-util", - "http 1.4.0", + "http 1.4.1", "http-body", "http-body-util", "mime", @@ -9072,9 +9068,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.20.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "ucd-trie" @@ -9184,7 +9180,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" dependencies = [ "base64 0.22.1", - "http 1.4.0", + "http 1.4.1", "httparse", "log", ] @@ -9276,9 +9272,9 @@ checksum = "e2eebbbfe4093922c2b6734d7c679ebfebd704a0d7e56dfcb0d05818ce28977d" [[package]] name = "uuid" -version = "1.23.1" +version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -9388,9 +9384,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.121" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" dependencies = [ "cfg-if", "once_cell", @@ -9401,9 +9397,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.71" +version = "0.4.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" dependencies = [ "js-sys", "wasm-bindgen", @@ -9411,9 +9407,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.121" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -9421,9 +9417,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.121" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" dependencies = [ "bumpalo", "proc-macro2", @@ -9434,9 +9430,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.121" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" dependencies = [ "unicode-ident", ] @@ -9490,9 +9486,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.98" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" dependencies = [ "js-sys", "wasm-bindgen", @@ -9949,9 +9945,9 @@ checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" [[package]] name = "winnow" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" dependencies = [ "memchr", ] @@ -10175,18 +10171,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.48" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.48" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" dependencies = [ "proc-macro2", "quote", @@ -10195,9 +10191,9 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] diff --git a/Cargo.toml b/Cargo.toml index 3d3963d07..de21a317a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -163,12 +163,12 @@ lb-zksign = { default-features = false, package = "logo lb-zone-sdk = { default-features = false, package = "logos-blockchain-zone-sdk", path = "./zone-sdk" } lb_config = { default-features = false, package = "lb_config", path = "./tools/config" } lb_network = { default-features = false, package = "logos-blockchain-network-service", path = "./services/network" } -lbc-poc-sys = { default-features = false, git = "https://github.com/logos-blockchain/logos-blockchain-circuits.git", package = "logos-blockchain-circuits-poc-sys", rev = "e6a8495dd39ed6fe94b071e8c60faa4dcdf0a619" } # v0.5.0 -lbc-pol-sys = { default-features = false, git = "https://github.com/logos-blockchain/logos-blockchain-circuits.git", package = "logos-blockchain-circuits-pol-sys", rev = "e6a8495dd39ed6fe94b071e8c60faa4dcdf0a619" } # v0.5.0 -lbc-poq-sys = { default-features = false, git = "https://github.com/logos-blockchain/logos-blockchain-circuits.git", package = "logos-blockchain-circuits-poq-sys", rev = "e6a8495dd39ed6fe94b071e8c60faa4dcdf0a619" } # v0.5.0 -lbc-rapidsnark-sys = { default-features = false, git = "https://github.com/logos-blockchain/logos-blockchain-circuits.git", package = "logos-blockchain-circuits-rapidsnark-sys", rev = "e6a8495dd39ed6fe94b071e8c60faa4dcdf0a619" } # v0.5.0 -lbc-signature-sys = { default-features = false, git = "https://github.com/logos-blockchain/logos-blockchain-circuits.git", package = "logos-blockchain-circuits-signature-sys", rev = "e6a8495dd39ed6fe94b071e8c60faa4dcdf0a619" } # v0.5.0 -lbc-types = { default-features = false, git = "https://github.com/logos-blockchain/logos-blockchain-circuits.git", package = "logos-blockchain-circuits-types", rev = "e6a8495dd39ed6fe94b071e8c60faa4dcdf0a619" } # v0.5.0 +lbc-poc-sys = { default-features = false, git = "https://github.com/logos-blockchain/logos-blockchain-circuits.git", package = "logos-blockchain-circuits-poc-sys", tag = "v0.5.1" } +lbc-pol-sys = { default-features = false, git = "https://github.com/logos-blockchain/logos-blockchain-circuits.git", package = "logos-blockchain-circuits-pol-sys", tag = "v0.5.1" } +lbc-poq-sys = { default-features = false, git = "https://github.com/logos-blockchain/logos-blockchain-circuits.git", package = "logos-blockchain-circuits-poq-sys", tag = "v0.5.1" } +lbc-rapidsnark-sys = { default-features = false, git = "https://github.com/logos-blockchain/logos-blockchain-circuits.git", package = "logos-blockchain-circuits-rapidsnark-sys", tag = "v0.5.1" } +lbc-signature-sys = { default-features = false, git = "https://github.com/logos-blockchain/logos-blockchain-circuits.git", package = "logos-blockchain-circuits-signature-sys", tag = "v0.5.1" } +lbc-types = { default-features = false, git = "https://github.com/logos-blockchain/logos-blockchain-circuits.git", package = "logos-blockchain-circuits-types", tag = "v0.5.1" } lbp-error = { default-features = false, package = "logos-blockchain-proofs-error", path = "./zk/proofs/error" } testing-framework-core = { default-features = false, git = "https://github.com/logos-blockchain/logos-blockchain-testing.git", tag = "dev_snapshot_2026_06_03_4733e56a" } testing-framework-runner-compose = { default-features = false, git = "https://github.com/logos-blockchain/logos-blockchain-testing.git", tag = "dev_snapshot_2026_06_03_4733e56a" } diff --git a/blend/message/Cargo.toml b/blend/message/Cargo.toml index fe221e560..4e8322e25 100644 --- a/blend/message/Cargo.toml +++ b/blend/message/Cargo.toml @@ -20,12 +20,12 @@ itertools = { workspace = true } lb-blend-crypto = { workspace = true } lb-blend-proofs = { workspace = true } lb-core = { workspace = true } +lb-cryptarchia-engine = { workspace = true } lb-groth16 = { workspace = true } lb-key-management-system-keys = { workspace = true } lb-log-targets = { workspace = true } lb-utils = { features = ["rng"], workspace = true } serde = { workspace = true } -serde-big-array = { workspace = true } serde_with = { features = ["alloc"], workspace = true } thiserror = { workspace = true } tracing = { workspace = true } diff --git a/blend/message/src/crypto/proofs.rs b/blend/message/src/crypto/proofs.rs index 081c67bea..51d6ab6e3 100644 --- a/blend/message/src/crypto/proofs.rs +++ b/blend/message/src/crypto/proofs.rs @@ -1,4 +1,3 @@ -use core::mem::swap; use std::time::Instant; use lb_blend_proofs::{ @@ -23,7 +22,6 @@ use crate::encap::ProofsVerifier; /// verified. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct PoQVerificationInputsMinusSigningKey { - pub session: u64, pub core: CoreInputs, pub leader: LeaderInputs, } @@ -35,7 +33,6 @@ impl Default for PoQVerificationInputsMinusSigningKey { use lb_groth16::{Field as _, Fr}; Self { - session: 1, core: CoreInputs { zk_root: ZkHash::default(), quota: 1, @@ -63,7 +60,6 @@ pub enum Error { #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct RealProofsVerifier { current_inputs: PoQVerificationInputsMinusSigningKey, - previous_epoch_inputs: Option, } impl ProofsVerifier for RealProofsVerifier { @@ -73,41 +69,20 @@ impl ProofsVerifier for RealProofsVerifier { tracing::trace!("Generating new proof verifier with public inputs: {public_inputs:?}"); Self { current_inputs: public_inputs, - previous_epoch_inputs: None, } } - fn start_epoch_transition(&mut self, new_pol_inputs: LeaderInputs) { - let old_epoch_inputs = { - let mut new_pol_inputs = new_pol_inputs; - swap(&mut self.current_inputs.leader, &mut new_pol_inputs); - new_pol_inputs - }; - tracing::trace!( - "Transitioning epochs for proof verifier from: {old_epoch_inputs:?} to: {new_pol_inputs:?}" - ); - self.previous_epoch_inputs = Some(old_epoch_inputs); - } - - fn complete_epoch_transition(&mut self) { - self.previous_epoch_inputs = None; - } - fn verify_proof_of_quota( &self, proof: ProofOfQuota, signing_key: &Ed25519PublicKey, ) -> Result { - let PoQVerificationInputsMinusSigningKey { - core, - leader, - session, - } = self.current_inputs; + let PoQVerificationInputsMinusSigningKey { core, leader } = self.current_inputs; // Try with current input, and if it fails, try with the previous one, if any // (i.e., within the epoch transition period). tracing::trace!( - "Verifying proof of quota with key nullifier {:?}, signing key: {signing_key:?}, session {session:?}, public core inputs: {core:?} and leader inputs: {leader:?}.", + "Verifying proof of quota with key nullifier {:?}, signing key: {signing_key:?}, public core inputs: {core:?} and leader inputs: {leader:?}.", hex::encode(fr_to_bytes(&proof.key_nullifier())) ); let start = Instant::now(); @@ -115,31 +90,9 @@ impl ProofsVerifier for RealProofsVerifier { .verify(&PublicInputs { core, leader, - session, signing_key: *signing_key.as_inner(), }) - .or_else(|_| { - let Some(previous_epoch_inputs) = self.previous_epoch_inputs else { - tracing::debug!("Input proof invalid and no previous epoch to try with"); - return Err(Error::ProofOfQuota(quota::Error::InvalidProof)); - }; - tracing::trace!( - "Verifying same proof of quota with previous epoch leader inputs: {previous_epoch_inputs:?}." - ); - proof - .verify(&PublicInputs { - core, - leader: previous_epoch_inputs, - session, - signing_key: *signing_key.as_inner(), - }) - .map_err(Error::ProofOfQuota) - .inspect_err(|_| { - tracing::debug!( - "Input proof invalid with both current and previous epoch public inputs" - ); - }) - }); + .map_err(Error::ProofOfQuota); tracing::trace!( "Proof verification time: {} ms.", @@ -157,130 +110,3 @@ impl ProofsVerifier for RealProofsVerifier { proof.verify(inputs).map_err(Error::ProofOfSelection) } } - -#[cfg(test)] -mod tests { - use lb_blend_proofs::quota::inputs::prove::public::LeaderInputs; - use lb_core::crypto::ZkHash; - use lb_groth16::{Field as _, Fr}; - - use crate::{ - crypto::proofs::{PoQVerificationInputsMinusSigningKey, RealProofsVerifier}, - encap::ProofsVerifier as _, - }; - - fn epoch_1_leader() -> LeaderInputs { - LeaderInputs { - pol_ledger_aged: ZkHash::ONE, - pol_epoch_nonce: ZkHash::ONE, - message_quota: 2, - lottery_0: Fr::ONE, - lottery_1: Fr::ONE, - } - } - - #[test] - fn new_verifier_has_no_previous_epoch() { - let verifier = RealProofsVerifier::new(PoQVerificationInputsMinusSigningKey::default()); - assert!(verifier.previous_epoch_inputs.is_none()); - assert_eq!( - verifier.current_inputs.leader, - PoQVerificationInputsMinusSigningKey::default().leader - ); - } - - #[test] - fn start_epoch_transition_stores_previous_epoch() { - let initial = PoQVerificationInputsMinusSigningKey::default(); - let mut verifier = RealProofsVerifier::new(initial); - let new_leader = epoch_1_leader(); - - verifier.start_epoch_transition(new_leader); - - // Current should be updated to new epoch. - assert_eq!(verifier.current_inputs.leader, new_leader); - // Previous should hold the old epoch's leader inputs. - assert_eq!(verifier.previous_epoch_inputs, Some(initial.leader)); - } - - #[test] - fn complete_epoch_transition_clears_previous_epoch() { - let mut verifier = RealProofsVerifier::new(PoQVerificationInputsMinusSigningKey::default()); - verifier.start_epoch_transition(epoch_1_leader()); - - assert!(verifier.previous_epoch_inputs.is_some()); - - verifier.complete_epoch_transition(); - - assert!( - verifier.previous_epoch_inputs.is_none(), - "Previous epoch inputs must be cleared after completing transition" - ); - assert_eq!(verifier.current_inputs.leader, epoch_1_leader()); - } - - #[test] - fn consecutive_epoch_transitions_replace_previous() { - let initial = PoQVerificationInputsMinusSigningKey::default(); - let mut verifier = RealProofsVerifier::new(initial); - - let leader_1 = epoch_1_leader(); - verifier.start_epoch_transition(leader_1); - assert_eq!(verifier.previous_epoch_inputs, Some(initial.leader)); - - // Start another transition without completing the first. - let leader_2 = LeaderInputs { - pol_ledger_aged: ZkHash::ZERO, - pol_epoch_nonce: ZkHash::ONE, - message_quota: 3, - lottery_0: Fr::ZERO, - lottery_1: Fr::ONE, - }; - verifier.start_epoch_transition(leader_2); - - // Previous should now be epoch 1 (not initial epoch 0). - assert_eq!(verifier.current_inputs.leader, leader_2); - assert_eq!(verifier.previous_epoch_inputs, Some(leader_1)); - } - - #[test] - fn complete_then_new_epoch_transition() { - let initial = PoQVerificationInputsMinusSigningKey::default(); - let mut verifier = RealProofsVerifier::new(initial); - - // Epoch 0 → 1 - let leader_1 = epoch_1_leader(); - verifier.start_epoch_transition(leader_1); - verifier.complete_epoch_transition(); - assert!(verifier.previous_epoch_inputs.is_none()); - assert_eq!(verifier.current_inputs.leader, leader_1); - - // Epoch 1 → 2 - let leader_2 = LeaderInputs { - pol_ledger_aged: ZkHash::ZERO, - pol_epoch_nonce: ZkHash::ONE, - message_quota: 3, - lottery_0: Fr::ZERO, - lottery_1: Fr::ONE, - }; - verifier.start_epoch_transition(leader_2); - assert_eq!(verifier.current_inputs.leader, leader_2); - assert_eq!( - verifier.previous_epoch_inputs, - Some(leader_1), - "After new transition, previous must be the completed epoch 1" - ); - } - - #[test] - fn session_and_core_inputs_preserved_across_epoch_transitions() { - let initial = PoQVerificationInputsMinusSigningKey::default(); - let mut verifier = RealProofsVerifier::new(initial); - - verifier.start_epoch_transition(epoch_1_leader()); - - // Session and core inputs should not change during epoch transitions. - assert_eq!(verifier.current_inputs.session, initial.session); - assert_eq!(verifier.current_inputs.core, initial.core); - } -} diff --git a/blend/message/src/encap/mod.rs b/blend/message/src/encap/mod.rs index f9fe329f9..697300800 100644 --- a/blend/message/src/encap/mod.rs +++ b/blend/message/src/encap/mod.rs @@ -1,5 +1,5 @@ use lb_blend_proofs::{ - quota::{ProofOfQuota, VerifiedProofOfQuota, inputs::prove::public::LeaderInputs}, + quota::{ProofOfQuota, VerifiedProofOfQuota}, selection::{ProofOfSelection, VerifiedProofOfSelection, inputs::VerifyInputs}, }; use lb_key_management_system_keys::keys::Ed25519PublicKey; @@ -13,21 +13,14 @@ pub mod validated; #[cfg(test)] mod tests; -/// A session-bound `PoQ` verifier. +/// An epoch-bound `PoQ` verifier. pub trait ProofsVerifier { type Error; /// Create a new proof verifier with the public inputs corresponding to the - /// current Blend session and cryptarchia epoch. + /// current epoch. fn new(public_inputs: PoQVerificationInputsMinusSigningKey) -> Self; - /// Start a new epoch while still maintaining the old one around for - /// messages that are propagated around the bound between two epochs. - fn start_epoch_transition(&mut self, new_pol_inputs: LeaderInputs); - /// Complete the transition period and discard any messages generated in the - /// previous epoch. - fn complete_epoch_transition(&mut self); - /// Proof of Quota verification logic. fn verify_proof_of_quota( &self, diff --git a/blend/message/src/encap/tests.rs b/blend/message/src/encap/tests.rs index 868bf3350..5591f2a2c 100644 --- a/blend/message/src/encap/tests.rs +++ b/blend/message/src/encap/tests.rs @@ -1,7 +1,7 @@ use core::convert::Infallible; use lb_blend_proofs::{ - quota::{ProofOfQuota, VerifiedProofOfQuota, inputs::prove::public::LeaderInputs}, + quota::{ProofOfQuota, VerifiedProofOfQuota}, selection::{ProofOfSelection, VerifiedProofOfSelection, inputs::VerifyInputs}, }; use lb_core::codec::{DeserializeOp as _, SerializeOp as _}; @@ -33,10 +33,6 @@ impl ProofsVerifier for NeverFailingProofsVerifier { Self } - fn start_epoch_transition(&mut self, _new_pol_inputs: LeaderInputs) {} - - fn complete_epoch_transition(&mut self) {} - fn verify_proof_of_quota( &self, proof: ProofOfQuota, @@ -65,10 +61,6 @@ impl ProofsVerifier for AlwaysFailingProofOfQuotaVerifier { Self } - fn start_epoch_transition(&mut self, _new_pol_inputs: LeaderInputs) {} - - fn complete_epoch_transition(&mut self) {} - fn verify_proof_of_quota( &self, _proof: ProofOfQuota, @@ -97,10 +89,6 @@ impl ProofsVerifier for AlwaysFailingProofOfSelectionVerifier { Self } - fn start_epoch_transition(&mut self, _new_pol_inputs: LeaderInputs) {} - - fn complete_epoch_transition(&mut self) {} - fn verify_proof_of_quota( &self, proof: ProofOfQuota, diff --git a/blend/message/src/error.rs b/blend/message/src/error.rs index 7a57a3099..8466a513a 100644 --- a/blend/message/src/error.rs +++ b/blend/message/src/error.rs @@ -24,7 +24,7 @@ pub enum Error { SignatureVerificationFailed, #[error("Node is not a core node")] NotCoreNodeReceiver, - #[error("Node has generated the maximum number of allowed Proof of Quota this session")] + #[error("Node has generated the maximum number of allowed Proof of Quota this epoch")] NoMoreProofOfQuotas, #[error("Attempted to generate a leadership proof without any secret PoL info provided.")] NoLeadershipInfoProvided, diff --git a/blend/message/src/reward/activity.rs b/blend/message/src/reward/activity.rs index c4110ff70..8aed1d21c 100644 --- a/blend/message/src/reward/activity.rs +++ b/blend/message/src/reward/activity.rs @@ -1,6 +1,6 @@ use derivative::Derivative; use lb_blend_proofs::selection::inputs::VerifyInputs; -use lb_core::sdp::SessionNumber; +use lb_cryptarchia_engine::Epoch; use serde::Serialize; use tracing::debug; @@ -12,23 +12,20 @@ use crate::{ }, }; -/// An activity proof for a session, made of the blending token +/// An activity proof for an epoch, made of the blending token /// that has the smallest Hamming distance satisfying the activity threshold. #[derive(Derivative, Serialize)] #[derivative(Debug)] pub struct ActivityProof { - session_number: SessionNumber, + epoch: Epoch, #[derivative(Debug = "ignore")] token: BlendingToken, } impl ActivityProof { #[must_use] - pub const fn new(session_number: SessionNumber, token: BlendingToken) -> Self { - Self { - session_number, - token, - } + pub const fn new(epoch: Epoch, token: BlendingToken) -> Self { + Self { epoch, token } } #[must_use] @@ -57,14 +54,14 @@ impl ActivityProof { )?; Ok(Self::new( - proof.session, + proof.epoch, BlendingToken::new(proof.signing_key, proof_of_quota, proof_of_selection), )) } } /// Computes the activity threshold, which is the expected maximum Hamming -/// distance from any blending token in a session to the next session +/// distance from any blending token in an epoch to the next epoch /// randomness. pub fn activity_threshold( token_count_bit_len: u64, @@ -86,7 +83,7 @@ pub fn activity_threshold( impl From<&ActivityProof> for lb_core::sdp::blend::ActivityProof { fn from(proof: &ActivityProof) -> Self { Self { - session: proof.session_number, + epoch: proof.epoch, signing_key: *proof.token.signing_key(), proof_of_quota: (*proof.token.proof_of_quota()).into(), proof_of_selection: (*proof.token.proof_of_selection()).into(), diff --git a/blend/message/src/reward/session.rs b/blend/message/src/reward/epoch.rs similarity index 75% rename from blend/message/src/reward/session.rs rename to blend/message/src/reward/epoch.rs index d8e8f8463..2bcab03f0 100644 --- a/blend/message/src/reward/session.rs +++ b/blend/message/src/reward/epoch.rs @@ -1,30 +1,30 @@ use core::fmt::{self, Debug, Formatter}; -use std::ops::{Add as _, Deref}; +use std::ops::Add as _; -use lb_blend_crypto::blake2b512; -use lb_core::{crypto::ZkHash, sdp::SessionNumber}; -use lb_groth16::fr_to_bytes; +use lb_core::crypto::ZkHash; +use lb_cryptarchia_engine::Epoch; +use lb_groth16::{Fr, FrBytes, fr_to_bytes}; use lb_utils::math::{F64Ge1, NonNegativeF64}; use serde::{Deserialize, Serialize}; use crate::reward::{BlendingToken, activity, token::HammingDistance}; -/// Session-specific information to compute an activity proof. -pub struct SessionInfo { - pub(crate) session_number: SessionNumber, - pub(crate) session_randomness: SessionRandomness, +/// Epoch-specific information to compute an activity proof. +pub struct EpochInfo { + pub(crate) epoch: Epoch, + pub(crate) epoch_randomness: EpochRandomness, pub(crate) token_evaluation: BlendingTokenEvaluation, } -impl SessionInfo { +impl EpochInfo { pub fn new( - session_number: SessionNumber, + epoch: Epoch, pol_epoch_nonce: &ZkHash, num_core_nodes: u64, core_quota: u64, activity_threshold_sensitivity: u64, ) -> Result { - let session_randomness = SessionRandomness::new(session_number, pol_epoch_nonce); + let epoch_randomness = (*pol_epoch_nonce).into(); let token_evaluation = BlendingTokenEvaluation::new( core_quota, num_core_nodes, @@ -32,19 +32,19 @@ impl SessionInfo { )?; Ok(Self { - session_number, - session_randomness, + epoch, + epoch_randomness, token_evaluation, }) } #[must_use] - pub const fn session_randomness(&self) -> SessionRandomness { - self.session_randomness + pub const fn epoch_randomness(&self) -> EpochRandomness { + self.epoch_randomness } } -/// Parameters to evaluate a blending token for a session. +/// Parameters to evaluate a blending token for an epoch. #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct BlendingTokenEvaluation { token_count_byte_len: u64, @@ -71,15 +71,15 @@ impl BlendingTokenEvaluation { } /// Calculate the Hamming distance of the given blending token to the next - /// session randomness, and return it only if it is not larger than the + /// epoch randomness, and return it only if it is not larger than the /// activity threshold. #[must_use] pub fn evaluate( &self, token: &BlendingToken, - next_session_randomness: SessionRandomness, + next_epoch_randomness: EpochRandomness, ) -> Option { - let distance = token.hamming_distance(self.token_count_byte_len, next_session_randomness); + let distance = token.hamming_distance(self.token_count_byte_len, next_epoch_randomness); tracing::trace!( "Evaluated blending token {:?} for activity proof. Calculated Hamming distance = {distance:?}", token.signing_key() @@ -93,42 +93,26 @@ impl BlendingTokenEvaluation { } } -/// Deterministic unbiased randomness for a session. +/// Deterministic unbiased randomness for an epoch. #[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SessionRandomness(#[serde(with = "serde_big_array::BigArray")] [u8; 64]); +pub struct EpochRandomness(#[serde(with = "lb_groth16::serde::serde_fr")] Fr); -impl Debug for SessionRandomness { +impl Debug for EpochRandomness { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "SessionRandomness({})", hex::encode(self.0)) + write!(f, "EpochRandomness({})", hex::encode(self.as_bytes())) } } -impl Deref for SessionRandomness { - type Target = [u8]; - - fn deref(&self) -> &Self::Target { - &self.0 +impl From for EpochRandomness { + fn from(value: Fr) -> Self { + Self(value) } } -impl From<[u8; 64]> for SessionRandomness { - fn from(bytes: [u8; 64]) -> Self { - Self(bytes) - } -} - -const SESSION_RANDOMNESS_TAG: [u8; 27] = *b"BLEND_SESSION_RANDOMNESS_V1"; - -impl SessionRandomness { - /// Derive the session randomness from the given session number and epoch - /// nonce. +impl EpochRandomness { #[must_use] - pub fn new(session_number: SessionNumber, epoch_nonce: &ZkHash) -> Self { - Self(blake2b512(&[ - &SESSION_RANDOMNESS_TAG, - &fr_to_bytes(epoch_nonce), - &session_number.to_le_bytes(), - ])) + pub fn as_bytes(&self) -> FrBytes { + fr_to_bytes(&self.0) } } @@ -141,7 +125,7 @@ pub enum Error { } /// The number of bits that can represent the maximum number of blending -/// tokens generated during a single session. +/// tokens generated during a single epoch. pub fn token_count_bit_len(core_quota: u64, num_core_nodes: u64) -> Result { let total_core_quota = core_quota .checked_mul(num_core_nodes) @@ -179,6 +163,7 @@ pub fn activity_threshold( #[cfg(test)] mod tests { use lb_blend_proofs::{quota::VerifiedProofOfQuota, selection::VerifiedProofOfSelection}; + use lb_groth16::Field as _; use lb_key_management_system_keys::keys::Ed25519Key; use super::*; @@ -231,17 +216,17 @@ mod tests { VerifiedProofOfQuota::from_bytes_unchecked([0; _]), VerifiedProofOfSelection::from_bytes_unchecked([0; _]), ), - SessionRandomness::from([0; 64]), + EpochRandomness::from(Fr::ZERO), ); assert_eq!(maybe_distance, Some(8.into())); let maybe_distance = evaluation.evaluate( &BlendingToken::new( - Ed25519Key::from_bytes(&[1; _]).public_key(), - VerifiedProofOfQuota::from_bytes_unchecked([1; _]), - VerifiedProofOfSelection::from_bytes_unchecked([1; _]), + Ed25519Key::from_bytes(&[0; _]).public_key(), + VerifiedProofOfQuota::from_bytes_unchecked([0; _]), + VerifiedProofOfSelection::from_bytes_unchecked([0; _]), ), - SessionRandomness::from([0; 64]), + EpochRandomness::from(ZkHash::from(100)), ); assert_eq!(maybe_distance, None); } diff --git a/blend/message/src/reward/mod.rs b/blend/message/src/reward/mod.rs index 9036b1111..02e19c288 100644 --- a/blend/message/src/reward/mod.rs +++ b/blend/message/src/reward/mod.rs @@ -1,34 +1,34 @@ mod activity; -mod session; +mod epoch; mod token; use std::collections::HashSet; pub use activity::ActivityProof; -use lb_core::sdp::SessionNumber; +pub use epoch::EpochInfo; +use lb_cryptarchia_engine::Epoch; use lb_log_targets::blend; use serde::{Deserialize, Serialize}; -pub use session::SessionInfo; pub use token::{BlendingToken, HammingDistance}; -pub use crate::reward::session::{BlendingTokenEvaluation, Error, SessionRandomness}; +pub use crate::reward::epoch::{BlendingTokenEvaluation, EpochRandomness, Error}; const LOG_TARGET: &str = blend::message::REWARD; -/// Holds blending tokens collected during a single session. +/// Holds blending tokens collected during a single epoch. #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SessionBlendingTokenCollector { - session_number: SessionNumber, +pub struct EpochBlendingTokenCollector { + epoch: Epoch, token_evaluation: BlendingTokenEvaluation, tokens: HashSet, } -impl SessionBlendingTokenCollector { +impl EpochBlendingTokenCollector { #[must_use] - pub fn new(session_info: &SessionInfo) -> Self { + pub fn new(epoch_info: &EpochInfo) -> Self { Self { - session_number: session_info.session_number, - token_evaluation: session_info.token_evaluation, + epoch: epoch_info.epoch, + token_evaluation: epoch_info.token_evaluation, tokens: HashSet::new(), } } @@ -38,21 +38,21 @@ impl SessionBlendingTokenCollector { } #[must_use] - pub fn rotate_session( + pub fn rotate_epoch( self, - new_session_info: &SessionInfo, - ) -> (Self, OldSessionBlendingTokenCollector) { - let new_collector = Self::new(new_session_info); - let old_collector = OldSessionBlendingTokenCollector { + new_epoch_info: &EpochInfo, + ) -> (Self, OldEpochBlendingTokenCollector) { + let new_collector = Self::new(new_epoch_info); + let old_collector = OldEpochBlendingTokenCollector { collector: self, - next_session_randomness: new_session_info.session_randomness(), + next_epoch_randomness: new_epoch_info.epoch_randomness(), }; (new_collector, old_collector) } #[must_use] - pub const fn session_number(&self) -> SessionNumber { - self.session_number + pub const fn epoch(&self) -> Epoch { + self.epoch } #[cfg(any(test, feature = "unsafe-test-functions"))] @@ -62,19 +62,19 @@ impl SessionBlendingTokenCollector { } } -/// Holds blending tokens collected during the old session. +/// Holds blending tokens collected during the old epoch. #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct OldSessionBlendingTokenCollector { - collector: SessionBlendingTokenCollector, - next_session_randomness: SessionRandomness, +pub struct OldEpochBlendingTokenCollector { + collector: EpochBlendingTokenCollector, + next_epoch_randomness: EpochRandomness, } -impl OldSessionBlendingTokenCollector { +impl OldEpochBlendingTokenCollector { pub fn collect(&mut self, token: BlendingToken) { self.collector.collect(token); } - /// Computes an activity proof for this session by consuming tokens. + /// Computes an activity proof for this epoch by consuming tokens. /// /// It returns `None` if there was no blending token satisfying the /// activity threshold calculated. @@ -84,10 +84,10 @@ impl OldSessionBlendingTokenCollector { // which is <= activity threshold. tracing::trace!( LOG_TARGET, - "Computing activity proof for session {} with activity threshold {:?} and next session randomness {:?} for {} candidate tokens", - self.collector.session_number(), + "Computing activity proof for epoch {} with activity threshold {:?} and next epoch randomness {:?} for {} candidate tokens", + self.collector.epoch(), self.collector.token_evaluation.activity_threshold(), - self.next_session_randomness, + self.next_epoch_randomness, self.collector.tokens.len() ); let (winning_activity_proof, distance) = self @@ -100,7 +100,7 @@ impl OldSessionBlendingTokenCollector { let distance = self .collector .token_evaluation - .evaluate(&token, self.next_session_randomness) + .evaluate(&token, self.next_epoch_randomness) .map(|distance| (token, distance)); tracing::trace!( @@ -111,25 +111,20 @@ impl OldSessionBlendingTokenCollector { distance }) .min_by_key(|(_, distance)| *distance) - .map(|(token, distance)| { - ( - ActivityProof::new(self.collector.session_number, token), - distance, - ) - })?; + .map(|(token, distance)| (ActivityProof::new(self.collector.epoch, token), distance))?; tracing::trace!( LOG_TARGET, - "Computed activity proof for session {}: {winning_activity_proof:?} with Hamming distance {distance:?}", - self.collector.session_number, + "Computed activity proof for epoch {}: {winning_activity_proof:?} with Hamming distance {distance:?}", + self.collector.epoch, ); Some(winning_activity_proof) } #[must_use] - pub const fn session_number(&self) -> SessionNumber { - self.collector.session_number() + pub const fn epoch(&self) -> Epoch { + self.collector.epoch() } #[cfg(any(test, feature = "unsafe-test-functions"))] @@ -159,9 +154,9 @@ mod tests { fn test_blending_token_collector() { let num_core_nodes = 2; let core_quota = 15; - let session_info = - SessionInfo::new(1, &ZkHash::from(1), num_core_nodes, core_quota, 1).unwrap(); - let mut tokens = SessionBlendingTokenCollector::new(&session_info); + let epoch_info = + EpochInfo::new(1.into(), &ZkHash::from(1), num_core_nodes, core_quota, 1).unwrap(); + let mut tokens = EpochBlendingTokenCollector::new(&epoch_info); assert!(tokens.tokens().is_empty()); // Insert `total_core_quota-1` tokens. @@ -176,10 +171,10 @@ mod tests { i += 1; } - // Prepare a new session info. - let session_info = - SessionInfo::new(2, &ZkHash::from(2), num_core_nodes, core_quota, 1).unwrap(); - let (_, mut tokens) = tokens.rotate_session(&session_info); + // Prepare a new epoch info. + let epoch_info = + EpochInfo::new(2.into(), &ZkHash::from(2), num_core_nodes, core_quota, 1).unwrap(); + let (_, mut tokens) = tokens.rotate_epoch(&epoch_info); // Insert one more tokens. // Now,`total_core_quota` tokens have been collected. diff --git a/blend/message/src/reward/token.rs b/blend/message/src/reward/token.rs index 3dd2c6aa3..628a9521b 100644 --- a/blend/message/src/reward/token.rs +++ b/blend/message/src/reward/token.rs @@ -7,7 +7,7 @@ use lb_core::codec::SerializeOp as _; use lb_key_management_system_keys::keys::Ed25519PublicKey; use serde::{Deserialize, Serialize}; -use crate::reward::session::SessionRandomness; +use crate::reward::epoch::EpochRandomness; /// A blending token consisting of a proof of quota and a proof of selection. #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -32,20 +32,23 @@ impl BlendingToken { } /// Computes the Hamming distance between this blending token and the next - /// session randomness. + /// epoch randomness. #[must_use] pub fn hamming_distance( &self, token_count_byte_len: u64, - next_session_randomness: SessionRandomness, + next_epoch_randomness: EpochRandomness, ) -> HammingDistance { let token = self .to_bytes() .expect("BlendingToken should be serializable"); let token_hash = hash(&token, token_count_byte_len as usize); - let session_randomness_hash = hash(&next_session_randomness, token_count_byte_len as usize); + let epoch_randomness_hash = hash( + &next_epoch_randomness.as_bytes(), + token_count_byte_len as usize, + ); - HammingDistance::new(&token_hash, &session_randomness_hash) + HammingDistance::new(&token_hash, &epoch_randomness_hash) } #[must_use] @@ -107,6 +110,7 @@ impl From for HammingDistance { #[cfg(test)] mod tests { use lb_blend_proofs::{quota::PROOF_OF_QUOTA_SIZE, selection::PROOF_OF_SELECTION_SIZE}; + use lb_groth16::{Field as _, Fr}; use lb_key_management_system_keys::keys::Ed25519Key; use super::*; @@ -150,7 +154,7 @@ mod tests { #[test] fn test_blending_token_hamming_distance() { let token = blending_token(1, 1, 2); - assert_eq!(token.hamming_distance(1, [3u8; 64].into()), 2.into()); + assert_eq!(token.hamming_distance(1, Fr::ONE.into()), 6.into()); } fn blending_token( diff --git a/blend/network/Cargo.toml b/blend/network/Cargo.toml index 384af2795..dd16021f0 100644 --- a/blend/network/Cargo.toml +++ b/blend/network/Cargo.toml @@ -20,7 +20,7 @@ hex = { workspace = true } lb-blend-message = { workspace = true } lb-blend-proofs = { workspace = true } lb-blend-scheduling = { workspace = true } -lb-core = { workspace = true } +lb-cryptarchia-engine = { workspace = true } lb-key-management-system-keys = { workspace = true } lb-libp2p = { workspace = true } lb-log-targets = { workspace = true } diff --git a/blend/network/src/core/mod.rs b/blend/network/src/core/mod.rs index 91cfadc26..3b3960254 100644 --- a/blend/network/src/core/mod.rs +++ b/blend/network/src/core/mod.rs @@ -5,6 +5,7 @@ pub mod with_edge; mod tests; use lb_blend_scheduling::membership::Membership; +use lb_cryptarchia_engine::Epoch; use libp2p::{PeerId, StreamProtocol}; use self::{ @@ -33,7 +34,7 @@ impl NetworkBehaviour, u64), + current_epoch_info: (Membership, Epoch), local_peer_id: PeerId, protocol_name: StreamProtocol, ) -> Self { @@ -41,22 +42,21 @@ impl NetworkBehaviour, u64)) { - self.with_core_mut() - .start_new_session(new_session_info.clone()); - self.with_edge_mut().start_new_session(new_session_info.0); + pub fn start_new_epoch(&mut self, new_epoch_info: (Membership, Epoch)) { + self.with_core_mut().start_new_epoch(new_epoch_info.clone()); + self.with_edge_mut().start_new_epoch(new_epoch_info.0); } pub const fn with_core(&self) -> &CoreToCoreBehaviour { @@ -77,7 +77,7 @@ impl NetworkBehaviour Self { Self( EncapsulatedMessageWithVerifiedPublicHeader::try_new( - &generate_valid_inputs(0), + &generate_valid_inputs(0.into()), PayloadType::Data, payload.try_into().unwrap(), ) @@ -128,13 +128,13 @@ impl AsRef for TestEncapsulatedMess } } -pub struct TestEncapsulatedMessageWithSession(EncapsulatedMessageWithVerifiedPublicHeader); +pub struct TestEncapsulatedMessageWithEpoch(EncapsulatedMessageWithVerifiedPublicHeader); -impl TestEncapsulatedMessageWithSession { - pub fn new(session: SessionNumber, payload: &[u8]) -> Self { +impl TestEncapsulatedMessageWithEpoch { + pub fn new(epoch: Epoch, payload: &[u8]) -> Self { Self( EncapsulatedMessageWithVerifiedPublicHeader::try_new( - &generate_valid_inputs(session), + &generate_valid_inputs(epoch), PayloadType::Data, payload.try_into().unwrap(), ) @@ -143,7 +143,7 @@ impl TestEncapsulatedMessageWithSession { } } -impl Deref for TestEncapsulatedMessageWithSession { +impl Deref for TestEncapsulatedMessageWithEpoch { type Target = EncapsulatedMessageWithVerifiedPublicHeader; fn deref(&self) -> &Self::Target { @@ -151,11 +151,11 @@ impl Deref for TestEncapsulatedMessageWithSession { } } -fn generate_valid_inputs(session: SessionNumber) -> Vec { +fn generate_valid_inputs(epoch: Epoch) -> Vec { repeat_with(UnsecuredEd25519Key::generate_with_blake_rng) .take(3) .map(|recipient_signing_key| { - let proofs = session_based_mock_blend_proof(session); + let proofs = epoch_based_mock_blend_proof(epoch); EncapsulationInput::try_new( UnsecuredEd25519Key::generate_with_blake_rng(), &recipient_signing_key.public_key(), @@ -167,17 +167,17 @@ fn generate_valid_inputs(session: SessionNumber) -> Vec { .collect::>() } -fn session_based_mock_blend_proof(session: SessionNumber) -> BlendLayerProof { - let session_bytes = session.to_le_bytes(); +fn epoch_based_mock_blend_proof(epoch: Epoch) -> BlendLayerProof { + let epoch_bytes = epoch.into_inner().to_le_bytes(); BlendLayerProof { proof_of_quota: VerifiedProofOfQuota::from_bytes_unchecked({ let mut bytes = [0u8; _]; - bytes[..session_bytes.len()].copy_from_slice(&session_bytes); + bytes[..epoch_bytes.len()].copy_from_slice(&epoch_bytes); bytes }), proof_of_selection: VerifiedProofOfSelection::from_bytes_unchecked({ let mut bytes = [0u8; _]; - bytes[..session_bytes.len()].copy_from_slice(&session_bytes); + bytes[..epoch_bytes.len()].copy_from_slice(&epoch_bytes); bytes }), ephemeral_signing_key: UnsecuredEd25519Key::generate_with_blake_rng(), diff --git a/blend/network/src/core/with_core/behaviour/mod.rs b/blend/network/src/core/with_core/behaviour/mod.rs index 6c1888f4c..4e2fa7a51 100644 --- a/blend/network/src/core/with_core/behaviour/mod.rs +++ b/blend/network/src/core/with_core/behaviour/mod.rs @@ -15,6 +15,7 @@ use lb_blend_message::encap::validated::{ EncapsulatedMessageWithVerifiedPublicHeader, EncapsulatedMessageWithVerifiedSignature, }; use lb_blend_scheduling::membership::Membership; +use lb_cryptarchia_engine::Epoch; use lb_log_targets::blend; use libp2p::{ Multiaddr, PeerId, StreamProtocol, @@ -32,7 +33,7 @@ use crate::core::with_core::{ ConnectionHandler, FromBehaviour, ToBehaviour, conn_maintenance::ConnectionMonitor, }, message_cache::MessageCache, - old_session::OldSession, + old_epoch::OldEpoch, utils::{ forward_validated_message_and_update_cache, handle_received_serialized_encapsulated_message_and_update_cache, @@ -43,7 +44,7 @@ use crate::core::with_core::{ mod handler; mod message_cache; -mod old_session; +mod old_epoch; mod utils; #[cfg(test)] @@ -112,16 +113,16 @@ pub struct Behaviour { /// as malicious by our peers. message_cache: MessageCache, observation_window_clock_provider: ObservationWindowClockProvider, - current_session_info: (Membership, u64), + current_epoch_info: (Membership, Epoch), /// The [minimum, maximum] peering degree of this node. peering_degree: RangeInclusive, local_peer_id: PeerId, protocol_name: StreamProtocol, /// The minimum Blend network size for messages to be relayed between peers. minimum_network_size: NonZeroUsize, - /// States for processing messages from the old session + /// States for processing messages from the old epoch /// before the transition period has passed. - old_session: Option, + old_epoch: Option, } #[derive(Debug, Eq, PartialEq, Clone, Copy)] @@ -185,7 +186,7 @@ pub enum Event { Message { message: Box, sender: PeerId, - session: u64, + epoch: Epoch, }, /// A peer on a given connection has been detected as unhealthy. UnhealthyPeer(PeerId), @@ -221,7 +222,7 @@ impl Behaviour { pub fn new( config: &Config, observation_window_clock_provider: ObservationWindowClockProvider, - session_info: (Membership, u64), + epoch_info: (Membership, Epoch), local_peer_id: PeerId, protocol_name: StreamProtocol, ) -> Self { @@ -230,19 +231,19 @@ impl Behaviour { events: VecDeque::new(), waker: None, observation_window_clock_provider, - message_cache: MessageCache::new_with_peer_capacity(session_info.0.size()), - current_session_info: session_info, + message_cache: MessageCache::new_with_peer_capacity(epoch_info.0.size()), + current_epoch_info: epoch_info, peering_degree: config.peering_degree.clone(), connections_waiting_upgrade: HashMap::new(), local_peer_id, protocol_name, minimum_network_size: config.minimum_network_size, - old_session: None, + old_epoch: None, } } - pub(crate) fn start_new_session(&mut self, new_session_info: (Membership, u64)) { - let current_session_number = self.current_session_info.1; + pub(crate) fn start_new_epoch(&mut self, new_epoch_info: (Membership, Epoch)) { + let current_epoch_number = self.current_epoch_info.1; // Close any connections that were still waiting to be upgraded. Without // this, a late `FullyNegotiated` event from one of those handlers would @@ -253,29 +254,29 @@ impl Behaviour { for (connection, _) in pending_upgrades { self.close_connection(connection); } - self.current_session_info = new_session_info; + self.current_epoch_info = new_epoch_info; - self.stop_old_session(); + self.stop_old_epoch(); - self.old_session = Some(OldSession::new( + self.old_epoch = Some(OldEpoch::new( mem::take(&mut self.negotiated_peers) .into_iter() .map(|(peer_id, details)| (peer_id, details.connection_id)) .collect(), mem::take(&mut self.message_cache), - current_session_number, + current_epoch_number, )); - tracing::debug!(target: LOG_TARGET, "Started a new session by passing negotiated peers and exchanged message IDs to the old session. Now, no negotiated peers in the current session."); + tracing::debug!(target: LOG_TARGET, "Started a new epoch by passing negotiated peers and exchanged message IDs to the old epoch. Now, no negotiated peers in the current epoch."); } - pub(crate) fn finish_session_transition(&mut self) { - self.stop_old_session(); + pub(crate) fn finish_epoch_transition(&mut self) { + self.stop_old_epoch(); } - fn stop_old_session(&mut self) { - if let Some(old_session) = self.old_session.take() { - let mut events = old_session.stop(); + fn stop_old_epoch(&mut self) { + if let Some(old_epoch) = self.old_epoch.take() { + let mut events = old_epoch.stop(); let num_events = events.len(); self.events.append(&mut events); if num_events > 0 { @@ -306,61 +307,61 @@ impl Behaviour { /// Force send a message to a peer, as long as the peer is connected, no /// matter the state the connection is in. #[cfg(any(test, feature = "unsafe-test-functions"))] - pub fn force_send_message_to_current_session_peer( + pub fn force_send_message_to_current_epoch_peer( &mut self, message: &EncapsulatedMessageWithVerifiedPublicHeader, peer_id: PeerId, ) -> Result<(), SendError> { - self.force_send_message_to_peer_at_session(message, peer_id, self.current_session_info.1) + self.force_send_message_to_peer_at_epoch(message, peer_id, self.current_epoch_info.1) } /// Force send a message to a peer, as long as the peer is connected, no /// matter the state the connection is in. #[cfg(any(test, feature = "unsafe-test-functions"))] - fn force_send_message_to_peer_at_session( + fn force_send_message_to_peer_at_epoch( &mut self, message: &EncapsulatedMessageWithVerifiedPublicHeader, peer_id: PeerId, - session: u64, + epoch: Epoch, ) -> Result<(), SendError> { let serialized_message = lb_blend_scheduling::serialize_encapsulated_message_with_verified_public_header( message, ); - self.force_send_serialized_message_to_peer_at_session(serialized_message, peer_id, session) + self.force_send_serialized_message_to_peer_at_epoch(serialized_message, peer_id, epoch) } /// Force send a serialized message to a peer (without trying to deserialize /// nor validating it first), as long as the peer is connected, no /// matter the state the connection is in. #[cfg(test)] - fn force_send_serialized_message_to_current_session_peer( + fn force_send_serialized_message_to_current_epoch_peer( &mut self, serialized_message: Vec, peer_id: PeerId, ) -> Result<(), SendError> { - self.force_send_serialized_message_to_peer_at_session( + self.force_send_serialized_message_to_peer_at_epoch( serialized_message, peer_id, - self.current_session_info.1, + self.current_epoch_info.1, ) } #[cfg(any(test, feature = "unsafe-test-functions"))] - pub fn force_send_serialized_message_to_peer_at_session( + pub fn force_send_serialized_message_to_peer_at_epoch( &mut self, serialized_message: Vec, peer_id: PeerId, - session: u64, + epoch: Epoch, ) -> Result<(), SendError> { - if session != self.current_session_info.1 { - let Some(old_session) = &mut self.old_session else { - return Err(SendError::InvalidSession); + if epoch != self.current_epoch_info.1 { + let Some(old_epoch) = &mut self.old_epoch else { + return Err(SendError::InvalidEpoch); }; - return old_session.force_send_serialized_message_to_peer_at_session( + return old_epoch.force_send_serialized_message_to_peer_at_epoch( serialized_message, peer_id, - session, + epoch, ); } @@ -372,7 +373,7 @@ impl Behaviour { tracing::trace!( target: LOG_TARGET, - "Notifying handler with peer {peer_id:?} on current session connection {connection_id:?} to deliver already-serialized message." + "Notifying handler with peer {peer_id:?} on current epoch connection {connection_id:?} to deliver already-serialized message." ); self.events.push_back(ToSwarm::NotifyHandler { peer_id, @@ -387,12 +388,10 @@ impl Behaviour { &self.negotiated_peers } - /// Returns the peer IDs of the old session's negotiated peers, if a - /// session transition is in progress. - pub fn old_session_peer_ids(&self) -> Option + '_> { - self.old_session - .as_ref() - .map(OldSession::negotiated_peer_ids) + /// Returns the peer IDs of the old epoch's negotiated peers, if an + /// epoch transition is in progress. + pub fn old_epoch_peer_ids(&self) -> Option + '_> { + self.old_epoch.as_ref().map(OldEpoch::negotiated_peer_ids) } fn try_wake(&mut self) { @@ -455,7 +454,7 @@ impl Behaviour { } fn is_network_large_enough(&self) -> bool { - self.current_session_info.0.size() >= self.minimum_network_size.get() + self.current_epoch_info.0.size() >= self.minimum_network_size.get() } /// Handle a new negotiated connection. @@ -473,7 +472,7 @@ impl Behaviour { /// returned from the `Either::Left` branch of /// [`Self::handle_established_inbound_connection`] /// [`Self::handle_established_outbound_connection`]), so the entry must be - /// present. Pending entries are proactively closed on session transition to + /// present. Pending entries are proactively closed on epoch transition to /// preserve this invariant. /// /// # Panics @@ -513,7 +512,7 @@ impl Behaviour { remote_peer_role: Endpoint, ) { // We need to check if we still have available connection slots, as it is - // possible, especially upon session transition, that more than the maximum + // possible, especially upon epoch transition, that more than the maximum // allowed number of peers are trying to connect to us. So once the stream is // actually upgraded, we downgrade it again if we do not have space left for it. // By not adding the new connection to the map of negotiated peers, the swarm @@ -703,7 +702,7 @@ impl Behaviour { ) -> Option { let peer_details = self.negotiated_peers.get_mut(&peer_id)?; // We double check we are dealing with the expected connection. - // This could be false if `connection_id` is from the old session. + // This could be false if `connection_id` is from the old epoch. if peer_details.connection_id != connection_id { tracing::trace!( target: LOG_TARGET, @@ -715,7 +714,7 @@ impl Behaviour { Some(mem::replace(&mut peer_details.negotiated_state, state)) } - /// Handle an unhealthy connection if it exists in the current session. + /// Handle an unhealthy connection if it exists in the current epoch. /// If not, it is ignored. #[expect( dead_code, @@ -735,7 +734,7 @@ impl Behaviour { } } - /// Handle a unhealthy connection if it exists in the current session. + /// Handle a unhealthy connection if it exists in the current epoch. /// If not, it is ignored. fn handle_healthy_connection(&mut self, (peer_id, connection_id): (PeerId, ConnectionId)) { // Notify swarm only on first transition into healthy state. @@ -806,24 +805,24 @@ impl Behaviour { } /// Publish an already-encapsulated and validated message to all connected - /// peers in the specified session. + /// peers in the specified epoch. pub fn publish_message_with_validated_header( &mut self, message: EncapsulatedMessageWithVerifiedPublicHeader, - intended_session: u64, + intended_epoch: Epoch, ) -> Result<(), SendError> { - if self.current_session_info.1 != intended_session { - let Some(old_session) = &mut self.old_session else { - return Err(SendError::InvalidSession); + if self.current_epoch_info.1 != intended_epoch { + let Some(old_epoch) = &mut self.old_epoch else { + return Err(SendError::InvalidEpoch); }; - return old_session.publish_message_with_validated_header(message, intended_session); + return old_epoch.publish_message_with_validated_header(message, intended_epoch); } self.forward_maybe_excluding(&message.into(), None) } /// Publish an already-encapsulated message with a valid public header - /// signature to all connected peers in the current session. - pub fn publish_message_with_validated_signature_to_current_session( + /// signature to all connected peers in the current epoch. + pub fn publish_message_with_validated_signature_to_current_epoch( &mut self, message: &EncapsulatedMessageWithVerifiedSignature, ) -> Result<(), SendError> { @@ -831,30 +830,30 @@ impl Behaviour { } /// Forwards a message with a valid public header signature to all - /// non-spammy peers in the specified session, except the + /// non-spammy peers in the specified epoch, except the /// [`except`] peer. /// - /// If the session is the previous session, the message is forwarded to the - /// peers in the old session. Otherwise, it is forwarded to the peers in - /// the current session. + /// If the epoch is the previous epoch, the message is forwarded to the + /// peers in the old epoch. Otherwise, it is forwarded to the peers in + /// the current epoch. /// /// Returns [`Error::NoPeers`] if there are no connected peers that support - /// the blend protocol, and [`Error::InvalidSession`] if the provided - /// session does not match neither the current session nor the old session. + /// the blend protocol, and [`Error::InvalidEpoch`] if the provided + /// epoch does not match neither the current epoch nor the old epoch. pub fn forward_message_with_validated_signature( &mut self, message: &EncapsulatedMessageWithVerifiedSignature, except: PeerId, - intended_session: u64, + intended_epoch: Epoch, ) -> Result<(), SendError> { - if self.current_session_info.1 != intended_session { - let Some(old_session) = &mut self.old_session else { - return Err(SendError::InvalidSession); + if self.current_epoch_info.1 != intended_epoch { + let Some(old_epoch) = &mut self.old_epoch else { + return Err(SendError::InvalidEpoch); }; - return old_session.forward_message_with_validated_signature( + return old_epoch.forward_message_with_validated_signature( message, except, - intended_session, + intended_epoch, ); } @@ -867,7 +866,7 @@ impl Behaviour { excluded_peer: Option, ) -> Result<(), SendError> { tracing::trace!( - "Forwarding message with id {:?} to current session peers. Negotiated peers: {:?}. Excluded peer: {excluded_peer:?}", + "Forwarding message with id {:?} to current epoch peers. Negotiated peers: {:?}. Excluded peer: {excluded_peer:?}", hex::encode(message.id()), self.negotiated_peers() ); @@ -897,10 +896,10 @@ impl Behaviour { serialized_message: &[u8], (from_peer_id, from_connection_id): (PeerId, ConnectionId), ) { - // First, try to handle the message in the context of the old session. - // If it is not part of the old session, try with the current session. - if let Some(old_session) = &mut self.old_session { - match old_session.handle_received_serialized_encapsulated_message( + // First, try to handle the message in the context of the old epoch. + // If it is not part of the old epoch, try with the current epoch. + if let Some(old_epoch) = &mut self.old_epoch { + match old_epoch.handle_received_serialized_encapsulated_message( serialized_message, (from_peer_id, from_connection_id), ) { @@ -921,9 +920,9 @@ impl Behaviour { from_peer_id, &mut self.events, self.waker.take(), - self.current_session_info.1, + self.current_epoch_info.1, ) { - tracing::debug!(target: LOG_TARGET, "Failed to handle message from the current session: {receive_error:?}"); + tracing::debug!(target: LOG_TARGET, "Failed to handle message from the current epoch: {receive_error:?}"); let spam_reason = match receive_error { ReceiveError::DuplicateMessageFromPeer(_) => SpamReason::DuplicateMessage, ReceiveError::InvalidHeaderSignature => SpamReason::InvalidHeaderSignature, @@ -986,7 +985,7 @@ where Ok(if !self.is_network_large_enough() { tracing::debug!(target: LOG_TARGET, "Denying inbound connection {connection_id:?} with peer {peer_id:?} because membership size is too small."); Either::Right(DummyConnectionHandler) - } else if self.current_session_info.0.contains(&peer_id) { + } else if self.current_epoch_info.0.contains(&peer_id) { tracing::trace!( target: LOG_TARGET, "Upgrading inbound connection {connection_id:?} with core peer {peer_id:?}." @@ -1035,7 +1034,7 @@ where Ok(if !self.is_network_large_enough() { tracing::debug!(target: LOG_TARGET, "Denying outbound connection {connection_id:?} with peer {peer_id:?} because membership size is too small."); Either::Right(DummyConnectionHandler) - } else if self.current_session_info.0.contains(&peer_id) { + } else if self.current_epoch_info.0.contains(&peer_id) { tracing::trace!( target: LOG_TARGET, "Upgrading outbound connection {connection_id:?} with core peer {peer_id:?}." @@ -1062,9 +1061,9 @@ where .. }) = event { - // Try to close the connection if it exists in the old session. - if let Some(old_session) = &mut self.old_session - && old_session.handle_closed_connection(&(peer_id, connection_id)) + // Try to close the connection if it exists in the old epoch. + if let Some(old_epoch) = &mut self.old_epoch + && old_epoch.handle_closed_connection(&(peer_id, connection_id)) { return; } @@ -1173,8 +1172,8 @@ where &mut self, cx: &mut Context<'_>, ) -> Poll>> { - if let Some(old_session) = &mut self.old_session - && let Poll::Ready(event) = old_session.poll(cx) + if let Some(old_epoch) = &mut self.old_epoch + && let Poll::Ready(event) = old_epoch.poll(cx) { return Poll::Ready(event); } diff --git a/blend/network/src/core/with_core/behaviour/old_session.rs b/blend/network/src/core/with_core/behaviour/old_epoch.rs similarity index 81% rename from blend/network/src/core/with_core/behaviour/old_session.rs rename to blend/network/src/core/with_core/behaviour/old_epoch.rs index e1479795c..9b361e8da 100644 --- a/blend/network/src/core/with_core/behaviour/old_session.rs +++ b/blend/network/src/core/with_core/behaviour/old_epoch.rs @@ -8,6 +8,7 @@ use either::Either; use lb_blend_message::encap::validated::{ EncapsulatedMessageWithVerifiedPublicHeader, EncapsulatedMessageWithVerifiedSignature, }; +use lb_cryptarchia_engine::Epoch; use lb_log_targets::blend; use libp2p::{ PeerId, @@ -29,44 +30,44 @@ use crate::core::with_core::{ const LOG_TARGET: &str = blend::network::core::core::behaviour::OLD; -/// Defines behaviours for processing messages from the old session -/// until the session transition period has passed. -pub struct OldSession { +/// Defines behaviours for processing messages from the old epoch +/// until the epoch transition period has passed. +pub struct OldEpoch { negotiated_peers: HashMap, events: VecDeque>>, waker: Option, message_cache: MessageCache, - session_number: u64, + epoch: Epoch, } -impl OldSession { +impl OldEpoch { #[must_use] pub const fn new( negotiated_peers: HashMap, message_cache: MessageCache, - session_number: u64, + epoch: Epoch, ) -> Self { Self { negotiated_peers, message_cache, events: VecDeque::new(), waker: None, - session_number, + epoch, } } /// Publish an encapsulated message with a validated public header to all /// negotiated peers. /// - /// If the specified session does not match the current session, it returns + /// If the specified epoch does not match the current epoch, it returns /// an error without sending the message. pub(super) fn publish_message_with_validated_header( &mut self, message: EncapsulatedMessageWithVerifiedPublicHeader, - intended_session: u64, + intended_epoch: Epoch, ) -> Result<(), SendError> { - if self.session_number != intended_session { - return Err(SendError::InvalidSession); + if self.epoch != intended_epoch { + return Err(SendError::InvalidEpoch); } forward_validated_message_and_update_cache( &(message.into()), @@ -80,16 +81,16 @@ impl OldSession { /// Forward an encapsulated message with a validated signature to all /// negotiated peers, except the specified one. /// - /// If the specified session does not match the current session, it returns + /// If the specified epoch does not match the current epoch, it returns /// an error without sending the message. pub(super) fn forward_message_with_validated_signature( &mut self, message: &EncapsulatedMessageWithVerifiedSignature, except: PeerId, - intended_session: u64, + intended_epoch: Epoch, ) -> Result<(), SendError> { - if self.session_number != intended_session { - return Err(SendError::InvalidSession); + if self.epoch != intended_epoch { + return Err(SendError::InvalidEpoch); } forward_validated_message_and_update_cache( message, @@ -104,14 +105,14 @@ impl OldSession { } #[cfg(any(test, feature = "unsafe-test-functions"))] - pub(super) fn force_send_serialized_message_to_peer_at_session( + pub(super) fn force_send_serialized_message_to_peer_at_epoch( &mut self, serialized_message: Vec, peer_id: PeerId, - session: u64, + epoch: Epoch, ) -> Result<(), SendError> { - if session != self.session_number { - return Err(SendError::InvalidSession); + if epoch != self.epoch { + return Err(SendError::InvalidEpoch); } let Some(connection_id) = self.negotiated_peers.get(&peer_id) else { @@ -119,7 +120,7 @@ impl OldSession { }; tracing::trace!( target: LOG_TARGET, - "Notifying handler with peer {peer_id:?} on old session connection {connection_id:?} to deliver already-serialized message." + "Notifying handler with peer {peer_id:?} on old epoch connection {connection_id:?} to deliver already-serialized message." ); self.events.push_back(ToSwarm::NotifyHandler { peer_id, @@ -133,7 +134,7 @@ impl OldSession { /// Handles a message received from a peer. /// /// # Returns - /// - [`Ok(false)`] if the connection is not part of the session. + /// - [`Ok(false)`] if the connection is not part of the epoch. /// - [`Ok(true)`] if the message was successfully processed and forwarded. /// - [`Err(Error)`] if the message is invalid or has already been /// exchanged. @@ -152,9 +153,9 @@ impl OldSession { from_peer_id, &mut self.events, self.waker.take(), - self.session_number, + self.epoch, ).inspect_err(|receive_error| { - tracing::debug!(target: LOG_TARGET, "Failed to handle message from the old session: {receive_error:?}. Closing connection with spammy peer."); + tracing::debug!(target: LOG_TARGET, "Failed to handle message from the old epoch: {receive_error:?}. Closing connection with spammy peer."); self.events.push_back(ToSwarm::NotifyHandler { peer_id: from_peer_id, handler: NotifyHandler::One(from_connection_id), @@ -166,10 +167,10 @@ impl OldSession { Ok(true) } - /// Stops the old session by returning events to close all the substreams - /// in the old session. + /// Stops the old epoch by returning events to close all the substreams + /// in the old epoch. /// - /// It should be called once the session transition period has passed. + /// It should be called once the epoch transition period has passed. pub fn stop(self) -> VecDeque>> { let mut events = VecDeque::with_capacity(self.negotiated_peers.len()); for (&peer_id, &connection_id) in &self.negotiated_peers { @@ -182,7 +183,7 @@ impl OldSession { events } - /// Checks if the connection is part of the old session. + /// Checks if the connection is part of the old epoch. #[must_use] pub fn is_negotiated(&self, (peer_id, connection_id): &(PeerId, ConnectionId)) -> bool { self.negotiated_peers @@ -190,7 +191,7 @@ impl OldSession { .is_some_and(|&id| id == *connection_id) } - /// Returns the peer IDs of all negotiated peers in the old session. + /// Returns the peer IDs of all negotiated peers in the old epoch. pub fn negotiated_peer_ids(&self) -> impl Iterator { self.negotiated_peers.keys() } @@ -198,7 +199,7 @@ impl OldSession { /// Should be called when a connection is detected as closed. /// /// It removes the connection from the states and returns [`true`] - /// if the connection was part of the old session. + /// if the connection was part of the old epoch. pub fn handle_closed_connection( &mut self, (peer_id, connection_id): &(PeerId, ConnectionId), diff --git a/blend/network/src/core/with_core/behaviour/tests/connection_maintenance.rs b/blend/network/src/core/with_core/behaviour/tests/connection_maintenance.rs index 3ffec2808..e494bde10 100644 --- a/blend/network/src/core/with_core/behaviour/tests/connection_maintenance.rs +++ b/blend/network/src/core/with_core/behaviour/tests/connection_maintenance.rs @@ -16,7 +16,7 @@ use crate::core::{ }, }; -#[ignore = "TODO: enable this logic after investigating session/epoch transition issues. Test disabled because we don't let connections turn spammy because of too many messages now until we have proper observation window values."] +#[ignore = "TODO: enable this logic after investigating epoch transition issues. Test disabled because we don't let connections turn spammy because of too many messages now until we have proper observation window values."] #[test(tokio::test)] async fn detect_spammy_peer() { let (mut identities, nodes) = new_nodes_with_empty_address(2); @@ -41,13 +41,13 @@ async fn detect_spammy_peer() { // Send two messages when only one was expected. dialing_swarm .behaviour_mut() - .publish_message_with_validated_signature_to_current_session( + .publish_message_with_validated_signature_to_current_epoch( &TestEncapsulatedMessage::new(b"msg1").into_inner().into(), ) .unwrap(); dialing_swarm .behaviour_mut() - .publish_message_with_validated_signature_to_current_session( + .publish_message_with_validated_signature_to_current_epoch( &TestEncapsulatedMessage::new(b"msg2").into_inner().into(), ) .unwrap(); @@ -80,7 +80,7 @@ async fn detect_spammy_peer() { } } -#[ignore = "TODO: enable this logic after investigating session/epoch transition issues. Test disabled because we don't let connections turn unhealthy now until we have proper observation window values."] +#[ignore = "TODO: enable this logic after investigating epoch transition issues. Test disabled because we don't let connections turn unhealthy now until we have proper observation window values."] #[test(tokio::test)] async fn detect_unhealthy_peer() { let (mut identities, nodes) = new_nodes_with_empty_address(2); @@ -141,7 +141,7 @@ async fn detect_unhealthy_peer() { ); } -#[ignore = "TODO: enable this logic after investigating session/epoch transition issues. Test disabled because we don't let connections turn unhealthy now until we have proper observation window values."] +#[ignore = "TODO: enable this logic after investigating epoch transition issues. Test disabled because we don't let connections turn unhealthy now until we have proper observation window values."] #[test(tokio::test)] async fn restore_healthy_peer() { let (mut identities, nodes) = new_nodes_with_empty_address(2); @@ -166,7 +166,7 @@ async fn restore_healthy_peer() { // Send a message to the listening swarm to revert from unhealthy to healthy. dialing_swarm .behaviour_mut() - .force_send_message_to_current_session_peer( + .force_send_message_to_current_epoch_peer( &TestEncapsulatedMessage::new(b"msg").into_inner(), *listening_swarm.local_peer_id(), ) diff --git a/blend/network/src/core/with_core/behaviour/tests/session.rs b/blend/network/src/core/with_core/behaviour/tests/epoch.rs similarity index 70% rename from blend/network/src/core/with_core/behaviour/tests/session.rs rename to blend/network/src/core/with_core/behaviour/tests/epoch.rs index 2c74c6b9f..dc2ad1a03 100644 --- a/blend/network/src/core/with_core/behaviour/tests/session.rs +++ b/blend/network/src/core/with_core/behaviour/tests/epoch.rs @@ -1,13 +1,14 @@ use core::time::Duration; use futures::StreamExt as _; +use lb_cryptarchia_engine::Epoch; use lb_libp2p::SwarmEvent; use libp2p_swarm_test::SwarmExt as _; use test_log::test; use tokio::{select, time::sleep}; use crate::core::{ - tests::utils::{TestEncapsulatedMessageWithSession, TestSwarm}, + tests::utils::{TestEncapsulatedMessageWithEpoch, TestSwarm}, with_core::{ behaviour::{ Event, @@ -21,7 +22,7 @@ use crate::core::{ #[test(tokio::test)] async fn publish_message() { - let mut session = 0; + let mut epoch = Epoch::new(0); let (mut identities, nodes) = new_nodes_with_empty_address(2); let mut dialer = TestSwarm::new(&identities.next().unwrap(), |id| { BehaviourBuilder::new(id).with_membership(&nodes).build() @@ -33,31 +34,31 @@ async fn publish_message() { listener.listen().with_memory_addr_external().await; dialer.connect_and_wait_for_upgrade(&mut listener).await; - // Start a new session before sending any message through the connection. - session += 1; + // Start a new epoch before sending any message through the connection. + epoch = Epoch::new(epoch.into_inner() + 1); let memberships = build_memberships(&[&dialer, &listener]); dialer .behaviour_mut() - .start_new_session((memberships[0].clone(), session)); + .start_new_epoch((memberships[0].clone(), epoch)); listener .behaviour_mut() - .start_new_session((memberships[1].clone(), session)); + .start_new_epoch((memberships[1].clone(), epoch)); // Send a message but expect [`Error::NoPeers`] - // because we haven't establish connections for the new session. - let test_message = TestEncapsulatedMessageWithSession::new(session, b"msg"); + // because we haven't establish connections for the new epoch. + let test_message = TestEncapsulatedMessageWithEpoch::new(epoch, b"msg"); let result = dialer .behaviour_mut() - .publish_message_with_validated_header(test_message.clone(), session); + .publish_message_with_validated_header(test_message.clone(), epoch); assert_eq!(result, Err(SendError::NoPeers)); - // Establish a connection for the new session. + // Establish a connection for the new epoch. dialer.connect_and_wait_for_upgrade(&mut listener).await; // Now we can send the message successfully. dialer .behaviour_mut() - .publish_message_with_validated_header(test_message.clone(), session) + .publish_message_with_validated_header(test_message.clone(), epoch) .unwrap(); loop { select! { @@ -75,14 +76,14 @@ async fn publish_message() { assert_eq!( dialer .behaviour_mut() - .publish_message_with_validated_header(test_message.clone(), 1), + .publish_message_with_validated_header(test_message.clone(), 1.into()), Err(SendError::DuplicateMessage) ); } #[test(tokio::test)] async fn forward_message() { - let old_session = 0; + let old_epoch = Epoch::new(0); let (mut identities, nodes) = new_nodes_with_empty_address(4); let mut sender = TestSwarm::new(&identities.next().unwrap(), |id| { BehaviourBuilder::new(id).with_membership(&nodes).build() @@ -108,42 +109,42 @@ async fn forward_message() { sender.connect_and_wait_for_upgrade(&mut forwarder).await; forwarder.connect_and_wait_for_upgrade(&mut receiver1).await; - // Before sending any message, start a new session + // Before sending any message, start a new epoch // only for the forwarder, receiver1, and receiver2. - // And, connect the forwarder to the receiver2 for the new session. + // And, connect the forwarder to the receiver2 for the new epoch. // Then, the topology looks like: - // - Old session: sender -> forwarder -> receiver1 - // - New session: forwarder -> receiver2 - let new_session = old_session + 1; + // - Old epoch: sender -> forwarder -> receiver1 + // - New epoch: forwarder -> receiver2 + let new_epoch = Epoch::new(old_epoch.into_inner() + 1); let memberships = build_memberships(&[&sender, &forwarder, &receiver1, &receiver2]); forwarder .behaviour_mut() - .start_new_session((memberships[1].clone(), new_session)); + .start_new_epoch((memberships[1].clone(), new_epoch)); receiver1 .behaviour_mut() - .start_new_session((memberships[2].clone(), new_session)); + .start_new_epoch((memberships[2].clone(), new_epoch)); receiver2 .behaviour_mut() - .start_new_session((memberships[3].clone(), new_session)); + .start_new_epoch((memberships[3].clone(), new_epoch)); forwarder.connect_and_wait_for_upgrade(&mut receiver2).await; - // The sender publishes a message built with the old session to the forwarder. - let test_message = TestEncapsulatedMessageWithSession::new(old_session, b"msg"); + // The sender publishes a message built with the old epoch to the forwarder. + let test_message = TestEncapsulatedMessageWithEpoch::new(old_epoch, b"msg"); sender .behaviour_mut() - .publish_message_with_validated_header(test_message.clone(), old_session) + .publish_message_with_validated_header(test_message.clone(), old_epoch) .unwrap(); // We expect that the message goes through the forwarder and receiver1 - // even though the forwarder is connected to the receiver2 in the new session. + // even though the forwarder is connected to the receiver2 in the new epoch. loop { select! { _ = sender.select_next_some() => {} event = forwarder.select_next_some() => { - if let SwarmEvent::Behaviour(Event::Message { message, session, sender }) = event { + if let SwarmEvent::Behaviour(Event::Message { message, epoch, sender }) = event { assert_eq!(message.id(), test_message.id()); forwarder.behaviour_mut() - .forward_message_with_validated_signature(&message, sender, session) + .forward_message_with_validated_signature(&message, sender, epoch) .unwrap(); } } @@ -157,19 +158,19 @@ async fn forward_message() { } } - // Now we start the new session for the sender as well. - // Also, connect the sender to the forwarder for the new session. + // Now we start the new epoch for the sender as well. + // Also, connect the sender to the forwarder for the new epoch. sender .behaviour_mut() - .start_new_session((memberships[0].clone(), new_session)); + .start_new_epoch((memberships[0].clone(), new_epoch)); sender.connect_and_wait_for_upgrade(&mut forwarder).await; - // The sender publishes a new message built with the new session to the + // The sender publishes a new message built with the new epoch to the // forwarder. - let test_message = TestEncapsulatedMessageWithSession::new(new_session, b"msg"); + let test_message = TestEncapsulatedMessageWithEpoch::new(new_epoch, b"msg"); sender .behaviour_mut() - .publish_message_with_validated_header(test_message.clone(), new_session) + .publish_message_with_validated_header(test_message.clone(), new_epoch) .unwrap(); // We expect that the message goes through the forwarder and receiver2. @@ -177,10 +178,10 @@ async fn forward_message() { select! { _ = sender.select_next_some() => {} event = forwarder.select_next_some() => { - if let SwarmEvent::Behaviour(Event::Message { message, session, sender }) = event { + if let SwarmEvent::Behaviour(Event::Message { message, epoch, sender }) = event { assert_eq!(message.id(), test_message.id()); forwarder.behaviour_mut() - .forward_message_with_validated_signature(&message, sender, session) + .forward_message_with_validated_signature(&message, sender, epoch) .unwrap(); } } @@ -196,8 +197,8 @@ async fn forward_message() { } #[test(tokio::test)] -async fn finish_session_transition() { - let mut session = 0; +async fn finish_epoch_transition() { + let mut epoch = Epoch::new(0); let (mut identities, nodes) = new_nodes_with_empty_address(2); let mut dialer = TestSwarm::new(&identities.next().unwrap(), |id| { BehaviourBuilder::new(id).with_membership(&nodes).build() @@ -209,19 +210,19 @@ async fn finish_session_transition() { listener.listen().with_memory_addr_external().await; dialer.connect_and_wait_for_upgrade(&mut listener).await; - // Start a new session. - session += 1; + // Start a new epoch. + epoch = Epoch::new(epoch.into_inner() + 1); let memberships = build_memberships(&[&dialer, &listener]); dialer .behaviour_mut() - .start_new_session((memberships[0].clone(), session)); + .start_new_epoch((memberships[0].clone(), epoch)); listener .behaviour_mut() - .start_new_session((memberships[1].clone(), session)); + .start_new_epoch((memberships[1].clone(), epoch)); // Finish the transition period - dialer.behaviour_mut().finish_session_transition(); - listener.behaviour_mut().finish_session_transition(); + dialer.behaviour_mut().finish_epoch_transition(); + listener.behaviour_mut().finish_epoch_transition(); // Expect that the connection is closed after 10s (default swarm timeout). loop { @@ -237,8 +238,8 @@ async fn finish_session_transition() { } #[test(tokio::test)] -async fn old_session_message_not_forwarded_back_to_sender() { - let old_session = 0; +async fn old_epoch_message_not_forwarded_back_to_sender() { + let old_epoch = Epoch::new(0); let (mut identities, nodes) = new_nodes_with_empty_address(3); let mut sender = TestSwarm::new(&identities.next().unwrap(), |id| { BehaviourBuilder::new(id).with_membership(&nodes).build() @@ -255,35 +256,35 @@ async fn old_session_message_not_forwarded_back_to_sender() { forwarder.listen().with_memory_addr_external().await; receiver.listen().with_memory_addr_external().await; - // Topology: sender -> forwarder -> receiver (current session). + // Topology: sender -> forwarder -> receiver (current epoch). sender.connect_and_wait_for_upgrade(&mut forwarder).await; forwarder.connect_and_wait_for_upgrade(&mut receiver).await; - // Forwarder starts a new session. Both the sender and the receiver - // connections move into the forwarder's old session. - let new_session = old_session + 1; + // Forwarder starts a new epoch. Both the sender and the receiver + // connections move into the forwarder's old epoch. + let new_epoch = Epoch::new(old_epoch.into_inner() + 1); let memberships = build_memberships(&[&sender, &forwarder, &receiver]); forwarder .behaviour_mut() - .start_new_session((memberships[1].clone(), new_session)); + .start_new_epoch((memberships[1].clone(), new_epoch)); - // Sender publishes a message for the old session. - let test_message = TestEncapsulatedMessageWithSession::new(old_session, b"msg"); + // Sender publishes a message for the old epoch. + let test_message = TestEncapsulatedMessageWithEpoch::new(old_epoch, b"msg"); sender .behaviour_mut() - .publish_message_with_validated_header(test_message.clone(), old_session) + .publish_message_with_validated_header(test_message.clone(), old_epoch) .unwrap(); - // Forwarder receives the message via the old session and forwards it, + // Forwarder receives the message via the old epoch and forwards it, // excluding the original sender. Only receiver should receive the message. loop { select! { _ = sender.select_next_some() => {} forwarder_event = forwarder.select_next_some() => { - if let SwarmEvent::Behaviour(Event::Message { message, session, sender: msg_sender }) = forwarder_event { + if let SwarmEvent::Behaviour(Event::Message { message, epoch, sender: msg_sender }) = forwarder_event { assert_eq!(message.id(), test_message.id()); forwarder.behaviour_mut() - .forward_message_with_validated_signature(&message, msg_sender, session) + .forward_message_with_validated_signature(&message, msg_sender, epoch) .unwrap(); } } @@ -314,13 +315,13 @@ async fn old_session_message_not_forwarded_back_to_sender() { assert!( !sender_received_message_back, - "Old session should not forward the message back to the original sender" + "Old epoch should not forward the message back to the original sender" ); } #[test(tokio::test)] -async fn publish_to_invalid_session_returns_error() { - let session = 1; +async fn publish_to_invalid_epoch_returns_error() { + let epoch = Epoch::new(1); let (mut identities, nodes) = new_nodes_with_empty_address(2); let mut dialer = TestSwarm::new(&identities.next().unwrap(), |id| { BehaviourBuilder::new(id).with_membership(&nodes).build() @@ -332,27 +333,27 @@ async fn publish_to_invalid_session_returns_error() { listener.listen().with_memory_addr_external().await; dialer.connect_and_wait_for_upgrade(&mut listener).await; - // Start the first session and connect. + // Start the first epoch and connect. let memberships = build_memberships(&[&dialer, &listener]); dialer .behaviour_mut() - .start_new_session((memberships[0].clone(), session)); + .start_new_epoch((memberships[0].clone(), epoch)); listener .behaviour_mut() - .start_new_session((memberships[1].clone(), session)); + .start_new_epoch((memberships[1].clone(), epoch)); dialer.connect_and_wait_for_upgrade(&mut listener).await; - // Attempt to publish to a session that neither matches the current nor old. - let test_message = TestEncapsulatedMessageWithSession::new(999, b"invalid-session"); + // Attempt to publish to an epoch that neither matches the current nor old. + let test_message = TestEncapsulatedMessageWithEpoch::new(999.into(), b"invalid-epoch"); let result = dialer .behaviour_mut() - .publish_message_with_validated_header(test_message.clone(), 999); - assert_eq!(result, Err(SendError::InvalidSession)); + .publish_message_with_validated_header(test_message.clone(), 999.into()); + assert_eq!(result, Err(SendError::InvalidEpoch)); } #[test(tokio::test)] -async fn forward_to_invalid_session_returns_error() { - let session = 1; +async fn forward_to_invalid_epoch_returns_error() { + let epoch = Epoch::new(1); let (mut identities, nodes) = new_nodes_with_empty_address(2); let mut dialer = TestSwarm::new(&identities.next().unwrap(), |id| { BehaviourBuilder::new(id).with_membership(&nodes).build() @@ -364,30 +365,29 @@ async fn forward_to_invalid_session_returns_error() { listener.listen().with_memory_addr_external().await; dialer.connect_and_wait_for_upgrade(&mut listener).await; - // Start the first session and connect. + // Start the first epoch and connect. let memberships = build_memberships(&[&dialer, &listener]); dialer .behaviour_mut() - .start_new_session((memberships[0].clone(), session)); + .start_new_epoch((memberships[0].clone(), epoch)); listener .behaviour_mut() - .start_new_session((memberships[1].clone(), session)); + .start_new_epoch((memberships[1].clone(), epoch)); dialer.connect_and_wait_for_upgrade(&mut listener).await; - // Attempt to forward a message to an invalid session. - let test_message = TestEncapsulatedMessageWithSession::new(999, b"invalid-session"); + // Attempt to forward a message to an invalid epoch. + let test_message = TestEncapsulatedMessageWithEpoch::new(999.into(), b"invalid-epoch"); let fake_sender = *listener.local_peer_id(); let sig_verified: lb_blend_message::encap::validated::EncapsulatedMessageWithVerifiedSignature = (*test_message).clone().into(); let result = dialer .behaviour_mut() - .forward_message_with_validated_signature(&sig_verified, fake_sender, 999); - assert_eq!(result, Err(SendError::InvalidSession)); + .forward_message_with_validated_signature(&sig_verified, fake_sender, 999.into()); + assert_eq!(result, Err(SendError::InvalidEpoch)); } #[test(tokio::test)] -async fn event_message_carries_session_number() { - let mut session = 0; +async fn event_message_carries_epoch_number() { let (mut identities, nodes) = new_nodes_with_empty_address(2); let mut dialer = TestSwarm::new(&identities.next().unwrap(), |id| { BehaviourBuilder::new(id).with_membership(&nodes).build() @@ -399,31 +399,31 @@ async fn event_message_carries_session_number() { listener.listen().with_memory_addr_external().await; dialer.connect_and_wait_for_upgrade(&mut listener).await; - // Start session 1 and connect. - session += 1; + // Start epoch 1 and connect. + let epoch = Epoch::new(1); let memberships = build_memberships(&[&dialer, &listener]); dialer .behaviour_mut() - .start_new_session((memberships[0].clone(), session)); + .start_new_epoch((memberships[0].clone(), epoch)); listener .behaviour_mut() - .start_new_session((memberships[1].clone(), session)); + .start_new_epoch((memberships[1].clone(), epoch)); dialer.connect_and_wait_for_upgrade(&mut listener).await; - // Send a message for session 1. - let test_message = TestEncapsulatedMessageWithSession::new(session, b"session-check"); + // Send a message for epoch 1. + let test_message = TestEncapsulatedMessageWithEpoch::new(epoch, b"epoch-check"); dialer .behaviour_mut() - .publish_message_with_validated_header(test_message.clone(), session) + .publish_message_with_validated_header(test_message.clone(), epoch) .unwrap(); loop { select! { _ = dialer.select_next_some() => {} event = listener.select_next_some() => { - if let SwarmEvent::Behaviour(Event::Message { message, session: event_session, .. }) = event { + if let SwarmEvent::Behaviour(Event::Message { message, epoch: event_epoch, .. }) = event { assert_eq!(message.id(), test_message.id()); - assert_eq!(event_session, session, "Event::Message must carry the correct session number"); + assert_eq!(event_epoch, epoch, "Event::Message must carry the correct epoch number"); break; } } @@ -431,10 +431,10 @@ async fn event_message_carries_session_number() { } } -/// After `start_new_session()`, current `negotiated_peers` must be empty and -/// old peers must live inside the `OldSession`. +/// After `start_new_epoch()`, current `negotiated_peers` must be empty and +/// old peers must live inside the `OldEpoch`. #[test(tokio::test)] -async fn start_new_session_moves_peers_to_old_session() { +async fn start_new_epoch_moves_peers_to_old_epoch() { let (mut identities, nodes) = new_nodes_with_empty_address(3); let mut node_a = TestSwarm::new(&identities.next().unwrap(), |id| { BehaviourBuilder::new(id) @@ -454,33 +454,33 @@ async fn start_new_session_moves_peers_to_old_session() { node_a.connect_and_wait_for_upgrade(&mut node_b).await; node_a.connect_and_wait_for_upgrade(&mut node_c).await; - // Before session transition: node_a has 2 negotiated peers. + // Before epoch transition: node_a has 2 negotiated peers. assert_eq!(node_a.behaviour().negotiated_peers.len(), 2); - assert!(node_a.behaviour().old_session.is_none()); + assert!(node_a.behaviour().old_epoch.is_none()); - // Start a new session. + // Start a new epoch. let memberships = build_memberships(&[&node_a, &node_b, &node_c]); node_a .behaviour_mut() - .start_new_session((memberships[0].clone(), 1)); + .start_new_epoch((memberships[0].clone(), 1.into())); - // After session transition: current negotiated_peers must be empty - // and old_session must exist. + // After epoch transition: current negotiated_peers must be empty + // and old_epoch must exist. assert_eq!( node_a.behaviour().negotiated_peers.len(), 0, - "Current session negotiated peers must be reset after start_new_session" + "Current epoch negotiated peers must be reset after start_new_epoch" ); assert!( - node_a.behaviour().old_session.is_some(), - "Old session must be created after start_new_session" + node_a.behaviour().old_epoch.is_some(), + "Old epoch must be created after start_new_epoch" ); } -/// `finish_session_transition()` emits close substream events for all peers -/// in the old session, generating `PeerDisconnected` events. +/// `finish_epoch_transition()` emits close substream events for all peers +/// in the old epoch, generating `PeerDisconnected` events. #[test(tokio::test)] -async fn finish_session_transition_emits_peer_disconnected_for_old_session_peers() { +async fn finish_epoch_transition_emits_peer_disconnected_for_old_epoch_peers() { let (mut identities, nodes) = new_nodes_with_empty_address(3); let mut node_a = TestSwarm::new(&identities.next().unwrap(), |id| { BehaviourBuilder::new(id) @@ -500,22 +500,22 @@ async fn finish_session_transition_emits_peer_disconnected_for_old_session_peers node_a.connect_and_wait_for_upgrade(&mut node_b).await; node_a.connect_and_wait_for_upgrade(&mut node_c).await; - // Start a new session to move current peers into old session. + // Start a new epoch to move current peers into old epoch. let memberships = build_memberships(&[&node_a, &node_b, &node_c]); node_a .behaviour_mut() - .start_new_session((memberships[0].clone(), 1)); + .start_new_epoch((memberships[0].clone(), 1.into())); - // Finish the transition; this should close all old session connections. - node_a.behaviour_mut().finish_session_transition(); + // Finish the transition; this should close all old epoch connections. + node_a.behaviour_mut().finish_epoch_transition(); - // Old session should now be gone. + // Old epoch should now be gone. assert!( - node_a.behaviour().old_session.is_none(), - "Old session must be cleared after finish_session_transition" + node_a.behaviour().old_epoch.is_none(), + "Old epoch must be cleared after finish_epoch_transition" ); - // Drive the swarms until both connections from the old session close. + // Drive the swarms until both connections from the old epoch close. let mut closed_count = 0usize; loop { select! { @@ -530,16 +530,16 @@ async fn finish_session_transition_emits_peer_disconnected_for_old_session_peers } } () = sleep(Duration::from_secs(15)) => { - panic!("Timed out waiting for old session connections to close"); + panic!("Timed out waiting for old epoch connections to close"); } } } } -/// Multiple consecutive `start_new_session` calls should discard the previous -/// old session, moving current peers into a new old session each time. +/// Multiple consecutive `start_new_epoch` calls should discard the previous +/// old epoch, moving current peers into a new old epoch each time. #[test(tokio::test)] -async fn consecutive_session_transitions_replace_old_session() { +async fn consecutive_epoch_transitions_replace_old_epoch() { let (mut identities, nodes) = new_nodes_with_empty_address(2); let mut dialer = TestSwarm::new(&identities.next().unwrap(), |id| { BehaviourBuilder::new(id).with_membership(&nodes).build() @@ -551,40 +551,40 @@ async fn consecutive_session_transitions_replace_old_session() { listener.listen().with_memory_addr_external().await; dialer.connect_and_wait_for_upgrade(&mut listener).await; - // First session transition: move the current peer into old session. + // First epoch transition: move the current peer into old epoch. let memberships = build_memberships(&[&dialer, &listener]); dialer .behaviour_mut() - .start_new_session((memberships[0].clone(), 1)); + .start_new_epoch((memberships[0].clone(), 1.into())); listener .behaviour_mut() - .start_new_session((memberships[1].clone(), 1)); - assert!(dialer.behaviour().old_session.is_some()); + .start_new_epoch((memberships[1].clone(), 1.into())); + assert!(dialer.behaviour().old_epoch.is_some()); - // Re-establish a connection for session 1 so there is something to move - // into old session again. + // Re-establish a connection for epoch 1 so there is something to move + // into old epoch again. dialer.connect_and_wait_for_upgrade(&mut listener).await; assert_eq!(dialer.behaviour().negotiated_peers.len(), 1); - // Second session transition: old session from session 0 gets stopped - // and current peers from session 1 move into old session. + // Second epoch transition: old epoch from epoch 0 gets stopped + // and current peers from epoch 1 move into old epoch. let memberships = build_memberships(&[&dialer, &listener]); dialer .behaviour_mut() - .start_new_session((memberships[0].clone(), 2)); + .start_new_epoch((memberships[0].clone(), 2.into())); listener .behaviour_mut() - .start_new_session((memberships[1].clone(), 2)); - assert!(dialer.behaviour().old_session.is_some()); + .start_new_epoch((memberships[1].clone(), 2.into())); + assert!(dialer.behaviour().old_epoch.is_some()); assert_eq!( dialer.behaviour().negotiated_peers.len(), 0, - "Current negotiated peers must be empty after session transition" + "Current negotiated peers must be empty after epoch transition" ); - // Drive the swarm until the original (session 0) connection closes - // (due to stop_old_session called for session 0 peers inside the second - // start_new_session). + // Drive the swarm until the original (epoch 0) connection closes + // (due to stop_old_epoch called for epoch 0 peers inside the second + // start_new_epoch). loop { select! { _ = listener.select_next_some() => {} @@ -594,16 +594,16 @@ async fn consecutive_session_transitions_replace_old_session() { } } () = sleep(Duration::from_secs(15)) => { - panic!("Timed out waiting for old session 0 connections to close"); + panic!("Timed out waiting for old epoch 0 connections to close"); } } } } -/// Verify that after session transition, re-bootstrapping into the new session +/// Verify that after epoch transition, re-bootstrapping into the new epoch /// respects peering degree limits. #[test(tokio::test)] -async fn session_transition_reboots_peering_degree() { +async fn epoch_transition_reboots_peering_degree() { let (mut identities, nodes) = new_nodes_with_empty_address(4); let mut node_a = TestSwarm::new(&identities.next().unwrap(), |id| { BehaviourBuilder::new(id) @@ -632,29 +632,29 @@ async fn session_transition_reboots_peering_degree() { assert_eq!(node_a.behaviour().negotiated_peers.len(), 2); assert_eq!(node_a.behaviour().available_connection_slots(), 0); - // Start session transition - all current peers move to old session. + // Start epoch transition - all current peers move to old epoch. let memberships = build_memberships(&[&node_a, &node_b, &node_c, &node_d]); node_a .behaviour_mut() - .start_new_session((memberships[0].clone(), 1)); + .start_new_epoch((memberships[0].clone(), 1.into())); node_b .behaviour_mut() - .start_new_session((memberships[1].clone(), 1)); + .start_new_epoch((memberships[1].clone(), 1.into())); node_c .behaviour_mut() - .start_new_session((memberships[2].clone(), 1)); + .start_new_epoch((memberships[2].clone(), 1.into())); node_d .behaviour_mut() - .start_new_session((memberships[3].clone(), 1)); + .start_new_epoch((memberships[3].clone(), 1.into())); - // After transition, new session has no peers, so all slots are available. + // After transition, new epoch has no peers, so all slots are available. assert_eq!( node_a.behaviour().available_connection_slots(), 2, - "All peering degree slots must be available after session transition" + "All peering degree slots must be available after epoch transition" ); - // Connect to new peers in the new session. + // Connect to new peers in the new epoch. node_a.connect_and_wait_for_upgrade(&mut node_b).await; node_a.connect_and_wait_for_upgrade(&mut node_d).await; assert_eq!(node_a.behaviour().negotiated_peers.len(), 2); diff --git a/blend/network/src/core/with_core/behaviour/tests/message_handling.rs b/blend/network/src/core/with_core/behaviour/tests/message_handling.rs index 6da5465e4..34aa42f66 100644 --- a/blend/network/src/core/with_core/behaviour/tests/message_handling.rs +++ b/blend/network/src/core/with_core/behaviour/tests/message_handling.rs @@ -8,7 +8,7 @@ use test_log::test; use tokio::{select, time::sleep}; use crate::core::{ - tests::utils::{TestEncapsulatedMessage, TestEncapsulatedMessageWithSession, TestSwarm}, + tests::utils::{TestEncapsulatedMessage, TestEncapsulatedMessageWithEpoch, TestSwarm}, with_core::{ behaviour::{ Event, NegotiatedPeerState, SpamReason, @@ -41,7 +41,7 @@ async fn message_sending_and_reception() { let test_message_id = test_message.id(); dialing_swarm .behaviour_mut() - .publish_message_with_validated_signature_to_current_session( + .publish_message_with_validated_signature_to_current_epoch( &test_message.as_ref().clone().into(), ) .unwrap(); @@ -89,7 +89,7 @@ async fn message_sending_and_reception() { assert_eq!( dialing_swarm .behaviour_mut() - .publish_message_with_validated_signature_to_current_session( + .publish_message_with_validated_signature_to_current_epoch( &test_message.as_ref().clone().into() ), Err(SendError::DuplicateMessage) @@ -113,7 +113,7 @@ async fn undeserializable_message_received() { dialing_swarm .behaviour_mut() - .force_send_serialized_message_to_current_session_peer( + .force_send_serialized_message_to_current_epoch_peer( b"msg".to_vec(), *listening_swarm.local_peer_id(), ) @@ -163,7 +163,7 @@ async fn duplicate_message_received_from_same_peer() { let test_message = TestEncapsulatedMessage::new(b"msg"); dialing_swarm .behaviour_mut() - .publish_message_with_validated_signature_to_current_session( + .publish_message_with_validated_signature_to_current_epoch( &test_message.as_ref().clone().into(), ) .unwrap(); @@ -190,7 +190,7 @@ async fn duplicate_message_received_from_same_peer() { // This is a duplicate message, so the listener will mark the dialer as spammy. dialing_swarm .behaviour_mut() - .force_send_message_to_current_session_peer( + .force_send_message_to_current_epoch_peer( &test_message.into_inner(), *listening_swarm.local_peer_id(), ) @@ -249,13 +249,13 @@ async fn duplicate_message_received_from_different_peers() { let test_message = TestEncapsulatedMessage::new(b"msg"); dialing_swarm_1 .behaviour_mut() - .publish_message_with_validated_signature_to_current_session( + .publish_message_with_validated_signature_to_current_epoch( &test_message.as_ref().clone().into(), ) .unwrap(); dialing_swarm_2 .behaviour_mut() - .publish_message_with_validated_signature_to_current_session( + .publish_message_with_validated_signature_to_current_epoch( &test_message.as_ref().clone().into(), ) .unwrap(); @@ -297,7 +297,7 @@ async fn invalid_signature_message_received() { let invalid_public_header_message = TestEncapsulatedMessage::new_with_invalid_signature(b""); dialing_swarm .behaviour_mut() - .force_send_message_to_current_session_peer( + .force_send_message_to_current_epoch_peer( &invalid_public_header_message.as_ref().clone(), *listening_swarm.local_peer_id(), ) @@ -348,7 +348,7 @@ async fn message_already_forwarded_silently_ignored_when_received_from_peer() { // Node A forwards X to Node B. In Node A's cache X is now `Forwarded`. node_a .behaviour_mut() - .publish_message_with_validated_signature_to_current_session( + .publish_message_with_validated_signature_to_current_epoch( &test_message.as_ref().clone().into(), ) .unwrap(); @@ -371,7 +371,7 @@ async fn message_already_forwarded_silently_ignored_when_received_from_peer() { // silently dropped - no event, no spam marking. node_b .behaviour_mut() - .force_send_message_to_current_session_peer( + .force_send_message_to_current_epoch_peer( &test_message.into_inner(), *node_a.local_peer_id(), ) @@ -408,7 +408,7 @@ async fn message_already_forwarded_silently_ignored_when_received_from_peer() { } #[test(tokio::test)] -async fn duplicate_message_in_old_session_disconnects_peer_without_swarm_notification() { +async fn duplicate_message_in_old_epoch_disconnects_peer_without_swarm_notification() { let (mut identities, nodes) = new_nodes_with_empty_address(2); let mut sender = TestSwarm::new(&identities.next().unwrap(), |id| { BehaviourBuilder::new(id).with_membership(&nodes).build() @@ -425,7 +425,7 @@ async fn duplicate_message_in_old_session_disconnects_peer_without_swarm_notific // Sender publishes X. Receiver marks it as `Processed` in its cache. sender .behaviour_mut() - .publish_message_with_validated_signature_to_current_session( + .publish_message_with_validated_signature_to_current_epoch( &test_message.as_ref().clone().into(), ) .unwrap(); @@ -441,25 +441,25 @@ async fn duplicate_message_in_old_session_disconnects_peer_without_swarm_notific } } - // Receiver starts a new session. Sender's connection moves to the old - // session together with the existing message cache (which contains X as + // Receiver starts a new epoch. Sender's connection moves to the old + // epoch together with the existing message cache (which contains X as // `Processed`). let memberships = build_memberships(&[&sender, &receiver]); receiver .behaviour_mut() - .start_new_session((memberships[1].clone(), 1)); + .start_new_epoch((memberships[1].clone(), 1.into())); // Wait long enough so that the connection monitor does not fire // `TooManyMessages` instead. sleep(Duration::from_secs(3)).await; // Sender sends X again, bypassing its own `Forwarded` guard. From - // receiver's point of view this arrives over the old-session connection. - // The old-session handler detects a duplicate from the same peer, + // receiver's point of view this arrives over the old-epoch connection. + // The old-epoch handler detects a duplicate from the same peer, // closes the connection, but must NOT emit a `PeerDisconnected` event. sender .behaviour_mut() - .force_send_message_to_current_session_peer( + .force_send_message_to_current_epoch_peer( &test_message.into_inner(), *receiver.local_peer_id(), ) @@ -488,16 +488,16 @@ async fn duplicate_message_in_old_session_disconnects_peer_without_swarm_notific assert!( connection_closed, - "Connection with spammy old-session peer must be closed" + "Connection with spammy old-epoch peer must be closed" ); assert!( !peer_disconnected_event, - "No PeerDisconnected event must be emitted for a spammy old-session peer" + "No PeerDisconnected event must be emitted for a spammy old-epoch peer" ); } #[test(tokio::test)] -async fn undeserializable_message_in_old_session_closes_connection_without_swarm_notification() { +async fn undeserializable_message_in_old_epoch_closes_connection_without_swarm_notification() { let (mut identities, nodes) = new_nodes_with_empty_address(2); let mut sender = TestSwarm::new(&identities.next().unwrap(), |id| { BehaviourBuilder::new(id).with_membership(&nodes).build() @@ -509,17 +509,17 @@ async fn undeserializable_message_in_old_session_closes_connection_without_swarm receiver.listen().with_memory_addr_external().await; sender.connect_and_wait_for_upgrade(&mut receiver).await; - // Receiver starts a new session. Sender's connection moves to the old - // session. + // Receiver starts a new epoch. Sender's connection moves to the old + // epoch. let memberships = build_memberships(&[&sender, &receiver]); receiver .behaviour_mut() - .start_new_session((memberships[1].clone(), 1)); + .start_new_epoch((memberships[1].clone(), 1.into())); - // Sender sends garbage data over the old-session connection. + // Sender sends garbage data over the old-epoch connection. sender .behaviour_mut() - .force_send_serialized_message_to_current_session_peer( + .force_send_serialized_message_to_current_epoch_peer( b"garbage".to_vec(), *receiver.local_peer_id(), ) @@ -547,16 +547,16 @@ async fn undeserializable_message_in_old_session_closes_connection_without_swarm assert!( connection_closed, - "Connection with spammy old-session peer must be closed" + "Connection with spammy old-epoch peer must be closed" ); assert!( !peer_disconnected_event, - "No PeerDisconnected event must be emitted for a spammy old-session peer" + "No PeerDisconnected event must be emitted for a spammy old-epoch peer" ); } #[test(tokio::test)] -async fn spammy_old_session_peer_does_not_affect_current_session() { +async fn spammy_old_epoch_peer_does_not_affect_current_epoch() { let (mut identities, nodes) = new_nodes_with_empty_address(2); let mut sender = TestSwarm::new(&identities.next().unwrap(), |id| { BehaviourBuilder::new(id).with_membership(&nodes).build() @@ -568,36 +568,36 @@ async fn spammy_old_session_peer_does_not_affect_current_session() { receiver.listen().with_memory_addr_external().await; sender.connect_and_wait_for_upgrade(&mut receiver).await; - // Receiver starts a new session. Sender's connection moves to the old - // session. + // Receiver starts a new epoch. Sender's connection moves to the old + // epoch. let memberships = build_memberships(&[&sender, &receiver]); receiver .behaviour_mut() - .start_new_session((memberships[1].clone(), 1)); + .start_new_epoch((memberships[1].clone(), 1.into())); - // Re-connect for the new session. + // Re-connect for the new epoch. sender .behaviour_mut() - .start_new_session((memberships[0].clone(), 1)); + .start_new_epoch((memberships[0].clone(), 1.into())); sender.connect_and_wait_for_upgrade(&mut receiver).await; - // Sender sends garbage over the old-session connection. This should - // close the old-session connection but NOT mark the peer as spammy in - // the current session. + // Sender sends garbage over the old-epoch connection. This should + // close the old-epoch connection but NOT mark the peer as spammy in + // the current epoch. sender .behaviour_mut() - .force_send_serialized_message_to_peer_at_session( + .force_send_serialized_message_to_peer_at_epoch( b"garbage".to_vec(), *receiver.local_peer_id(), - 0, + 0.into(), ) .unwrap(); - // Wait for the old-session connection to close. + // Wait for the old-epoch connection to close. loop { select! { () = sleep(Duration::from_secs(15)) => { - panic!("Timed out waiting for old-session connection to close"); + panic!("Timed out waiting for old-epoch connection to close"); } _ = sender.select_next_some() => {} event = receiver.select_next_some() => { @@ -608,18 +608,18 @@ async fn spammy_old_session_peer_does_not_affect_current_session() { } } - // Now verify the current session connection is healthy by sending a + // Now verify the current epoch connection is healthy by sending a // valid message through it. - let test_message = TestEncapsulatedMessageWithSession::new(1, b"after-spam"); + let test_message = TestEncapsulatedMessageWithEpoch::new(1.into(), b"after-spam"); sender .behaviour_mut() - .publish_message_with_validated_header(test_message.clone(), 1) + .publish_message_with_validated_header(test_message.clone(), 1.into()) .unwrap(); loop { select! { () = sleep(Duration::from_secs(15)) => { - panic!("Timed out waiting for message on current session - current session connection was incorrectly affected by old session spam"); + panic!("Timed out waiting for message on current epoch - current epoch connection was incorrectly affected by old epoch spam"); } _ = sender.select_next_some() => {} event = receiver.select_next_some() => { @@ -633,7 +633,7 @@ async fn spammy_old_session_peer_does_not_affect_current_session() { } #[test(tokio::test)] -async fn duplicate_message_from_old_session_after_session_rotation_is_suppressed() { +async fn duplicate_message_from_old_epoch_after_epoch_rotation_is_suppressed() { let (mut identities, nodes) = new_nodes_with_empty_address(3); let mut sender_a = TestSwarm::new(&identities.next().unwrap(), |id| { BehaviourBuilder::new(id).with_membership(&nodes).build() @@ -653,11 +653,11 @@ async fn duplicate_message_from_old_session_after_session_rotation_is_suppressed sender_b.connect_and_wait_for_upgrade(&mut receiver).await; // Sender A sends message X. Receiver processes it and stores X as - // `Processed` in its current-session message cache. + // `Processed` in its current-epoch message cache. let test_message = TestEncapsulatedMessage::new(b"msg"); sender_a .behaviour_mut() - .publish_message_with_validated_signature_to_current_session( + .publish_message_with_validated_signature_to_current_epoch( &test_message.as_ref().clone().into(), ) .unwrap(); @@ -674,21 +674,21 @@ async fn duplicate_message_from_old_session_after_session_rotation_is_suppressed } } - // Receiver starts a new session. The message cache now containing X - // as `Processed` is transferred into the old session object, + // Receiver starts a new epoch. The message cache now containing X + // as `Processed` is transferred into the old epoch object, // alongside the connections to both sender_a and sender_b. let memberships = build_memberships(&[&sender_a, &sender_b, &receiver]); receiver .behaviour_mut() - .start_new_session((memberships[2].clone(), 1)); + .start_new_epoch((memberships[2].clone(), 1.into())); // Sender B sends the identical message X through its (still-open) // connection to receiver. From receiver's point of view this connection - // now belongs to the old session. Because X is already in the transferred + // now belongs to the old epoch. Because X is already in the transferred // cache, receiver must NOT emit a second `Message` event. sender_b .behaviour_mut() - .publish_message_with_validated_signature_to_current_session( + .publish_message_with_validated_signature_to_current_epoch( &test_message.as_ref().clone().into(), ) .unwrap(); @@ -709,6 +709,6 @@ async fn duplicate_message_from_old_session_after_session_rotation_is_suppressed assert!( !duplicate_message_received, - "Receiver must not re-emit a message that was already processed in the previous session" + "Receiver must not re-emit a message that was already processed in the previous epoch" ); } diff --git a/blend/network/src/core/with_core/behaviour/tests/mod.rs b/blend/network/src/core/with_core/behaviour/tests/mod.rs index b23cba320..1f5b00bc3 100644 --- a/blend/network/src/core/with_core/behaviour/tests/mod.rs +++ b/blend/network/src/core/with_core/behaviour/tests/mod.rs @@ -1,5 +1,5 @@ mod bootstrapping; mod connection_maintenance; +mod epoch; mod message_handling; -mod session; mod utils; diff --git a/blend/network/src/core/with_core/behaviour/tests/utils.rs b/blend/network/src/core/with_core/behaviour/tests/utils.rs index 411373ffa..ad0679bf6 100644 --- a/blend/network/src/core/with_core/behaviour/tests/utils.rs +++ b/blend/network/src/core/with_core/behaviour/tests/utils.rs @@ -133,10 +133,10 @@ impl BehaviourBuilder { observation_window_clock_provider: self .provider .unwrap_or_else(|| IntervalProviderBuilder::default().build()), - current_session_info: ( + current_epoch_info: ( self.membership .unwrap_or_else(|| Membership::new_without_local(&[])), - 0, + 0.into(), ), peering_degree: self.peering_degree.unwrap_or(1..=1), local_peer_id: PublicKey::from(self.local_public_key).into(), @@ -144,7 +144,7 @@ impl BehaviourBuilder { minimum_network_size: self .minimum_network_size .unwrap_or_else(|| 1usize.try_into().unwrap()), - old_session: None, + old_epoch: None, message_cache: MessageCache::new(), } } diff --git a/blend/network/src/core/with_core/behaviour/utils.rs b/blend/network/src/core/with_core/behaviour/utils.rs index e689b39e9..9fa1a7b0b 100644 --- a/blend/network/src/core/with_core/behaviour/utils.rs +++ b/blend/network/src/core/with_core/behaviour/utils.rs @@ -6,6 +6,7 @@ use lb_blend_message::encap::validated::EncapsulatedMessageWithVerifiedSignature use lb_blend_scheduling::{ deserialize_encapsulated_message, serialize_encapsulated_message_with_verified_signature, }; +use lb_cryptarchia_engine::Epoch; use libp2p::{ PeerId, swarm::{ConnectionId, NotifyHandler, ToSwarm}, @@ -22,15 +23,15 @@ use crate::core::with_core::{ /// The message cache is also updated accordingly to mark the sent message as /// processed if it was sent to at least one peer, or to ignore it if it has /// already been forwarded before. -pub fn forward_validated_message_and_update_cache<'session, PeerConnections>( +pub fn forward_validated_message_and_update_cache<'epoch, PeerConnections>( message: &EncapsulatedMessageWithVerifiedSignature, peer_connections: PeerConnections, - events_queue: &'session mut VecDeque>>, - message_cache: &'session mut MessageCache, + events_queue: &'epoch mut VecDeque>>, + message_cache: &'epoch mut MessageCache, waker: Option, ) -> Result<(), SendError> where - PeerConnections: Iterator, + PeerConnections: Iterator, { if message_cache.is_message_forwarded(&message.clone().into()) { return Err(SendError::DuplicateMessage); @@ -74,7 +75,7 @@ pub fn handle_received_serialized_encapsulated_message_and_update_cache( sender: PeerId, events_queue: &mut VecDeque>>, waker: Option, - session_number: u64, + epoch: Epoch, ) -> Result<(), ReceiveError> { // Deserialize the message. let deserialized_encapsulated_message = deserialize_encapsulated_message(serialized_message) @@ -103,7 +104,7 @@ pub fn handle_received_serialized_encapsulated_message_and_update_cache( events_queue.push_back(ToSwarm::GenerateEvent(Event::Message { message: Box::new(validated_message), sender, - session: session_number, + epoch, })); if let Some(waker) = waker { waker.wake(); diff --git a/blend/network/src/core/with_core/error.rs b/blend/network/src/core/with_core/error.rs index 02552ebcd..bda5c51c0 100644 --- a/blend/network/src/core/with_core/error.rs +++ b/blend/network/src/core/with_core/error.rs @@ -6,8 +6,8 @@ pub enum SendError { NoPeers, /// The message being sent is a duplicate of a previous sent message. DuplicateMessage, - /// The session associated with the message being sent is invalid. - InvalidSession, + /// The epoch associated with the message being sent is invalid. + InvalidEpoch, } #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] diff --git a/blend/network/src/core/with_edge/behaviour/mod.rs b/blend/network/src/core/with_edge/behaviour/mod.rs index 74d66685d..29692bc2f 100644 --- a/blend/network/src/core/with_edge/behaviour/mod.rs +++ b/blend/network/src/core/with_edge/behaviour/mod.rs @@ -73,13 +73,13 @@ impl Behaviour { #[must_use] pub fn new( config: &Config, - current_session_info: Membership, + current_epoch_info: Membership, protocol_name: StreamProtocol, ) -> Self { Self { events: VecDeque::new(), waker: None, - current_membership: current_session_info, + current_membership: current_epoch_info, connection_timeout: config.connection_timeout, upgraded_edge_peers: HashSet::with_capacity(config.max_incoming_connections), max_incoming_connections: config.max_incoming_connections, @@ -88,8 +88,8 @@ impl Behaviour { } } - pub(crate) fn start_new_session(&mut self, new_session_info: Membership) { - self.current_membership = new_session_info; + pub(crate) fn start_new_epoch(&mut self, new_epoch_info: Membership) { + self.current_membership = new_epoch_info; // Close all the connections without waiting for the transition period, // so that edge nodes can retry with the new membership. let peers = mem::take(&mut self.upgraded_edge_peers); @@ -112,7 +112,7 @@ impl Behaviour { fn handle_negotiated_connection(&mut self, connection: (PeerId, ConnectionId)) { // We need to check if we still have available connection slots, as it - // is possible, especially upon session transition, that more + // is possible, especially upon epoch transition, that more // than the maximum allowed number of peers are trying to // connect to us. So once we stream is actually upgraded, we // downgrade it again if we do not have space left for it. This will diff --git a/blend/network/src/core/with_edge/behaviour/tests/session.rs b/blend/network/src/core/with_edge/behaviour/tests/epoch.rs similarity index 82% rename from blend/network/src/core/with_edge/behaviour/tests/session.rs rename to blend/network/src/core/with_edge/behaviour/tests/epoch.rs index f55bc8091..adb7e43c6 100644 --- a/blend/network/src/core/with_edge/behaviour/tests/session.rs +++ b/blend/network/src/core/with_edge/behaviour/tests/epoch.rs @@ -15,10 +15,10 @@ use crate::core::{ with_edge::behaviour::tests::utils::{BehaviourBuilder, StreamBehaviourExt as _}, }; -/// After `start_new_session()`, the edge behaviour must immediately close all -/// existing upgraded edge peer connections (no dual-session for edge). +/// After `start_new_epoch()`, the edge behaviour must immediately close all +/// existing upgraded edge peer connections (no dual-epoch for edge). #[test(tokio::test)] -async fn start_new_session_closes_all_edge_connections() { +async fn start_new_epoch_closes_all_edge_connections() { let core_membership_peer = PeerId::random(); let mut edge_swarm = TestSwarm::new_ephemeral(|_| StreamBehaviour::new()); let mut blend_swarm = TestSwarm::new_ephemeral(|_| { @@ -36,20 +36,18 @@ async fn start_new_session_closes_all_edge_connections() { assert_eq!(blend_swarm.behaviour().upgraded_edge_peers.len(), 1); - // Start a new session: all edge connections should be closed immediately. + // Start a new epoch: all edge connections should be closed immediately. let new_membership = Membership::new_without_local(&[Node { address: Multiaddr::empty(), id: core_membership_peer, public_key: Ed25519PublicKey::from_bytes(&[0; ED25519_PUBLIC_KEY_SIZE]).unwrap(), }]); - blend_swarm - .behaviour_mut() - .start_new_session(new_membership); + blend_swarm.behaviour_mut().start_new_epoch(new_membership); assert_eq!( blend_swarm.behaviour().upgraded_edge_peers.len(), 0, - "All edge peers must be removed immediately after start_new_session" + "All edge peers must be removed immediately after start_new_epoch" ); // Drive the swarms until the connection actually closes. @@ -63,18 +61,18 @@ async fn start_new_session_closes_all_edge_connections() { } } () = sleep(Duration::from_secs(15)) => { - panic!("Timed out waiting for edge connection to close after session transition"); + panic!("Timed out waiting for edge connection to close after epoch transition"); } } } } -/// After a session transition with an updated membership, the edge behaviour +/// After an epoch transition with an updated membership, the edge behaviour /// should accept new edge connections based on the new membership. #[test(tokio::test)] -async fn session_transition_updates_membership_for_new_connections() { +async fn epoch_transition_updates_membership_for_new_connections() { // Initially, edge_swarm_1's peer id is in the membership (core), so it - // should be rejected. After session transition with a different membership + // should be rejected. After epoch transition with a different membership // that does NOT contain edge_swarm_1, it becomes an edge node. let mut edge_swarm = TestSwarm::new_ephemeral(|_| StreamBehaviour::new()); let edge_peer_id = *edge_swarm.local_peer_id(); @@ -110,7 +108,7 @@ async fn session_transition_updates_membership_for_new_connections() { } } - // Transition to a new session where the membership no longer includes + // Transition to a new epoch where the membership no longer includes // edge_peer_id. Now edge_peer_id is a real edge node. let other_core_peer = PeerId::random(); let new_membership = Membership::new_without_local(&[Node { @@ -118,9 +116,7 @@ async fn session_transition_updates_membership_for_new_connections() { id: other_core_peer, public_key: Ed25519PublicKey::from_bytes(&[0; _]).unwrap(), }]); - blend_swarm - .behaviour_mut() - .start_new_session(new_membership); + blend_swarm.behaviour_mut().start_new_epoch(new_membership); // Now edge_swarm should be able to connect and upgrade. let _stream = edge_swarm @@ -130,6 +126,6 @@ async fn session_transition_updates_membership_for_new_connections() { assert_eq!( blend_swarm.behaviour().upgraded_edge_peers.len(), 1, - "Edge peer should be accepted after session transition updated membership" + "Edge peer should be accepted after epoch transition updated membership" ); } diff --git a/blend/network/src/core/with_edge/behaviour/tests/mod.rs b/blend/network/src/core/with_edge/behaviour/tests/mod.rs index 1d50e93ca..b2f8caa11 100644 --- a/blend/network/src/core/with_edge/behaviour/tests/mod.rs +++ b/blend/network/src/core/with_edge/behaviour/tests/mod.rs @@ -1,4 +1,4 @@ mod bootstrapping; +mod epoch; mod message_handling; -mod session; mod utils; diff --git a/blend/proofs/src/quota/fixtures.rs b/blend/proofs/src/quota/fixtures.rs index ef313b39b..cfdc57e05 100644 --- a/blend/proofs/src/quota/fixtures.rs +++ b/blend/proofs/src/quota/fixtures.rs @@ -34,8 +34,6 @@ pub fn valid_proof_of_core_quota_inputs( .unwrap() .into(), }, - // Not relevant for core quota proofs - session: 1, leader: LeaderInputs { pol_epoch_nonce: BigUint::from(1u64).into(), pol_ledger_aged: BigUint::from(1u64).into(), @@ -168,8 +166,6 @@ pub fn valid_proof_of_leadership_quota_inputs( lottery_0, lottery_1, }, - // Not relevant for leadership quota proofs - session: 1, core: CoreInputs { zk_root: BigUint::from(1u64).into(), quota: 1, diff --git a/blend/proofs/src/quota/inputs/prove/mod.rs b/blend/proofs/src/quota/inputs/prove/mod.rs index 7b7f4c1ab..3e0c21b29 100644 --- a/blend/proofs/src/quota/inputs/prove/mod.rs +++ b/blend/proofs/src/quota/inputs/prove/mod.rs @@ -29,7 +29,6 @@ impl TryFrom for PoQWitnessInputs { core_root: value.public.core.zk_root, pol_epoch_nonce: value.public.leader.pol_epoch_nonce, pol_ledger_aged: value.public.leader.pol_ledger_aged, - session: value.public.session, lottery_0: value.public.leader.lottery_0, lottery_1: value.public.leader.lottery_1, }; diff --git a/blend/proofs/src/quota/inputs/prove/private.rs b/blend/proofs/src/quota/inputs/prove/private.rs index 1a80a43e6..347d114d4 100644 --- a/blend/proofs/src/quota/inputs/prove/private.rs +++ b/blend/proofs/src/quota/inputs/prove/private.rs @@ -5,7 +5,10 @@ use zeroize::ZeroizeOnDrop; use crate::{ CorePathAndSelectors, ZkHash, - quota::{SelectionRandomnessSecretInput, inputs::prove::PublicInputs}, + quota::{ + SelectionRandomnessSecretInput, + inputs::prove::{PublicInputs, public::LeaderInputs}, + }, }; /// Private inputs for all types of Proof of Quota. Spec: . @@ -48,12 +51,17 @@ impl Inputs { #[must_use] pub fn get_secret_selection_randomness_sk( &self, - PublicInputs { session, .. }: &PublicInputs, + PublicInputs { + leader: LeaderInputs { + pol_epoch_nonce, .. + }, + .. + }: &PublicInputs, ) -> SelectionRandomnessSecretInput { match &self.proof_type { ProofType::CoreQuota(core_quota_private_inputs) => { SelectionRandomnessSecretInput::Core { - session_number: *session, + epoch_nonce: *pol_epoch_nonce, sk: core_quota_private_inputs.core_sk, } } diff --git a/blend/proofs/src/quota/inputs/prove/public.rs b/blend/proofs/src/quota/inputs/prove/public.rs index 588660adf..a1b9ef54d 100644 --- a/blend/proofs/src/quota/inputs/prove/public.rs +++ b/blend/proofs/src/quota/inputs/prove/public.rs @@ -9,7 +9,6 @@ use crate::{ZkHash, quota::Ed25519PublicKey}; #[derive(Clone, Copy)] pub struct Inputs { pub signing_key: Ed25519PublicKey, - pub session: u64, pub core: CoreInputs, pub leader: LeaderInputs, } @@ -18,7 +17,6 @@ impl Debug for Inputs { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.debug_struct("Inputs") .field("signing_key", &hex::encode(self.signing_key.as_bytes())) - .field("session", &self.session) .field("core", &self.core) .field("leader", &self.leader) .finish() @@ -32,7 +30,6 @@ impl Default for Inputs { Self { signing_key: Ed25519PublicKey::from_bytes(&[0; ED25519_PUBLIC_KEY_SIZE]).unwrap(), - session: 1, core: CoreInputs::default(), leader: LeaderInputs::default(), } diff --git a/blend/proofs/src/quota/inputs/verify.rs b/blend/proofs/src/quota/inputs/verify.rs index 83964342e..7c5620aef 100644 --- a/blend/proofs/src/quota/inputs/verify.rs +++ b/blend/proofs/src/quota/inputs/verify.rs @@ -45,7 +45,6 @@ impl From for PoQVerifierInput { leader_quota: value.prove_inputs.leader.message_quota, pol_epoch_nonce: value.prove_inputs.leader.pol_epoch_nonce, pol_ledger_aged: value.prove_inputs.leader.pol_ledger_aged, - session: value.prove_inputs.session, lottery_0: value.prove_inputs.leader.lottery_0, lottery_1: value.prove_inputs.leader.lottery_1, } diff --git a/blend/proofs/src/quota/mod.rs b/blend/proofs/src/quota/mod.rs index dbb03d960..733e0bc9c 100644 --- a/blend/proofs/src/quota/mod.rs +++ b/blend/proofs/src/quota/mod.rs @@ -210,7 +210,7 @@ impl PartialEq for VerifiedProofOfQuota { pub enum SelectionRandomnessSecretInput { Core { sk: ZkHash, - session_number: u64, + epoch_nonce: ZkHash, }, Leadership { note_secret_key: ZkHash, @@ -228,9 +228,7 @@ fn generate_secret_selection_randomness( key_index: u64, ) -> ZkHash { let (first_element, second_element) = match input { - SelectionRandomnessSecretInput::Core { sk, session_number } => { - (sk, (*session_number).into()) - } + SelectionRandomnessSecretInput::Core { sk, epoch_nonce } => (sk, (*epoch_nonce)), SelectionRandomnessSecretInput::Leadership { note_secret_key, slot_number, diff --git a/blend/proofs/src/quota/tests.rs b/blend/proofs/src/quota/tests.rs index 50204df7a..fc8860ec4 100644 --- a/blend/proofs/src/quota/tests.rs +++ b/blend/proofs/src/quota/tests.rs @@ -133,12 +133,10 @@ fn generate_inputs() -> PoQInputs { lottery_0: Fr::ZERO, lottery_1: Fr::ZERO, }; - let session = 1; let signing_key = Ed25519PublicKey::from_bytes(&[10; ED25519_PUBLIC_KEY_SIZE]).unwrap(); PublicInputs { core: core_inputs, leader: leader_inputs, - session, signing_key, } }; diff --git a/blend/scheduling/src/cover_traffic.rs b/blend/scheduling/src/cover_traffic.rs index f766b78ec..f40692e65 100644 --- a/blend/scheduling/src/cover_traffic.rs +++ b/blend/scheduling/src/cover_traffic.rs @@ -14,12 +14,12 @@ const LOG_TARGET: &str = blend::scheduling::COVER; /// A scheduler for cover messages that can yield a new cover message when /// polled, as per the specification. -pub struct SessionCoverTraffic { +pub struct EpochCoverTraffic { /// The clock ticking at the beginning of each new round. round_clock: RoundClock, /// The number of cover messages still to be emitted. remaining_messages: u64, - /// The number of rounds remaining in this session (including the current + /// The number of rounds remaining in this epoch (including the current /// one). remaining_rounds: Round, /// The count of data messages generated by the node that have not yet been @@ -33,39 +33,30 @@ pub struct SessionCoverTraffic { rng: Rng, } -impl SessionCoverTraffic +impl EpochCoverTraffic where Rng: rand::Rng, { /// Initialize the scheduler with the required details. pub fn new( Settings { - additional_safety_intervals, - expected_intervals_per_session, - rounds_per_interval, + rounds_per_epoch, message_count, }: Settings, rng: Rng, round_clock: RoundClock, ) -> Self { - let total_intervals = expected_intervals_per_session - .get() - .checked_add(additional_safety_intervals) - .expect("Overflow when calculating total intervals per session."); - let total_rounds = total_intervals - .checked_mul(rounds_per_interval.get()) - .expect("Overflow when calculating total rounds per session."); - trace!(target: LOG_TARGET, "Creating new cover message scheduler with {total_rounds} total rounds."); + trace!(target: LOG_TARGET, "Creating new cover message scheduler with {rounds_per_epoch} total rounds."); assert!( - message_count <= total_rounds, + message_count <= rounds_per_epoch.get(), "The number of messages to emit must be at most the total number of rounds." ); Self { round_clock, remaining_messages: message_count, - remaining_rounds: u128::from(total_rounds).into(), + remaining_rounds: NonZeroU128::from(rounds_per_epoch).get().into(), unprocessed_data_messages: 0, rng, } @@ -132,7 +123,7 @@ where } } -impl SessionCoverTraffic { +impl EpochCoverTraffic { #[cfg(test)] pub const fn with_test_values( round_clock: RoundClock, @@ -157,7 +148,7 @@ impl SessionCoverTraffic { /// Notify the scheduler that a new data message has been queued and will be /// released in the next round, which will result in one of the unscheduled - /// cover messages from now to the end of the session to be randomly + /// cover messages from now to the end of the epoch to be randomly /// skipped. pub fn notify_new_data_message(&mut self) { self.unprocessed_data_messages = self @@ -168,7 +159,7 @@ impl SessionCoverTraffic { } } -impl Stream for SessionCoverTraffic +impl Stream for EpochCoverTraffic where RoundClock: Stream + Unpin, Rng: rand::Rng + Unpin, @@ -197,14 +188,9 @@ where /// The settings to initialize the cover message scheduler. #[derive(Debug, Clone, Copy)] pub struct Settings { - /// The number of intervals to use as the safety buffer in case of longer - /// sessions, as per the spec. - pub additional_safety_intervals: u64, - /// The number of expected intervals per session. - pub expected_intervals_per_session: NonZeroU64, - /// The number of rounds per interval. - pub rounds_per_interval: NonZeroU64, - /// The maximum number of messages to generate in this session, before + /// The number of rounds per epoch. + pub rounds_per_epoch: NonZeroU64, + /// The maximum number of messages to generate in this epoch, before /// accounting for any generated data messages. pub message_count: u64, } @@ -217,11 +203,11 @@ mod tests { use rand::rngs::OsRng; use tokio_stream::iter; - use crate::cover_traffic::SessionCoverTraffic; + use crate::cover_traffic::EpochCoverTraffic; #[tokio::test] async fn no_emission_on_empty_schedule() { - let mut scheduler = SessionCoverTraffic { + let mut scheduler = EpochCoverTraffic { round_clock: iter([0u128]).map(Into::into), remaining_messages: 0, remaining_rounds: 1.into(), @@ -235,7 +221,7 @@ mod tests { #[tokio::test] async fn no_emission_on_unscheduled_round() { - let mut scheduler = SessionCoverTraffic { + let mut scheduler = EpochCoverTraffic { round_clock: iter([0u128]).map(Into::into), remaining_messages: 0, remaining_rounds: 0.into(), @@ -251,7 +237,7 @@ mod tests { async fn emission_on_scheduled_round_without_processed_messages() { // With remaining_messages == remaining_rounds, probability is 1.0, // so this round is guaranteed to be a release round. - let mut scheduler = SessionCoverTraffic { + let mut scheduler = EpochCoverTraffic { round_clock: iter([0u128]).map(Into::into), remaining_messages: 1, remaining_rounds: 1.into(), @@ -271,7 +257,7 @@ mod tests { // guaranteed to be a release round. With 1 unprocessed data message // and 1 pending cover message, the skip threshold is 1/1 = 1.0, so // the cover message is always skipped. - let mut scheduler = SessionCoverTraffic { + let mut scheduler = EpochCoverTraffic { round_clock: iter([0u128]).map(Into::into), remaining_messages: 1, remaining_rounds: 1.into(), @@ -292,7 +278,7 @@ mod tests { // remaining_messages == remaining_rounds == 3 guarantees every round // is a release round. With 2 unprocessed data messages, exactly 2 // release rounds will be skipped and 1 cover message will be emitted. - let mut scheduler = SessionCoverTraffic { + let mut scheduler = EpochCoverTraffic { round_clock: iter([0u128, 1u128, 2u128]).map(Into::into), remaining_messages: 3, remaining_rounds: 3.into(), @@ -318,7 +304,7 @@ mod tests { #[test] fn notify_new_data_message_without_scheduled_rounds() { - let mut scheduler = SessionCoverTraffic { + let mut scheduler = EpochCoverTraffic { round_clock: empty(), remaining_messages: 0, remaining_rounds: 0.into(), @@ -331,7 +317,7 @@ mod tests { #[test] fn notify_new_data_message_with_scheduled_rounds() { - let mut scheduler = SessionCoverTraffic { + let mut scheduler = EpochCoverTraffic { round_clock: empty(), remaining_messages: 1, remaining_rounds: 1.into(), diff --git a/blend/scheduling/src/epoch.rs b/blend/scheduling/src/epoch.rs new file mode 100644 index 000000000..bfb86f1a8 --- /dev/null +++ b/blend/scheduling/src/epoch.rs @@ -0,0 +1,228 @@ +use std::{ + future::Future as _, + pin::Pin, + task::{Context, Poll}, + time::Duration, +}; + +use futures::StreamExt as _; +use tokio::time::{Sleep, sleep}; + +use crate::stream::{FirstReadyStreamError, UninitializedFirstReadyStream}; + +/// A staging type that initializes a [`EpochEventStream`] by consuming +/// the first [`Event`] from the underlying stream, expected to be yielded +/// within a short timeout. +pub struct UninitializedEpochEventStream { + stream: UninitializedFirstReadyStream, + transition_period: Duration, +} + +impl UninitializedEpochEventStream { + #[must_use] + pub const fn new(event_stream: EventStream, transition_period: Duration) -> Self { + Self { + stream: UninitializedFirstReadyStream::new(event_stream), + transition_period, + } + } +} + +impl UninitializedEpochEventStream +where + EventStream: futures::Stream + Unpin, +{ + /// Initializes a [`EpochEventStream`] by consuming the first [`Epoch`] + /// from the underlying stream. + /// + /// It returns the first [`Epoch`] and the initialized + /// [`EpochEventStream`], awaiting the first epoch for as long as + /// necessary. + /// It returns an error only if the underlying stream closes before yielding + /// an epoch. + pub async fn await_first_ready( + self, + ) -> Result<(EventStream::Item, EpochEventStream), FirstReadyStreamError> { + let (first_epoch, remaining_stream) = self.stream.first().await?; + Ok(( + first_epoch, + EpochEventStream::new(remaining_stream, self.transition_period), + )) + } +} + +#[derive(Clone, Debug)] +pub enum EpochEvent { + NewEpoch(Event), + TransitionPeriodExpired, +} + +/// A stream that alternates between yielding [`EpochEvent::NewEpoch`] +/// and [`EpochEvent::TransitionPeriodExpired`]. +/// +/// It wraps a stream of [`Epoch`]s and yields a [`EpochEvent::NewEpoch`] +/// as soon as a new [`Epoch`] is available from the inner stream. +/// Then, it yields a [`EpochEvent::TransitionPeriodExpired`] after +/// the transition period has elapsed. +/// +/// # Stream Timeline +/// ```text +/// event stream : O--E-------O--E--------------O--E------- +/// epoch stream : |----E1----|--------E2-------|----E3---- +/// +/// (O: NewEpoch, E: TransitionPeriodExpired, E*: Epochs) +/// ``` +pub struct EpochEventStream { + event_stream: EventStream, + transition_period: Duration, + transition_period_timer: Option>>, +} + +impl EpochEventStream { + #[must_use] + const fn new(event_stream: EventStream, transition_period: Duration) -> Self { + Self { + event_stream, + transition_period, + transition_period_timer: None, + } + } +} + +impl futures::Stream for EpochEventStream +where + EventStream: futures::Stream + Unpin, +{ + type Item = EpochEvent; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + // Check if a new epoch is available. + match self.event_stream.poll_next_unpin(cx) { + Poll::Ready(Some(epoch)) => { + // Start the transition period timer, and yield the new epoch. + // If the previous transition period timer has not been expired yet, + // it will be overwritten. + self.transition_period_timer = Some(Box::pin(sleep(self.transition_period))); + return Poll::Ready(Some(EpochEvent::NewEpoch(epoch))); + } + Poll::Ready(None) => return Poll::Ready(None), + Poll::Pending => {} + } + + // Check if the transition period has expired. + if let Some(timer) = &mut self.transition_period_timer + && timer.as_mut().poll(cx).is_ready() + { + self.transition_period_timer = None; + return Poll::Ready(Some(EpochEvent::TransitionPeriodExpired)); + } + + Poll::Pending + } +} + +#[cfg(test)] +mod tests { + use futures::StreamExt as _; + use tokio::time::{Instant, interval}; + use tokio_stream::wrappers::IntervalStream; + + use super::*; + + #[tokio::test] + async fn yield_two_events_alternately() { + let epoch_duration = Duration::from_secs(1); + let transition_period = Duration::from_millis(200); + let time_tolerance = Duration::from_millis(100); + + let mut stream = EpochEventStream::new( + Box::pin(IntervalStream::new(interval(epoch_duration))), + transition_period, + ); + + // NewEpoch should be emitted immediately. + let start_time = Instant::now(); + assert!(matches!(stream.next().await, Some(EpochEvent::NewEpoch(_)))); + let elapsed = start_time.elapsed(); + let tolerance = Duration::from_millis(50); + assert!(elapsed <= tolerance, "elapsed:{elapsed:?}"); + + // TransitionEnd should be emitted after transition_period. + let start_time = Instant::now(); + assert!(matches!( + stream.next().await, + Some(EpochEvent::TransitionPeriodExpired) + )); + let elapsed = start_time.elapsed(); + assert!( + elapsed.abs_diff(transition_period) <= time_tolerance, + "elapsed:{elapsed:?}, expected:{transition_period:?}", + ); + + // NewEpoch should be emitted after epoch_duration - transition_period. + let start_time = Instant::now(); + assert!(matches!(stream.next().await, Some(EpochEvent::NewEpoch(_)))); + let elapsed = start_time.elapsed(); + assert!( + elapsed.abs_diff(epoch_duration.checked_sub(transition_period).unwrap()) + <= time_tolerance, + "elapsed:{elapsed:?}, expected:{:?}", + epoch_duration.checked_sub(transition_period).unwrap() + ); + + // TransitionEnd should be emitted after transition_period. + let start_time = Instant::now(); + assert!(matches!( + stream.next().await, + Some(EpochEvent::TransitionPeriodExpired) + )); + let elapsed = start_time.elapsed(); + assert!( + elapsed.abs_diff(transition_period) <= time_tolerance, + "elapsed:{elapsed:?}, expected:{transition_period:?}", + ); + } + + #[tokio::test] + async fn transition_period_shorter_than_epoch() { + let epoch_duration = Duration::from_millis(500); + let transition_period = Duration::from_millis(600); + let time_tolerance = Duration::from_millis(50); + + let mut stream = EpochEventStream::new( + Box::pin(IntervalStream::new(interval(epoch_duration))), + transition_period, + ); + + // NewEpoch should be emitted immediately. + let start_time = Instant::now(); + assert!(matches!(stream.next().await, Some(EpochEvent::NewEpoch(_)))); + let elapsed = start_time.elapsed(); + assert!(elapsed <= time_tolerance, "elapsed:{elapsed:?}"); + + // NewEpoch should be emitted again after epoch_duration. + let start_time = Instant::now(); + assert!(matches!(stream.next().await, Some(EpochEvent::NewEpoch(_)))); + let elapsed = start_time.elapsed(); + assert!( + elapsed.abs_diff(epoch_duration) <= time_tolerance, + "elapsed:{elapsed:?}, expected:{epoch_duration:?}", + ); + } + + #[tokio::test] + async fn first_ready_stream_yields_first_item_immediately() { + // Use an underlying stream that yields the first item nearly immediately. + let stream = UninitializedFirstReadyStream::new( + IntervalStream::new(interval(Duration::from_secs(1))) + .enumerate() + .map(|(i, _)| i), + ); + + let (first, mut stream) = stream.first().await.expect("first item should be yielded"); + assert_eq!(first, 0); + // Next items are yielded normally. + assert_eq!(stream.next().await, Some(1)); + assert_eq!(stream.next().await, Some(2)); + } +} diff --git a/blend/scheduling/src/lib.rs b/blend/scheduling/src/lib.rs index b6a202b8a..e1cbb1155 100644 --- a/blend/scheduling/src/lib.rs +++ b/blend/scheduling/src/lib.rs @@ -1,12 +1,12 @@ +pub mod epoch; pub mod membership; pub mod message_blend; -pub mod session; pub use message_blend::crypto::{ deserialize_encapsulated_message, serialize_encapsulated_message_with_verified_public_header, serialize_encapsulated_message_with_verified_signature, }; pub mod message_scheduler; -pub use message_scheduler::SessionMessageScheduler; +pub use message_scheduler::EpochMessageScheduler; pub mod stream; mod cover_traffic; diff --git a/blend/scheduling/src/membership.rs b/blend/scheduling/src/membership.rs index fa189ee4d..58d6ac1b3 100644 --- a/blend/scheduling/src/membership.rs +++ b/blend/scheduling/src/membership.rs @@ -8,7 +8,7 @@ use multiaddr::Multiaddr; use rand::{Rng, seq::IteratorRandom as _}; use serde::{Deserialize, Serialize}; -/// A set of core nodes in a session. +/// A set of core nodes in an epoch. #[derive(Clone, Debug)] pub struct Membership { /// All nodes, including local and remote. diff --git a/blend/scheduling/src/message_blend/crypto/core_and_leader/send.rs b/blend/scheduling/src/message_blend/crypto/core_and_leader/send.rs index aad16eb34..3718c1854 100644 --- a/blend/scheduling/src/message_blend/crypto/core_and_leader/send.rs +++ b/blend/scheduling/src/message_blend/crypto/core_and_leader/send.rs @@ -5,9 +5,7 @@ use lb_blend_message::{ Error, PaddedPayloadBody, PayloadType, crypto::proofs::PoQVerificationInputsMinusSigningKey, input::EncapsulationInput, }; -use lb_blend_proofs::quota::inputs::prove::{ - private::ProofOfLeadershipQuotaInputs, public::LeaderInputs, -}; +use lb_blend_proofs::quota::inputs::prove::private::ProofOfLeadershipQuotaInputs; use lb_cryptarchia_engine::Epoch; use lb_groth16::fr_to_bytes; use lb_key_management_system_keys::keys::X25519PrivateKey; @@ -16,28 +14,27 @@ use crate::{ membership::Membership, message_blend::{ crypto::{ - EncapsulatedMessageWithVerifiedPublicHeader, SessionCryptographicProcessorSettings, + EncapsulatedMessageWithVerifiedPublicHeader, EpochCryptographicProcessorSettings, }, provers::{ProofsGeneratorSettings, core_and_leader::CoreAndLeaderProofsGenerator}, }, }; -/// [`SessionCryptographicProcessor`] is responsible for only wrapping +/// [`EpochCryptographicProcessor`] is responsible for only wrapping /// cover and data messages for the message indistinguishability. /// -/// Each instance is meant to be used during a single session. -pub struct SessionCryptographicProcessor { +/// Each instance is meant to be used during a single epoch. +pub struct EpochCryptographicProcessor { num_blend_layers: NonZeroU64, /// The non-ephemeral encryption key (NEK) for decapsulating messages. non_ephemeral_encryption_key: X25519PrivateKey, membership: Membership, - session: u64, proofs_generator: ProofsGenerator, _phantom: PhantomData, } impl - SessionCryptographicProcessor + EpochCryptographicProcessor { pub(super) const fn non_ephemeral_encryption_key(&self) -> &X25519PrivateKey { &self.non_ephemeral_encryption_key @@ -47,10 +44,6 @@ impl &self.membership } - pub const fn session(&self) -> u64 { - self.session - } - #[cfg(test)] pub const fn proofs_generator(&self) -> &ProofsGenerator { &self.proofs_generator @@ -58,20 +51,20 @@ impl } impl - SessionCryptographicProcessor + EpochCryptographicProcessor where ProofsGenerator: CoreAndLeaderProofsGenerator, { #[must_use] pub fn new( - settings: SessionCryptographicProcessorSettings, + settings: EpochCryptographicProcessorSettings, membership: Membership, public_info: PoQVerificationInputsMinusSigningKey, core_proof_of_quota_generator: CorePoQGenerator, epoch: Epoch, ) -> Self { tracing::trace!( - "Creating session cryptographic processor with public info {public_info:?} and epoch {epoch:?}" + "Creating epoch cryptographic processor with public info {public_info:?} and epoch {epoch:?}" ); let generator_settings = ProofsGeneratorSettings { @@ -89,35 +82,22 @@ where generator_settings, core_proof_of_quota_generator, ), - session: public_info.session, _phantom: PhantomData, } } - pub fn rotate_epoch(&mut self, new_epoch_public_info: LeaderInputs, new_epoch: Epoch) { - tracing::trace!( - "Rotating epoch with new public info {new_epoch_public_info:?} and new epoch {new_epoch:?}" - ); - self.proofs_generator - .rotate_epoch(new_epoch_public_info, new_epoch); - } - pub fn set_epoch_private( &mut self, new_epoch_private: ProofOfLeadershipQuotaInputs, - new_epoch_public_info: LeaderInputs, - new_epoch: Epoch, + target_epoch: Epoch, ) { - self.proofs_generator.set_epoch_private( - new_epoch_private, - new_epoch_public_info, - new_epoch, - ); + self.proofs_generator + .set_epoch_private(new_epoch_private, target_epoch); } } impl - SessionCryptographicProcessor + EpochCryptographicProcessor where NodeId: Eq + Hash + 'static, ProofsGenerator: CoreAndLeaderProofsGenerator, @@ -229,67 +209,18 @@ mod test { use lb_cryptarchia_engine::Epoch; use lb_groth16::{Field as _, Fr}; use lb_key_management_system_keys::keys::{ED25519_PUBLIC_KEY_SIZE, Ed25519PublicKey}; - use multiaddr::{Multiaddr, PeerId}; + use libp2p::PeerId; + use multiaddr::Multiaddr; - use super::SessionCryptographicProcessor; + use super::EpochCryptographicProcessor; use crate::{ membership::{Membership, Node}, message_blend::crypto::{ - SessionCryptographicProcessorSettings, + EpochCryptographicProcessorSettings, test_utils::{MockCorePoQGenerator, TestEpochChangeCoreAndLeaderProofsGenerator}, }, }; - #[test] - fn epoch_rotation() { - let mut processor = SessionCryptographicProcessor::< - _, - _, - TestEpochChangeCoreAndLeaderProofsGenerator, - >::new( - SessionCryptographicProcessorSettings { - non_ephemeral_encryption_key: [0; _].into(), - num_blend_layers: NonZeroU64::new(1).unwrap(), - }, - Membership::new_without_local(&[Node { - address: Multiaddr::empty(), - id: PeerId::random(), - public_key: Ed25519PublicKey::from_bytes(&[0; ED25519_PUBLIC_KEY_SIZE]).unwrap(), - }]), - PoQVerificationInputsMinusSigningKey { - session: 1, - core: CoreInputs { - quota: 1, - zk_root: ZkHash::ZERO, - }, - leader: LeaderInputs { - message_quota: 1, - pol_epoch_nonce: ZkHash::ZERO, - pol_ledger_aged: ZkHash::ZERO, - lottery_0: Fr::ZERO, - lottery_1: Fr::ZERO, - }, - }, - MockCorePoQGenerator, - Epoch::new(0), - ); - - let new_leader_inputs = LeaderInputs { - pol_ledger_aged: ZkHash::ONE, - pol_epoch_nonce: ZkHash::ONE, - message_quota: 2, - lottery_0: Fr::ONE, - lottery_1: Fr::ONE, - }; - - processor.rotate_epoch(new_leader_inputs, Epoch::new(1)); - - assert_eq!( - processor.proofs_generator.0.public_inputs.leader, - new_leader_inputs - ); - } - #[test] fn set_epoch_private() { let leader_inputs = LeaderInputs { @@ -299,31 +230,28 @@ mod test { lottery_0: Fr::ZERO, lottery_1: Fr::ZERO, }; - let mut processor = SessionCryptographicProcessor::< - _, - _, - TestEpochChangeCoreAndLeaderProofsGenerator, - >::new( - SessionCryptographicProcessorSettings { - non_ephemeral_encryption_key: [0; _].into(), - num_blend_layers: NonZeroU64::new(1).unwrap(), - }, - Membership::new_without_local(&[Node { - address: Multiaddr::empty(), - id: PeerId::random(), - public_key: Ed25519PublicKey::from_bytes(&[0; ED25519_PUBLIC_KEY_SIZE]).unwrap(), - }]), - PoQVerificationInputsMinusSigningKey { - session: 1, - core: CoreInputs { - quota: 1, - zk_root: ZkHash::ZERO, + let mut processor = + EpochCryptographicProcessor::<_, _, TestEpochChangeCoreAndLeaderProofsGenerator>::new( + EpochCryptographicProcessorSettings { + non_ephemeral_encryption_key: [0; _].into(), + num_blend_layers: NonZeroU64::new(1).unwrap(), }, - leader: leader_inputs, - }, - MockCorePoQGenerator, - Epoch::new(0), - ); + Membership::new_without_local(&[Node { + address: Multiaddr::empty(), + id: PeerId::random(), + public_key: Ed25519PublicKey::from_bytes(&[0; ED25519_PUBLIC_KEY_SIZE]) + .unwrap(), + }]), + PoQVerificationInputsMinusSigningKey { + core: CoreInputs { + quota: 1, + zk_root: ZkHash::ZERO, + }, + leader: leader_inputs, + }, + MockCorePoQGenerator, + Epoch::new(0), + ); let new_private_inputs = ProofOfLeadershipQuotaInputs { aged_path_and_selectors: [(ZkHash::ONE, true); _], @@ -334,8 +262,8 @@ mod test { transaction_hash: ZkHash::ONE, }; - processor.set_epoch_private(new_private_inputs.clone(), leader_inputs, Epoch::new(1)); + processor.set_epoch_private(new_private_inputs.clone(), Epoch::new(1)); - assert!(processor.proofs_generator.1 == Some(new_private_inputs)); + assert!(processor.proofs_generator.0 == Some(new_private_inputs)); } } diff --git a/blend/scheduling/src/message_blend/crypto/core_and_leader/send_and_receive.rs b/blend/scheduling/src/message_blend/crypto/core_and_leader/send_and_receive.rs index 475ffff0c..2d8915914 100644 --- a/blend/scheduling/src/message_blend/crypto/core_and_leader/send_and_receive.rs +++ b/blend/scheduling/src/message_blend/crypto/core_and_leader/send_and_receive.rs @@ -8,49 +8,47 @@ use lb_blend_message::{ validated::RequiredProofOfSelectionVerificationInputs, }, }; -use lb_blend_proofs::quota::inputs::prove::public::LeaderInputs; use lb_cryptarchia_engine::Epoch; use crate::{ membership::Membership, message_blend::{ crypto::{ - EncapsulatedMessageWithVerifiedPublicHeader, SessionCryptographicProcessorSettings, - core_and_leader::send::SessionCryptographicProcessor as SenderSessionCryptographicProcessor, + EncapsulatedMessageWithVerifiedPublicHeader, EpochCryptographicProcessorSettings, + core_and_leader::send::EpochCryptographicProcessor as SenderEpochCryptographicProcessor, }, provers::core_and_leader::CoreAndLeaderProofsGenerator, }, }; -/// [`SessionCryptographicProcessor`] is responsible for wrapping both cover and +/// [`EpochCryptographicProcessor`] is responsible for wrapping both cover and /// data messages and unwrapping messages for the message indistinguishability. /// -/// Each instance is meant to be used during a single session. +/// Each instance is meant to be used during a single epoch. /// /// This processor is suitable for core nodes. -pub struct SessionCryptographicProcessor -{ - sender_processor: - SenderSessionCryptographicProcessor, +pub struct EpochCryptographicProcessor { + sender_processor: SenderEpochCryptographicProcessor, proofs_verifier: ProofsVerifier, + epoch: Epoch, } impl - SessionCryptographicProcessor + EpochCryptographicProcessor where ProofsGenerator: CoreAndLeaderProofsGenerator, ProofsVerifier: ProofsVerifierTrait, { #[must_use] pub fn new( - settings: SessionCryptographicProcessorSettings, + settings: EpochCryptographicProcessorSettings, membership: Membership, public_info: PoQVerificationInputsMinusSigningKey, core_proof_of_quota_generator: CorePoQGenerator, epoch: Epoch, ) -> Self { Self { - sender_processor: SenderSessionCryptographicProcessor::new( + sender_processor: SenderEpochCryptographicProcessor::new( settings, membership, public_info, @@ -58,27 +56,25 @@ where epoch, ), proofs_verifier: ProofsVerifier::new(public_info), + epoch, } } - - pub fn rotate_epoch(&mut self, new_epoch_public: LeaderInputs, new_epoch: Epoch) { - self.sender_processor - .rotate_epoch(new_epoch_public, new_epoch); - self.proofs_verifier - .start_epoch_transition(new_epoch_public); - } } impl - SessionCryptographicProcessor + EpochCryptographicProcessor { pub const fn verifier(&self) -> &ProofsVerifier { &self.proofs_verifier } + + pub const fn epoch(&self) -> Epoch { + self.epoch + } } impl - SessionCryptographicProcessor + EpochCryptographicProcessor where ProofsVerifier: ProofsVerifierTrait, { @@ -98,18 +94,14 @@ where &self.proofs_verifier, ) } - - pub fn complete_epoch_transition(&mut self) { - self.proofs_verifier.complete_epoch_transition(); - } } // `Deref` and `DerefMut` so we can call the `encapsulate*` methods exposed by // the send-only processor. impl Deref - for SessionCryptographicProcessor + for EpochCryptographicProcessor { - type Target = SenderSessionCryptographicProcessor; + type Target = SenderEpochCryptographicProcessor; fn deref(&self) -> &Self::Target { &self.sender_processor @@ -117,7 +109,7 @@ impl Deref } impl DerefMut - for SessionCryptographicProcessor + for EpochCryptographicProcessor { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.sender_processor @@ -142,8 +134,8 @@ mod test { use crate::{ membership::{Membership, Node}, message_blend::crypto::{ - SessionCryptographicProcessorSettings, - core_and_leader::send_and_receive::SessionCryptographicProcessor, + EpochCryptographicProcessorSettings, + core_and_leader::send_and_receive::EpochCryptographicProcessor, test_utils::{ MockCorePoQGenerator, TestEpochChangeCoreAndLeaderProofsGenerator, TestEpochChangeProofsVerifier, @@ -151,218 +143,6 @@ mod test { }, }; - #[test] - fn epoch_rotation() { - let mut processor = SessionCryptographicProcessor::< - _, - _, - TestEpochChangeCoreAndLeaderProofsGenerator, - TestEpochChangeProofsVerifier, - >::new( - SessionCryptographicProcessorSettings { - non_ephemeral_encryption_key: [0; _].into(), - num_blend_layers: NonZeroU64::new(1).unwrap(), - }, - Membership::new_without_local(&[Node { - address: Multiaddr::empty(), - id: PeerId::random(), - public_key: Ed25519PublicKey::from_bytes(&[0; ED25519_PUBLIC_KEY_SIZE]).unwrap(), - }]), - PoQVerificationInputsMinusSigningKey { - session: 1, - core: CoreInputs { - quota: 1, - zk_root: ZkHash::ZERO, - }, - leader: LeaderInputs { - message_quota: 1, - pol_epoch_nonce: ZkHash::ZERO, - pol_ledger_aged: ZkHash::ZERO, - lottery_0: Fr::ZERO, - lottery_1: Fr::ZERO, - }, - }, - MockCorePoQGenerator, - Epoch::new(0), - ); - - let new_leader_inputs = LeaderInputs { - pol_ledger_aged: ZkHash::ONE, - pol_epoch_nonce: ZkHash::ONE, - message_quota: 2, - lottery_0: Fr::ONE, - lottery_1: Fr::ONE, - }; - - processor.rotate_epoch(new_leader_inputs, Epoch::new(1)); - - assert_eq!(processor.proofs_verifier.0.leader, new_leader_inputs); - assert_eq!( - processor.proofs_verifier.1, - Some(LeaderInputs { - message_quota: 1, - pol_epoch_nonce: ZkHash::ZERO, - pol_ledger_aged: ZkHash::ZERO, - lottery_0: Fr::ZERO, - lottery_1: Fr::ZERO, - }) - ); - assert_eq!( - processor.proofs_generator().0.public_inputs.leader, - new_leader_inputs - ); - - processor.complete_epoch_transition(); - - assert!(processor.proofs_verifier.1.is_none()); - } - - /// Consecutive epoch rotations: each call replaces the previous epoch - /// inputs with the old current inputs. - #[test] - fn consecutive_epoch_rotations() { - let initial_leader = LeaderInputs { - message_quota: 1, - pol_epoch_nonce: ZkHash::ZERO, - pol_ledger_aged: ZkHash::ZERO, - lottery_0: Fr::ZERO, - lottery_1: Fr::ZERO, - }; - let mut processor = SessionCryptographicProcessor::< - _, - _, - TestEpochChangeCoreAndLeaderProofsGenerator, - TestEpochChangeProofsVerifier, - >::new( - SessionCryptographicProcessorSettings { - non_ephemeral_encryption_key: [0; _].into(), - num_blend_layers: NonZeroU64::new(1).unwrap(), - }, - Membership::new_without_local(&[Node { - address: Multiaddr::empty(), - id: PeerId::random(), - public_key: Ed25519PublicKey::from_bytes(&[0; ED25519_PUBLIC_KEY_SIZE]).unwrap(), - }]), - PoQVerificationInputsMinusSigningKey { - session: 1, - core: CoreInputs { - quota: 1, - zk_root: ZkHash::ZERO, - }, - leader: initial_leader, - }, - MockCorePoQGenerator, - Epoch::new(0), - ); - - // Rotate to epoch 1. - let epoch_1_leader = LeaderInputs { - pol_ledger_aged: ZkHash::ONE, - pol_epoch_nonce: ZkHash::ONE, - message_quota: 2, - lottery_0: Fr::ONE, - lottery_1: Fr::ONE, - }; - processor.rotate_epoch(epoch_1_leader, Epoch::new(1)); - assert_eq!(processor.proofs_verifier.0.leader, epoch_1_leader); - assert_eq!(processor.proofs_verifier.1, Some(initial_leader)); - - // Rotate again to epoch 2 without completing the epoch 0→1 transition. - // The previous epoch inputs should now be epoch 1's inputs. - let epoch_2_leader = LeaderInputs { - pol_ledger_aged: ZkHash::ZERO, - pol_epoch_nonce: ZkHash::ONE, - message_quota: 3, - lottery_0: Fr::ZERO, - lottery_1: Fr::ONE, - }; - processor.rotate_epoch(epoch_2_leader, Epoch::new(2)); - assert_eq!(processor.proofs_verifier.0.leader, epoch_2_leader); - assert_eq!(processor.proofs_verifier.1, Some(epoch_1_leader)); - assert_eq!( - processor.proofs_generator().0.public_inputs.leader, - epoch_2_leader - ); - - // Complete the transition — clears previous epoch. - processor.complete_epoch_transition(); - assert!(processor.proofs_verifier.1.is_none()); - assert_eq!(processor.proofs_verifier.0.leader, epoch_2_leader); - } - - /// `complete_epoch_transition` followed by - /// `NewEpochAndOldEpochTransitionExpired` pattern: complete first, then - /// rotate. - #[test] - fn complete_then_rotate_epoch() { - let initial_leader = LeaderInputs { - message_quota: 1, - pol_epoch_nonce: ZkHash::ZERO, - pol_ledger_aged: ZkHash::ZERO, - lottery_0: Fr::ZERO, - lottery_1: Fr::ZERO, - }; - let mut processor = SessionCryptographicProcessor::< - _, - _, - TestEpochChangeCoreAndLeaderProofsGenerator, - TestEpochChangeProofsVerifier, - >::new( - SessionCryptographicProcessorSettings { - non_ephemeral_encryption_key: [0; _].into(), - num_blend_layers: NonZeroU64::new(1).unwrap(), - }, - Membership::new_without_local(&[Node { - address: Multiaddr::empty(), - id: PeerId::random(), - public_key: Ed25519PublicKey::from_bytes(&[0; ED25519_PUBLIC_KEY_SIZE]).unwrap(), - }]), - PoQVerificationInputsMinusSigningKey { - session: 1, - core: CoreInputs { - quota: 1, - zk_root: ZkHash::ZERO, - }, - leader: initial_leader, - }, - MockCorePoQGenerator, - Epoch::new(0), - ); - - // Rotate to epoch 1. - let epoch_1_leader = LeaderInputs { - pol_ledger_aged: ZkHash::ONE, - pol_epoch_nonce: ZkHash::ONE, - message_quota: 2, - lottery_0: Fr::ONE, - lottery_1: Fr::ONE, - }; - processor.rotate_epoch(epoch_1_leader, Epoch::new(1)); - - // Simulate NewEpochAndOldEpochTransitionExpired: - // first complete, then rotate. - processor.complete_epoch_transition(); - assert!(processor.proofs_verifier.1.is_none()); - - let epoch_2_leader = LeaderInputs { - pol_ledger_aged: ZkHash::ZERO, - pol_epoch_nonce: ZkHash::ONE, - message_quota: 3, - lottery_0: Fr::ZERO, - lottery_1: Fr::ONE, - }; - processor.rotate_epoch(epoch_2_leader, Epoch::new(2)); - - // Verifier now has epoch 2 as current, epoch 1 as previous. - assert_eq!(processor.proofs_verifier.0.leader, epoch_2_leader); - assert_eq!(processor.proofs_verifier.1, Some(epoch_1_leader)); - // Generator updated to epoch 2. - assert_eq!( - processor.proofs_generator().0.public_inputs.leader, - epoch_2_leader - ); - } - /// `set_epoch_private` propagates private inputs for leader proof /// generation. #[test] @@ -374,13 +154,13 @@ mod test { lottery_0: Fr::ZERO, lottery_1: Fr::ZERO, }; - let mut processor = SessionCryptographicProcessor::< + let mut processor = EpochCryptographicProcessor::< _, _, TestEpochChangeCoreAndLeaderProofsGenerator, TestEpochChangeProofsVerifier, >::new( - SessionCryptographicProcessorSettings { + EpochCryptographicProcessorSettings { non_ephemeral_encryption_key: [0; _].into(), num_blend_layers: NonZeroU64::new(1).unwrap(), }, @@ -390,7 +170,6 @@ mod test { public_key: Ed25519PublicKey::from_bytes(&[0; ED25519_PUBLIC_KEY_SIZE]).unwrap(), }]), PoQVerificationInputsMinusSigningKey { - session: 1, core: CoreInputs { quota: 1, zk_root: ZkHash::ZERO, @@ -401,7 +180,7 @@ mod test { Epoch::new(0), ); - assert!(processor.proofs_generator().1.is_none()); + assert!(processor.proofs_generator().0.is_none()); let private_inputs = ProofOfLeadershipQuotaInputs { aged_path_and_selectors: [(ZkHash::ONE, true); _], @@ -412,8 +191,8 @@ mod test { transaction_hash: ZkHash::ONE, }; - processor.set_epoch_private(private_inputs.clone(), initial_leader, Epoch::new(1)); + processor.set_epoch_private(private_inputs.clone(), Epoch::new(1)); - assert!(processor.proofs_generator().1 == Some(private_inputs)); + assert!(processor.proofs_generator().0 == Some(private_inputs)); } } diff --git a/blend/scheduling/src/message_blend/crypto/leader/send.rs b/blend/scheduling/src/message_blend/crypto/leader/send.rs index e42f58d2d..2ab471ffc 100644 --- a/blend/scheduling/src/message_blend/crypto/leader/send.rs +++ b/blend/scheduling/src/message_blend/crypto/leader/send.rs @@ -5,9 +5,7 @@ use lb_blend_message::{ Error, PaddedPayloadBody, PayloadType, crypto::proofs::PoQVerificationInputsMinusSigningKey, input::EncapsulationInput, }; -use lb_blend_proofs::quota::inputs::prove::{ - private::ProofOfLeadershipQuotaInputs, public::LeaderInputs, -}; +use lb_blend_proofs::quota::inputs::prove::private::ProofOfLeadershipQuotaInputs; use lb_cryptarchia_engine::Epoch; use crate::{ @@ -21,20 +19,27 @@ use crate::{ }, }; -/// [`SessionCryptographicProcessor`] is responsible for only wrapping data +/// [`EpochCryptographicProcessor`] is responsible for only wrapping data /// messages (no cover messages) for the message indistinguishability. /// -/// Each instance is meant to be used during a single session. +/// Each instance is meant to be used during a single epoch. /// /// This processor is suitable for non-core nodes that do not need to generate /// any cover traffic and are hence only interested in blending data messages. -pub struct SessionCryptographicProcessor { +pub struct EpochCryptographicProcessor { num_blend_layers: NonZeroU64, membership: Membership, proofs_generator: ProofsGenerator, + epoch: Epoch, } -impl SessionCryptographicProcessor +impl EpochCryptographicProcessor { + pub const fn epoch(&self) -> Epoch { + self.epoch + } +} + +impl EpochCryptographicProcessor where ProofsGenerator: LeaderProofsGenerator, { @@ -57,21 +62,12 @@ where num_blend_layers, membership, proofs_generator: ProofsGenerator::new(generator_settings, private_info), + epoch, } } - - pub fn rotate_epoch( - &mut self, - new_epoch_public: LeaderInputs, - new_private_inputs: ProofOfLeadershipQuotaInputs, - new_epoch: Epoch, - ) { - self.proofs_generator - .rotate_epoch(new_epoch_public, new_private_inputs, new_epoch); - } } -impl SessionCryptographicProcessor +impl EpochCryptographicProcessor where NodeId: Eq + Hash + 'static, ProofsGenerator: LeaderProofsGenerator, @@ -146,86 +142,3 @@ where )) } } - -#[cfg(test)] -mod test { - use std::num::NonZeroU64; - - use lb_blend_message::crypto::proofs::PoQVerificationInputsMinusSigningKey; - use lb_blend_proofs::quota::inputs::prove::{ - private::ProofOfLeadershipQuotaInputs, - public::{CoreInputs, LeaderInputs}, - }; - use lb_core::crypto::ZkHash; - use lb_cryptarchia_engine::Epoch; - use lb_groth16::{Field as _, Fr}; - use lb_key_management_system_keys::keys::{ED25519_PUBLIC_KEY_SIZE, Ed25519PublicKey}; - use libp2p::{Multiaddr, PeerId}; - - use super::SessionCryptographicProcessor; - use crate::{ - membership::{Membership, Node}, - message_blend::crypto::test_utils::TestEpochChangeLeaderProofsGenerator, - }; - - #[test] - fn epoch_rotation() { - let mut processor = - SessionCryptographicProcessor::<_, TestEpochChangeLeaderProofsGenerator>::new( - NonZeroU64::new(1).unwrap(), - Membership::new_without_local(&[Node { - address: Multiaddr::empty(), - id: PeerId::random(), - public_key: Ed25519PublicKey::from_bytes(&[0; ED25519_PUBLIC_KEY_SIZE]) - .unwrap(), - }]), - PoQVerificationInputsMinusSigningKey { - session: 1, - core: CoreInputs { - quota: 1, - zk_root: ZkHash::ZERO, - }, - leader: LeaderInputs { - message_quota: 1, - pol_epoch_nonce: ZkHash::ZERO, - pol_ledger_aged: ZkHash::ZERO, - lottery_0: Fr::ZERO, - lottery_1: Fr::ZERO, - }, - }, - ProofOfLeadershipQuotaInputs { - aged_path_and_selectors: [(ZkHash::ZERO, false); _], - note_value: 1, - output_number: 1, - secret_key: ZkHash::ZERO, - slot: 1, - transaction_hash: ZkHash::ZERO, - }, - Epoch::new(0), - ); - - let new_leader_inputs = LeaderInputs { - pol_ledger_aged: ZkHash::ONE, - pol_epoch_nonce: ZkHash::ONE, - message_quota: 2, - lottery_0: Fr::ONE, - lottery_1: Fr::ONE, - }; - let new_private_inputs = ProofOfLeadershipQuotaInputs { - aged_path_and_selectors: [(ZkHash::ONE, true); _], - note_value: 2, - output_number: 2, - secret_key: ZkHash::ONE, - slot: 2, - transaction_hash: ZkHash::ONE, - }; - - processor.rotate_epoch(new_leader_inputs, new_private_inputs.clone(), Epoch::new(1)); - - assert_eq!( - processor.proofs_generator.0.public_inputs.leader, - new_leader_inputs - ); - assert!(processor.proofs_generator.1 == new_private_inputs); - } -} diff --git a/blend/scheduling/src/message_blend/crypto/mod.rs b/blend/scheduling/src/message_blend/crypto/mod.rs index 9d9a5b1ee..e37cb76cd 100644 --- a/blend/scheduling/src/message_blend/crypto/mod.rs +++ b/blend/scheduling/src/message_blend/crypto/mod.rs @@ -15,18 +15,18 @@ use lb_key_management_system_keys::keys::X25519PrivateKey; pub mod core_and_leader; pub use self::core_and_leader::{ - send::SessionCryptographicProcessor as CoreAndLeaderSenderOnlySessionCryptographicProcessor, - send_and_receive::SessionCryptographicProcessor as CoreAndLeaderSendAndReceiveSessionCryptographicProcessor, + send::EpochCryptographicProcessor as CoreAndLeaderSenderOnlyEpochCryptographicProcessor, + send_and_receive::EpochCryptographicProcessor as CoreAndLeaderSendAndReceiveEpochCryptographicProcessor, }; pub mod leader; -pub use self::leader::send::SessionCryptographicProcessor as LeaderSenderOnlySessionCryptographicProcessor; +pub use self::leader::send::EpochCryptographicProcessor as LeaderSenderOnlyEpochCryptographicProcessor; #[cfg(test)] mod test_utils; #[derive(Clone, Derivative)] #[derivative(Debug)] -pub struct SessionCryptographicProcessorSettings { +pub struct EpochCryptographicProcessorSettings { /// The non-ephemeral encryption key (NEK) derived from the secret key /// corresponding to the public key registered in the membership (SDP). #[derivative(Debug = "ignore")] diff --git a/blend/scheduling/src/message_blend/crypto/test_utils.rs b/blend/scheduling/src/message_blend/crypto/test_utils.rs index f8abf92c2..6137ef121 100644 --- a/blend/scheduling/src/message_blend/crypto/test_utils.rs +++ b/blend/scheduling/src/message_blend/crypto/test_utils.rs @@ -8,57 +8,21 @@ use lb_blend_message::{ use lb_blend_proofs::{ quota::{ self, ProofOfQuota, VerifiedProofOfQuota, - inputs::prove::{ - PublicInputs, private::ProofOfLeadershipQuotaInputs, public::LeaderInputs, - }, + inputs::prove::{PublicInputs, private::ProofOfLeadershipQuotaInputs}, }, selection::{ProofOfSelection, VerifiedProofOfSelection, inputs::VerifyInputs}, }; use lb_core::crypto::ZkHash; use lb_cryptarchia_engine::Epoch; -use lb_key_management_system_keys::keys::{Ed25519PublicKey, UnsecuredEd25519Key}; +use lb_key_management_system_keys::keys::Ed25519PublicKey; use crate::message_blend::{ CoreProofOfQuotaGenerator, provers::{ BlendLayerProof, ProofsGeneratorSettings, core_and_leader::CoreAndLeaderProofsGenerator, - leader::LeaderProofsGenerator, }, }; -pub struct TestEpochChangeLeaderProofsGenerator( - pub ProofsGeneratorSettings, - pub ProofOfLeadershipQuotaInputs, -); - -#[async_trait] -impl LeaderProofsGenerator for TestEpochChangeLeaderProofsGenerator { - fn new( - settings: ProofsGeneratorSettings, - private_inputs: ProofOfLeadershipQuotaInputs, - ) -> Self { - Self(settings, private_inputs) - } - - fn rotate_epoch( - &mut self, - new_epoch_public: LeaderInputs, - new_private_inputs: ProofOfLeadershipQuotaInputs, - _new_epoch: Epoch, - ) { - self.0.public_inputs.leader = new_epoch_public; - self.1 = new_private_inputs; - } - - async fn get_next_proof(&mut self) -> BlendLayerProof { - BlendLayerProof { - proof_of_quota: VerifiedProofOfQuota::from_bytes_unchecked([0; _]), - proof_of_selection: VerifiedProofOfSelection::from_bytes_unchecked([0; _]), - ephemeral_signing_key: UnsecuredEd25519Key::from_bytes(&[0; _]), - } - } -} - pub struct MockCorePoQGenerator; impl CoreProofOfQuotaGenerator for MockCorePoQGenerator { @@ -77,30 +41,25 @@ impl CoreProofOfQuotaGenerator for MockCorePoQGenerator { } } -pub struct TestEpochChangeCoreAndLeaderProofsGenerator( - pub ProofsGeneratorSettings, - pub Option, -); +pub struct TestEpochChangeCoreAndLeaderProofsGenerator(pub Option); #[async_trait] impl CoreAndLeaderProofsGenerator for TestEpochChangeCoreAndLeaderProofsGenerator { - fn new(settings: ProofsGeneratorSettings, _proof_of_quota_generator: CorePoQGenerator) -> Self { - Self(settings, None) - } - - fn rotate_epoch(&mut self, new_epoch_public: LeaderInputs, _new_epoch: Epoch) { - self.0.public_inputs.leader = new_epoch_public; + fn new( + _settings: ProofsGeneratorSettings, + _proof_of_quota_generator: CorePoQGenerator, + ) -> Self { + Self(None) } fn set_epoch_private( &mut self, new_epoch_private: ProofOfLeadershipQuotaInputs, - _new_epoch_public: LeaderInputs, - _new_epoch: Epoch, + _target_epoch: Epoch, ) { - self.1 = Some(new_epoch_private); + self.0 = Some(new_epoch_private); } async fn get_next_core_proof(&mut self) -> Option { @@ -112,26 +71,14 @@ impl CoreAndLeaderProofsGenerator } } -pub struct TestEpochChangeProofsVerifier( - pub PoQVerificationInputsMinusSigningKey, - pub Option, -); +pub struct TestEpochChangeProofsVerifier; #[async_trait] impl ProofsVerifier for TestEpochChangeProofsVerifier { type Error = Infallible; - fn new(public_inputs: PoQVerificationInputsMinusSigningKey) -> Self { - Self(public_inputs, None) - } - - fn start_epoch_transition(&mut self, new_pol_inputs: LeaderInputs) { - self.1 = Some(self.0.leader); - self.0.leader = new_pol_inputs; - } - - fn complete_epoch_transition(&mut self) { - self.1 = None; + fn new(_public_inputs: PoQVerificationInputsMinusSigningKey) -> Self { + Self } fn verify_proof_of_quota( diff --git a/blend/scheduling/src/message_blend/provers/core/mod.rs b/blend/scheduling/src/message_blend/provers/core/mod.rs index 28cfa31d0..4abd555a2 100644 --- a/blend/scheduling/src/message_blend/provers/core/mod.rs +++ b/blend/scheduling/src/message_blend/provers/core/mod.rs @@ -1,15 +1,11 @@ -use core::pin::Pin; +use core::{marker::PhantomData, pin::Pin}; use async_trait::async_trait; use futures::stream::{self, Stream, StreamExt as _}; use lb_blend_message::crypto::{ key_ext::Ed25519SecretKeyExt as _, proofs::PoQVerificationInputsMinusSigningKey, }; -use lb_blend_proofs::{ - quota::inputs::prove::{PublicInputs, public::LeaderInputs}, - selection::VerifiedProofOfSelection, -}; -use lb_cryptarchia_engine::Epoch; +use lb_blend_proofs::{quota::inputs::prove::PublicInputs, selection::VerifiedProofOfSelection}; use lb_groth16::fr_to_bytes; use lb_key_management_system_keys::keys::UnsecuredEd25519Key; use lb_log_targets::blend; @@ -29,28 +25,18 @@ const LOG_TARGET: &str = blend::scheduling::proofs::CORE; /// Proof generator for core `PoQ` variants. #[async_trait] pub trait CoreProofsGenerator: Sized { - /// Instantiate a new generator for the duration of a session. + /// Instantiate a new generator for the duration of an epoch. fn new(settings: ProofsGeneratorSettings, proof_of_quota_generator: PoQGenerator) -> Self; - /// Notify the proof generator that a new epoch has started mid-session. - /// This will trigger proof re-generation due to the change in the set of - /// public inputs. - fn rotate_epoch(&mut self, new_epoch_public: LeaderInputs); /// Request a new core proof from the prover. It returns `None` if the - /// maximum core quota has already been reached for this session. + /// maximum core quota has already been reached for this epoch. async fn get_next_proof(&mut self) -> Option; } pub struct RealCoreProofsGenerator { remaining_quota: u64, pub(super) settings: ProofsGeneratorSettings, - pub(super) proof_of_quota_generator: PoQGenerator, proofs_stream: Pin + Send + Sync>>, -} - -impl RealCoreProofsGenerator { - pub(super) const fn current_epoch(&self) -> Epoch { - self.settings.epoch - } + _phantom: PhantomData, } #[async_trait] @@ -62,35 +48,16 @@ where Self { proofs_stream: Box::pin(create_proof_stream( settings.public_inputs, - proof_of_quota_generator.clone(), + proof_of_quota_generator, 0, buffer_size(settings.encapsulation_layers.get() as usize), )), - proof_of_quota_generator, remaining_quota: settings.public_inputs.core.quota, settings, + _phantom: PhantomData, } } - fn rotate_epoch(&mut self, new_epoch_public: LeaderInputs) { - tracing::info!(target: LOG_TARGET, "Rotating epoch..."); - - // On epoch rotation, we maintain the remaining session quota for core proofs - // and we only update the PoL part of the public inputs, before regenerating all - // proofs. - self.settings.public_inputs.leader = new_epoch_public; - let next_key_index = self - .settings - .public_inputs - .core - .quota - .checked_sub(self.remaining_quota) - .expect("Remaining quota should never be larger than total quota."); - - // Compute new proofs with the updated settings. - self.generate_new_proofs_stream(next_key_index); - } - async fn get_next_proof(&mut self) -> Option { let start = Instant::now(); self.remaining_quota = self.remaining_quota.checked_sub(1)?; @@ -100,27 +67,6 @@ where } } -impl RealCoreProofsGenerator -where - PoQGenerator: CoreProofOfQuotaGenerator + Clone + Send + Sync + 'static, -{ - // This will kill the previous running task, if any, since we swap the receiver - // channel, hence the old task will fail to send new proofs and will abort on - // its own. - fn generate_new_proofs_stream(&mut self, starting_key_index: u64) { - if self.remaining_quota == 0 { - return; - } - - self.proofs_stream = Box::pin(create_proof_stream( - self.settings.public_inputs, - self.proof_of_quota_generator.clone(), - starting_key_index, - buffer_size(self.settings.encapsulation_layers.get() as usize), - )); - } -} - fn create_proof_stream( public_inputs: PoQVerificationInputsMinusSigningKey, proof_of_quota_generator: Generator, @@ -156,7 +102,6 @@ where signing_key: ephemeral_signing_key.public_key().into_inner(), core: public_inputs.core, leader: public_inputs.leader, - session: public_inputs.session, }, key_index, ).await.expect("Core PoQ generation should not fail."); diff --git a/blend/scheduling/src/message_blend/provers/core/tests.rs b/blend/scheduling/src/message_blend/provers/core/tests.rs index 9c6838038..c9871213c 100644 --- a/blend/scheduling/src/message_blend/provers/core/tests.rs +++ b/blend/scheduling/src/message_blend/provers/core/tests.rs @@ -7,7 +7,7 @@ use crate::message_blend::provers::{ core::{CoreProofsGenerator as _, RealCoreProofsGenerator}, test_utils::{ CorePoQGeneratorFromPrivateCoreQuotaInputs, - poq_public_inputs_from_session_public_inputs_and_signing_key, valid_proof_of_quota_inputs, + poq_public_inputs_from_epoch_public_inputs_and_signing_key, valid_proof_of_quota_inputs, }, }; @@ -32,12 +32,9 @@ async fn proof_generation() { let verified_proof_of_quota = proof .proof_of_quota .into_inner() - .verify( - &poq_public_inputs_from_session_public_inputs_and_signing_key(( - public_inputs, - proof.ephemeral_signing_key.public_key(), - )), - ) + .verify(&poq_public_inputs_from_epoch_public_inputs_and_signing_key( + (public_inputs, proof.ephemeral_signing_key.public_key()), + )) .unwrap(); proof .proof_of_selection @@ -54,116 +51,3 @@ async fn proof_generation() { // Next proof should be `None` since we ran out of core quota. assert!(core_proofs_generator.get_next_proof().await.is_none()); } - -#[test(tokio::test)] -async fn epoch_rotation() { - let core_quota = 10; - let (public_inputs, private_inputs) = valid_proof_of_quota_inputs(core_quota); - - let mut core_proofs_generator = RealCoreProofsGenerator::new( - ProofsGeneratorSettings { - local_node_index: None, - membership_size: 1, - public_inputs, - encapsulation_layers: 1.try_into().unwrap(), - epoch: Epoch::new(0), - }, - CorePoQGeneratorFromPrivateCoreQuotaInputs::new(private_inputs.clone()), - ); - - // Request all but the last proof, before rotating epoch (with the same public - // data because proofs use hard-coded fixtures). - for _ in 0..(core_quota - 1) { - let proof = core_proofs_generator.get_next_proof().await.unwrap(); - let verified_proof_of_quota = proof - .proof_of_quota - .into_inner() - .verify( - &poq_public_inputs_from_session_public_inputs_and_signing_key(( - public_inputs, - proof.ephemeral_signing_key.public_key(), - )), - ) - .unwrap(); - proof - .proof_of_selection - .into_inner() - .verify(&VerifyInputs { - expected_node_index: 0, - key_nullifier: verified_proof_of_quota.key_nullifier(), - total_membership_size: 1, - }) - .unwrap(); - } - - // Generate and verify last proof. - let proof = core_proofs_generator.get_next_proof().await.unwrap(); - let verified_proof_of_quota = proof - .proof_of_quota - .into_inner() - .verify( - &poq_public_inputs_from_session_public_inputs_and_signing_key(( - public_inputs, - proof.ephemeral_signing_key.public_key(), - )), - ) - .unwrap(); - proof - .proof_of_selection - .into_inner() - .verify(&VerifyInputs { - expected_node_index: 0, - key_nullifier: verified_proof_of_quota.key_nullifier(), - total_membership_size: 1, - }) - .unwrap(); - - // Next proof should be `None` since we ran out of core quota. - assert!(core_proofs_generator.get_next_proof().await.is_none()); - - // Rotating epoch and requesting a new proof will also return `None.` - - core_proofs_generator.rotate_epoch(public_inputs.leader); - assert!(core_proofs_generator.get_next_proof().await.is_none()); -} - -/// Mid-quota epoch rotation: consume some proofs, rotate epoch, and verify the -/// remaining quota is preserved (i.e. exactly the unconsumed proofs remain). -#[test(tokio::test)] -async fn epoch_rotation_mid_quota_preserves_remaining() { - let core_quota = 10; - let proofs_before_rotation = 4; - let (public_inputs, private_inputs) = valid_proof_of_quota_inputs(core_quota); - - let mut core_proofs_generator = RealCoreProofsGenerator::new( - ProofsGeneratorSettings { - local_node_index: None, - membership_size: 1, - public_inputs, - encapsulation_layers: 1.try_into().unwrap(), - epoch: Epoch::new(0), - }, - CorePoQGeneratorFromPrivateCoreQuotaInputs::new(private_inputs), - ); - - // Consume some proofs before rotating. - for _ in 0..proofs_before_rotation { - assert!(core_proofs_generator.get_next_proof().await.is_some()); - } - - // Rotate epoch mid-quota. - core_proofs_generator.rotate_epoch(public_inputs.leader); - - // Remaining quota should allow exactly (core_quota - proofs_before_rotation) - // more proofs. - let remaining = core_quota - proofs_before_rotation; - for i in 0..remaining { - assert!( - core_proofs_generator.get_next_proof().await.is_some(), - "Expected proof {i} of {remaining} remaining after rotation" - ); - } - - // Quota should now be exhausted. - assert!(core_proofs_generator.get_next_proof().await.is_none()); -} diff --git a/blend/scheduling/src/message_blend/provers/core_and_leader/mod.rs b/blend/scheduling/src/message_blend/provers/core_and_leader/mod.rs index ca3ef5f75..e3b5f6cf3 100644 --- a/blend/scheduling/src/message_blend/provers/core_and_leader/mod.rs +++ b/blend/scheduling/src/message_blend/provers/core_and_leader/mod.rs @@ -1,10 +1,6 @@ -use core::cmp::Ordering; - use async_trait::async_trait; use lb_blend_message::crypto::proofs::PoQVerificationInputsMinusSigningKey; -use lb_blend_proofs::quota::inputs::prove::{ - private::ProofOfLeadershipQuotaInputs, public::LeaderInputs, -}; +use lb_blend_proofs::quota::inputs::prove::private::ProofOfLeadershipQuotaInputs; use lb_cryptarchia_engine::Epoch; use lb_log_targets::blend; @@ -31,27 +27,21 @@ const LOG_TARGET: &str = blend::scheduling::proofs::CORE_AND_LEADER; /// generation for those nodes with low stake. #[async_trait] pub trait CoreAndLeaderProofsGenerator: Sized { - /// Instantiate a new generator for the duration of a session. + /// Instantiate a new generator for the duration of an epoch. fn new( settings: ProofsGeneratorSettings, core_proof_of_quota_generator: CorePoQGenerator, ) -> Self; - /// Notify the proof generator that a new epoch has started mid-session. - /// This will trigger core proof re-generation due to the change in the set - /// of public inputs. Previously computed leader proofs are discarded and - /// re-computation is halted until the new epoch private info are provided. - fn rotate_epoch(&mut self, new_epoch_public: LeaderInputs, new_epoch: Epoch); /// Notify the proof generator about winning `PoL` slots and their related /// info. After this information is provided for a new epoch, the generator /// will be able to provide leadership `PoQ` variants. fn set_epoch_private( &mut self, new_epoch_private: ProofOfLeadershipQuotaInputs, - new_epoch_public: LeaderInputs, - new_epoch: Epoch, + reference_epoch: Epoch, ); /// Request a new core proof from the prover. It returns `None` if the - /// maximum core quota has already been reached for this session. + /// maximum core quota has already been reached for this epoch. async fn get_next_core_proof(&mut self) -> Option; /// Request a new leadership proof from the prover. It returns `None` if no /// secret `PoL` info has been provided for the current epoch. @@ -92,76 +82,35 @@ where } } - // Changes epoch-related info for the core generator, and stops the old leader - // generator if it's still on the previous epoch. If not, `rotate_epoch` is - // effectively a no-op. - #[expect( - clippy::cognitive_complexity, - reason = "TODO: address this in a dedicated refactor" - )] - fn rotate_epoch(&mut self, new_epoch_public: LeaderInputs, new_epoch: Epoch) { - match self.core_proofs_generator.current_epoch().cmp(&new_epoch) { - Ordering::Less => { - tracing::info!(target: LOG_TARGET, "Rotating epoch..."); - self.core_proofs_generator.rotate_epoch(new_epoch_public); - } - Ordering::Equal => { - tracing::debug!(target: LOG_TARGET, "Core proofs generator already on the new epoch, ignoring the new public epoch info received."); - } - Ordering::Greater => { - panic!( - "Public epoch info should never provide an epoch smaller than what the core proofs generator returns as current, as the public epoch info should never lag behind." - ); - } - } - - let Some(leader_proofs_generator) = self.leader_proofs_generator.take() else { - return; - }; - - match leader_proofs_generator.current_epoch().cmp(&new_epoch) { - Ordering::Less => { - tracing::debug!(target: LOG_TARGET, "Stopping old epoch leadership proofs generator until new secret PoL info is provided."); - } - Ordering::Equal => { - tracing::debug!(target: LOG_TARGET, "Leadership proofs generator already on the new epoch, ignoring the new public epoch info received."); - self.leader_proofs_generator = Some(leader_proofs_generator); - } - Ordering::Greater => { - panic!( - "Secret PoL info for new epoch should never provide an epoch greater than what the public epoch info returns, as the two should always be yielded together, or at most the secret PoL info would lag behind if the node has no or close to no stake." - ); - } - } - } - // Creates a new leader proofs generator with the provided public+private // secret. fn set_epoch_private( &mut self, new_epoch_private: ProofOfLeadershipQuotaInputs, - new_epoch_public: LeaderInputs, - new_epoch: Epoch, + reference_epoch: Epoch, ) { - // Update core proof generation and optionally deactivates leadership proof - // generation, which is then re-created below. - self.rotate_epoch(new_epoch_public, new_epoch); - - let current_session_local_node_index = self.core_proofs_generator.settings.local_node_index; - let current_session_membership_size = self.core_proofs_generator.settings.membership_size; - let current_session_core_public_inputs = + // TODO: Change trait API to avoid runtime panics. + let (current_generator_epoch, current_leader_inputs) = ( + self.core_proofs_generator.settings.epoch, + self.core_proofs_generator.settings.public_inputs.leader, + ); + assert!( + current_generator_epoch == reference_epoch, + "set_epoch_private should be called with a reference epoch matching the current core proofs generator's epoch." + ); + let current_epoch_local_node_index = self.core_proofs_generator.settings.local_node_index; + let current_epoch_membership_size = self.core_proofs_generator.settings.membership_size; + let current_epoch_core_public_inputs = self.core_proofs_generator.settings.public_inputs.core; - let current_session = self.core_proofs_generator.settings.public_inputs.session; self.leader_proofs_generator = Some(RealLeaderProofsGenerator::new( ProofsGeneratorSettings { - epoch: new_epoch, - local_node_index: current_session_local_node_index, - membership_size: current_session_membership_size, + epoch: reference_epoch, + local_node_index: current_epoch_local_node_index, + membership_size: current_epoch_membership_size, public_inputs: PoQVerificationInputsMinusSigningKey { - core: current_session_core_public_inputs, - session: current_session, - leader: new_epoch_public, + core: current_epoch_core_public_inputs, + leader: current_leader_inputs, }, encapsulation_layers: self.core_proofs_generator.settings.encapsulation_layers, }, @@ -174,7 +123,6 @@ where tracing::trace!( target: LOG_TARGET, epoch = ?self.core_proofs_generator.settings.epoch, - session = self.core_proofs_generator.settings.public_inputs.session, quota = self.core_proofs_generator.settings.public_inputs.core.quota, membership_size = self.core_proofs_generator.settings.membership_size, local_node_index = ?self.core_proofs_generator.settings.local_node_index, @@ -193,7 +141,6 @@ where tracing::trace!( target: LOG_TARGET, epoch = ?leader_proofs_generator.settings.epoch, - session = leader_proofs_generator.settings.public_inputs.session, quota = leader_proofs_generator.settings.public_inputs.core.quota, membership_size = leader_proofs_generator.settings.membership_size, local_node_index = ?leader_proofs_generator.settings.local_node_index, diff --git a/blend/scheduling/src/message_blend/provers/core_and_leader/tests.rs b/blend/scheduling/src/message_blend/provers/core_and_leader/tests.rs index dd82e2e06..625941039 100644 --- a/blend/scheduling/src/message_blend/provers/core_and_leader/tests.rs +++ b/blend/scheduling/src/message_blend/provers/core_and_leader/tests.rs @@ -7,7 +7,7 @@ use crate::message_blend::provers::{ core_and_leader::{CoreAndLeaderProofsGenerator as _, RealCoreAndLeaderProofsGenerator}, test_utils::{ CorePoQGeneratorFromPrivateCoreQuotaInputs, - poq_public_inputs_from_session_public_inputs_and_signing_key, valid_proof_of_leader_inputs, + poq_public_inputs_from_epoch_public_inputs_and_signing_key, valid_proof_of_leader_inputs, valid_proof_of_quota_inputs, }, }; @@ -36,12 +36,9 @@ async fn proof_generation() { let verified_proof_of_quota = proof .proof_of_quota .into_inner() - .verify( - &poq_public_inputs_from_session_public_inputs_and_signing_key(( - core_public_inputs, - proof.ephemeral_signing_key.public_key(), - )), - ) + .verify(&poq_public_inputs_from_epoch_public_inputs_and_signing_key( + (core_public_inputs, proof.ephemeral_signing_key.public_key()), + )) .unwrap(); proof .proof_of_selection @@ -76,11 +73,7 @@ async fn proof_generation() { encapsulation_layers: 1.try_into().unwrap(), epoch: Epoch::new(0), }); - core_and_leader_proofs_generator.set_epoch_private( - leadership_private_inputs, - leadership_public_inputs.leader, - Epoch::new(1), - ); + core_and_leader_proofs_generator.set_epoch_private(leadership_private_inputs, Epoch::new(0)); for _ in 0..leadership_quota { let proof = core_and_leader_proofs_generator @@ -90,12 +83,12 @@ async fn proof_generation() { let verified_proof_of_quota = proof .proof_of_quota .into_inner() - .verify( - &poq_public_inputs_from_session_public_inputs_and_signing_key(( + .verify(&poq_public_inputs_from_epoch_public_inputs_and_signing_key( + ( leadership_public_inputs, proof.ephemeral_signing_key.public_key(), - )), - ) + ), + )) .unwrap(); proof .proof_of_selection @@ -109,96 +102,6 @@ async fn proof_generation() { } } -#[test(tokio::test)] -async fn epoch_rotation() { - let core_quota = 10; - let (public_inputs, private_inputs) = valid_proof_of_quota_inputs(core_quota); - - let mut core_and_leader_proofs_generator = RealCoreAndLeaderProofsGenerator::new( - ProofsGeneratorSettings { - local_node_index: None, - membership_size: 1, - public_inputs, - encapsulation_layers: 1.try_into().unwrap(), - epoch: Epoch::new(1), - }, - CorePoQGeneratorFromPrivateCoreQuotaInputs::new(private_inputs), - ); - - // Request all but the last proof, before rotating epoch (with the same public - // data because proofs use hard-coded fixtures). - for _ in 0..(core_quota - 1) { - let proof = core_and_leader_proofs_generator - .get_next_core_proof() - .await - .unwrap(); - let verified_proof_of_quota = proof - .proof_of_quota - .into_inner() - .verify( - &poq_public_inputs_from_session_public_inputs_and_signing_key(( - public_inputs, - proof.ephemeral_signing_key.public_key(), - )), - ) - .unwrap(); - proof - .proof_of_selection - .into_inner() - .verify(&VerifyInputs { - expected_node_index: 0, - key_nullifier: verified_proof_of_quota.key_nullifier(), - total_membership_size: 1, - }) - .unwrap(); - } - - // Verify any traces of leader proofs have been removed. - assert!( - core_and_leader_proofs_generator - .leader_proofs_generator - .is_none() - ); - assert!( - core_and_leader_proofs_generator - .get_next_leader_proof() - .await - .is_none() - ); - // Generate and verify last proof. - let proof = core_and_leader_proofs_generator - .get_next_core_proof() - .await - .unwrap(); - let verified_proof_of_quota = proof - .proof_of_quota - .into_inner() - .verify( - &poq_public_inputs_from_session_public_inputs_and_signing_key(( - public_inputs, - proof.ephemeral_signing_key.public_key(), - )), - ) - .unwrap(); - proof - .proof_of_selection - .into_inner() - .verify(&VerifyInputs { - expected_node_index: 0, - key_nullifier: verified_proof_of_quota.key_nullifier(), - total_membership_size: 1, - }) - .unwrap(); - - // Next proof should be `None` since we ran out of core quota. - assert!( - core_and_leader_proofs_generator - .get_next_core_proof() - .await - .is_none() - ); -} - #[test(tokio::test)] async fn epoch_private_info() { let core_quota = 10; @@ -211,18 +114,24 @@ async fn epoch_private_info() { ProofsGeneratorSettings { local_node_index: None, membership_size: 1, - public_inputs: leadership_public_inputs, + public_inputs: core_public_inputs, encapsulation_layers: 1.try_into().unwrap(), epoch: Epoch::new(0), }, CorePoQGeneratorFromPrivateCoreQuotaInputs::new(core_private_inputs.clone()), ); - core_and_leader_proofs_generator.set_epoch_private( - leadership_private_inputs, - leadership_public_inputs.leader, - Epoch::new(1), - ); + // Switch to leadership inputs before wiring leader private epoch info, because + // we use fixtures that yield different public inputs. + core_and_leader_proofs_generator.override_settings(ProofsGeneratorSettings { + local_node_index: None, + membership_size: 1, + public_inputs: leadership_public_inputs, + encapsulation_layers: 1.try_into().unwrap(), + epoch: Epoch::new(0), + }); + + core_and_leader_proofs_generator.set_epoch_private(leadership_private_inputs, Epoch::new(0)); // Leadership proof should be generated and verified correctly. let proof = core_and_leader_proofs_generator @@ -232,12 +141,12 @@ async fn epoch_private_info() { let verified_proof_of_quota = proof .proof_of_quota .into_inner() - .verify( - &poq_public_inputs_from_session_public_inputs_and_signing_key(( + .verify(&poq_public_inputs_from_epoch_public_inputs_and_signing_key( + ( leadership_public_inputs, proof.ephemeral_signing_key.public_key(), - )), - ) + ), + )) .unwrap(); proof .proof_of_selection @@ -257,12 +166,12 @@ async fn epoch_private_info() { let verified_proof_of_quota = proof .proof_of_quota .into_inner() - .verify( - &poq_public_inputs_from_session_public_inputs_and_signing_key(( + .verify(&poq_public_inputs_from_epoch_public_inputs_and_signing_key( + ( leadership_public_inputs, proof.ephemeral_signing_key.public_key(), - )), - ) + ), + )) .unwrap(); proof .proof_of_selection @@ -284,7 +193,6 @@ async fn epoch_private_info() { encapsulation_layers: 1.try_into().unwrap(), epoch: Epoch::new(0), }); - core_and_leader_proofs_generator.rotate_epoch(core_public_inputs.leader, Epoch::new(1)); // We test that core proof generation still works fine let proof = core_and_leader_proofs_generator @@ -294,12 +202,9 @@ async fn epoch_private_info() { let verified_proof_of_quota = proof .proof_of_quota .into_inner() - .verify( - &poq_public_inputs_from_session_public_inputs_and_signing_key(( - core_public_inputs, - proof.ephemeral_signing_key.public_key(), - )), - ) + .verify(&poq_public_inputs_from_epoch_public_inputs_and_signing_key( + (core_public_inputs, proof.ephemeral_signing_key.public_key()), + )) .unwrap(); proof .proof_of_selection @@ -312,138 +217,3 @@ async fn epoch_private_info() { }) .unwrap(); } - -/// `rotate_epoch` with the same epoch as the current one should be a no-op: -/// the leader generator (if present) should be preserved. -#[test(tokio::test)] -async fn rotate_epoch_equal_is_noop() { - let core_quota = 5; - let leadership_quota = 10; - let (_, core_private_inputs) = valid_proof_of_quota_inputs(core_quota); - let (leadership_public_inputs, leadership_private_inputs) = - valid_proof_of_leader_inputs(leadership_quota); - - let mut generator = RealCoreAndLeaderProofsGenerator::new( - ProofsGeneratorSettings { - local_node_index: None, - membership_size: 1, - public_inputs: leadership_public_inputs, - encapsulation_layers: 1.try_into().unwrap(), - epoch: Epoch::new(1), - }, - CorePoQGeneratorFromPrivateCoreQuotaInputs::new(core_private_inputs), - ); - - // Provide private info so a leader generator exists. - generator.set_epoch_private( - leadership_private_inputs, - leadership_public_inputs.leader, - Epoch::new(1), - ); - assert!(generator.leader_proofs_generator.is_some()); - - // Rotating with the same epoch should be a no-op - leader generator - // preserved. - generator.rotate_epoch(leadership_public_inputs.leader, Epoch::new(1)); - assert!( - generator.leader_proofs_generator.is_some(), - "Leader generator should be preserved when rotating to the same epoch" - ); - - // Leader proofs should still work. - let proof = generator.get_next_leader_proof().await; - assert!(proof.is_some()); -} - -/// `rotate_epoch` drops the leader generator. `set_epoch_private` then -/// recreates it for the new epoch. -#[test(tokio::test)] -async fn rotate_epoch_drops_leader_then_set_epoch_private_recreates() { - let core_quota = 5; - let leadership_quota = 10; - let (_, core_private_inputs) = valid_proof_of_quota_inputs(core_quota); - let (leadership_public_inputs, leadership_private_inputs) = - valid_proof_of_leader_inputs(leadership_quota); - - let mut generator = RealCoreAndLeaderProofsGenerator::new( - ProofsGeneratorSettings { - local_node_index: None, - membership_size: 1, - public_inputs: leadership_public_inputs, - encapsulation_layers: 1.try_into().unwrap(), - epoch: Epoch::new(0), - }, - CorePoQGeneratorFromPrivateCoreQuotaInputs::new(core_private_inputs), - ); - - // Provide private info for epoch 0. - generator.set_epoch_private( - leadership_private_inputs.clone(), - leadership_public_inputs.leader, - Epoch::new(0), - ); - assert!(generator.leader_proofs_generator.is_some()); - - // Rotate to epoch 1 - leader generator should be dropped because its - // epoch is behind. - generator.rotate_epoch(leadership_public_inputs.leader, Epoch::new(1)); - assert!( - generator.leader_proofs_generator.is_none(), - "Leader generator should be dropped after rotating to a newer epoch" - ); - assert!(generator.get_next_leader_proof().await.is_none()); - - // Provide private info for epoch 1 - leader generator should be - // recreated. - generator.set_epoch_private( - leadership_private_inputs, - leadership_public_inputs.leader, - Epoch::new(1), - ); - assert!( - generator.leader_proofs_generator.is_some(), - "Leader generator should be recreated after set_epoch_private" - ); - let proof = generator.get_next_leader_proof().await; - assert!(proof.is_some()); -} - -/// Two consecutive `rotate_epoch` calls without `set_epoch_private` in between: -/// the first drops the leader generator, the second is a no-op on the leader -/// side since there is nothing to drop. -#[test(tokio::test)] -async fn double_rotate_epoch_without_set_epoch_private() { - let core_quota = 10; - let (public_inputs, private_inputs) = valid_proof_of_quota_inputs(core_quota); - - let mut generator = RealCoreAndLeaderProofsGenerator::new( - ProofsGeneratorSettings { - local_node_index: None, - membership_size: 1, - public_inputs, - encapsulation_layers: 1.try_into().unwrap(), - epoch: Epoch::new(0), - }, - CorePoQGeneratorFromPrivateCoreQuotaInputs::new(private_inputs), - ); - - // Consume some core proofs. - for _ in 0u8..3 { - assert!(generator.get_next_core_proof().await.is_some()); - } - - // First rotation: epoch 0 -> 1. - generator.rotate_epoch(public_inputs.leader, Epoch::new(1)); - assert!(generator.leader_proofs_generator.is_none()); - - // Second rotation: epoch 1 -> 2. - generator.rotate_epoch(public_inputs.leader, Epoch::new(2)); - assert!(generator.leader_proofs_generator.is_none()); - - // Core proofs should still work - remaining quota preserved across rotations. - // We consumed 3 out of 10, so 7 remain. - for _ in 0u8..7 { - assert!(generator.get_next_core_proof().await.is_some()); - } - assert!(generator.get_next_core_proof().await.is_none()); -} diff --git a/blend/scheduling/src/message_blend/provers/leader/mod.rs b/blend/scheduling/src/message_blend/provers/leader/mod.rs index 9303acc5b..54a9fdf6c 100644 --- a/blend/scheduling/src/message_blend/provers/leader/mod.rs +++ b/blend/scheduling/src/message_blend/provers/leader/mod.rs @@ -8,10 +8,7 @@ use lb_blend_message::crypto::{ use lb_blend_proofs::{ quota::{ VerifiedProofOfQuota, - inputs::prove::{ - PrivateInputs, PublicInputs, private::ProofOfLeadershipQuotaInputs, - public::LeaderInputs, - }, + inputs::prove::{PrivateInputs, PublicInputs, private::ProofOfLeadershipQuotaInputs}, }, selection::VerifiedProofOfSelection, }; @@ -40,24 +37,22 @@ pub trait LeaderProofsGenerator: Sized { /// `PoL` values. fn new(settings: ProofsGeneratorSettings, private_inputs: ProofOfLeadershipQuotaInputs) -> Self; - /// Signal an epoch transition in the middle of the current session, with - /// new public and secret inputs. - fn rotate_epoch( - &mut self, - new_epoch_public: LeaderInputs, - new_private_inputs: ProofOfLeadershipQuotaInputs, - new_epoch: Epoch, - ); /// Get the next leadership proof. async fn get_next_proof(&mut self) -> BlendLayerProof; } pub struct RealLeaderProofsGenerator { pub(super) settings: ProofsGeneratorSettings, - private_inputs: ProofOfLeadershipQuotaInputs, proofs_stream: Pin + Send + Sync>>, } +impl RealLeaderProofsGenerator { + #[must_use] + pub const fn epoch(&self) -> Epoch { + self.settings.epoch + } +} + #[async_trait] impl LeaderProofsGenerator for RealLeaderProofsGenerator { fn new( @@ -66,7 +61,6 @@ impl LeaderProofsGenerator for RealLeaderProofsGenerator { ) -> Self { Self { settings, - private_inputs: private_inputs.clone(), proofs_stream: Box::pin(create_proof_stream( settings.public_inputs, private_inputs, @@ -75,24 +69,6 @@ impl LeaderProofsGenerator for RealLeaderProofsGenerator { } } - fn rotate_epoch( - &mut self, - new_epoch_public: LeaderInputs, - new_private: ProofOfLeadershipQuotaInputs, - new_epoch: Epoch, - ) { - tracing::info!(target: LOG_TARGET, "Rotating epoch..."); - - // On epoch rotation, we maintain the current session info and only change the - // PoL relevant parts. - self.settings.public_inputs.leader = new_epoch_public; - self.settings.epoch = new_epoch; - self.private_inputs = new_private; - - // Compute new proofs with the updated settings. - self.generate_new_proofs_stream(); - } - async fn get_next_proof(&mut self) -> BlendLayerProof { let start = Instant::now(); let proof = self @@ -105,20 +81,6 @@ impl LeaderProofsGenerator for RealLeaderProofsGenerator { } } -impl RealLeaderProofsGenerator { - fn generate_new_proofs_stream(&mut self) { - self.proofs_stream = Box::pin(create_proof_stream( - self.settings.public_inputs, - self.private_inputs.clone(), - buffer_size(self.settings.public_inputs.leader.message_quota as usize), - )); - } - - pub(super) const fn current_epoch(&self) -> Epoch { - self.settings.epoch - } -} - fn create_proof_stream( public_inputs: PoQVerificationInputsMinusSigningKey, private_inputs: ProofOfLeadershipQuotaInputs, @@ -131,7 +93,7 @@ fn create_proof_stream( stream::iter(0u64..) .map(move |current_index| { // This represents the total number of encapsulations sent out for each message. - // E.g., for a session with data message replication factor of `1`, we get + // E.g., for an epoch with data message replication factor of `1`, we get // indices `0` to `2` that belong to the first copy encapsulation, and indices // `3` to `5` that belong to the second copy encapsulation. // In the end, because the expected maximum message quota is `6` (if we take `3` @@ -156,7 +118,6 @@ fn create_proof_stream( signing_key: ephemeral_signing_key.public_key().into_inner(), core: public_inputs.core, leader: public_inputs.leader, - session: public_inputs.session, }, PrivateInputs::new_proof_of_leadership_quota_inputs( message_release_index, diff --git a/blend/scheduling/src/message_blend/provers/leader/tests.rs b/blend/scheduling/src/message_blend/provers/leader/tests.rs index 91f161924..192d2b33f 100644 --- a/blend/scheduling/src/message_blend/provers/leader/tests.rs +++ b/blend/scheduling/src/message_blend/provers/leader/tests.rs @@ -9,7 +9,7 @@ use crate::message_blend::provers::{ ProofsGeneratorSettings, leader::{LeaderProofsGenerator as _, RealLeaderProofsGenerator}, test_utils::{ - poq_public_inputs_from_session_public_inputs_and_signing_key, valid_proof_of_leader_inputs, + poq_public_inputs_from_epoch_public_inputs_and_signing_key, valid_proof_of_leader_inputs, }, }; @@ -34,12 +34,9 @@ async fn proof_generation() { let verified_proof_of_quota = proof .proof_of_quota .into_inner() - .verify( - &poq_public_inputs_from_session_public_inputs_and_signing_key(( - public_inputs, - proof.ephemeral_signing_key.public_key(), - )), - ) + .verify(&poq_public_inputs_from_epoch_public_inputs_and_signing_key( + (public_inputs, proof.ephemeral_signing_key.public_key()), + )) .unwrap(); proof .proof_of_selection @@ -62,131 +59,3 @@ async fn proof_generation() { .await .unwrap(); } - -#[test(tokio::test)] -async fn epoch_rotation() { - let leadership_quota = 15; - let (public_inputs, private_inputs) = valid_proof_of_leader_inputs(leadership_quota); - - let mut leader_proofs_generator = RealLeaderProofsGenerator::new( - ProofsGeneratorSettings { - local_node_index: None, - membership_size: 1, - public_inputs, - encapsulation_layers: 1.try_into().unwrap(), - epoch: Epoch::new(0), - }, - private_inputs, - ); - - let proof = leader_proofs_generator.get_next_proof().await; - let verified_proof_of_quota = proof - .proof_of_quota - .into_inner() - .verify( - &poq_public_inputs_from_session_public_inputs_and_signing_key(( - public_inputs, - proof.ephemeral_signing_key.public_key(), - )), - ) - .unwrap(); - proof - .proof_of_selection - .into_inner() - .verify(&VerifyInputs { - expected_node_index: 0, - key_nullifier: verified_proof_of_quota.key_nullifier(), - total_membership_size: 1, - }) - .unwrap(); - - // Generate and verify new proof. - let proof = leader_proofs_generator.get_next_proof().await; - let verified_proof_of_quota = proof - .proof_of_quota - .into_inner() - .verify( - &poq_public_inputs_from_session_public_inputs_and_signing_key(( - public_inputs, - proof.ephemeral_signing_key.public_key(), - )), - ) - .unwrap(); - proof - .proof_of_selection - .into_inner() - .verify(&VerifyInputs { - expected_node_index: 0, - key_nullifier: verified_proof_of_quota.key_nullifier(), - total_membership_size: 1, - }) - .unwrap(); -} - -/// Verify that calling `rotate_epoch` actually updates the epoch and -/// regenerates proofs, unlike the above test which only generates proofs -/// without rotating. -#[test(tokio::test)] -async fn rotate_epoch_updates_epoch_and_regenerates_proofs() { - let leadership_quota = 15; - let (public_inputs, private_inputs) = valid_proof_of_leader_inputs(leadership_quota); - - let mut leader_proofs_generator = RealLeaderProofsGenerator::new( - ProofsGeneratorSettings { - local_node_index: None, - membership_size: 1, - public_inputs, - encapsulation_layers: 1.try_into().unwrap(), - epoch: Epoch::new(0), - }, - private_inputs.clone(), - ); - - // Generate one proof on epoch 0. - drop(leader_proofs_generator.get_next_proof().await); - assert_eq!(leader_proofs_generator.current_epoch(), Epoch::new(0)); - - // Rotate to epoch 1 - this should update epoch and regenerate the proofs - // stream. - leader_proofs_generator.rotate_epoch( - public_inputs.leader, - private_inputs.clone(), - Epoch::new(1), - ); - assert_eq!(leader_proofs_generator.current_epoch(), Epoch::new(1)); - - // Proofs should still be generated successfully after rotation. - let proof = leader_proofs_generator.get_next_proof().await; - proof - .proof_of_quota - .into_inner() - .verify( - &poq_public_inputs_from_session_public_inputs_and_signing_key(( - public_inputs, - proof.ephemeral_signing_key.public_key(), - )), - ) - .unwrap(); - - // Rotate to epoch 2. - leader_proofs_generator.rotate_epoch(public_inputs.leader, private_inputs, Epoch::new(2)); - assert_eq!(leader_proofs_generator.current_epoch(), Epoch::new(2)); - - // Proofs should still verify after a second rotation. - let proof = timeout( - Duration::from_secs(20), - leader_proofs_generator.get_next_proof(), - ) - .await - .unwrap(); - proof - .proof_of_quota - .into_inner() - .verify( - &poq_public_inputs_from_session_public_inputs_and_signing_key(( - public_inputs, - proof.ephemeral_signing_key.public_key(), - )), - ) - .unwrap(); -} diff --git a/blend/scheduling/src/message_blend/provers/mod.rs b/blend/scheduling/src/message_blend/provers/mod.rs index 60ec3ba94..04a6a63d2 100644 --- a/blend/scheduling/src/message_blend/provers/mod.rs +++ b/blend/scheduling/src/message_blend/provers/mod.rs @@ -1,9 +1,6 @@ use ::core::num::NonZeroU64; use lb_blend_message::crypto::proofs::PoQVerificationInputsMinusSigningKey; -use lb_blend_proofs::{ - quota::{VerifiedProofOfQuota, inputs::prove::public::CoreInputs}, - selection::VerifiedProofOfSelection, -}; +use lb_blend_proofs::{quota::VerifiedProofOfQuota, selection::VerifiedProofOfSelection}; use lb_cryptarchia_engine::Epoch; use lb_key_management_system_keys::keys::UnsecuredEd25519Key; @@ -32,11 +29,3 @@ pub struct ProofsGeneratorSettings { pub encapsulation_layers: NonZeroU64, pub epoch: Epoch, } - -#[derive(Debug, Clone, Copy)] -pub struct NewCoreSessionPublicInputs { - pub session: u64, - pub local_node_index: usize, - pub membership_size: usize, - pub inputs: CoreInputs, -} diff --git a/blend/scheduling/src/message_blend/provers/test_utils.rs b/blend/scheduling/src/message_blend/provers/test_utils.rs index af31a9be0..a501eae19 100644 --- a/blend/scheduling/src/message_blend/provers/test_utils.rs +++ b/blend/scheduling/src/message_blend/provers/test_utils.rs @@ -13,47 +13,30 @@ use lb_key_management_system_keys::keys::{ED25519_PUBLIC_KEY_SIZE, Ed25519Public use crate::message_blend::CoreProofOfQuotaGenerator; -pub const fn poq_public_inputs_from_session_public_inputs_and_signing_key( - ( - PoQVerificationInputsMinusSigningKey { - core, - leader, - session, - }, - signing_key, - ): (PoQVerificationInputsMinusSigningKey, Ed25519PublicKey), +pub const fn poq_public_inputs_from_epoch_public_inputs_and_signing_key( + (PoQVerificationInputsMinusSigningKey { core, leader }, signing_key): ( + PoQVerificationInputsMinusSigningKey, + Ed25519PublicKey, + ), ) -> PoQPublicInputs { PoQPublicInputs { signing_key: signing_key.into_inner(), core, leader, - session, } } pub fn valid_proof_of_quota_inputs( core_quota: u64, ) -> (PoQVerificationInputsMinusSigningKey, ProofOfCoreQuotaInputs) { - let ( - PoQPublicInputs { - core, - leader, - session, - .. - }, - private_inputs, - ) = valid_proof_of_core_quota_inputs( + let (PoQPublicInputs { core, leader, .. }, private_inputs) = valid_proof_of_core_quota_inputs( Ed25519PublicKey::from_bytes(&[0; ED25519_PUBLIC_KEY_SIZE]) .unwrap() .into_inner(), core_quota, ); ( - PoQVerificationInputsMinusSigningKey { - core, - leader, - session, - }, + PoQVerificationInputsMinusSigningKey { core, leader }, private_inputs, ) } @@ -64,26 +47,15 @@ pub fn valid_proof_of_leader_inputs( PoQVerificationInputsMinusSigningKey, ProofOfLeadershipQuotaInputs, ) { - let ( - PoQPublicInputs { - core, - leader, - session, - .. - }, - private_inputs, - ) = valid_proof_of_leadership_quota_inputs( - Ed25519PublicKey::from_bytes(&[0; ED25519_PUBLIC_KEY_SIZE]) - .unwrap() - .into_inner(), - leader_quota, - ); + let (PoQPublicInputs { core, leader, .. }, private_inputs) = + valid_proof_of_leadership_quota_inputs( + Ed25519PublicKey::from_bytes(&[0; ED25519_PUBLIC_KEY_SIZE]) + .unwrap() + .into_inner(), + leader_quota, + ); ( - PoQVerificationInputsMinusSigningKey { - core, - leader, - session, - }, + PoQVerificationInputsMinusSigningKey { core, leader }, private_inputs, ) } diff --git a/blend/scheduling/src/message_scheduler/epoch_info.rs b/blend/scheduling/src/message_scheduler/epoch_info.rs new file mode 100644 index 000000000..646414100 --- /dev/null +++ b/blend/scheduling/src/message_scheduler/epoch_info.rs @@ -0,0 +1,12 @@ +use lb_cryptarchia_engine::Epoch; + +/// Information regarding an epoch that the message scheduler needs to +/// initialize its sub-streams. +#[derive(Debug, Clone, Copy)] +pub struct EpochInfo { + /// The initial quota that the cover message scheduler will use to + /// pre-compute the rounds to yield a new message. + pub core_quota: u64, + /// The identifier for the current epoch. + pub epoch: Epoch, +} diff --git a/blend/scheduling/src/message_scheduler/mod.rs b/blend/scheduling/src/message_scheduler/mod.rs index 1eade836e..6af938f56 100644 --- a/blend/scheduling/src/message_scheduler/mod.rs +++ b/blend/scheduling/src/message_scheduler/mod.rs @@ -16,16 +16,16 @@ use tokio_stream::wrappers::IntervalStream; use tracing::trace; use crate::{ - cover_traffic::SessionCoverTraffic, + cover_traffic::EpochCoverTraffic, message_scheduler::{ + epoch_info::EpochInfo, round_info::{RoundClock, RoundInfo, RoundReleaseType}, - session_info::SessionInfo, }, - release_delayer::SessionProcessedMessageDelayer, + release_delayer::EpochProcessedMessageDelayer, }; +pub mod epoch_info; pub mod round_info; -pub mod session_info; #[cfg(test)] mod tests; @@ -39,28 +39,28 @@ pub trait ProcessedMessageScheduler { fn schedule_processed_message(&mut self, message: ProcessedMessage); } -/// Message scheduler that is valid only for a specific session. -pub struct SessionMessageScheduler { +/// Message scheduler that is valid only for a specific epoch. +pub struct EpochMessageScheduler { /// The module responsible for randomly generated cover messages, given the - /// allowed session quota and accounting for data messages generated within - /// the session. - cover_traffic: SessionCoverTraffic, + /// allowed epoch quota and accounting for data messages generated within + /// the epoch. + cover_traffic: EpochCoverTraffic, /// The module responsible for delaying the release of processed messages /// that have not been fully decapsulated. - release_delayer: SessionProcessedMessageDelayer, + release_delayer: EpochProcessedMessageDelayer, /// The queue of data messages that are stored in between rounds. data_messages: Vec, /// The multi-consumer stream forked on each sub-stream. round_clock: RoundClock, } -impl SessionMessageScheduler +impl EpochMessageScheduler where Rng: RngCore + Clone + Unpin, ProcessedMessage: Debug + Unpin, DataMessage: Debug + Unpin, { - pub fn new(session_info: SessionInfo, rng: Rng, settings: Settings) -> Self { + pub fn new(epoch_info: EpochInfo, rng: Rng, settings: Settings) -> Self { let interval = { let mut interval = interval(settings.round_duration); interval.set_missed_tick_behavior(MissedTickBehavior::Skip); @@ -74,19 +74,17 @@ where ) .fork(); - let cover_traffic = SessionCoverTraffic::new( + let cover_traffic = EpochCoverTraffic::new( crate::cover_traffic::Settings { - additional_safety_intervals: settings.additional_safety_intervals, - expected_intervals_per_session: settings.expected_intervals_per_session, - rounds_per_interval: settings.rounds_per_interval, - message_count: session_info + rounds_per_epoch: settings.rounds_per_epoch, + message_count: epoch_info .core_quota .div_ceil(settings.num_blend_layers.into()), }, rng.clone(), Box::new(round_clock.clone()) as RoundClock, ); - let release_delayer = SessionProcessedMessageDelayer::new( + let release_delayer = EpochProcessedMessageDelayer::new( crate::release_delayer::Settings { maximum_release_delay_in_rounds: settings.maximum_release_delay_in_rounds, }, @@ -102,27 +100,23 @@ where } } - pub fn consume(self) -> OldSessionMessageScheduler { - OldSessionMessageScheduler(self.release_delayer) + pub fn consume(self) -> OldEpochMessageScheduler { + OldEpochMessageScheduler(self.release_delayer) } - pub fn rotate_session( + pub fn rotate_epoch( self, - new_session_info: SessionInfo, + new_epoch_info: EpochInfo, settings: Settings, - ) -> (Self, OldSessionMessageScheduler) { + ) -> (Self, OldEpochMessageScheduler) { ( - Self::new( - new_session_info, - self.release_delayer.rng().clone(), - settings, - ), - OldSessionMessageScheduler(self.release_delayer), + Self::new(new_epoch_info, self.release_delayer.rng().clone(), settings), + OldEpochMessageScheduler(self.release_delayer), ) } /// Notify the cover message submodule that a new data message has been - /// generated in this session, which will reduce the number of cover + /// generated in this epoch, which will reduce the number of cover /// messages generated going forward. pub fn queue_data_message(&mut self, message: DataMessage) { self.data_messages.push(message); @@ -130,13 +124,11 @@ where } } -impl - SessionMessageScheduler -{ +impl EpochMessageScheduler { #[cfg(test)] pub fn with_test_values( - cover_traffic: SessionCoverTraffic, - release_delayer: SessionProcessedMessageDelayer, + cover_traffic: EpochCoverTraffic, + release_delayer: EpochProcessedMessageDelayer, round_clock: RoundClock, data_messages: Vec, ) -> Self { @@ -151,13 +143,13 @@ impl #[cfg(any(test, feature = "unsafe-test-functions"))] pub fn release_delayer( &self, - ) -> &SessionProcessedMessageDelayer { + ) -> &EpochProcessedMessageDelayer { &self.release_delayer } } impl ProcessedMessageScheduler - for SessionMessageScheduler + for EpochMessageScheduler { fn schedule_processed_message(&mut self, message: ProcessedMessage) { self.release_delayer.schedule_message(message); @@ -165,7 +157,7 @@ impl ProcessedMessageScheduler Stream - for SessionMessageScheduler + for EpochMessageScheduler where Rng: rand::Rng + Clone + Unpin, ProcessedMessage: Debug + Unpin, @@ -246,11 +238,9 @@ where #[derive(Debug, Clone, Copy)] pub struct Settings { - pub additional_safety_intervals: u64, - pub expected_intervals_per_session: NonZeroU64, pub maximum_release_delay_in_rounds: NonZeroU64, pub round_duration: Duration, - pub rounds_per_interval: NonZeroU64, + pub rounds_per_epoch: NonZeroU64, pub num_blend_layers: NonZeroU64, } @@ -258,34 +248,32 @@ pub struct Settings { impl Default for Settings { fn default() -> Self { Self { - additional_safety_intervals: 0, - expected_intervals_per_session: NonZeroU64::try_from(1).unwrap(), maximum_release_delay_in_rounds: NonZeroU64::try_from(1).unwrap(), round_duration: Duration::from_secs(1), - rounds_per_interval: NonZeroU64::try_from(1).unwrap(), + rounds_per_epoch: NonZeroU64::try_from(1).unwrap(), num_blend_layers: NonZeroU64::try_from(1).unwrap(), } } } -/// Message scheduler that is only for an old session during session transition. +/// Message scheduler that is only for an old epoch during epoch transition. /// -/// Unlike [`SessionMessageScheduler`], this supports only scheduling processed +/// Unlike [`EpochMessageScheduler`], this supports only scheduling processed /// messages. Data messages cannot be scheduled, and it does not generate cover /// messages. -pub struct OldSessionMessageScheduler( - SessionProcessedMessageDelayer, +pub struct OldEpochMessageScheduler( + EpochProcessedMessageDelayer, ); impl ProcessedMessageScheduler - for OldSessionMessageScheduler + for OldEpochMessageScheduler { fn schedule_processed_message(&mut self, message: ProcessedMessage) { self.0.schedule_message(message); } } -impl Stream for OldSessionMessageScheduler +impl Stream for OldEpochMessageScheduler where Rng: rand::Rng + Unpin, ProcessedMessage: Unpin, @@ -297,11 +285,11 @@ where } } -impl OldSessionMessageScheduler { +impl OldEpochMessageScheduler { #[cfg(any(test, feature = "unsafe-test-functions"))] pub fn release_delayer( &self, - ) -> &SessionProcessedMessageDelayer { + ) -> &EpochProcessedMessageDelayer { &self.0 } } diff --git a/blend/scheduling/src/message_scheduler/session_info.rs b/blend/scheduling/src/message_scheduler/session_info.rs deleted file mode 100644 index 8e1765cd3..000000000 --- a/blend/scheduling/src/message_scheduler/session_info.rs +++ /dev/null @@ -1,40 +0,0 @@ -use core::{ - fmt, - fmt::{Display, Formatter}, -}; - -use futures::Stream; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct Session(u128); - -impl From for Session { - fn from(value: u128) -> Self { - Self(value) - } -} - -impl From for u128 { - fn from(session: Session) -> Self { - session.0 - } -} - -impl Display for Session { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -/// Information regarding a session that the message scheduler needs to -/// initialize its sub-streams. -#[derive(Debug, Clone, Copy)] -pub struct SessionInfo { - /// The initial quota that the cover message scheduler will use to - /// pre-compute the rounds to yield a new message. - pub core_quota: u64, - /// The identifier for the current session. - pub session_number: Session, -} - -pub type SessionClock = Box + Unpin>; diff --git a/blend/scheduling/src/message_scheduler/tests.rs b/blend/scheduling/src/message_scheduler/tests.rs index 066ca92de..537abe3ce 100644 --- a/blend/scheduling/src/message_scheduler/tests.rs +++ b/blend/scheduling/src/message_scheduler/tests.rs @@ -9,23 +9,23 @@ use rand::SeedableRng as _; use tokio_stream::iter; use crate::{ - cover_traffic::SessionCoverTraffic, + cover_traffic::EpochCoverTraffic, message_scheduler::{ - SessionMessageScheduler, + EpochMessageScheduler, round_info::{Round, RoundInfo, RoundReleaseType}, }, - release_delayer::SessionProcessedMessageDelayer, + release_delayer::EpochProcessedMessageDelayer, }; #[tokio::test] async fn no_substream_ready_and_no_data_messages() { let rng = BlakeRng::from_entropy(); let rounds = [Round::from(0)]; - let mut scheduler = SessionMessageScheduler::<_, (), ()>::with_test_values( + let mut scheduler = EpochMessageScheduler::<_, (), ()>::with_test_values( // No cover messages to emit, tick will yield round `0`. - SessionCoverTraffic::with_test_values(Box::new(iter(rounds)), 0, 1.into(), rng.clone(), 0), + EpochCoverTraffic::with_test_values(Box::new(iter(rounds)), 0, 1.into(), rng.clone(), 0), // Round `1` scheduled, tick will yield round `0`. - SessionProcessedMessageDelayer::with_test_values( + EpochProcessedMessageDelayer::with_test_values( NonZeroU64::try_from(1).unwrap(), 1u128.into(), rng, @@ -47,11 +47,11 @@ async fn no_substream_ready_and_no_data_messages() { async fn no_substream_ready_with_data_messages() { let rng = BlakeRng::from_entropy(); let rounds = [Round::from(0)]; - let mut scheduler = SessionMessageScheduler::<_, (), u32>::with_test_values( + let mut scheduler = EpochMessageScheduler::<_, (), u32>::with_test_values( // No cover messages to emit, tick will yield round `0`. - SessionCoverTraffic::with_test_values(Box::new(iter(rounds)), 0, 1.into(), rng.clone(), 0), + EpochCoverTraffic::with_test_values(Box::new(iter(rounds)), 0, 1.into(), rng.clone(), 0), // Round `1` scheduled, tick will yield round `0`. - SessionProcessedMessageDelayer::with_test_values( + EpochProcessedMessageDelayer::with_test_values( NonZeroU64::try_from(1).unwrap(), 1u128.into(), rng, @@ -81,11 +81,11 @@ async fn no_substream_ready_with_data_messages() { async fn cover_traffic_substream_ready() { let rng = BlakeRng::from_entropy(); let rounds = [Round::from(0)]; - let mut scheduler = SessionMessageScheduler::<_, (), u32>::with_test_values( + let mut scheduler = EpochMessageScheduler::<_, (), u32>::with_test_values( // 1 cover message over 1 round: guaranteed emission. - SessionCoverTraffic::with_test_values(Box::new(iter(rounds)), 1, 1.into(), rng.clone(), 0), + EpochCoverTraffic::with_test_values(Box::new(iter(rounds)), 1, 1.into(), rng.clone(), 0), // Round `1` scheduled, tick will yield round `0`. - SessionProcessedMessageDelayer::with_test_values( + EpochProcessedMessageDelayer::with_test_values( NonZeroU64::try_from(1).unwrap(), 1u128.into(), rng, @@ -112,11 +112,11 @@ async fn cover_traffic_substream_ready() { async fn release_delayer_substream_ready() { let rng = BlakeRng::from_entropy(); let rounds = [Round::from(0)]; - let mut scheduler = SessionMessageScheduler::<_, u32, u32>::with_test_values( + let mut scheduler = EpochMessageScheduler::<_, u32, u32>::with_test_values( // No cover messages to emit. - SessionCoverTraffic::with_test_values(Box::new(iter(rounds)), 0, 1.into(), rng.clone(), 0), + EpochCoverTraffic::with_test_values(Box::new(iter(rounds)), 0, 1.into(), rng.clone(), 0), // Round `0` scheduled, tick will yield round `0`. - SessionProcessedMessageDelayer::with_test_values( + EpochProcessedMessageDelayer::with_test_values( NonZeroU64::try_from(1).unwrap(), 0u128.into(), rng, @@ -143,11 +143,11 @@ async fn release_delayer_substream_ready() { async fn both_substreams_ready() { let rng = BlakeRng::from_entropy(); let rounds = [Round::from(0)]; - let mut scheduler = SessionMessageScheduler::<_, u32, ()>::with_test_values( + let mut scheduler = EpochMessageScheduler::<_, u32, ()>::with_test_values( // 1 cover message over 1 round: guaranteed emission. - SessionCoverTraffic::with_test_values(Box::new(iter(rounds)), 1, 1.into(), rng.clone(), 0), + EpochCoverTraffic::with_test_values(Box::new(iter(rounds)), 1, 1.into(), rng.clone(), 0), // Round `0` scheduled, tick will yield round `0`. - SessionProcessedMessageDelayer::with_test_values( + EpochProcessedMessageDelayer::with_test_values( NonZeroU64::try_from(1).unwrap(), 0u128.into(), rng, @@ -180,13 +180,13 @@ async fn round_change() { Round::from(2), Round::from(3), ]; - let mut scheduler = SessionMessageScheduler::<_, (), u32>::with_test_values( + let mut scheduler = EpochMessageScheduler::<_, (), u32>::with_test_values( // 2 cover messages over 2 remaining rounds: every round is guaranteed to be a // release round. After the first emission, a data message will cause the second // release to be skipped (threshold = 1/1 = 1.0). - SessionCoverTraffic::with_test_values(Box::new(iter(rounds)), 2, 2.into(), rng.clone(), 0), + EpochCoverTraffic::with_test_values(Box::new(iter(rounds)), 2, 2.into(), rng.clone(), 0), // Round `3` scheduled, tick will yield rounds `0` through `3`. - SessionProcessedMessageDelayer::with_test_values( + EpochProcessedMessageDelayer::with_test_values( NonZeroU64::try_from(1).unwrap(), 3u128.into(), rng, diff --git a/blend/scheduling/src/release_delayer.rs b/blend/scheduling/src/release_delayer.rs index 2e75dcdeb..cd6927d22 100644 --- a/blend/scheduling/src/release_delayer.rs +++ b/blend/scheduling/src/release_delayer.rs @@ -15,7 +15,7 @@ const LOG_TARGET: &str = blend::scheduling::DELAY; /// A scheduler for delaying processed messages and batch them into release /// rounds, as per the specification. -pub struct SessionProcessedMessageDelayer { +pub struct EpochProcessedMessageDelayer { /// The maximum delay, in terms of rounds, between two consecutive release /// rounds. maximum_release_delay_in_rounds: NonZeroU64, @@ -31,7 +31,7 @@ pub struct SessionProcessedMessageDelayer { } impl - SessionProcessedMessageDelayer + EpochProcessedMessageDelayer where Rng: rand::Rng, { @@ -58,7 +58,7 @@ where } impl - SessionProcessedMessageDelayer + EpochProcessedMessageDelayer { #[cfg(test)] pub const fn with_test_values( @@ -126,7 +126,7 @@ where } impl Stream - for SessionProcessedMessageDelayer + for EpochProcessedMessageDelayer where RoundClock: Stream + Unpin, Rng: rand::Rng + Unpin, @@ -179,11 +179,11 @@ mod tests { use rand::SeedableRng as _; use tokio_stream::iter; - use crate::release_delayer::SessionProcessedMessageDelayer; + use crate::release_delayer::EpochProcessedMessageDelayer; #[test] fn poll_none_on_unscheduled_round() { - let mut delayer = SessionProcessedMessageDelayer::<_, _, ()> { + let mut delayer = EpochProcessedMessageDelayer::<_, _, ()> { maximum_release_delay_in_rounds: NonZeroU64::new(3).expect("Non-zero usize"), next_release_round: 1u128.into(), rng: BlakeRng::from_entropy(), @@ -199,7 +199,7 @@ mod tests { #[test] fn poll_empty_on_scheduled_round_with_empty_queue() { - let mut delayer = SessionProcessedMessageDelayer::<_, _, ()> { + let mut delayer = EpochProcessedMessageDelayer::<_, _, ()> { maximum_release_delay_in_rounds: NonZeroU64::new(3).expect("Non-zero usize"), next_release_round: 1u128.into(), rng: BlakeRng::from_entropy(), @@ -216,7 +216,7 @@ mod tests { #[test] fn poll_non_empty_on_scheduled_round_with_non_empty_queue() { - let mut delayer = SessionProcessedMessageDelayer::<_, _, ()> { + let mut delayer = EpochProcessedMessageDelayer::<_, _, ()> { maximum_release_delay_in_rounds: NonZeroU64::new(3).expect("Non-zero usize"), next_release_round: 1u128.into(), rng: BlakeRng::from_entropy(), @@ -236,7 +236,7 @@ mod tests { #[test] fn add_message_to_queue() { - let mut delayer = SessionProcessedMessageDelayer::<_, _, bool> { + let mut delayer = EpochProcessedMessageDelayer::<_, _, bool> { maximum_release_delay_in_rounds: NonZeroU64::new(3).expect("Non-zero usize"), next_release_round: 1u128.into(), rng: BlakeRng::from_entropy(), diff --git a/blend/scheduling/src/session.rs b/blend/scheduling/src/session.rs deleted file mode 100644 index 77aa09052..000000000 --- a/blend/scheduling/src/session.rs +++ /dev/null @@ -1,262 +0,0 @@ -use std::{ - future::Future as _, - pin::Pin, - task::{Context, Poll}, - time::Duration, -}; - -use futures::StreamExt as _; -use tokio::time::{Sleep, sleep}; - -use crate::stream::{FirstReadyStreamError, UninitializedFirstReadyStream}; - -/// A staging type that initializes a [`SessionEventStream`] by consuming -/// the first [`Session`] from the underlying stream, expected to be yielded -/// within a short timeout. -pub struct UninitializedSessionEventStream { - stream: UninitializedFirstReadyStream, - transition_period: Duration, -} - -impl UninitializedSessionEventStream { - #[must_use] - pub const fn new( - session_stream: Stream, - first_ready_timeout: Duration, - transition_period: Duration, - ) -> Self { - Self { - stream: UninitializedFirstReadyStream::new(session_stream, first_ready_timeout), - transition_period, - } - } -} - -impl UninitializedSessionEventStream -where - Stream: futures::Stream + Unpin, -{ - /// Initializes a [`SessionEventStream`] by consuming the first [`Session`] - /// from the underlying stream. - /// - /// It returns the first [`Session`] and the initialized - /// [`SessionEventStream`]. - /// It returns an error if the first session is not yielded within the - /// configured timeout. - pub async fn await_first_ready( - self, - ) -> Result<(Session, SessionEventStream), FirstReadyStreamError> { - let (first_session, remaining_stream) = self.stream.first().await?; - Ok(( - first_session, - SessionEventStream::new(remaining_stream, self.transition_period), - )) - } -} - -#[derive(Clone, Debug)] -pub enum SessionEvent { - NewSession(Session), - TransitionPeriodExpired, -} - -/// A stream that alternates between yielding [`SessionEvent::NewSession`] -/// and [`SessionEvent::TransitionPeriodExpired`]. -/// -/// It wraps a stream of [`Session`]s and yields a [`SessionEvent::NewSession`] -/// as soon as a new [`Session`] is available from the inner stream. -/// Then, it yields a [`SessionEvent::TransitionPeriodExpired`] after -/// the transition period has elapsed. -/// -/// # Stream Timeline -/// ```text -/// event stream : O--E-------O--E--------------O--E------- -/// session stream: |----S1----|--------S2-------|----S3---- -/// -/// (O: NewSession, E: TransitionPeriodExpired, S*: Sessions) -/// ``` -pub struct SessionEventStream { - session_stream: Stream, - transition_period: Duration, - transition_period_timer: Option>>, -} - -impl SessionEventStream { - #[must_use] - const fn new(session_stream: Stream, transition_period: Duration) -> Self { - Self { - session_stream, - transition_period, - transition_period_timer: None, - } - } -} - -impl futures::Stream for SessionEventStream -where - Stream: futures::Stream + Unpin, -{ - type Item = SessionEvent; - - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - // Check if a new session is available. - match self.session_stream.poll_next_unpin(cx) { - Poll::Ready(Some(session)) => { - // Start the transition period timer, and yield the new session. - // If the previous transition period timer has not been expired yet, - // it will be overwritten. - self.transition_period_timer = Some(Box::pin(sleep(self.transition_period))); - return Poll::Ready(Some(SessionEvent::NewSession(session))); - } - Poll::Ready(None) => return Poll::Ready(None), - Poll::Pending => {} - } - - // Check if the transition period has expired. - if let Some(timer) = &mut self.transition_period_timer - && timer.as_mut().poll(cx).is_ready() - { - self.transition_period_timer = None; - return Poll::Ready(Some(SessionEvent::TransitionPeriodExpired)); - } - - Poll::Pending - } -} - -#[cfg(test)] -mod tests { - use futures::StreamExt as _; - use tokio::time::{Instant, interval, interval_at}; - use tokio_stream::wrappers::IntervalStream; - - use super::*; - - #[tokio::test] - async fn yield_two_events_alternately() { - let session_duration = Duration::from_secs(1); - let transition_period = Duration::from_millis(200); - let time_tolerance = Duration::from_millis(100); - - let mut stream = SessionEventStream::new( - Box::pin(IntervalStream::new(interval(session_duration))), - transition_period, - ); - - // NewSession should be emitted immediately. - let start_time = Instant::now(); - assert!(matches!( - stream.next().await, - Some(SessionEvent::NewSession(_)) - )); - let elapsed = start_time.elapsed(); - let tolerance = Duration::from_millis(50); - assert!(elapsed <= tolerance, "elapsed:{elapsed:?}"); - - // TransitionEnd should be emitted after transition_period. - let start_time = Instant::now(); - assert!(matches!( - stream.next().await, - Some(SessionEvent::TransitionPeriodExpired) - )); - let elapsed = start_time.elapsed(); - assert!( - elapsed.abs_diff(transition_period) <= time_tolerance, - "elapsed:{elapsed:?}, expected:{transition_period:?}", - ); - - // NewSession should be emitted after session_duration - transition_period. - let start_time = Instant::now(); - assert!(matches!( - stream.next().await, - Some(SessionEvent::NewSession(_)) - )); - let elapsed = start_time.elapsed(); - assert!( - elapsed.abs_diff(session_duration.checked_sub(transition_period).unwrap()) - <= time_tolerance, - "elapsed:{elapsed:?}, expected:{:?}", - session_duration.checked_sub(transition_period).unwrap() - ); - - // TransitionEnd should be emitted after transition_period. - let start_time = Instant::now(); - assert!(matches!( - stream.next().await, - Some(SessionEvent::TransitionPeriodExpired) - )); - let elapsed = start_time.elapsed(); - assert!( - elapsed.abs_diff(transition_period) <= time_tolerance, - "elapsed:{elapsed:?}, expected:{transition_period:?}", - ); - } - - #[tokio::test] - async fn transition_period_shorter_than_session() { - let session_duration = Duration::from_millis(500); - let transition_period = Duration::from_millis(600); - let time_tolerance = Duration::from_millis(50); - - let mut stream = SessionEventStream::new( - Box::pin(IntervalStream::new(interval(session_duration))), - transition_period, - ); - - // NewSession should be emitted immediately. - let start_time = Instant::now(); - assert!(matches!( - stream.next().await, - Some(SessionEvent::NewSession(_)) - )); - let elapsed = start_time.elapsed(); - assert!(elapsed <= time_tolerance, "elapsed:{elapsed:?}"); - - // NewSession should be emitted again after session_duration. - let start_time = Instant::now(); - assert!(matches!( - stream.next().await, - Some(SessionEvent::NewSession(_)) - )); - let elapsed = start_time.elapsed(); - assert!( - elapsed.abs_diff(session_duration) <= time_tolerance, - "elapsed:{elapsed:?}, expected:{session_duration:?}", - ); - } - - #[tokio::test] - async fn first_ready_stream_yields_first_item_immediately() { - // Use an underlying stream that yields the first item nearly immediately. - let stream = UninitializedFirstReadyStream::new( - IntervalStream::new(interval(Duration::from_secs(1))) - .enumerate() - .map(|(i, _)| i), - Duration::from_millis(100), - ); - - let (first, mut stream) = stream.first().await.expect("first item should be yielded"); - assert_eq!(first, 0); - // Next items are yielded normally. - assert_eq!(stream.next().await, Some(1)); - assert_eq!(stream.next().await, Some(2)); - } - - #[tokio::test] - async fn first_relay_stream_fails_if_first_item_is_not_ready() { - // Use an underlying stream that yield the first item too late. - let stream = UninitializedFirstReadyStream::new( - IntervalStream::new(interval_at( - // The first time will be yieled after 2s. - Instant::now() + Duration::from_secs(2), - Duration::from_secs(1), - )), - Duration::from_millis(100), - ); - - assert!(matches!( - stream.first().await, - Err(FirstReadyStreamError::FirstItemNotReady) - )); - } -} diff --git a/blend/scheduling/src/stream.rs b/blend/scheduling/src/stream.rs index 026144fc5..6309d5507 100644 --- a/blend/scheduling/src/stream.rs +++ b/blend/scheduling/src/stream.rs @@ -1,22 +1,19 @@ -use core::time::Duration; - use futures::StreamExt as _; -use tokio::time::timeout; -/// A stream wrapper that expects the underlying stream to yield its first item -/// within the given timeout. +/// A stream wrapper that consumes the first item from the underlying stream. /// /// Instead of implementing [`futures::Stream`], this provides only -/// [`Self::first`] that explicitly tries to read the first item and returns a -/// result. +/// [`Self::first`] that explicitly reads the first item and returns it together +/// with the remaining stream. It waits for the first item for as long as +/// necessary: blend membership and the slot clock only become available at +/// genesis, which may be arbitrarily far in the future, so there is no timeout. pub struct UninitializedFirstReadyStream { stream: Stream, - timeout: Duration, } impl UninitializedFirstReadyStream { - pub const fn new(stream: Stream, timeout: Duration) -> Self { - Self { stream, timeout } + pub const fn new(stream: Stream) -> Self { + Self { stream } } } @@ -24,13 +21,17 @@ impl UninitializedFirstReadyStream where Stream: futures::Stream + Unpin, { - /// Yields the first item if it is yielded within the timeout from the - /// underlying stream. - /// The remaining stream is also returned for continued use. + /// Yields the first item from the underlying stream, awaiting it for as + /// long as necessary. The remaining stream is also returned for + /// continued use. + /// + /// Returns [`FirstReadyStreamError::StreamClosed`] if the underlying stream + /// ends before yielding any item. pub async fn first(mut self) -> Result<(Stream::Item, Stream), FirstReadyStreamError> { - let item = timeout(self.timeout, self.stream.next()) + let item = self + .stream + .next() .await - .map_err(|_| FirstReadyStreamError::FirstItemNotReady)? .ok_or(FirstReadyStreamError::StreamClosed)?; Ok((item, self.stream)) } @@ -38,8 +39,6 @@ where #[derive(Debug, thiserror::Error)] pub enum FirstReadyStreamError { - #[error("The first item was not yielded in time")] - FirstItemNotReady, #[error("The underlying stream was closed before yielding the first item")] StreamClosed, } diff --git a/consensus/cryptarchia-engine/src/time.rs b/consensus/cryptarchia-engine/src/time.rs index 829a24221..4ed77e5e9 100644 --- a/consensus/cryptarchia-engine/src/time.rs +++ b/consensus/cryptarchia-engine/src/time.rs @@ -1,3 +1,8 @@ +use core::{ + cmp::Ordering, + fmt::{self, Display, Formatter}, + ops::AddAssign, +}; use std::{ num::NonZero, ops::{Add, Sub}, @@ -44,6 +49,41 @@ impl Epoch { pub const fn saturating_add(self, rhs: Self) -> Self { Self(self.0.saturating_add(rhs.0)) } + + #[must_use] + pub const fn saturating_sub(self, rhs: Self) -> Self { + Self(self.0.saturating_sub(rhs.0)) + } +} + +impl Display for Epoch { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "Epoch({})", self.0) + } +} + +impl PartialEq for Epoch { + fn eq(&self, other: &u32) -> bool { + self.0 == *other + } +} + +impl PartialEq for u32 { + fn eq(&self, other: &Epoch) -> bool { + *self == other.0 + } +} + +impl PartialOrd for Epoch { + fn partial_cmp(&self, other: &u32) -> Option { + self.0.partial_cmp(other) + } +} + +impl PartialOrd for u32 { + fn partial_cmp(&self, other: &Epoch) -> Option { + self.partial_cmp(&other.0) + } } impl Slot { @@ -163,6 +203,28 @@ impl Add for Epoch { } } +impl AddAssign for Epoch { + fn add_assign(&mut self, rhs: u32) { + self.0 += rhs; + } +} + +impl Add for Epoch { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + Self(self.0 + rhs.0) + } +} + +impl Sub for Epoch { + type Output = Self; + + fn sub(self, rhs: Self) -> Self::Output { + Self(self.0 - rhs.0) + } +} + #[derive(Copy, Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct EpochConfig { // The stake distribution is always taken at the beginning of the previous epoch. @@ -199,6 +261,11 @@ impl EpochConfig { pub fn starting_slot(&self, epoch: &Epoch, base_period_length: NonZero) -> Slot { Slot::from(u64::from(u32::from(*epoch)) * self.epoch_length(base_period_length)) } + + #[must_use] + pub fn last_slot(&self, epoch: Epoch, base_period_length: NonZero) -> Slot { + Slot::from(u64::from(epoch.into_inner() + 1) * self.epoch_length(base_period_length) - 1) + } } #[must_use] diff --git a/core/src/blend/mod.rs b/core/src/blend/mod.rs index 030983953..c550bfd12 100644 --- a/core/src/blend/mod.rs +++ b/core/src/blend/mod.rs @@ -4,19 +4,19 @@ use lb_utils::math::NonNegativeF64; #[must_use] pub fn core_quota( - rounds_per_session: NonZeroU64, + rounds_per_epoch: NonZeroU64, message_frequency_per_round: NonNegativeF64, num_blend_layers: NonZeroU64, membership_size: usize, ) -> u64 { - // `C`: Expected number of cover messages that are generated during a session by + // `C`: Expected number of cover messages that are generated during an epoch by // the core nodes. - let expected_number_of_session_messages = - rounds_per_session.get() as f64 * message_frequency_per_round.get(); + let expected_number_of_epoch_messages = + rounds_per_epoch.get() as f64 * message_frequency_per_round.get(); // `Q_c`: Messaging allowance that can be used by a core node during a single - // session. We assume `R_c` to be `0` for now, hence `Q_c = ceil(C * (ß_c + // epoch. We assume `R_c` to be `0` for now, hence `Q_c = ceil(C * (ß_c // + 0 * ß_c)) / N = ceil(C * ß_c) / N`. - ((expected_number_of_session_messages * num_blend_layers.get() as f64) / membership_size as f64) + ((expected_number_of_epoch_messages * num_blend_layers.get() as f64) / membership_size as f64) .ceil() as u64 } diff --git a/core/src/mantle/encoding.rs b/core/src/mantle/encoding.rs index fe02d5397..a206be985 100644 --- a/core/src/mantle/encoding.rs +++ b/core/src/mantle/encoding.rs @@ -42,7 +42,7 @@ use crate::{ // Maximum memory allocation size allowed for SDP activity metadata. // Protects against unbounded allocation in `decode_sdp_active` -const MAX_ENCODE_DECODE_METADATA_SIZE: u32 = 234; // `ActiveMessage` has a fixed size of 234 bytes +const MAX_ENCODE_DECODE_METADATA_SIZE: u32 = 230; // `ActiveMessage` has a fixed size of 230 bytes // Maximum byte size allowed for a locator in SDPDeclare operations. const LOCATOR_BYTES_SIZE_LIMIT: usize = 329usize; @@ -1252,7 +1252,7 @@ mod tests { let signing_key = Ed25519Key::from_bytes(&[1u8; 32]); let blend_proof = ActivityProof { - session: 42, + epoch: 42.into(), signing_key: signing_key.public_key(), proof_of_quota: VerifiedProofOfQuota::from_bytes_unchecked([0u8; 160]).into(), proof_of_selection: VerifiedProofOfSelection::from_bytes_unchecked([0u8; 32]).into(), @@ -1310,7 +1310,7 @@ mod tests { }; let blend_proof = ActivityProof { - session: u64::MAX, + epoch: u32::MAX.into(), signing_key: signing_key.public_key(), proof_of_quota: VerifiedProofOfQuota::from_bytes_unchecked([0u8; 160]).into(), proof_of_selection: VerifiedProofOfSelection::from_bytes_unchecked([0u8; 32]).into(), @@ -1842,7 +1842,7 @@ mod tests { #[test] fn test_encode_reject_excessive_sdp_active() { let blend_proof = ActivityProof { - session: u64::MAX, + epoch: u32::MAX.into(), signing_key: Ed25519Key::from_bytes(&[1; 32]).public_key(), proof_of_quota: VerifiedProofOfQuota::from_bytes_unchecked([0u8; 160]).into(), proof_of_selection: VerifiedProofOfSelection::from_bytes_unchecked([0u8; 32]).into(), diff --git a/core/src/mantle/ops/sdp/active.rs b/core/src/mantle/ops/sdp/active.rs index a9931ceb6..b93ade646 100644 --- a/core/src/mantle/ops/sdp/active.rs +++ b/core/src/mantle/ops/sdp/active.rs @@ -1,10 +1,10 @@ +use lb_cryptarchia_engine::Epoch; use lb_key_management_system_keys::keys::{ZkPublicKey, ZkSignature}; use lb_log_targets::mantle; use tracing::debug; use super::{SDPActiveOp, SdpError}; use crate::{ - block::BlockNumber, events::Events, mantle::{ TxHash, @@ -21,7 +21,7 @@ pub struct SDPActiveValidationContext<'a> { } pub struct SDPActiveExecutionContext { - pub block_number: BlockNumber, + pub epoch: Epoch, pub declarations: Declarations, } @@ -64,13 +64,13 @@ impl Operation> for SDPActiveOp { .get_mut(&self.declaration_id) .expect("The operation should have been validated"); - declaration.active = ctx.block_number; + declaration.active = ctx.epoch; declaration.nonce = self.nonce; debug!( target: LOG_TARGET, provider_id = ?declaration.provider_id, - active = declaration.active, - nonce = declaration.nonce, + active = ?declaration.active, + nonce = ?declaration.nonce, "updated declaration with active message" ); diff --git a/core/src/mantle/ops/sdp/declare.rs b/core/src/mantle/ops/sdp/declare.rs index d06790d88..0a8083a5c 100644 --- a/core/src/mantle/ops/sdp/declare.rs +++ b/core/src/mantle/ops/sdp/declare.rs @@ -1,3 +1,4 @@ +use lb_cryptarchia_engine::Epoch; use lb_key_management_system_keys::keys::{Ed25519Signature, ZkPublicKey, ZkSignature}; use super::{MAX_DECLARATION_LOCATOR, SDPDeclareOp, SdpError}; @@ -67,7 +68,7 @@ impl SDPDeclareValidationExt for SDPDeclareOp { mut ctx: SDPDeclareExecutionContext, ) -> Result<(SDPDeclareExecutionContext, Events), SdpError> { let declaration_id = self.id(); - let declaration = Declaration::new(ctx.block_number, self); + let declaration = Declaration::new(ctx.epoch, self); ctx.declarations = ctx.declarations.insert(declaration_id, declaration); let utxo = ctx .utxo_tree @@ -109,7 +110,7 @@ pub struct SDPDeclareGenesisValidationContext<'a> { pub struct SDPDeclareExecutionContext { pub utxo_tree: Utxos, - pub block_number: u64, + pub epoch: Epoch, pub declarations: Declarations, pub locked_notes: LockedNotes, pub min_stake: MinStake, diff --git a/core/src/mantle/ops/sdp/withdraw.rs b/core/src/mantle/ops/sdp/withdraw.rs index dc326673a..0fda90424 100644 --- a/core/src/mantle/ops/sdp/withdraw.rs +++ b/core/src/mantle/ops/sdp/withdraw.rs @@ -1,31 +1,30 @@ +use lb_cryptarchia_engine::Epoch; use lb_key_management_system_keys::keys::{ZkPublicKey, ZkSignature}; use lb_log_targets::mantle; use tracing::debug; use super::{SDPWithdrawOp, SdpError}; use crate::{ - block::BlockNumber, events::Events, mantle::{ TxHash, ledger::{Declarations, Operation}, }, - sdp::locked_notes::LockedNotes, + sdp::{NumberOfEpochs, locked_notes::LockedNotes}, }; const LOG_TARGET: &str = mantle::sdp::message::WITHDRAW; pub struct SDPWithdrawValidationContext<'a> { - pub lock_period: &'a u64, + pub lock_period: NumberOfEpochs, pub declarations: &'a Declarations, - pub block_number: &'a BlockNumber, + pub epoch: Epoch, pub locked_notes: &'a LockedNotes, pub tx_hash: &'a TxHash, pub sdp_withdraw_sig: &'a ZkSignature, } pub struct SDPWithdrawExecutionContext { - pub block_number: BlockNumber, pub declarations: Declarations, pub locked_notes: LockedNotes, } @@ -64,7 +63,7 @@ impl Operation> for SDPWithdrawOp { } // Check the note can be unlocked - if declaration.created + ctx.lock_period >= *ctx.block_number { + if declaration.created + ctx.lock_period >= ctx.epoch { return Err(SdpError::WithdrawalWhileLocked); } @@ -109,6 +108,8 @@ impl Operation> for SDPWithdrawOp { "updated declaration with withdraw message" ); + // TODO: Delay the unlock for SNAPSHOT_FINALIZATION_DELAY epochs to prevent + // stake-less service provision: https://www.notion.so/RFC-Remove-Concept-of-a-Session-f39261aa09df826db83e81613bb454dc?source=copy_link#357261aa09df80d79dcbeccd5487b5a5 let _ = ctx .locked_notes .unlock(declaration.service_type, &self.locked_note_id) diff --git a/core/src/sdp/blend.rs b/core/src/sdp/blend.rs index 65e8fcebe..b469e45cc 100644 --- a/core/src/sdp/blend.rs +++ b/core/src/sdp/blend.rs @@ -2,18 +2,19 @@ use lb_blend_proofs::{ quota::{PROOF_OF_QUOTA_SIZE, ProofOfQuota}, selection::{PROOF_OF_SELECTION_SIZE, ProofOfSelection}, }; +use lb_cryptarchia_engine::Epoch; use lb_key_management_system_keys::keys::ED25519_PUBLIC_KEY_SIZE; use nom::{IResult, Parser as _, bytes::complete::take, number::complete::u8 as nom_u8}; use serde::{Deserialize, Serialize}; use crate::{ mantle::ops::channel::Ed25519PublicKey, - sdp::{ACTIVE_METADATA_BLEND_TYPE, SessionNumber, parse_session_number}, + sdp::{ACTIVE_METADATA_BLEND_TYPE, parse_epoch}, }; #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] pub struct ActivityProof { - pub session: SessionNumber, + pub epoch: Epoch, pub signing_key: Ed25519PublicKey, pub proof_of_quota: ProofOfQuota, pub proof_of_selection: ProofOfSelection, @@ -29,7 +30,7 @@ impl ActivityProof { let proof_of_selection: [u8; _] = (&self.proof_of_selection).into(); let total_size = 2 // type + version byte - + size_of::() + + size_of::() + signing_key.len() + proof_of_quota.len() + proof_of_selection.len(); @@ -37,7 +38,7 @@ impl ActivityProof { let mut bytes = Vec::with_capacity(total_size); bytes.push(ACTIVE_METADATA_BLEND_TYPE); bytes.push(BLEND_ACTIVE_METADATA_VERSION_BYTE); - bytes.extend(&self.session.to_le_bytes()); + bytes.extend(&self.epoch.into_inner().to_le_bytes()); bytes.extend(&signing_key); bytes.extend(&proof_of_quota); bytes.extend(&proof_of_selection); @@ -68,7 +69,7 @@ fn parse_activity_proof(input: &[u8]) -> IResult<&[u8], ActivityProof> { nom::error::ErrorKind::Verify, ))); } - let (input, session) = parse_session_number(input)?; + let (input, epoch) = parse_epoch(input)?; let (input, signing_key) = parse_const_size_bytes::(input)?; let signing_key = Ed25519PublicKey::from_bytes(&signing_key) @@ -94,7 +95,7 @@ fn parse_activity_proof(input: &[u8]) -> IResult<&[u8], ActivityProof> { Ok(( input, ActivityProof { - session, + epoch, signing_key, proof_of_quota, proof_of_selection, @@ -121,7 +122,7 @@ mod tests { #[test] fn activity_proof_roundtrip() { let proof = ActivityProof { - session: 10, + epoch: 10.into(), signing_key: new_signing_key(0), proof_of_quota: new_proof_of_quota_unchecked(0), proof_of_selection: new_proof_of_selection_unchecked(1), @@ -136,7 +137,7 @@ mod tests { #[test] fn activity_proof_invalid_version() { let proof = ActivityProof { - session: 10, + epoch: 10.into(), signing_key: new_signing_key(0), proof_of_quota: new_proof_of_quota_unchecked(0), proof_of_selection: new_proof_of_selection_unchecked(1), @@ -161,7 +162,7 @@ mod tests { #[test] fn activity_proof_too_long() { let proof = ActivityProof { - session: 10, + epoch: 10.into(), signing_key: new_signing_key(0), proof_of_quota: new_proof_of_quota_unchecked(0), proof_of_selection: new_proof_of_selection_unchecked(1), @@ -177,7 +178,7 @@ mod tests { #[test] fn activity_metadata_roundtrip() { let proof = ActivityProof { - session: 10, + epoch: 10.into(), signing_key: new_signing_key(0), proof_of_quota: new_proof_of_quota_unchecked(0), proof_of_selection: new_proof_of_selection_unchecked(1), diff --git a/core/src/sdp/mod.rs b/core/src/sdp/mod.rs index d6403246d..82a252aa9 100644 --- a/core/src/sdp/mod.rs +++ b/core/src/sdp/mod.rs @@ -1,10 +1,15 @@ pub mod blend; pub mod locked_notes; -use core::str::FromStr; -use std::hash::Hash; +use core::{ + ops::{Add, Sub}, + str::FromStr, +}; +use std::{collections::HashMap, hash::Hash}; use blake2::{Blake2b, Digest as _}; +use bytes::Bytes; +use lb_cryptarchia_engine::Epoch; use lb_key_management_system_keys::keys::ZkPublicKey; use lb_utils::bounded_vec::LowerBoundedVec; use multiaddr::{Multiaddr, Protocol}; @@ -14,11 +19,11 @@ use strum::EnumIter; use crate::{ block::BlockNumber, + codec::{self, DeserializeOp as _, SerializeOp as _}, mantle::{NoteId, ops::channel::Ed25519PublicKey}, utils::{display_hex_bytes_newtype, serde_bytes_newtype}, }; -pub type SessionNumber = u64; pub type StakeThreshold = u64; const ACTIVE_METADATA_BLEND_TYPE: u8 = 0x01; @@ -31,17 +36,60 @@ pub struct MinStake { #[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct ServiceParameters { - pub lock_period: u64, - pub inactivity_period: u64, - pub retention_period: u64, - pub timestamp: BlockNumber, - pub session_duration: BlockNumber, + /// Minimum epochs during which a declaration cannot be withdrawn + pub lock_period: NumberOfEpochs, + /// Maximum epochs during which an activity message must be sent + pub inactivity_period: NumberOfEpochs, + /// Epochs after which a declaration can be safely deleted by Garbage + /// Collection + pub retention_period: NumberOfEpochs, + // Epoch number at which this parameter was set + pub epoch: Epoch, } -impl ServiceParameters { +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct NumberOfEpochs(Epoch); + +impl NumberOfEpochs { #[must_use] - pub const fn session_for_block(&self, block_number: BlockNumber) -> SessionNumber { - block_number / self.session_duration + pub const fn new(epoch: Epoch) -> Self { + Self(epoch) + } +} + +impl From for NumberOfEpochs { + fn from(value: u32) -> Self { + Self(value.into()) + } +} + +impl From for u32 { + fn from(this: NumberOfEpochs) -> Self { + this.0.into_inner() + } +} + +impl Add for NumberOfEpochs { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + Self(self.0 + rhs.0) + } +} + +impl Add for Epoch { + type Output = Self; + + fn add(self, rhs: NumberOfEpochs) -> Self::Output { + self + rhs.0 + } +} + +impl Sub for Epoch { + type Output = Self; + + fn sub(self, rhs: NumberOfEpochs) -> Self::Output { + self - rhs.0 } } @@ -182,9 +230,9 @@ pub struct Declaration { pub locked_note_id: NoteId, pub locators: Locators, pub zk_id: ZkPublicKey, - pub created: BlockNumber, - pub active: BlockNumber, - pub withdrawn: Option, + pub created: Epoch, + pub active: Epoch, + pub withdrawn: Option, pub nonce: Nonce, } @@ -194,23 +242,66 @@ pub struct ProviderInfo { pub zk_id: ZkPublicKey, } +const SNAPSHOT_FINALIZATION_DELAY: Epoch = Epoch::new(2); + impl Declaration { #[must_use] - pub fn new(block_number: BlockNumber, declaration_msg: &DeclarationMessage) -> Self { + pub fn new(epoch: Epoch, declaration_msg: &DeclarationMessage) -> Self { Self { service_type: declaration_msg.service_type, provider_id: declaration_msg.provider_id, locked_note_id: declaration_msg.locked_note_id, locators: declaration_msg.locators.clone(), zk_id: declaration_msg.zk_id, - created: block_number, - active: block_number, + created: epoch, + active: epoch + SNAPSHOT_FINALIZATION_DELAY, withdrawn: None, nonce: 0, } } } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] +pub struct Declarations(HashMap>); + +impl Declarations { + pub fn iter( + &self, + ) -> impl Iterator)> { + self.0.iter() + } +} + +impl From>> for Declarations { + fn from(value: HashMap>) -> Self { + Self(value) + } +} + +impl FromIterator<(ServiceType, HashMap)> for Declarations { + fn from_iter)>>( + iter: I, + ) -> Self { + Self(iter.into_iter().collect()) + } +} + +impl TryFrom for Declarations { + type Error = codec::Error; + + fn try_from(bytes: Bytes) -> Result { + Self::from_bytes(&bytes) + } +} + +impl TryFrom for Bytes { + type Error = codec::Error; + + fn try_from(this: Declarations) -> Result { + this.to_bytes() + } +} + #[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)] pub struct DeclarationMessage { pub service_type: ServiceType, @@ -289,16 +380,19 @@ impl ActivityMetadata { } } -fn parse_session_number(input: &[u8]) -> IResult<&[u8], SessionNumber> { - let (input, bytes) = take(size_of::()).parse(input)?; - let session_bytes: [u8; 8] = bytes +fn parse_epoch(input: &[u8]) -> IResult<&[u8], Epoch> { + let (input, bytes) = take(size_of::()).parse(input)?; + let epoch_bytes: [u8; 4] = bytes .try_into() .map_err(|_| nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Fail)))?; - Ok((input, SessionNumber::from_le_bytes(session_bytes))) + Ok((input, u32::from_le_bytes(epoch_bytes).into())) } #[cfg(test)] mod tests { + use lb_groth16::{Field as _, Fr}; + use lb_key_management_system_keys::keys::Ed25519Key; + use super::*; #[test] @@ -382,4 +476,28 @@ mod tests { "Input cannot be empty." ); } + + #[test] + fn declaration_initialization() { + let msg = DeclarationMessage { + service_type: ServiceType::BlendNetwork, + locators: vec!["/ip4/127.0.0.1/udp/3001/quic-v1".parse().unwrap()] + .try_into() + .unwrap(), + provider_id: Ed25519Key::from_bytes(&[0; _]).public_key().into(), + zk_id: ZkPublicKey::zero(), + locked_note_id: Fr::ZERO.into(), + }; + + let declaration = Declaration::new(Epoch::new(10), &msg); + assert_eq!(declaration.service_type, msg.service_type); + assert_eq!(declaration.provider_id, msg.provider_id); + assert_eq!(declaration.locked_note_id, msg.locked_note_id); + assert_eq!(declaration.locators, msg.locators); + assert_eq!(declaration.zk_id, msg.zk_id); + assert_eq!(declaration.created, Epoch::new(10)); + assert_eq!(declaration.active, Epoch::new(12)); // created + SNAPSHOT_FINALIZATION_DELAY + assert_eq!(declaration.withdrawn, None); + assert_eq!(declaration.nonce, 0); + } } diff --git a/kms/operators/src/blend/poq.rs b/kms/operators/src/blend/poq.rs index 3e164f1ae..93fc86998 100644 --- a/kms/operators/src/blend/poq.rs +++ b/kms/operators/src/blend/poq.rs @@ -69,7 +69,7 @@ impl SecureKeyOperator for PoQOperator { .await .map_err(Self::Error::FailedOperatorCall)?; drop(self.response_channel.send(poq_result).inspect_err(|_| { - trace!(target: LOG_TARGET, "Error sending generated proof of quota, most likely due to a session or epoch rotation that discarded the receiver side of the channel."); + trace!(target: LOG_TARGET, "Error sending generated proof of quota, most likely due to an epoch rotation that discarded the receiver side of the channel."); })); Ok(()) } diff --git a/ledger/src/config.rs b/ledger/src/config.rs index cc336d6d9..4415279b3 100644 --- a/ledger/src/config.rs +++ b/ledger/src/config.rs @@ -84,6 +84,12 @@ impl Config { self.epoch_config .epoch(slot, self.consensus_config.base_period_length()) } + + #[must_use] + pub fn last_slot(&self, epoch: Epoch) -> Slot { + self.epoch_config + .last_slot(epoch, self.consensus_config.base_period_length()) + } } #[cfg(test)] @@ -101,34 +107,37 @@ mod tests { #[test] fn epoch_snapshots() { + let epoch_config = EpochConfig { + epoch_stake_distribution_stabilization: NonZero::new(3u8).unwrap(), + epoch_period_nonce_buffer: NonZero::new(3).unwrap(), + epoch_period_nonce_stabilization: NonZero::new(4).unwrap(), + }; + let consensus_config = lb_cryptarchia_engine::Config::new( + NonZero::new(5).unwrap(), + NonNegativeRatio::new(1, 2.try_into().unwrap()), + 1f64.try_into().expect("1 > 0"), + ); + let epoch_length = epoch_config.epoch_length(consensus_config.base_period_length()); + let config = super::Config { - epoch_config: EpochConfig { - epoch_stake_distribution_stabilization: NonZero::new(3u8).unwrap(), - epoch_period_nonce_buffer: NonZero::new(3).unwrap(), - epoch_period_nonce_stabilization: NonZero::new(4).unwrap(), - }, - consensus_config: lb_cryptarchia_engine::Config::new( - NonZero::new(5).unwrap(), - NonNegativeRatio::new(1, 2.try_into().unwrap()), - 1f64.try_into().expect("1 > 0"), - ), + epoch_config, + consensus_config, sdp_config: crate::mantle::sdp::Config { service_params: Arc::new( [( ServiceType::BlendNetwork, ServiceParameters { - lock_period: 10, - inactivity_period: 1, - retention_period: 1, - timestamp: 0, - session_duration: 10, + lock_period: 10.into(), + inactivity_period: 1.into(), + retention_period: 1.into(), + epoch: 0.into(), }, )] .into(), ), service_rewards_params: ServiceRewardsParameters { blend: RewardsParameters { - rounds_per_session: NonZeroU64::new(10).unwrap(), + rounds_per_epoch: epoch_length.try_into().unwrap(), message_frequency_per_round: NonNegativeF64::try_from(1.0).unwrap(), num_blend_layers: NonZeroU64::new(3).unwrap(), minimum_network_size: NonZeroU64::new(1).unwrap(), @@ -154,34 +163,37 @@ mod tests { #[test] fn slot_to_epoch() { + let epoch_config = EpochConfig { + epoch_stake_distribution_stabilization: NonZero::new(3u8).unwrap(), + epoch_period_nonce_buffer: NonZero::new(3).unwrap(), + epoch_period_nonce_stabilization: NonZero::new(4).unwrap(), + }; + let consensus_config = lb_cryptarchia_engine::Config::new( + NonZero::new(5).unwrap(), + NonNegativeRatio::new(1, 2.try_into().unwrap()), + 1f64.try_into().expect("1 > 0"), + ); + let epoch_length = epoch_config.epoch_length(consensus_config.base_period_length()); + let config = super::Config { - epoch_config: EpochConfig { - epoch_stake_distribution_stabilization: NonZero::new(3u8).unwrap(), - epoch_period_nonce_buffer: NonZero::new(3).unwrap(), - epoch_period_nonce_stabilization: NonZero::new(4).unwrap(), - }, - consensus_config: lb_cryptarchia_engine::Config::new( - NonZero::new(5).unwrap(), - NonNegativeRatio::new(1, 2.try_into().unwrap()), - 1f64.try_into().expect("1 > 0"), - ), + epoch_config, + consensus_config, sdp_config: crate::mantle::sdp::Config { service_params: Arc::new( [( ServiceType::BlendNetwork, ServiceParameters { - lock_period: 10, - inactivity_period: 1, - retention_period: 1, - timestamp: 0, - session_duration: 10, + lock_period: 10.into(), + inactivity_period: 1.into(), + retention_period: 1.into(), + epoch: 0.into(), }, )] .into(), ), service_rewards_params: ServiceRewardsParameters { blend: RewardsParameters { - rounds_per_session: NonZeroU64::new(10).unwrap(), + rounds_per_epoch: epoch_length.try_into().unwrap(), message_frequency_per_round: NonNegativeF64::try_from(1.0).unwrap(), num_blend_layers: NonZeroU64::new(3).unwrap(), minimum_network_size: NonZeroU64::new(1).unwrap(), @@ -196,9 +208,9 @@ mod tests { }, faucet_pk: None, }; - assert_eq!(config.epoch(1.into()), 0.into()); - assert_eq!(config.epoch(100.into()), 1.into()); - assert_eq!(config.epoch(101.into()), 1.into()); - assert_eq!(config.epoch(200.into()), 2.into()); + assert_eq!(config.epoch(1.into()), 0); + assert_eq!(config.epoch(100.into()), 1); + assert_eq!(config.epoch(101.into()), 1); + assert_eq!(config.epoch(200.into()), 2); } } diff --git a/ledger/src/cryptarchia/block_density.rs b/ledger/src/cryptarchia/block_density.rs index 51f78fb58..d6a05b97e 100644 --- a/ledger/src/cryptarchia/block_density.rs +++ b/ledger/src/cryptarchia/block_density.rs @@ -98,7 +98,7 @@ mod tests { service_params: Arc::new(HashMap::new()), service_rewards_params: ServiceRewardsParameters { blend: RewardsParameters { - rounds_per_session: 10.try_into().unwrap(), + rounds_per_epoch: 10.try_into().unwrap(), message_frequency_per_round: 1.0.try_into().unwrap(), num_blend_layers: 3.try_into().unwrap(), minimum_network_size: 1.try_into().unwrap(), diff --git a/ledger/src/cryptarchia/mod.rs b/ledger/src/cryptarchia/mod.rs index 82480e1bc..967865c96 100644 --- a/ledger/src/cryptarchia/mod.rs +++ b/ledger/src/cryptarchia/mod.rs @@ -22,9 +22,12 @@ use lb_groth16::{Fr, fr_from_bytes}; use lb_key_management_system_keys::keys::ZkSignature; use lb_utxotree::MerklePath; -use crate::cryptarchia::{ - block_density::BlockDensity, - stake::{PRECISION, StakeInference}, +use crate::{ + cryptarchia::{ + block_density::BlockDensity, + stake::{PRECISION, StakeInference}, + }, + mantle::sdp::SdpLedger, }; // corresponds to the denominator of q @@ -51,7 +54,7 @@ pub type UtxoTree = lb_utxotree::UtxoTree; use super::{Balance, Config, LedgerError, mantle}; use crate::WINDOW_SIZE; -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub struct EpochState { /// The epoch this snapshot is for pub epoch: Epoch, @@ -69,10 +72,23 @@ pub struct EpochState { pub lottery_0: Fr, #[serde(with = "lb_groth16::serde::serde_fr")] pub lottery_1: Fr, + /// SDP service-provider declarations (membership) snapshot, frozen at the + /// same slot as the stake distribution (`stake_distribution_snapshot`). + /// Carried forward in ledger state so the epoch's membership is derivable + /// from the tip without walking storage. + /// + /// TODO: This field reaches "up" into the mantle layer (`SdpLedger`), which + /// is why the `&SdpLedger` is threaded through `update_from_ledger`, + /// `update_epoch_state`, `try_apply_header`, and `epoch_state_for_slot`. + /// That threading is scaffolding: long-term, `EpochState` should be lifted + /// out of `cryptarchia` to sit alongside `cryptarchia_ledger` and + /// `mantle_ledger` in the outer `LedgerState`, where the freeze can read + /// both sub-ledgers directly and these `&SdpLedger` parameters disappear. + pub sdp: SdpLedger, } impl EpochState { - fn update_from_ledger(self, ledger: &LedgerState, config: &Config) -> Self { + fn update_from_ledger(self, ledger: &LedgerState, sdp: &SdpLedger, config: &Config) -> Self { let nonce_snapshot_slot = config.nonce_snapshot(self.epoch); let nonce = if ledger.slot < nonce_snapshot_slot { ledger.nonce @@ -80,11 +96,14 @@ impl EpochState { self.nonce }; + // The SDP membership snapshot is frozen at the same slot as the stake + // distribution, so the two halves of the epoch's public info stay + // consistent. let stake_snapshot_slot = config.stake_distribution_snapshot(self.epoch); - let utxos = if ledger.slot < stake_snapshot_slot { - ledger.utxos.clone() + let (utxos, sdp) = if ledger.slot < stake_snapshot_slot { + (ledger.utxos.clone(), sdp.clone()) } else { - self.utxos + (self.utxos, self.sdp) }; Self { epoch: self.epoch, @@ -93,6 +112,7 @@ impl EpochState { total_stake: self.total_stake, lottery_0: self.lottery_0, lottery_1: self.lottery_1, + sdp, } } @@ -136,7 +156,7 @@ impl EpochState { /// NOTE: Most collection fields in this struct should use `rpds` /// since we keep a copy of this state for each block. #[derive(Derivative, serde::Serialize, serde::Deserialize)] -#[derivative(Clone, Eq, PartialEq)] +#[derivative(Clone, PartialEq)] pub struct LedgerState { // All available Unspent Transtaction Outputs (UTXOs) at the current slot // TODO: move UTXOs in the mantle ledger. There is no reason to keep them here @@ -178,7 +198,12 @@ impl LedgerState { clippy::too_many_lines, reason = "TODO: fix/refactor updating next_epoch_state" )] - fn update_epoch_state(self, slot: Slot, config: &Config) -> Result> { + fn update_epoch_state( + self, + slot: Slot, + sdp: &SdpLedger, + config: &Config, + ) -> Result> { if slot <= self.slot { return Err(LedgerError::InvalidSlot { parent: self.slot, @@ -197,7 +222,7 @@ impl LedgerState { let next_epoch_state = self .next_epoch_state .clone() - .update_from_ledger(&self, config); + .update_from_ledger(&self, sdp, config); // There are 3 cases to consider: // 1. We are in the same epoch as the parent state: Update the next epoch state @@ -253,6 +278,7 @@ impl LedgerState { total_stake, lottery_0, lottery_1, + sdp: sdp.clone(), }; let (new_price, new_ema) = update_storage_market( self.storage_gas_price, @@ -316,6 +342,7 @@ impl LedgerState { total_stake, lottery_0, lottery_1, + sdp: sdp.clone(), }; let next_epoch_state = EpochState { epoch: new_epoch + 1, @@ -324,6 +351,7 @@ impl LedgerState { total_stake, lottery_0, lottery_1, + sdp: sdp.clone(), }; Ok(Self { slot, @@ -390,6 +418,7 @@ impl LedgerState { self, slot: Slot, proof: &LeaderProof, + sdp: &SdpLedger, config: &Config, ) -> Result> where @@ -399,7 +428,7 @@ impl LedgerState { // Then, apply the proof and update the nonce. Finally, increment block density // since this function is called for a new block. Ok(self - .update_epoch_state(slot, config)? + .update_epoch_state(slot, sdp, config)? .try_apply_proof(slot, proof, config)? .update_nonce(&proof.entropy(), slot) .increment_block_density(slot)) @@ -491,6 +520,20 @@ impl LedgerState { &self.next_epoch_state } + /// Seeds the genesis epoch-state snapshots with the genesis SDP ledger. + /// + /// At genesis the cryptarchia ledger is built before the mantle `SdpLedger` + /// exists (the mantle ledger is derived from the cryptarchia epoch state), + /// so the initial epoch states start with an empty SDP snapshot. Once the + /// genesis `SdpLedger` is available, this seeds it into the epoch 0/1 + /// snapshots so they carry the genesis membership. Genesis use only. + #[must_use] + pub fn with_genesis_sdp(mut self, sdp: SdpLedger) -> Self { + self.next_epoch_state.sdp = sdp.clone(); + self.epoch_state.sdp = sdp; + self + } + #[must_use] pub const fn latest_utxos(&self) -> &UtxoTree { &self.utxos @@ -526,11 +569,12 @@ impl LedgerState { pub fn epoch_state_for_slot( &self, slot: Slot, + sdp: &SdpLedger, config: &Config, ) -> Result> { Ok(self .clone() - .update_epoch_state(slot, config)? + .update_epoch_state(slot, sdp, config)? .epoch_state() .clone()) } @@ -591,6 +635,7 @@ impl LedgerState { total_stake, lottery_0, lottery_1, + sdp: SdpLedger::new(1.into()), }, epoch_state: EpochState { epoch: 0.into(), @@ -599,6 +644,7 @@ impl LedgerState { total_stake, lottery_0, lottery_1, + sdp: SdpLedger::new(0.into()), }, block_density, stake_inference, @@ -770,7 +816,7 @@ pub mod tests { .unwrap() .clone() .cryptarchia_ledger - .update_epoch_state::(slot, ledger.config()) + .update_epoch_state::(slot, &SdpLedger::new(0.into()), ledger.config()) .unwrap(); let id = make_id(parent, slot, utxo); let proof = generate_proof(&ledger_state, &utxo, slot); @@ -829,30 +875,33 @@ pub mod tests { service_params.insert( lb_core::sdp::ServiceType::BlendNetwork, ServiceParameters { - lock_period: 10, - inactivity_period: 1, - retention_period: 1, - timestamp: 0, - session_duration: 10, + lock_period: 10.into(), + inactivity_period: 1.into(), + retention_period: 1.into(), + epoch: 0.into(), }, ); + let epoch_config = EpochConfig { + epoch_stake_distribution_stabilization: NonZero::new(3).unwrap(), + epoch_period_nonce_buffer: NonZero::new(3).unwrap(), + epoch_period_nonce_stabilization: NonZero::new(4).unwrap(), + }; + let consensus_config = lb_cryptarchia_engine::Config::new( + NonZero::new(1).unwrap(), + NonNegativeRatio::new(1, 10.try_into().unwrap()), + 1f64.try_into().expect("1 > 0"), + ); + let epoch_length = epoch_config.epoch_length(consensus_config.base_period_length()); + Config { - epoch_config: EpochConfig { - epoch_stake_distribution_stabilization: NonZero::new(3).unwrap(), - epoch_period_nonce_buffer: NonZero::new(3).unwrap(), - epoch_period_nonce_stabilization: NonZero::new(4).unwrap(), - }, - consensus_config: lb_cryptarchia_engine::Config::new( - NonZero::new(1).unwrap(), - NonNegativeRatio::new(1, 10.try_into().unwrap()), - 1f64.try_into().expect("1 > 0"), - ), + epoch_config, + consensus_config, sdp_config: mantle::sdp::Config { service_params: Arc::new(service_params), service_rewards_params: ServiceRewardsParameters { blend: rewards::blend::RewardsParameters { - rounds_per_session: NonZeroU64::new(10).unwrap(), + rounds_per_epoch: epoch_length.try_into().unwrap(), message_frequency_per_round: NonNegativeF64::try_from(1.0).unwrap(), num_blend_layers: NonZeroU64::new(3).unwrap(), minimum_network_size: NonZeroU64::new(1).unwrap(), @@ -898,6 +947,7 @@ pub mod tests { total_stake, lottery_0, lottery_1, + sdp: SdpLedger::new(1.into()), }, epoch_state: EpochState { epoch: 0.into(), @@ -906,6 +956,7 @@ pub mod tests { total_stake, lottery_0, lottery_1, + sdp: SdpLedger::new(0.into()), }, stake_inference, fee_window: [0.into(); 120], @@ -946,11 +997,13 @@ pub mod tests { // we still don't have transactions, so the only way to add a commitment to // spendable utxos and test epoch snapshotting is by doing this // manually - let mut block_state = ledger.states[&id].clone().cryptarchia_ledger; - block_state.utxos = block_state.utxos.insert(utxo_add.id(), utxo_add).0; - ledger - .states - .insert(id, full_ledger_state(block_state, &ledger.config)); + let block_ledger = ledger.states.get_mut(&id).unwrap(); + let new_utxos = block_ledger + .cryptarchia_ledger + .utxos + .insert(utxo_add.id(), utxo_add) + .0; + block_ledger.cryptarchia_ledger.utxos = new_utxos; id } @@ -994,10 +1047,7 @@ pub mod tests { ); let h_1 = update_ledger(&mut ledger, genesis, 10, utxos[0]).unwrap(); - assert_eq!( - ledger.states[&h_1].cryptarchia_ledger.epoch_state.epoch, - 0.into() - ); + assert_eq!(ledger.states[&h_1].cryptarchia_ledger.epoch_state.epoch, 0); let h_2 = update_ledger(&mut ledger, h_1, 60, utxos[1]).unwrap(); @@ -1006,8 +1056,8 @@ pub mod tests { // Epoch jump: epoch 0 -> 2 // Jump to the slot that is not the 1st slot of epoch 2 let h_4 = update_ledger(&mut ledger, h_3, 222, utxos[3]).unwrap(); - // nonce for epoch 2 should be taken at the end of slot 160, but in our case the - // last block is at slot 90 because of epoch jumps + // nonce for epoch 2 should be taken at the end of slot 160, but in our case + // the last block is at slot 90 because of epoch jumps assert_eq!( ledger.states[&h_4].cryptarchia_ledger.epoch_state.nonce, ledger.states[&h_3].cryptarchia_ledger.nonce, @@ -1102,7 +1152,12 @@ pub mod tests { // EPOCH 2 // the utxo is finally eligible 2 epochs after it was first minted - update_ledger(&mut ledger, h_0_1, 2 * epoch_length, utxo_1).unwrap(); + // + // First, advance to epoch 1 using the `utxo` in genesis, + // because SDP ledger doesn't support epoch jumps yet. + let h_1_1 = update_ledger(&mut ledger, h_0_1, epoch_length, utxo).unwrap(); + // Then, try to advance to epoch 2 using `utxo_1` + update_ledger(&mut ledger, h_1_1, 2 * epoch_length, utxo_1).unwrap(); } /// Verifies that the TSI chain is computed correctly across epoch @@ -1148,10 +1203,7 @@ pub mod tests { // 159] let h5 = update_ledger(&mut ledger, h4, 100, utxo).unwrap(); let ts1 = inference.total_stake_inference::(ts_genesis, 3); - assert_eq!( - ledger.states[&h5].cryptarchia_ledger.epoch_state.epoch, - 1.into() - ); + assert_eq!(ledger.states[&h5].cryptarchia_ledger.epoch_state.epoch, 1); assert_eq!( ledger.states[&h5] .cryptarchia_ledger @@ -1174,10 +1226,7 @@ pub mod tests { // Epoch 1 -> 2 transition -------------------- let h7 = update_ledger(&mut ledger, h6, 200, utxo).unwrap(); let ts2 = inference.total_stake_inference::(ts1, 2); - assert_eq!( - ledger.states[&h7].cryptarchia_ledger.epoch_state.epoch, - 2.into() - ); + assert_eq!(ledger.states[&h7].cryptarchia_ledger.epoch_state.epoch, 2); assert_eq!( ledger.states[&h7] .cryptarchia_ledger @@ -1198,12 +1247,12 @@ pub mod tests { let slot = Slot::genesis() + 10; let ledger_state2 = ledger_state .cryptarchia_ledger - .update_epoch_state::(slot, ledger_config) + .update_epoch_state::(slot, &SdpLedger::new(0.into()), ledger_config) .expect("Ledger needs to move forward"); let slot2 = Slot::genesis() + 1; let update_epoch_err = ledger_state2 - .update_epoch_state::(slot2, ledger_config) + .update_epoch_state::(slot2, &SdpLedger::new(0.into()), ledger_config) .err(); // Time cannot flow backwards @@ -1562,25 +1611,25 @@ pub mod tests { // Genesis state is at epoch 0, with epoch_state for epoch 0 and // next_epoch_state for epoch 1 - assert_eq!(ledger_state.epoch_state.epoch, 0.into()); - assert_eq!(ledger_state.next_epoch_state.epoch, 1.into()); + assert_eq!(ledger_state.epoch_state.epoch, 0); + assert_eq!(ledger_state.next_epoch_state.epoch, 1); let initial_total_stake = ledger_state.epoch_state.total_stake; // Query for epoch 0 (current epoch) - should return epoch_state let epoch_0_slot: Slot = (epoch_length - 1).into(); let epoch_0_state = ledger_state - .epoch_state_for_slot::(epoch_0_slot, &config) + .epoch_state_for_slot::(epoch_0_slot, &SdpLedger::new(0.into()), &config) .expect("Should return epoch state for current epoch"); - assert_eq!(epoch_0_state.epoch, 0.into()); + assert_eq!(epoch_0_state.epoch, 0); assert_eq!(epoch_0_state.total_stake, initial_total_stake); // Query for epoch 1 // Since epoch 0 has no block, total stake should be reduced let epoch_1_slot: Slot = (epoch_length + 1).into(); let epoch_1_state = ledger_state - .epoch_state_for_slot::(epoch_1_slot, &config) + .epoch_state_for_slot::(epoch_1_slot, &SdpLedger::new(0.into()), &config) .expect("Should return epoch state for next epoch"); - assert_eq!(epoch_1_state.epoch, 1.into()); + assert_eq!(epoch_1_state.epoch, 1); // With 0 density and LEARNING_RATE=1, total stake drops to minimum (1) assert_eq!( epoch_1_state.total_stake, 1, @@ -1590,9 +1639,9 @@ pub mod tests { // Query for epoch 3 (multiple skipped epochs) - stake stays at minimum let epoch_2_slot: Slot = (2 * epoch_length + 1).into(); let epoch_2_state = ledger_state - .epoch_state_for_slot::(epoch_2_slot, &config) + .epoch_state_for_slot::(epoch_2_slot, &SdpLedger::new(0.into()), &config) .expect("Should synthesize epoch state for skipped epoch"); - assert_eq!(epoch_2_state.epoch, 2.into()); + assert_eq!(epoch_2_state.epoch, 2); assert_eq!( epoch_2_state.total_stake, 1, "Total stake should remain at minimum" @@ -1613,20 +1662,25 @@ pub mod tests { // First, apply a header from epoch 0 to increase block density let slot = Slot::from(1); - assert_eq!(config.epoch(slot), 0.into()); + assert_eq!(config.epoch(slot), 0); let proof = generate_proof(&genesis_state, &utxo, slot); let ledger_state_1 = genesis_state - .try_apply_header::(slot, &proof, &config) + .try_apply_header::( + slot, + &proof, + &SdpLedger::new(0.into()), + &config, + ) .unwrap(); // Now, apply a header from the 2nd slot of epoch 2 let slot = Slot::from(config.epoch_length() * 2 + 1); - assert_eq!(config.epoch(slot), 2.into()); + assert_eq!(config.epoch(slot), 2); // First, synthesize epoch state for epoch 2 let synthesized_ledger_state = ledger_state_1 .clone() - .update_epoch_state::(slot, &config) + .update_epoch_state::(slot, &SdpLedger::new(0.into()), &config) .unwrap(); assert_eq!(synthesized_ledger_state.slot, slot); @@ -1638,11 +1692,16 @@ pub mod tests { // correctly to epoch 2 as the same as `synthesized_ledger_state`. let ledger_state_2 = ledger_state_1 .clone() - .try_apply_header::(slot, &proof, &config) + .try_apply_header::( + slot, + &proof, + &SdpLedger::new(0.into()), + &config, + ) .unwrap(); assert_eq!(ledger_state_2.slot, slot); assert_ne!(ledger_state_2.nonce, ledger_state_1.nonce); // advanced - assert_eq!(ledger_state_2.epoch_state.epoch, 2.into()); + assert_eq!(ledger_state_2.epoch_state.epoch, 2); } fn stake_inference_from_config(config: &Config) -> StakeInference { diff --git a/ledger/src/cryptarchia/stake.rs b/ledger/src/cryptarchia/stake.rs index 5ca099034..608b70257 100644 --- a/ledger/src/cryptarchia/stake.rs +++ b/ledger/src/cryptarchia/stake.rs @@ -185,7 +185,7 @@ mod tests { service_params: Arc::new(HashMap::new()), service_rewards_params: ServiceRewardsParameters { blend: blend::RewardsParameters { - rounds_per_session: 10.try_into().unwrap(), + rounds_per_epoch: 10.try_into().unwrap(), message_frequency_per_round: 1.0.try_into().unwrap(), num_blend_layers: 1.try_into().unwrap(), data_replication_factor: 0, diff --git a/ledger/src/lib.rs b/ledger/src/lib.rs index 6fbe49a6e..4ef9bf0e5 100644 --- a/ledger/src/lib.rs +++ b/ledger/src/lib.rs @@ -28,7 +28,6 @@ use lb_core::{ tx::{GasPrices, MantleTxContext, MantleTxGasContext}, }, proofs::leader_proof, - sdp::{Declaration, DeclarationId, ProviderId, ProviderInfo, ServiceType, SessionNumber}, }; use lb_cryptarchia_engine::Slot; use lb_groth16::{Field as _, Fr}; @@ -242,10 +241,20 @@ impl LedgerState { where LeaderProof: leader_proof::LeaderProof, { + let last_epoch_state = self.cryptarchia_ledger.epoch_state().clone(); let mut cryptarchia_ledger = self .cryptarchia_ledger - .try_apply_header::(slot, proof, config)?; + .try_apply_header::( + slot, + proof, + // TODO: threading SDP here because EpochState is currently embedded in + // CryptarchiaLedger. + // In the future, we will pull EpochState up into LedgerState. + &self.mantle_ledger.sdp, + config, + )?; let (mantle_ledger, reward_utxos) = self.mantle_ledger.try_apply_header( + &last_epoch_state, cryptarchia_ledger.epoch_state(), *proof.voucher_cm(), config, @@ -418,6 +427,9 @@ impl LedgerState { pub fn from_utxos(utxos: impl IntoIterator, config: &Config) -> Self { let cryptarchia_ledger = CryptarchiaLedger::from_utxos(utxos, config, Fr::ZERO); let mantle_ledger = MantleLedger::new(config, cryptarchia_ledger.epoch_state()); + // Seed the genesis epoch-state membership snapshots from the genesis SDP + // ledger, which only exists after the mantle ledger is built. + let cryptarchia_ledger = cryptarchia_ledger.with_genesis_sdp(mantle_ledger.sdp.clone()); Self { block_number: 0, cryptarchia_ledger, @@ -437,6 +449,9 @@ impl LedgerState { cryptarchia_ledger.latest_utxos(), cryptarchia_ledger.epoch_state(), )?; + // Seed the genesis epoch-state membership snapshots from the genesis SDP + // ledger (which carries the genesis declarations applied above). + let cryptarchia_ledger = cryptarchia_ledger.with_genesis_sdp(mantle_ledger.sdp.clone()); Ok(( Self { block_number: 0, @@ -474,7 +489,8 @@ impl LedgerState { slot: Slot, config: &Config, ) -> Result> { - self.cryptarchia_ledger.epoch_state_for_slot(slot, config) + self.cryptarchia_ledger + .epoch_state_for_slot(slot, &self.mantle_ledger.sdp, config) } #[must_use] @@ -492,24 +508,6 @@ impl LedgerState { &self.mantle_ledger } - #[must_use] - pub fn sdp_declarations(&self) -> Vec<(DeclarationId, Declaration)> { - self.mantle_ledger.sdp_declarations() - } - - #[must_use] - pub fn active_session_providers( - &self, - service_type: ServiceType, - ) -> Option> { - self.mantle_ledger.active_session_providers(service_type) - } - - #[must_use] - pub fn active_sessions(&self) -> HashMap { - self.mantle_ledger.active_sessions() - } - #[must_use] pub fn tx_context(&self) -> MantleTxContext { MantleTxContext { @@ -785,6 +783,18 @@ mod tests { (ledger, [0; 32], utxo) } + /// The genesis epoch-state membership snapshots must be seeded from the + /// genesis SDP ledger, not left as the empty `SdpLedger::new` placeholder + /// the cryptarchia genesis constructor initializes them with. + #[test] + fn genesis_seeds_epoch_state_sdp_from_mantle() { + let config = config(); + let ledger = LedgerState::from_utxos([utxo()], &config); + + assert_eq!(ledger.epoch_state().sdp, ledger.mantle_ledger.sdp); + assert_eq!(ledger.next_epoch_state().sdp, ledger.mantle_ledger.sdp); + } + fn create_test_keys_with_seed(seed: u8) -> (Ed25519Key, Ed25519PublicKey) { let signing_key = Ed25519Key::from_bytes(&[seed; 32]); let verifying_key = signing_key.public_key(); diff --git a/ledger/src/mantle/leader.rs b/ledger/src/mantle/leader.rs index 116293229..61f6e605a 100644 --- a/ledger/src/mantle/leader.rs +++ b/ledger/src/mantle/leader.rs @@ -269,20 +269,20 @@ mod tests { fn test_epoch_transition() { let state = LeaderState::new(); let state = state.try_apply_header(1.into(), Fr::ZERO.into()).unwrap(); - assert_eq!(state.epoch, 1.into()); + assert_eq!(state.epoch, 1); assert_eq!(state.vouchers_snapshot.count, 0); let state = state.try_apply_header(2.into(), Fr::ONE.into()).unwrap(); - assert_eq!(state.epoch, 2.into()); + assert_eq!(state.epoch, 2); assert_eq!(state.vouchers_snapshot.count, 1); let state = state .try_apply_header(2.into(), Fr::from(2u64).into()) .unwrap(); - assert_eq!(state.epoch, 2.into()); + assert_eq!(state.epoch, 2); assert_eq!(state.vouchers_snapshot.count, 1); let state = state .try_apply_header(3.into(), Fr::from(3u64).into()) .unwrap(); - assert_eq!(state.epoch, 3.into()); + assert_eq!(state.epoch, 3); assert_eq!(state.vouchers_snapshot.count, 3); let err = state .clone() @@ -298,7 +298,7 @@ mod tests { let state = state .try_apply_header(4.into(), Fr::from(5u64).into()) .unwrap(); - assert_eq!(state.epoch, 4.into()); + assert_eq!(state.epoch, 4); assert_eq!(state.vouchers_snapshot.count, 4); } diff --git a/ledger/src/mantle/mod.rs b/ledger/src/mantle/mod.rs index 2bce20119..3ff77085a 100644 --- a/ledger/src/mantle/mod.rs +++ b/ledger/src/mantle/mod.rs @@ -3,8 +3,6 @@ pub mod helpers; pub mod leader; pub mod sdp; -use std::collections::HashMap; - use lb_core::{ crypto::ZkHasher, events::Events, @@ -26,10 +24,7 @@ use lb_core::{ }, }, proofs::channel_multi_sig_proof::ChannelMultiSigProof, - sdp::{ - Declaration, DeclarationId, ProviderId, ProviderInfo, ServiceType, SessionNumber, - locked_notes::LockedNotes, - }, + sdp::locked_notes::LockedNotes, }; use lb_cryptarchia_engine::Slot; use lb_key_management_system_keys::keys::{Ed25519Signature, ZkSignature}; @@ -73,7 +68,7 @@ impl LedgerState { pub fn new(config: &Config, epoch_state: &EpochState) -> Self { Self { channels: channel::Channels::new(), - sdp: sdp::SdpLedger::new() + sdp: sdp::SdpLedger::new(epoch_state.epoch()) .with_blend_service(&config.sdp_config.service_rewards_params.blend, epoch_state), leaders: leader::LeaderState::new(), } @@ -128,24 +123,6 @@ impl LedgerState { Self { channels, ..self } } - #[must_use] - pub fn active_session_providers( - &self, - service_type: ServiceType, - ) -> Option> { - self.sdp.active_session_providers(service_type) - } - - #[must_use] - pub fn active_sessions(&self) -> HashMap { - self.sdp.active_sessions() - } - - #[must_use] - pub fn sdp_declarations(&self) -> Vec<(DeclarationId, Declaration)> { - self.sdp.declarations() - } - /// Get the root of the voucher commitments snapshot. #[must_use] pub const fn vouchers_snapshot_root(&self) -> RewardsRoot { @@ -165,12 +142,15 @@ impl LedgerState { pub fn try_apply_header( mut self, + last_epoch_state: &EpochState, epoch_state: &EpochState, voucher: VoucherCm, config: &Config, ) -> Result<(Self, Vec), Error> { self.leaders = self.leaders.try_apply_header(epoch_state.epoch, voucher)?; - let (new_sdp, reward_utxos) = self.sdp.try_apply_header(&config.sdp_config, epoch_state)?; + let (new_sdp, reward_utxos) = + self.sdp + .try_apply_header(&config.sdp_config, last_epoch_state, epoch_state)?; self.sdp = new_sdp; Ok((self, reward_utxos)) } diff --git a/ledger/src/mantle/sdp/mod.rs b/ledger/src/mantle/sdp/mod.rs index 63d5ff034..b9a8dfed2 100644 --- a/ledger/src/mantle/sdp/mod.rs +++ b/ledger/src/mantle/sdp/mod.rs @@ -1,6 +1,6 @@ pub mod rewards; -use std::collections::HashMap; +use std::{collections::HashMap, marker::PhantomData}; use lb_blend_message::crypto::proofs::RealProofsVerifier; use lb_core::{ @@ -17,11 +17,12 @@ use lb_core::{ }, }, sdp::{ - ActivityMetadata, Declaration, DeclarationId, MinStake, Nonce, ProviderId, ProviderInfo, - ServiceParameters, ServiceType, SessionNumber, + ActivityMetadata, Declaration, DeclarationId, MinStake, Nonce, ProviderId, + ServiceParameters, ServiceType, locked_notes::{self, LockedNotes}, }, }; +use lb_cryptarchia_engine::Epoch; use lb_key_management_system_keys::keys::{Ed25519Signature, ZkSignature}; use rewards::{Error as RewardsError, Rewards}; use tracing::warn; @@ -38,7 +39,7 @@ enum Service { impl Service { fn try_apply_header( self, - block_number: BlockNumber, + last_epoch_state: &EpochState, epoch_state: &EpochState, config: &ServiceParameters, rewards_params: &ServiceRewardsParameters, @@ -46,7 +47,7 @@ impl Service { match self { Self::BlendNetwork(state) => { let (new_state, utxos) = state.try_apply_header( - block_number, + last_epoch_state, epoch_state, config, &rewards_params.blend, @@ -62,19 +63,6 @@ impl Service { } } - const fn active_session(&self) -> &SessionState { - match self { - Self::BlendNetwork(state) => &state.active, - } - } - - #[cfg(test)] - const fn next_session(&self) -> &SessionState { - match self { - Self::BlendNetwork(state) => &state.next, - } - } - const fn declarations(&self) -> &Declarations { match self { Self::BlendNetwork(state) => &state.declarations, @@ -93,21 +81,23 @@ impl Service { } } - pub fn update_rewards( + #[expect( + clippy::unnecessary_wraps, + reason = "TODO: enable this after making the `rewards` module stable" + )] + pub const fn update_rewards( &mut self, - provider_id: ProviderId, - metadata: &ActivityMetadata, - block_number: BlockNumber, - rewards_params: &ServiceRewardsParameters, + _provider_id: ProviderId, + _metadata: &ActivityMetadata, + _rewards_params: &ServiceRewardsParameters, ) -> Result<(), Error> { match self { - Self::BlendNetwork(state) => { - state.rewards = state.rewards.update_active( - provider_id, - metadata, - block_number, - &rewards_params.blend, - )?; + Self::BlendNetwork(_state) => { + // TODO: enable this after making the `rewards` module stable + // state.rewards = + // state + // .rewards + // .update_active(provider_id, metadata, &rewards_params.blend)?; Ok(()) } } @@ -145,12 +135,8 @@ pub enum Error { ServiceNotFound(ServiceType), #[error("Duplicate sdp declaration id: {0:?}")] DuplicateDeclaration(DeclarationId), - #[error("Active session for service {0:?} not found")] - ActiveSessionNotFound(ServiceType), - #[error("Next session for service {0:?} not found")] - NextSessionNotFound(ServiceType), - #[error("Session parameters for {0:?} not found")] - SessionParamsNotFound(ServiceType), + #[error("Epoch parameters for {0:?} not found")] + EpochParamsNotFound(ServiceType), #[error("Service parameters are missing for {0:?}")] ServiceParamsNotFound(ServiceType), #[error("Can't update genesis state during different block number")] @@ -174,100 +160,70 @@ pub enum Error { SdpOp(#[from] lb_core::mantle::ops::sdp::SdpError), } -// State at the beginning of this session -#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct SessionState { - pub declarations: Declarations, - pub session_n: u64, -} - #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] struct ServiceState { - // state of declarations at block b + /// Declarations accumulated until the current block. declarations: Declarations, - // (current) active session. - // snapshot of `declarations` at the start of block ((b // config.session_duration) - 1) * - // config.session_duration - active: SessionState, - // next session. - // snapshot of `declarations` at the start of block (b // config.session_duration) * - // config.session_duration - next: SessionState, - // rewards calculation and tracking for this service - pub rewards: R, + // TODO: enable this after making the `rewards` module stable + // Rewards calculation and tracking for this service + // pub rewards: R, + _phantom: PhantomData, } -const fn is_active( - declaration: &Declaration, - current_block: u64, - config: &ServiceParameters, -) -> bool { - declaration.active - + (config.inactivity_period + config.retention_period) * config.session_duration - >= current_block +fn is_active(declaration: &Declaration, current_epoch: Epoch, config: &ServiceParameters) -> bool { + declaration.active + config.inactivity_period + config.retention_period >= current_epoch } impl ServiceState { fn try_apply_header( mut self, - block_number: u64, + _last_epoch_state: &EpochState, epoch_state: &EpochState, service_params: &ServiceParameters, - rewards_params: &R::Params, + _rewards_params: &R::Params, ) -> (Self, Vec) { - let current_session = service_params.session_for_block(block_number); - let reward_utxos; + // Remove expired declarations based on retention_period + self.declarations = self + .declarations + .iter() + .filter(|(_id, declaration)| { + let active = is_active(declaration, epoch_state.epoch(), service_params); + if !active { + warn!( + provider_id = ?declaration.provider_id, + latest_active_epoch = ?declaration.active, + current_epoch = ?epoch_state.epoch(), + service_params = ?service_params, + "removing declaration due to inactivity + retention + finalization_delay" + ); + } + active + }) + .map(|(id, declaration)| (*id, declaration.clone())) + .collect(); - // shift all session! - if current_session == self.active.session_n + 1 { - // Remove expired declarations based on retention_period - // This essentially duplicates the declaration set so it's only triggered at - // session boundaries - self.declarations = self - .declarations - .iter() - .filter(|(_id, declaration)| { - let active = is_active(declaration, block_number, service_params); - if !active { - warn!( - provider_id = ?declaration.provider_id, - latest_active_block = declaration.active, - current_block = block_number, - "removing declaration due to inactivity+retention" - ); - } - active - }) - .map(|(id, declaration)| (*id, declaration.clone())) - .collect(); - - // Update rewards with current session state and distribute rewards - (self.rewards, reward_utxos) = self.rewards.update_session( - &self.active, - epoch_state, - service_params, - rewards_params, - ); - self.active = self.next.clone(); - self.next = SessionState { - declarations: self.declarations.clone(), - session_n: self.next.session_n + 1, - }; - } else { - assert!( - current_session == self.active.session_n, - "Logos blockchain isn't ready for time travel yet: session_of_block={current_session}, active_session={}", - self.active.session_n - ); - self.rewards = self.rewards.update_epoch(epoch_state, rewards_params); - reward_utxos = Vec::new(); - } + // Update rewards with current epoch state and distribute rewards + let reward_utxos = Vec::new(); + // TODO: enable this after making the `rewards` module stable + // if last_epoch_state.epoch() < epoch_state.epoch() { + // (self.rewards, reward_utxos) = self.rewards.update_epoch( + // last_epoch_state, + // epoch_state, + // service_params, + // rewards_params, + // ); + // } (self, reward_utxos) } - fn add_income(&mut self, income: Value) { - self.rewards = self.rewards.add_income(income); + #[expect( + clippy::unused_self, + reason = "TODO: enable this after making the `rewards` module stable" + )] + const fn add_income(&self, _income: Value) { + // TODO: enable this after making the `rewards` module stable + // self.rewards = self.rewards.add_income(income); } fn contains(&self, declaration_id: &DeclarationId) -> bool { @@ -279,20 +235,21 @@ impl ServiceState { /// /// NOTE: Most collection fields in this struct should use `rpds` /// since we keep a copy of this state for each block. -#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct SdpLedger { services: rpds::HashTrieMapSync, locked_notes: LockedNotes, - block_number: u64, + // The epoch when this ledger was created + epoch: Epoch, } impl SdpLedger { #[must_use] - pub fn new() -> Self { + pub fn new(epoch: Epoch) -> Self { Self { services: rpds::HashTrieMapSync::new_sync(), locked_notes: LockedNotes::new(), - block_number: 0, + epoch, } } @@ -302,8 +259,8 @@ impl SdpLedger { epoch_state: &EpochState, ops: impl Iterator + 'a, ) -> Result<(Self, Events), Error> { - let mut sdp = - Self::new().with_blend_service(&config.service_rewards_params.blend, epoch_state); + let mut sdp = Self::new(epoch_state.epoch()) + .with_blend_service(&config.service_rewards_params.blend, epoch_state); let mut all_events = Events::new(); for (op, _) in ops { @@ -312,55 +269,46 @@ impl SdpLedger { all_events.extend(events); } - let blend = sdp - .services - .get_mut(&ServiceType::BlendNetwork) - .expect("SDP initialized with Blend in this method"); - - let Service::BlendNetwork(state) = blend; - state.active.declarations = state.declarations.clone(); - state.next.declarations = state.declarations.clone(); - Ok((sdp, all_events)) } #[must_use] pub fn with_blend_service( mut self, - rewards_settings: &blend::RewardsParameters, + _rewards_settings: &blend::RewardsParameters, epoch_state: &EpochState, ) -> Self { - let service = Service::BlendNetwork(Self::new_service_state(blend::Rewards::new( - rewards_settings, - epoch_state, - ))); + assert_eq!( + epoch_state.epoch, self.epoch, + "TODO: refactor to remove this assertion" + ); + let service = Service::BlendNetwork(Self::new_service_state( + // TODO: enable this after making the `rewards` module stable + // blend::Rewards::new( + // rewards_settings, + // epoch_state, + // ) + )); self.services = self.services.insert(ServiceType::BlendNetwork, service); self } #[must_use] - fn new_service_state(rewards: R) -> ServiceState { + fn new_service_state(// TODO: enabled this after making the `rewards` module stable + //rewards: R + ) -> ServiceState { ServiceState { declarations: rpds::RedBlackTreeMapSync::new_sync(), - active: SessionState { - declarations: rpds::RedBlackTreeMapSync::new_sync(), - session_n: 0, - }, - - next: SessionState { - declarations: rpds::RedBlackTreeMapSync::new_sync(), - session_n: 1, - }, - rewards, + _phantom: PhantomData, } } pub fn try_apply_header( &self, config: &Config, + last_epoch_state: &EpochState, epoch_state: &EpochState, ) -> Result<(Self, Vec), Error> { - let block_number = self.block_number + 1; // overflow? let mut all_reward_utxos = Vec::new(); let services = self @@ -370,9 +318,9 @@ impl SdpLedger { let service_params = config .service_params .get(service) - .ok_or(Error::SessionParamsNotFound(*service))?; + .ok_or(Error::EpochParamsNotFound(*service))?; let (new_state, reward_utxos) = service_state.clone().try_apply_header( - block_number, + last_epoch_state, epoch_state, service_params, &config.service_rewards_params, @@ -384,7 +332,7 @@ impl SdpLedger { Ok(( Self { - block_number, + epoch: epoch_state.epoch(), services, locked_notes: self.locked_notes.clone(), }, @@ -416,7 +364,7 @@ impl SdpLedger { op, SDPDeclareExecutionContext { utxo_tree: utxo_tree.clone(), - block_number: self.block_number, + epoch: self.epoch, declarations: service_state.declarations_clone(), locked_notes: self.locked_notes.clone(), min_stake: config.min_stake, @@ -457,7 +405,7 @@ impl SdpLedger { op, SDPDeclareExecutionContext { utxo_tree: utxo_tree.clone(), - block_number: self.block_number, + epoch: self.epoch, declarations: service_state.declarations_clone(), locked_notes: self.locked_notes.clone(), min_stake: config.min_stake, @@ -490,7 +438,7 @@ impl SdpLedger { // Execute SDP Active let (result, events) = op.execute(SDPActiveExecutionContext { - block_number: self.block_number, + epoch: self.epoch, declarations: service_state.declarations_clone(), })?; @@ -501,12 +449,7 @@ impl SdpLedger { .provider_id; service_state.update_declarations(result.declarations); - service_state.update_rewards( - provider_id, - &op.metadata, - self.block_number, - &config.service_rewards_params, - )?; + service_state.update_rewards(provider_id, &op.metadata, &config.service_rewards_params)?; Ok((self, events)) } @@ -525,9 +468,9 @@ impl SdpLedger { // Validate SDP Withdraw op.validate(&SDPWithdrawValidationContext { - lock_period: &config.lock_period, + lock_period: config.lock_period, declarations: service_state.declarations(), - block_number: &self.block_number, + epoch: self.epoch, locked_notes: &self.locked_notes, tx_hash: &tx_hash, sdp_withdraw_sig: zksig, @@ -535,7 +478,6 @@ impl SdpLedger { // Execute SDP Withdraw let (result, events) = op.execute(SDPWithdrawExecutionContext { - block_number: self.block_number, declarations: service_state.declarations_clone(), locked_notes: self.locked_notes.clone(), })?; @@ -559,49 +501,22 @@ impl SdpLedger { &self.locked_notes } + /// Declarations of all services, which have been accumulated until the + /// current block. #[must_use] - pub fn active_session_providers( - &self, - service_type: ServiceType, - ) -> Option> { - let service = self.services.get(&service_type)?; - - let providers = service - .active_session() - .declarations + pub fn declarations(&self) -> lb_core::sdp::Declarations { + self.services .iter() - .map(|(_, declaration)| { + .map(|(service_type, service_state)| { ( - declaration.provider_id, - ProviderInfo { - locators: declaration.locators.clone(), - zk_id: declaration.zk_id, - }, + *service_type, + service_state + .declarations() + .iter() + .map(|(declaration_id, declaration)| (*declaration_id, declaration.clone())) + .collect(), ) }) - .collect(); - - Some(providers) - } - - #[must_use] - pub fn active_sessions(&self) -> HashMap { - self.services - .iter() - .map(|(service_type, service)| (*service_type, service.active_session().session_n)) - .collect() - } - - #[must_use] - pub fn declarations(&self) -> Vec<(DeclarationId, Declaration)> { - self.services - .iter() - .flat_map(|(_, service_state)| { - service_state - .declarations() - .iter() - .map(|(declaration_id, declaration)| (*declaration_id, declaration.clone())) - }) .collect() } @@ -634,18 +549,6 @@ impl SdpLedger { Ok((service, params)) } - #[cfg(test)] - fn get_next_session(&self, service_type: ServiceType) -> Option<&SessionState> { - self.services.get(&service_type).map(Service::next_session) - } - - #[cfg(test)] - fn get_active_session(&self, service_type: ServiceType) -> Option<&SessionState> { - self.services - .get(&service_type) - .map(Service::active_session) - } - #[cfg(test)] fn get_declarations(&self, service_type: ServiceType) -> Option<&Declarations> { self.services.get(&service_type).map(Service::declarations) @@ -656,6 +559,7 @@ impl SdpLedger { mod tests { use std::{num::NonZeroU64, sync::Arc}; + use lb_blend_proofs::{quota::VerifiedProofOfQuota, selection::VerifiedProofOfSelection}; use lb_core::{crypto::ZkHash, mantle::ledger::Utxos, sdp::Locator}; use lb_groth16::{Field as _, Fr}; use lb_key_management_system_keys::keys::{Ed25519Key, ZkKey}; @@ -663,25 +567,16 @@ mod tests { use num_bigint::BigUint; use super::*; - use crate::cryptarchia::tests::{utxo, utxo_with_sk}; + use crate::cryptarchia::tests::utxo_with_sk; - fn setup() -> Config { + fn setup(service_params: ServiceParameters) -> Config { let mut params = HashMap::new(); - params.insert( - ServiceType::BlendNetwork, - ServiceParameters { - inactivity_period: 1, - lock_period: 10, - retention_period: 1, - timestamp: 0, - session_duration: 10, - }, - ); + params.insert(ServiceType::BlendNetwork, service_params); Config { service_params: Arc::new(params), service_rewards_params: ServiceRewardsParameters { blend: blend::RewardsParameters { - rounds_per_session: NonZeroU64::new(10).unwrap(), + rounds_per_epoch: NonZeroU64::new(10).unwrap(), message_frequency_per_round: NonNegativeF64::try_from(1.0).unwrap(), num_blend_layers: NonZeroU64::new(3).unwrap(), minimum_network_size: NonZeroU64::new(1).unwrap(), @@ -731,6 +626,19 @@ mod tests { .map(|(sdp_ledger, _)| sdp_ledger) } + fn apply_active_with_dummies( + sdp_ledger: SdpLedger, + op: &SDPActiveOp, + zk_sk: ZkKey, + config: &Config, + ) -> Result { + let tx_hash = TxHash([2u8; 32]); + let zk_sig = ZkKey::multi_sign(&[zk_sk], &tx_hash.to_fr()).unwrap(); + sdp_ledger + .apply_active_msg(op, &zk_sig, tx_hash, config) + .map(|(sdp_ledger, _)| sdp_ledger) + } + fn apply_withdraw_with_dummies( sdp_ledger: SdpLedger, op: &SDPWithdrawOp, @@ -746,65 +654,156 @@ mod tests { .map(|(sdp_ledger, _)| sdp_ledger) } - fn dummy_epoch_state() -> EpochState { - EpochState { - epoch: 0.into(), + fn dummy_epoch_state(epoch: Epoch, rewards_settings: &blend::RewardsParameters) -> EpochState { + let mut epoch_state = EpochState { + epoch, nonce: ZkHash::ZERO, utxos: UtxoTree::default(), total_stake: 100, lottery_0: Fr::ZERO, lottery_1: Fr::ZERO, - } + sdp: SdpLedger::new(epoch), + }; + + epoch_state.sdp = epoch_state + .sdp + .clone() + .with_blend_service(rewards_settings, &epoch_state); + + epoch_state } + /// A provider that hasn't submit a new active message during + /// `inactivity_period + retention_period` epochs must be removed. #[test] - fn test_update_active_provider() { - let config = setup(); - let service_a = ServiceType::BlendNetwork; - let utxo = utxo(); + fn gc_inactive_declaration() { + let config = setup(ServiceParameters { + lock_period: 10.into(), + // Set inactivity/retention periods very short to check that + // declaration is NOT removed before an activity message is submitted. + inactivity_period: 1.into(), + retention_period: 1.into(), + epoch: 0.into(), + }); + + // Init ledger with no declaration + let epoch0 = dummy_epoch_state(0.into(), &config.service_rewards_params.blend); + let mut ledger = epoch0.sdp.clone(); + + // Move forward to the epoch 1 + let mut last_epoch_state = epoch0.clone(); + let new_epoch_state = EpochState { + epoch: 1.into(), + nonce: ZkHash::from(1), + ..epoch0 + }; + (ledger, _) = ledger + .try_apply_header(&config, &last_epoch_state, &new_epoch_state) + .unwrap(); + last_epoch_state = new_epoch_state; + + // Add a declaration at epoch 1 + let (_utxo_sk, utxo) = utxo_with_sk(); let note_id = utxo.id(); let signing_key = create_signing_key(); - let zk_key = create_zk_key(0); - - let op = &SDPDeclareOp { - service_type: service_a, + let zk_key = create_zk_key(1); + let declare_op = &SDPDeclareOp { + service_type: ServiceType::BlendNetwork, locked_note_id: note_id, zk_id: zk_key.to_public_key(), provider_id: ProviderId(signing_key.public_key()), locators: "/ip4/1.1.1.1/udp/0".parse::().unwrap().into(), }; - let declaration_id = op.id(); - - // Initialize ledger with service config - let epoch_state = dummy_epoch_state(); - let sdp_ledger = - SdpLedger::new().with_blend_service(&config.service_rewards_params.blend, &epoch_state); - - // Apply declare at block 0 - let utxo_tree = utxo_tree(vec![utxo]); - let sdp_ledger = - apply_declare_with_dummies(&utxo_tree, sdp_ledger, op, &zk_key, &config).unwrap(); - - // Declaration is in service_state.declarations but not in sessions yet - let declarations = sdp_ledger.get_declarations(service_a).unwrap(); + let declaration_id = declare_op.id(); + let mut ledger = apply_declare_with_dummies( + &utxo_tree(vec![utxo]), + ledger, + declare_op, + &zk_key, + &config, + ) + .unwrap(); + let declarations = ledger.get_declarations(ServiceType::BlendNetwork).unwrap(); assert!(declarations.contains_key(&declaration_id)); - // Apply headers to reach block 10 (session boundary) - let mut sdp_ledger = sdp_ledger; - for _ in 0..10 { - (sdp_ledger, _) = sdp_ledger.try_apply_header(&config, &epoch_state).unwrap(); + // Move forward to the epoch 4 where the provider can submit an activity + // message. + // (The provider is expected to provide the service from epoch 3) + for epoch in 2..=4 { + let new_epoch_state = EpochState { + epoch: epoch.into(), + nonce: ZkHash::from(epoch), + ..last_epoch_state.clone() + }; + (ledger, _) = ledger + .try_apply_header(&config, &last_epoch_state, &new_epoch_state) + .unwrap(); + last_epoch_state = new_epoch_state; } + // Check that the declaration is still present. + let declarations = ledger.get_declarations(ServiceType::BlendNetwork).unwrap(); + assert!(declarations.contains_key(&declaration_id)); - // At block 10, declaration enters the next session 2 - let next_session = sdp_ledger.get_next_session(service_a).unwrap(); - assert_eq!(next_session.session_n, 2); - assert!(next_session.declarations.contains_key(&declaration_id)); - assert_eq!(next_session.declarations.size(), 1); + // Submit an activity message at epoch 4 + let active_op = SDPActiveOp { + declaration_id, + nonce: 1, + metadata: ActivityMetadata::Blend(Box::new(lb_core::sdp::blend::ActivityProof { + epoch: 3.into(), // proving activity from epoch 3 + signing_key: signing_key.public_key(), + proof_of_quota: VerifiedProofOfQuota::from_bytes_unchecked([0; _]).into(), + proof_of_selection: VerifiedProofOfSelection::from_bytes_unchecked([1; _]).into(), + })), + }; + let mut ledger = apply_active_with_dummies(ledger, &active_op, zk_key, &config).unwrap(); + let declaration = ledger.get_declarations(ServiceType::BlendNetwork).unwrap(); + assert_eq!( + declaration.get(&declaration_id).unwrap().active, + Epoch::new(4) // epoch when the activity message is submitted/accepted + ); + + // Move forward to the epoch 6. The declaration should be still present + // because the activity message was accepted at epoch 4. + for epoch in 5..=6 { + let new_epoch_state = EpochState { + epoch: epoch.into(), + nonce: ZkHash::from(epoch), + ..last_epoch_state.clone() + }; + (ledger, _) = ledger + .try_apply_header(&config, &last_epoch_state, &new_epoch_state) + .unwrap(); + last_epoch_state = new_epoch_state; + } + let declarations = ledger.get_declarations(ServiceType::BlendNetwork).unwrap(); + assert!(declarations.contains_key(&declaration_id)); + + // Move forward to epoch 7 where declaration should be removed + // because no activity message has been submitted since epoch 4 + let new_epoch_state = EpochState { + epoch: 7.into(), + nonce: ZkHash::from(7), + ..last_epoch_state.clone() + }; + (ledger, _) = ledger + .try_apply_header(&config, &last_epoch_state, &new_epoch_state) + .unwrap(); + let declarations = ledger.get_declarations(ServiceType::BlendNetwork).unwrap(); + assert!(!declarations.contains_key(&declaration_id)); } #[test] fn test_withdraw_provider() { - let config = setup(); + let config = setup(ServiceParameters { + lock_period: 10.into(), + // inactivity/retention periods should be longer than lock period + // for this test to avoid the declaration being removed due to + // inacitivity before we can test the withdraw logic. + inactivity_period: 20.into(), + retention_period: 20.into(), + epoch: 0.into(), + }); + let service_a = ServiceType::BlendNetwork; let (utxo_sk, utxo) = utxo_with_sk(); let note_id = utxo.id(); @@ -821,9 +820,8 @@ mod tests { let declaration_id = declare_op.id(); // Initialize ledger with service config and declare - let epoch_state = dummy_epoch_state(); - let sdp_ledger = - SdpLedger::new().with_blend_service(&config.service_rewards_params.blend, &epoch_state); + let epoch0 = dummy_epoch_state(0.into(), &config.service_rewards_params.blend); + let sdp_ledger = epoch0.sdp.clone(); let utxo_tree = utxo_tree(vec![utxo]); let sdp_ledger = @@ -834,10 +832,19 @@ mod tests { let declarations = sdp_ledger.get_declarations(service_a).unwrap(); assert!(declarations.contains_key(&declaration_id)); - // Move forward enough blocks to satisfy lock_period + // Move forward enough epochs to satisfy lock_period let mut sdp_ledger = sdp_ledger; - for _ in 0..11 { - (sdp_ledger, _) = sdp_ledger.try_apply_header(&config, &epoch_state).unwrap(); + let mut last_epoch_state = epoch0; + for epoch in 1..=11 { + let new_epoch_state = EpochState { + epoch: epoch.into(), + nonce: ZkHash::from(epoch), + ..last_epoch_state.clone() + }; + (sdp_ledger, _) = sdp_ledger + .try_apply_header(&config, &last_epoch_state, &new_epoch_state) + .unwrap(); + last_epoch_state = new_epoch_state; } // Withdraw the declaration @@ -854,440 +861,4 @@ mod tests { assert!(!declarations.contains_key(&declaration_id)); assert!(declarations.is_empty()); } - - #[test] - fn test_promote_session_with_updated_provider() { - let config = setup(); - let service_a = ServiceType::BlendNetwork; - let utxo = utxo(); - let note_id = utxo.id(); - let signing_key = create_signing_key(); - let zk_key = create_zk_key(0); - - let op = &SDPDeclareOp { - service_type: service_a, - locked_note_id: note_id, - zk_id: zk_key.to_public_key(), - provider_id: ProviderId(signing_key.public_key()), - locators: "/ip4/1.1.1.1/udp/0".parse::().unwrap().into(), - }; - let declaration_id = op.id(); - - // Initialize ledger with service config - let epoch_state = dummy_epoch_state(); - let sdp_ledger = - SdpLedger::new().with_blend_service(&config.service_rewards_params.blend, &epoch_state); - - // Declare at block 0 - let utxo_tree = utxo_tree(vec![utxo]); - let sdp_ledger = - apply_declare_with_dummies(&utxo_tree, sdp_ledger, op, &zk_key, &config).unwrap(); - - // Apply headers to reach block 10 (session boundary for session_duration=10) - let mut sdp_ledger = sdp_ledger; - for _ in 0..10 { - (sdp_ledger, _) = sdp_ledger.try_apply_header(&config, &epoch_state).unwrap(); - } - - // At block 10: `active` becomes session 1 (was empty `next`), - // `next` becomes session 2 (snapshot at block 10) - let active_session = sdp_ledger.get_active_session(service_a).unwrap(); - assert_eq!(active_session.session_n, 1); - assert!(active_session.declarations.is_empty()); // Active session 1 is empty - - // Check next session is now session 2 and contains declaration - let next_session = sdp_ledger.get_next_session(service_a).unwrap(); - assert_eq!(next_session.session_n, 2); - assert!(next_session.declarations.contains_key(&declaration_id)); - - // Continue to block 20 to see declaration become active - for _ in 0..10 { - (sdp_ledger, _) = sdp_ledger.try_apply_header(&config, &epoch_state).unwrap(); - } - - // At block 20: active becomes session 2 (with declaration) - let active_session = sdp_ledger.get_active_session(service_a).unwrap(); - assert_eq!(active_session.session_n, 2); - assert!(active_session.declarations.contains_key(&declaration_id)); - } - - #[test] - fn test_no_promotion() { - let config = setup(); - let service_a = ServiceType::BlendNetwork; - - // Initialize ledger with service config - let epoch_state = dummy_epoch_state(); - let mut sdp_ledger = - SdpLedger::new().with_blend_service(&config.service_rewards_params.blend, &epoch_state); - - // Apply headers to reach block 9 (still in session 0, promotion happens at - // block 10) - for _ in 0..9 { - (sdp_ledger, _) = sdp_ledger.try_apply_header(&config, &epoch_state).unwrap(); - } - - // Check active session is still session 0 with no declarations - let active_session = sdp_ledger.get_active_session(service_a).unwrap(); - assert_eq!(active_session.session_n, 0); - assert!(active_session.declarations.is_empty()); - - // Check next session is still session 1 - let next_session = sdp_ledger.get_next_session(service_a).unwrap(); - assert_eq!(next_session.session_n, 1); - } - - #[test] - fn test_promote_one_service() { - let config = setup(); - let service = ServiceType::BlendNetwork; // session_duration = 10 - - // Initialize ledger with Blend service - let epoch_state = dummy_epoch_state(); - let mut sdp_ledger = - SdpLedger::new().with_blend_service(&config.service_rewards_params.blend, &epoch_state); - - // Apply headers to reach block 10 (session boundary for BlendNetwork) - for _ in 0..10 { - (sdp_ledger, _) = sdp_ledger.try_apply_header(&config, &epoch_state).unwrap(); - } - - // Check BlendNetwork is promoted to session 1 - let active_session = sdp_ledger.get_active_session(service).unwrap(); - assert_eq!(active_session.session_n, 1); - let next_session = sdp_ledger.get_next_session(service).unwrap(); - assert_eq!(next_session.session_n, 2); - } - - #[test] - fn test_new_declarations_becoming_active_after_session_boundary() { - let config = setup(); - let service_a = ServiceType::BlendNetwork; - let signing_key = create_signing_key(); - let zk_key = create_zk_key(0); - - // Initialize ledger - let epoch_state = dummy_epoch_state(); - let mut sdp_ledger = - SdpLedger::new().with_blend_service(&config.service_rewards_params.blend, &epoch_state); - - // SESSION 0: Add a declaration at block 5 - for _ in 0..5 { - (sdp_ledger, _) = sdp_ledger.try_apply_header(&config, &epoch_state).unwrap(); - } - - let utxo = utxo(); - let declare_op = &SDPDeclareOp { - service_type: service_a, - locked_note_id: utxo.id(), - zk_id: zk_key.to_public_key(), - provider_id: ProviderId(signing_key.public_key()), - locators: "/ip4/1.1.1.1/udp/0".parse::().unwrap().into(), - }; - let declaration_id = declare_op.id(); - - let utxo_tree = utxo_tree(vec![utxo]); - sdp_ledger = - apply_declare_with_dummies(&utxo_tree, sdp_ledger, declare_op, &zk_key, &config) - .unwrap(); - - // Move to block 9 (last block of session 0) - for _ in 6..10 { - (sdp_ledger, _) = sdp_ledger.try_apply_header(&config, &epoch_state).unwrap(); - } - assert_eq!(sdp_ledger.block_number, 9); - - // Declaration is not in active or next sessions yet - let active_session = sdp_ledger.get_active_session(service_a).unwrap(); - assert_eq!(active_session.session_n, 0); - assert!(!active_session.declarations.contains_key(&declaration_id)); - - let next_session = sdp_ledger.get_next_session(service_a).unwrap(); - assert_eq!(next_session.session_n, 1); - assert!(next_session.declarations.is_empty()); - - // SESSION 1: Cross session boundary to block 10 - (sdp_ledger, _) = sdp_ledger.try_apply_header(&config, &epoch_state).unwrap(); - assert_eq!(sdp_ledger.block_number, 10); - - // Active session 1 is empty (was the empty next session 1) - let active_session = sdp_ledger.get_active_session(service_a).unwrap(); - assert_eq!(active_session.session_n, 1); - assert!(active_session.declarations.is_empty()); - - // Next session 2 now has the declaration (snapshot from block 10) - let next_session = sdp_ledger.get_next_session(service_a).unwrap(); - assert_eq!(next_session.session_n, 2); - assert!(next_session.declarations.contains_key(&declaration_id)); - - // SESSION 2: Cross to block 20 - for _ in 11..20 { - (sdp_ledger, _) = sdp_ledger.try_apply_header(&config, &epoch_state).unwrap(); - } - (sdp_ledger, _) = sdp_ledger.try_apply_header(&config, &epoch_state).unwrap(); - assert_eq!(sdp_ledger.block_number, 20); - - // Now the declaration is active in session 2 - let active_session = sdp_ledger.get_active_session(service_a).unwrap(); - assert_eq!(active_session.session_n, 2); - assert!(active_session.declarations.contains_key(&declaration_id)); - } - - #[test] - fn test_declaration_snapshot_timing() { - let config = setup(); - let service_a = ServiceType::BlendNetwork; - let signing_key = create_signing_key(); - let zk_key_1 = create_zk_key(1); - - let epoch_state = dummy_epoch_state(); - let mut sdp_ledger = - SdpLedger::new().with_blend_service(&config.service_rewards_params.blend, &epoch_state); - - // Add declaration at block 0 - let utxo_1 = utxo(); - let declare_op_1 = &SDPDeclareOp { - service_type: service_a, - locked_note_id: utxo_1.id(), - zk_id: zk_key_1.to_public_key(), - provider_id: ProviderId(signing_key.public_key()), - locators: "/ip4/1.1.1.1/udp/0".parse::().unwrap().into(), - }; - let declaration_id_1 = declare_op_1.id(); - - let utxo_tree_1 = utxo_tree(vec![utxo_1]); - sdp_ledger = - apply_declare_with_dummies(&utxo_tree_1, sdp_ledger, declare_op_1, &zk_key_1, &config) - .unwrap(); - - // Move to block 9 (last block before session boundary) - for _ in 1..10 { - (sdp_ledger, _) = sdp_ledger.try_apply_header(&config, &epoch_state).unwrap(); - } - - // Save state at block 9 - let sdp_ledger_block_9 = sdp_ledger.clone(); - - // Add another declaration at block 10 (after session boundary) - (sdp_ledger, _) = sdp_ledger.try_apply_header(&config, &epoch_state).unwrap(); - assert_eq!(sdp_ledger.block_number, 10); - - let zk_key_2 = create_zk_key(2); - let utxo_2 = utxo(); - let declare_op_2 = &SDPDeclareOp { - service_type: service_a, - locked_note_id: utxo_2.id(), - zk_id: zk_key_2.to_public_key(), - provider_id: ProviderId(signing_key.public_key()), - locators: "/ip4/1.1.1.1/udp/0".parse::().unwrap().into(), - }; - let declaration_id_2 = declare_op_2.id(); - - let utxo_tree_2 = utxo_tree(vec![utxo_1, utxo_2]); - sdp_ledger = - apply_declare_with_dummies(&utxo_tree_2, sdp_ledger, declare_op_2, &zk_key_2, &config) - .unwrap(); - - // Jump to session 2 (block 20) - for _ in 11..20 { - (sdp_ledger, _) = sdp_ledger.try_apply_header(&config, &epoch_state).unwrap(); - } - (sdp_ledger, _) = sdp_ledger.try_apply_header(&config, &epoch_state).unwrap(); - - // Active session (session 2) should contain both declarations - let active_session = sdp_ledger.get_active_session(service_a).unwrap(); - assert!(active_session.declarations.contains_key(&declaration_id_1)); - assert!(!active_session.declarations.contains_key(&declaration_id_2)); - - // Now test from the block 9 state - jumping directly to block 20 - let mut sdp_ledger_from_9 = sdp_ledger_block_9; - for _ in 10..20 { - (sdp_ledger_from_9, _) = sdp_ledger_from_9 - .try_apply_header(&config, &epoch_state) - .unwrap(); - } - (sdp_ledger_from_9, _) = sdp_ledger_from_9 - .try_apply_header(&config, &epoch_state) - .unwrap(); - - // Active session should only contain declaration_id_1 - // because declaration_id_2 was never added in this timeline - let active_session_from_9 = sdp_ledger_from_9.get_active_session(service_a).unwrap(); - assert!( - active_session_from_9 - .declarations - .contains_key(&declaration_id_1) - ); - assert!( - !active_session_from_9 - .declarations - .contains_key(&declaration_id_2) - ); - } - - #[test] - fn test_session_jump() { - let config = setup(); - let service_a = ServiceType::BlendNetwork; - let signing_key = create_signing_key(); - let zk_key = create_zk_key(0); - - let epoch_state = dummy_epoch_state(); - let mut sdp_ledger = - SdpLedger::new().with_blend_service(&config.service_rewards_params.blend, &epoch_state); - - // Add declaration at block 3 - for _ in 0..3 { - (sdp_ledger, _) = sdp_ledger.try_apply_header(&config, &epoch_state).unwrap(); - } - - let utxo = utxo(); - let declare_op = &SDPDeclareOp { - service_type: service_a, - locked_note_id: utxo.id(), - zk_id: zk_key.to_public_key(), - provider_id: ProviderId(signing_key.public_key()), - locators: "/ip4/1.1.1.1/udp/0".parse::().unwrap().into(), - }; - let declaration_id = declare_op.id(); - - let utxo_tree = utxo_tree(vec![utxo]); - sdp_ledger = - apply_declare_with_dummies(&utxo_tree, sdp_ledger, declare_op, &zk_key, &config) - .unwrap(); - - // Jump directly from block 3 to block 25 (skipping session 1 entirely) - for _ in 4..25 { - (sdp_ledger, _) = sdp_ledger.try_apply_header(&config, &epoch_state).unwrap(); - } - (sdp_ledger, _) = sdp_ledger.try_apply_header(&config, &epoch_state).unwrap(); - assert_eq!(sdp_ledger.block_number, 25); - - // Declaration snapshots should be taken from the last known state - // Active session (session 2, which started at block 20) should contain the - // declaration - let active_session = sdp_ledger.get_active_session(service_a).unwrap(); - assert_eq!(active_session.session_n, 2); - assert!(active_session.declarations.contains_key(&declaration_id)); - - // Next session (session 3) should also contain the declaration - let next_session = sdp_ledger.get_next_session(service_a).unwrap(); - assert_eq!(next_session.session_n, 3); - assert!(next_session.declarations.contains_key(&declaration_id)); - } - - #[test] - #[expect(clippy::cognitive_complexity, reason = "sessions are complex :)")] - fn test_session_boundary() { - // Test a declaration at block 9 is available in session 2 but a declaration in - // block 10 is not - let config = setup(); - let service_a = ServiceType::BlendNetwork; - let signing_key = create_signing_key(); - let zk_key_1 = create_zk_key(1); - - let epoch_state = dummy_epoch_state(); - let mut sdp_ledger = - SdpLedger::new().with_blend_service(&config.service_rewards_params.blend, &epoch_state); - - // Move to block 9 (last block of session 0) - for _ in 0..9 { - (sdp_ledger, _) = sdp_ledger.try_apply_header(&config, &epoch_state).unwrap(); - } - assert_eq!(sdp_ledger.block_number, 9); - - let active_session = sdp_ledger.get_active_session(service_a).unwrap(); - assert_eq!(active_session.session_n, 0); - assert!(active_session.declarations.is_empty()); - - let next_session = sdp_ledger.get_next_session(service_a).unwrap(); - assert_eq!(next_session.session_n, 1); - assert!(next_session.declarations.is_empty()); - - // Create first declaration at block 9 - let utxo_1 = utxo(); - let declare_op_1 = &SDPDeclareOp { - service_type: service_a, - locked_note_id: utxo_1.id(), - zk_id: zk_key_1.to_public_key(), - provider_id: ProviderId(signing_key.public_key()), - locators: "/ip4/1.1.1.1/udp/0".parse::().unwrap().into(), - }; - let declaration_id_1 = declare_op_1.id(); - - let utxo_tree_1 = utxo_tree(vec![utxo_1]); - sdp_ledger = - apply_declare_with_dummies(&utxo_tree_1, sdp_ledger, declare_op_1, &zk_key_1, &config) - .unwrap(); - - // Cross to block 10 (session boundary - start of session 1) - // At this point, the snapshot for next session 2 is taken - (sdp_ledger, _) = sdp_ledger.try_apply_header(&config, &epoch_state).unwrap(); - assert_eq!(sdp_ledger.block_number, 10); - - let active_session = sdp_ledger.get_active_session(service_a).unwrap(); - assert_eq!(active_session.session_n, 1); - assert!(active_session.declarations.is_empty()); - - // Next session 2 should contain declaration_1 (made at block 9) - let next_session = sdp_ledger.get_next_session(service_a).unwrap(); - assert_eq!(next_session.session_n, 2); - assert!(next_session.declarations.contains_key(&declaration_id_1)); - - // Create second declaration at block 10 (first block of session 1) - let zk_key_2 = create_zk_key(2); - let utxo_2 = utxo(); - let declare_op_2 = &SDPDeclareOp { - service_type: service_a, - locked_note_id: utxo_2.id(), - zk_id: zk_key_2.to_public_key(), - provider_id: ProviderId(signing_key.public_key()), - locators: "/ip4/1.1.1.1/udp/0".parse::().unwrap().into(), - }; - let declaration_id_2 = declare_op_2.id(); - - let utxo_tree_2 = utxo_tree(vec![utxo_1, utxo_2]); - sdp_ledger = - apply_declare_with_dummies(&utxo_tree_2, sdp_ledger, declare_op_2, &zk_key_2, &config) - .unwrap(); - - // Next session 2 still only has declaration_1 (snapshot was already taken at - // block 10) - let next_session = sdp_ledger.get_next_session(service_a).unwrap(); - assert_eq!(next_session.session_n, 2); - assert!(next_session.declarations.contains_key(&declaration_id_1)); - assert!(!next_session.declarations.contains_key(&declaration_id_2)); - - // Jump to block 20 (start of session 2) - for _ in 11..20 { - (sdp_ledger, _) = sdp_ledger.try_apply_header(&config, &epoch_state).unwrap(); - } - (sdp_ledger, _) = sdp_ledger.try_apply_header(&config, &epoch_state).unwrap(); - assert_eq!(sdp_ledger.block_number, 20); - - // Active session 2 has declaration_1 (from block 9) - let active_session = sdp_ledger.get_active_session(service_a).unwrap(); - assert_eq!(active_session.session_n, 2); - assert!(active_session.declarations.contains_key(&declaration_id_1)); - assert!(!active_session.declarations.contains_key(&declaration_id_2)); - - // Next session 3 has both declarations (snapshot from block 20) - let next_session = sdp_ledger.get_next_session(service_a).unwrap(); - assert_eq!(next_session.session_n, 3); - assert!(next_session.declarations.contains_key(&declaration_id_1)); - assert!(next_session.declarations.contains_key(&declaration_id_2)); - - // Jump to block 30 (start of session 3) - for _ in 21..30 { - (sdp_ledger, _) = sdp_ledger.try_apply_header(&config, &epoch_state).unwrap(); - } - (sdp_ledger, _) = sdp_ledger.try_apply_header(&config, &epoch_state).unwrap(); - assert_eq!(sdp_ledger.block_number, 30); - - // Active session 3 now has both declarations - let active_session = sdp_ledger.get_active_session(service_a).unwrap(); - assert_eq!(active_session.session_n, 3); - assert!(active_session.declarations.contains_key(&declaration_id_1)); - assert!(active_session.declarations.contains_key(&declaration_id_2)); - } } diff --git a/ledger/src/mantle/sdp/rewards/blend/current_epoch.rs b/ledger/src/mantle/sdp/rewards/blend/current_epoch.rs new file mode 100644 index 000000000..bdd7b2ae1 --- /dev/null +++ b/ledger/src/mantle/sdp/rewards/blend/current_epoch.rs @@ -0,0 +1,213 @@ +use lb_blend_crypto::merkle::sort_nodes_and_build_merkle_tree; +use lb_blend_message::{ + crypto::proofs::PoQVerificationInputsMinusSigningKey, + encap::ProofsVerifier as ProofsVerifierTrait, reward::EpochRandomness, +}; +use lb_blend_proofs::quota::inputs::prove::public::{CoreInputs, LeaderInputs}; +use lb_core::{ + crypto::ZkHash, + mantle::Value, + sdp::{Declaration, ProviderId, ServiceType}, +}; +use lb_cryptarchia_engine::Epoch; +use lb_key_management_system_keys::keys::ZkPublicKey; +use rpds::HashTrieMapSync; +use tracing::debug; + +use crate::{ + EpochState, + mantle::sdp::rewards::blend::{LOG_TARGET, RewardsParameters, target_epoch::TargetEpochState}, +}; + +/// Immutable state of the current epoch. +/// The epoch is `E` if `E-1` is the target epoch for which rewards +/// are being calculated. +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct CurrentEpochState { + epoch: Epoch, + /// Current epoch randomness + epoch_randomness: EpochRandomness, + /// The leader input derived from the current epoch state. + /// These will be used to create the proof verifier after the next + /// epoch update. + leader_input: LeaderInputs, +} + +impl CurrentEpochState { + pub fn new(epoch_state: &EpochState, settings: &RewardsParameters) -> Self { + Self { + epoch: epoch_state.epoch(), + epoch_randomness: (*epoch_state.nonce()).into(), + leader_input: settings.leader_inputs(epoch_state), + } + } + + pub const fn epoch(&self) -> Epoch { + self.epoch + } + + pub const fn epoch_randomness(&self) -> EpochRandomness { + self.epoch_randomness + } +} + +/// Collects income for the current epoch. +/// The current epoch is `E` if `E-1` is the target epoch for which rewards +/// are being calculated. +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct CurrentEpochTracker { + /// Collecting service rewards over the epoch + epoch_income: Value, +} + +impl CurrentEpochTracker { + pub fn new() -> Self { + Self { + epoch_income: Value::default(), + } + } + + pub(crate) const fn add_block_rewards(&self, block_rewards: Value) -> Self { + Self { + epoch_income: self.epoch_income + block_rewards, + } + } + + /// Finalizes the current epoch tracker. + /// + /// It returns [`CurrentEpochTrackerOutput::WithTargetEpoch`] by + /// creating a [`TargetEpochState`] using the collected information, + /// if the network size of the new target epoch is not below the + /// minimum required. Otherwise, it returns + /// [`CurrentEpochTrackerOutput::WithoutTargetEpoch`]. + pub fn finalize( + &self, + current_reward_epoch_state: &CurrentEpochState, + last_epoch_state: &EpochState, + next_epoch_state: &EpochState, + settings: &RewardsParameters, + ) -> CurrentEpochTrackerOutput + where + ProofsVerifier: ProofsVerifierTrait, + { + assert_eq!( + last_epoch_state.epoch, + current_reward_epoch_state.epoch(), + "last_epoch_state.epoch({}) must match current_reward_epoch_state.epoch({})", + last_epoch_state.epoch, + current_reward_epoch_state.epoch(), + ); + assert!( + last_epoch_state.epoch < next_epoch_state.epoch, + "next_epoch_state.epoch({}) must be greater than last_epoch_state.epoch({})", + next_epoch_state.epoch, + last_epoch_state.epoch, + ); + + let declarations = last_epoch_state + .sdp + .declarations() + .iter() + .filter(|(service_type, _)| matches!(service_type, ServiceType::BlendNetwork)) + .flat_map(|(_, declarations)| declarations.values()) + .cloned() + .collect::>(); + + if declarations.len() < settings.minimum_network_size.get() as usize { + debug!(target: LOG_TARGET, "Declaration count({}) is below minimum network size({}). Switching to WithoutTargetEpoch mode", + declarations.len(), + settings.minimum_network_size.get() + ); + return CurrentEpochTrackerOutput::WithoutTargetEpoch { + current_epoch_state: CurrentEpochState::new(next_epoch_state, settings), + current_epoch_tracker: Self::new(), + }; + } + + let (providers, zk_root) = Self::providers_and_zk_root(declarations.iter()); + + let (core_quota, token_evaluation) = settings.core_quota_and_token_evaluation( + providers.size() as u64, + ).expect("evaluation parameters shouldn't overflow. panicking since we can't process the new epoch"); + + let proof_verifier = Self::create_proof_verifier( + current_reward_epoch_state.leader_input, + zk_root, + core_quota, + ); + + CurrentEpochTrackerOutput::WithTargetEpoch { + target_epoch_state: TargetEpochState::new( + last_epoch_state.epoch(), + providers, + token_evaluation, + proof_verifier, + self.epoch_income, + ), + current_epoch_state: CurrentEpochState::new(next_epoch_state, settings), + current_epoch_tracker: Self::new(), + } + } + + #[expect(single_use_lifetimes, reason = "lifetime is required")] + fn providers_and_zk_root<'d>( + declarations: impl Iterator, + ) -> (HashTrieMapSync, ZkHash) { + let mut providers = declarations + .map(|declaration| (declaration.provider_id, declaration.zk_id)) + .collect::>(); + + let zk_root = + sort_nodes_and_build_merkle_tree(&mut providers, |(_, zk_id)| zk_id.into_inner()) + .expect("Should not fail to build merkle tree of core nodes' zk public keys") + .root(); + + let providers = providers + .into_iter() + .enumerate() + .map(|(i, (provider_id, zk_id))| { + ( + provider_id, + ( + zk_id, + u64::try_from(i).expect("provider index must fit in u64"), + ), + ) + }) + .collect(); + + (providers, zk_root) + } + + fn create_proof_verifier( + leader_input: LeaderInputs, + zk_root: ZkHash, + core_quota: u64, + ) -> ProofsVerifier { + ProofsVerifier::new(PoQVerificationInputsMinusSigningKey { + core: CoreInputs { + zk_root, + quota: core_quota, + }, + leader: leader_input, + }) + } +} + +/// Result of finalizing the [`CurrentEpochTracker`]. +pub enum CurrentEpochTrackerOutput { + /// Target epoch has been built with the information collected by + /// the current epoch tracker. + /// Also, the new current epoch state and tracker have been initialized. + WithTargetEpoch { + target_epoch_state: TargetEpochState, + current_epoch_state: CurrentEpochState, + current_epoch_tracker: CurrentEpochTracker, + }, + /// No target epoch has been built because the network size in the + /// epoch is below the minimum required. + WithoutTargetEpoch { + current_epoch_state: CurrentEpochState, + current_epoch_tracker: CurrentEpochTracker, + }, +} diff --git a/ledger/src/mantle/sdp/rewards/blend/current_session.rs b/ledger/src/mantle/sdp/rewards/blend/current_session.rs deleted file mode 100644 index e98eda42f..000000000 --- a/ledger/src/mantle/sdp/rewards/blend/current_session.rs +++ /dev/null @@ -1,213 +0,0 @@ -use lb_blend_crypto::merkle::sort_nodes_and_build_merkle_tree; -use lb_blend_message::{ - crypto::proofs::PoQVerificationInputsMinusSigningKey, - encap::ProofsVerifier as ProofsVerifierTrait, reward::SessionRandomness, -}; -use lb_blend_proofs::quota::inputs::prove::public::{CoreInputs, LeaderInputs}; -use lb_core::{ - crypto::ZkHash, - mantle::Value, - sdp::{ProviderId, SessionNumber}, -}; -use lb_cryptarchia_engine::Epoch; -use lb_key_management_system_keys::keys::ZkPublicKey; -use rpds::HashTrieMapSync; -use tracing::debug; - -use crate::{ - EpochState, - mantle::sdp::{ - SessionState, - rewards::blend::{LOG_TARGET, RewardsParameters, target_session::TargetSessionState}, - }, -}; - -/// Immutable state of the current session. -/// The current session is `s` if `s-1` is the target session for which rewards -/// are being calculated. -#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct CurrentSessionState { - /// Current session randomness - session_randomness: SessionRandomness, -} - -impl CurrentSessionState { - const fn new(session_randomness: SessionRandomness) -> Self { - Self { session_randomness } - } - - pub const fn session_randomness(&self) -> SessionRandomness { - self.session_randomness - } -} - -/// Collects epoch states seen in the current session. -/// The current session is `s` if `s-1` is the target session for which rewards -/// are being calculated. -#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct CurrentSessionTracker { - /// Collecting leader inputs derived from epoch states seen in the current - /// session. These will be used to create proof verifiers after the next - /// session update. - leader_inputs: HashTrieMapSync, - /// Collecting service rewards over the session - session_income: Value, -} - -impl CurrentSessionTracker { - pub fn new(first_epoch_state: &EpochState, settings: &RewardsParameters) -> Self { - Self { - leader_inputs: std::iter::once(( - first_epoch_state.epoch, - settings.leader_inputs(first_epoch_state), - )) - .collect(), - session_income: Value::default(), - } - } - - pub fn collect_epoch(&self, epoch_state: &EpochState, settings: &RewardsParameters) -> Self { - Self { - leader_inputs: self - .leader_inputs - .insert(epoch_state.epoch, settings.leader_inputs(epoch_state)), - session_income: self.session_income, - } - } - - pub(crate) fn add_block_rewards(&self, block_rewards: Value) -> Self { - Self { - leader_inputs: self.leader_inputs.clone(), - session_income: self.session_income + block_rewards, - } - } - - /// Finalizes the current session tracker. - /// - /// It returns [`CurrentSessionTrackerOutput::WithTargetSession`] by - /// creating a [`TargetSessionState`] using the collected information, - /// if the network size of the new target session is not below the - /// minimum required. Otherwise, it returns - /// [`CurrentSessionTrackerOutput::WithoutTargetSession`]. - pub fn finalize( - &self, - last_active_session_state: &SessionState, - next_session_first_epoch_state: &EpochState, - settings: &RewardsParameters, - ) -> CurrentSessionTrackerOutput - where - ProofsVerifier: ProofsVerifierTrait, - { - if last_active_session_state.declarations.size() - < settings.minimum_network_size.get() as usize - { - debug!(target: LOG_TARGET, "Declaration count({}) is below minimum network size({}). Switching to WithoutTargetSession mode", - last_active_session_state.declarations.size(), - settings.minimum_network_size.get() - ); - return CurrentSessionTrackerOutput::WithoutTargetSession(Self::new( - next_session_first_epoch_state, - settings, - )); - } - - let (providers, zk_root) = Self::providers_and_zk_root(last_active_session_state); - - let (core_quota, token_evaluation) = settings.core_quota_and_token_evaluation( - providers.size() as u64, - ).expect("evaluation parameters shouldn't overflow. panicking since we can't process the new session"); - - let proof_verifiers = Self::create_proof_verifiers( - self.leader_inputs.values().copied(), - last_active_session_state.session_n, - zk_root, - core_quota, - ); - - CurrentSessionTrackerOutput::WithTargetSession { - target_session_state: TargetSessionState::new( - last_active_session_state.session_n, - providers, - token_evaluation, - proof_verifiers, - self.session_income, - ), - current_session_state: CurrentSessionState::new(SessionRandomness::new( - last_active_session_state.session_n + 1, - &next_session_first_epoch_state.nonce, - )), - current_session_tracker: Self::new(next_session_first_epoch_state, settings), - } - } - - fn providers_and_zk_root( - session_state: &SessionState, - ) -> (HashTrieMapSync, ZkHash) { - let mut providers = session_state - .declarations - .values() - .map(|declaration| (declaration.provider_id, declaration.zk_id)) - .collect::>(); - - let zk_root = - sort_nodes_and_build_merkle_tree(&mut providers, |(_, zk_id)| zk_id.into_inner()) - .expect("Should not fail to build merkle tree of core nodes' zk public keys") - .root(); - - let providers = providers - .into_iter() - .enumerate() - .map(|(i, (provider_id, zk_id))| { - ( - provider_id, - ( - zk_id, - u64::try_from(i).expect("provider index must fit in u64"), - ), - ) - }) - .collect(); - - (providers, zk_root) - } - - fn create_proof_verifiers( - leader_inputs: impl Iterator, - session: SessionNumber, - zk_root: ZkHash, - core_quota: u64, - ) -> Vec { - leader_inputs - .map(|leader| { - ProofsVerifier::new(PoQVerificationInputsMinusSigningKey { - session, - core: CoreInputs { - zk_root, - quota: core_quota, - }, - leader, - }) - }) - .collect() - } - - #[cfg(test)] - pub fn epoch_count(&self) -> usize { - self.leader_inputs.size() - } -} - -/// Result of finalizing the [`CurrentSessionTracker`]. -pub enum CurrentSessionTrackerOutput { - /// Target session has been built with the information collected by - /// the current session tracker. - /// Also, the new current session state and tracker have been initialized. - WithTargetSession { - target_session_state: TargetSessionState, - current_session_state: CurrentSessionState, - current_session_tracker: CurrentSessionTracker, - }, - /// No target session has been built because the network size in the - /// session is below the minimum required. - WithoutTargetSession(CurrentSessionTracker), -} diff --git a/ledger/src/mantle/sdp/rewards/blend/mod.rs b/ledger/src/mantle/sdp/rewards/blend/mod.rs index daa0b79a3..30d579e13 100644 --- a/ledger/src/mantle/sdp/rewards/blend/mod.rs +++ b/ledger/src/mantle/sdp/rewards/blend/mod.rs @@ -1,5 +1,5 @@ -mod current_session; -mod target_session; +mod current_epoch; +mod target_epoch; use std::{fmt::Debug, num::NonZeroU64}; @@ -9,7 +9,6 @@ use lb_blend_message::{ use lb_blend_proofs::quota::inputs::prove::public::LeaderInputs; use lb_core::{ blend::core_quota, - block::BlockNumber, mantle::{Utxo, Value}, sdp::{ActivityMetadata, ProviderId, ServiceParameters}, }; @@ -17,16 +16,11 @@ use lb_utils::math::NonNegativeF64; use crate::{ EpochState, - mantle::sdp::{ - SessionState, - rewards::{ - Error, - blend::{ - current_session::{ - CurrentSessionState, CurrentSessionTracker, CurrentSessionTrackerOutput, - }, - target_session::{TargetSessionState, TargetSessionTracker}, - }, + mantle::sdp::rewards::{ + Error, + blend::{ + current_epoch::{CurrentEpochState, CurrentEpochTracker, CurrentEpochTrackerOutput}, + target_epoch::{TargetEpochState, TargetEpochTracker}, }, }, }; @@ -34,23 +28,24 @@ use crate::{ const LOG_TARGET: &str = "ledger::mantle::rewards::blend"; /// Tracks Blend rewards based on activity proofs submitted by providers. -/// Activity proofs for the session `s-1` must be submitted during session `s`. +/// Activity proofs for the epoch `E-1` must be submitted during epoch `E`. #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum Rewards { - /// State before the first target session is finalized, or if the target - /// session has less than the minimum required number of declarations. + /// State before the first target epoch is finalized, or if the target + /// epoch has less than the minimum required number of declarations. /// No activity messages are accepted in this state. - WithoutTargetSession { - current_session_tracker: CurrentSessionTracker, + WithoutTargetEpoch { + current_epoch_state: CurrentEpochState, + current_epoch_tracker: CurrentEpochTracker, }, - /// State after a new target session `s-1` is finalized. - /// This tracks activity proofs for the target session `s-1` submitted - /// during the current session `s`. - WithTargetSession { - target_session_state: TargetSessionState, - target_session_tracker: Box, - current_session_state: CurrentSessionState, - current_session_tracker: CurrentSessionTracker, + /// State after a new target epoch `E-1` is finalized. + /// This tracks activity proofs for the target epoch `E-1` submitted + /// during the current epoch `E`. + WithTargetEpoch { + target_epoch_state: TargetEpochState, + target_epoch_tracker: Box, + current_epoch_state: CurrentEpochState, + current_epoch_tracker: CurrentEpochTracker, }, } @@ -64,85 +59,85 @@ where &self, provider_id: ProviderId, metadata: &ActivityMetadata, - _block_number: BlockNumber, params: &Self::Params, ) -> Result { match self { - Self::WithoutTargetSession { .. } => { + Self::WithoutTargetEpoch { .. } => { // Reject all activity messages. - Err(Error::TargetSessionNotSet) + Err(Error::TargetEpochNotSet) } - Self::WithTargetSession { - target_session_state, - target_session_tracker, - current_session_state, - current_session_tracker, + Self::WithTargetEpoch { + target_epoch_state, + target_epoch_tracker, + current_epoch_state, + current_epoch_tracker, } => { let ActivityMetadata::Blend(proof) = metadata; - let (zk_id, hamming_distance) = target_session_state.verify_proof( + let (zk_id, hamming_distance) = target_epoch_state.verify_proof( &provider_id, proof, - current_session_state, + current_epoch_state, params, )?; - let target_session_tracker = target_session_tracker.insert( + let target_epoch_tracker = target_epoch_tracker.insert( provider_id, - target_session_state.session_number(), + target_epoch_state.epoch(), zk_id, hamming_distance, )?; - Ok(Self::WithTargetSession { - target_session_state: target_session_state.clone(), - target_session_tracker: Box::new(target_session_tracker), - current_session_state: current_session_state.clone(), - current_session_tracker: current_session_tracker.clone(), + Ok(Self::WithTargetEpoch { + target_epoch_state: target_epoch_state.clone(), + target_epoch_tracker: Box::new(target_epoch_tracker), + current_epoch_state: current_epoch_state.clone(), + current_epoch_tracker: current_epoch_tracker.clone(), }) } } } - fn update_session( + fn update_epoch( &self, - last_active: &SessionState, - next_session_first_epoch_state: &EpochState, + last_epoch_state: &EpochState, + next_epoch_state: &EpochState, _config: &ServiceParameters, params: &Self::Params, ) -> (Self, Vec) { match self { - Self::WithoutTargetSession { - current_session_tracker, + Self::WithoutTargetEpoch { + current_epoch_state, + current_epoch_tracker, } => ( - Self::from_current_session_tracker_output( - current_session_tracker.finalize( - last_active, - next_session_first_epoch_state, + Self::from_current_epoch_tracker_output( + current_epoch_tracker.finalize( + current_epoch_state, + last_epoch_state, + next_epoch_state, params, ), - TargetSessionTracker::new(), + TargetEpochTracker::new(), ), Vec::new(), ), - Self::WithTargetSession { - target_session_state, - target_session_tracker, - current_session_tracker, - .. + Self::WithTargetEpoch { + target_epoch_state, + target_epoch_tracker, + current_epoch_state, + current_epoch_tracker, } => { - let (target_session_tracker, rewards) = target_session_tracker.finalize( - target_session_state.session_number(), - target_session_state.session_income(), - ); + let (target_epoch_tracker, rewards) = + target_epoch_tracker.finalize(target_epoch_state); - let new_state = Self::from_current_session_tracker_output( - current_session_tracker.finalize( - last_active, - next_session_first_epoch_state, + let new_state = Self::from_current_epoch_tracker_output( + current_epoch_tracker.finalize( + current_epoch_state, + last_epoch_state, + next_epoch_state, params, ), - target_session_tracker, + target_epoch_tracker, ); (new_state, rewards) @@ -150,44 +145,25 @@ where } } - fn update_epoch(&self, epoch_state: &EpochState, params: &Self::Params) -> Self { - match self { - Self::WithoutTargetSession { - current_session_tracker, - } => Self::WithoutTargetSession { - current_session_tracker: current_session_tracker.collect_epoch(epoch_state, params), - }, - Self::WithTargetSession { - target_session_state, - target_session_tracker, - current_session_state, - current_session_tracker, - } => Self::WithTargetSession { - target_session_state: target_session_state.clone(), - target_session_tracker: target_session_tracker.clone(), - current_session_state: current_session_state.clone(), - current_session_tracker: current_session_tracker.collect_epoch(epoch_state, params), - }, - } - } - fn add_income(&self, income: Value) -> Self { match self { - Self::WithoutTargetSession { - current_session_tracker, - } => Self::WithoutTargetSession { - current_session_tracker: current_session_tracker.add_block_rewards(income), + Self::WithoutTargetEpoch { + current_epoch_state, + current_epoch_tracker, + } => Self::WithoutTargetEpoch { + current_epoch_state: current_epoch_state.clone(), + current_epoch_tracker: current_epoch_tracker.add_block_rewards(income), }, - Self::WithTargetSession { - target_session_state, - target_session_tracker, - current_session_state, - current_session_tracker, - } => Self::WithTargetSession { - target_session_state: target_session_state.clone(), - target_session_tracker: target_session_tracker.clone(), - current_session_state: current_session_state.clone(), - current_session_tracker: current_session_tracker.add_block_rewards(income), + Self::WithTargetEpoch { + target_epoch_state, + target_epoch_tracker, + current_epoch_state, + current_epoch_tracker, + } => Self::WithTargetEpoch { + target_epoch_state: target_epoch_state.clone(), + target_epoch_tracker: target_epoch_tracker.clone(), + current_epoch_state: current_epoch_state.clone(), + current_epoch_tracker: current_epoch_tracker.add_block_rewards(income), }, } } @@ -195,12 +171,14 @@ where impl Rewards { /// Create a new uninitialized [`Rewards`] that doesn't accept activity - /// messages until the first session update. + /// messages until the first epoch update. #[must_use] pub fn new(settings: &RewardsParameters, epoch_state: &EpochState) -> Self { - let current_session_tracker = CurrentSessionTracker::new(epoch_state, settings); - Self::WithoutTargetSession { - current_session_tracker, + let current_epoch_state = CurrentEpochState::new(epoch_state, settings); + let current_epoch_tracker = CurrentEpochTracker::new(); + Self::WithoutTargetEpoch { + current_epoch_state, + current_epoch_tracker, } } } @@ -209,33 +187,35 @@ impl Rewards where ProofsVerifier: ProofsVerifierTrait + Clone + Debug + PartialEq + Send + Sync, { - fn from_current_session_tracker_output( - current_session_output: CurrentSessionTrackerOutput, - target_session_tracker: TargetSessionTracker, + fn from_current_epoch_tracker_output( + current_epoch_output: CurrentEpochTrackerOutput, + target_epoch_tracker: TargetEpochTracker, ) -> Self { - match current_session_output { - CurrentSessionTrackerOutput::WithTargetSession { - target_session_state, - current_session_state, - current_session_tracker, - } => Self::WithTargetSession { - target_session_state, - target_session_tracker: Box::new(target_session_tracker), - current_session_state, - current_session_tracker, + match current_epoch_output { + CurrentEpochTrackerOutput::WithTargetEpoch { + target_epoch_state, + current_epoch_state, + current_epoch_tracker, + } => Self::WithTargetEpoch { + target_epoch_state, + target_epoch_tracker: Box::new(target_epoch_tracker), + current_epoch_state, + current_epoch_tracker, + }, + CurrentEpochTrackerOutput::WithoutTargetEpoch { + current_epoch_state, + current_epoch_tracker, + } => Self::WithoutTargetEpoch { + current_epoch_state, + current_epoch_tracker, }, - CurrentSessionTrackerOutput::WithoutTargetSession(current_session_tracker) => { - Self::WithoutTargetSession { - current_session_tracker, - } - } } } } #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct RewardsParameters { - pub rounds_per_session: NonZeroU64, + pub rounds_per_epoch: NonZeroU64, pub message_frequency_per_round: NonNegativeF64, pub num_blend_layers: NonZeroU64, pub data_replication_factor: u64, @@ -249,7 +229,7 @@ impl RewardsParameters { num_core_nodes: u64, ) -> Result<(u64, BlendingTokenEvaluation), lb_blend_message::reward::Error> { let core_quota = core_quota( - self.rounds_per_session, + self.rounds_per_epoch, self.message_frequency_per_round, self.num_blend_layers, num_core_nodes as usize, @@ -290,24 +270,21 @@ mod tests { crypto::ZkHash, sdp::{ServiceType, blend}, }; - use lb_groth16::Field as _; + use lb_groth16::{Field as _, Fr}; use lb_key_management_system_keys::keys::{Ed25519Key, Ed25519PublicKey}; use super::*; use crate::mantle::sdp::rewards::{ Rewards as _, - test_utils::{ - create_provider_id, create_service_parameters, create_test_session_state, - dummy_epoch_state, dummy_epoch_state_with, - }, + test_utils::{create_epoch_state, create_provider_id, create_service_parameters}, }; fn create_blend_rewards_params( - rounds_per_session: u64, + rounds_per_epoch: u64, minimum_network_size: u64, ) -> RewardsParameters { RewardsParameters { - rounds_per_session: rounds_per_session.try_into().unwrap(), + rounds_per_epoch: rounds_per_epoch.try_into().unwrap(), message_frequency_per_round: NonNegativeF64::try_from(1.0).unwrap(), num_blend_layers: NonZeroU64::new(3).unwrap(), minimum_network_size: minimum_network_size.try_into().unwrap(), @@ -329,61 +306,61 @@ mod tests { } #[test] - fn test_blend_no_reward_calculated_after_session_0() { - // Create a reward tracker - let epoch_state = dummy_epoch_state(); + fn test_blend_no_reward_calculated_after_epoch0() { + // Create epoch0 with providers let params = create_blend_rewards_params(864_000, 1); - let rewards_tracker = Rewards::::new(¶ms, &epoch_state); - - // Create session_0 with providers - let session_0 = create_test_session_state( + let epoch0 = create_epoch_state( &[create_provider_id(1), create_provider_id(2)], ServiceType::BlendNetwork, - 0, - ); - - // Update session from 0 to 1 - let (_, rewards) = rewards_tracker.update_session( - &session_0, - &dummy_epoch_state(), - &create_service_parameters(), + 0.into(), + Fr::ZERO, ¶ms, ); - // No rewards should be returned yet because session0 just ended, - // and the reward calculation for the session0 just began. + // Create a reward tracker based on epoch0 + let rewards_tracker = Rewards::::new(¶ms, &epoch0); + + // Update epoch from 0 to 1 + let epoch1 = EpochState { + epoch: 1.into(), + nonce: Fr::ONE, + ..epoch0.clone() + }; + let (_, rewards) = + rewards_tracker.update_epoch(&epoch0, &epoch1, &create_service_parameters(), ¶ms); + + // No rewards should be returned yet because epoch0 just ended, + // and the reward calculation for the epoch0 just began. assert_eq!(rewards.len(), 0); } #[test] fn test_rewards_with_no_activity_proofs() { - // Create a reward tracker, and update session from 0 to 1. + // Create a reward tracker, and update epoch from 0 to 1. let config = create_service_parameters(); - let epoch_state = dummy_epoch_state(); let params = create_blend_rewards_params(864_000, 1); - let (rewards_tracker, _) = - Rewards::::new(¶ms, &epoch_state).update_session( - &create_test_session_state( - &[create_provider_id(1), create_provider_id(2)], - ServiceType::BlendNetwork, - 0, - ), - &epoch_state, - &config, - ¶ms, - ); - - // Update session from 1 to 2 without any activity proofs submitted. - let (_, rewards) = rewards_tracker.update_session( - &create_test_session_state( - &[create_provider_id(1), create_provider_id(2)], - ServiceType::BlendNetwork, - 1, - ), - &epoch_state, - &config, + let epoch0 = create_epoch_state( + &[create_provider_id(1), create_provider_id(2)], + ServiceType::BlendNetwork, + 0.into(), + Fr::ZERO, ¶ms, ); + let epoch1 = EpochState { + epoch: 1.into(), + nonce: Fr::ONE, + ..epoch0.clone() + }; + let (rewards_tracker, _) = Rewards::::new(¶ms, &epoch0) + .update_epoch(&epoch0, &epoch1, &config, ¶ms); + + // Update epoch from 1 to 2 without any activity proofs submitted. + let epoch2 = EpochState { + epoch: 2.into(), + nonce: ZkHash::from(2), + ..epoch1.clone() + }; + let (_, rewards) = rewards_tracker.update_epoch(&epoch1, &epoch2, &config, ¶ms); assert_eq!(rewards.len(), 0); } @@ -394,36 +371,36 @@ mod tests { let provider3 = create_provider_id(3); let provider4 = create_provider_id(4); - // Create a reward tracker, accumulate session income during session 0, - // and update session from 0 to 1. + // Create a reward tracker, accumulate epoch income during epoch 0, + // and update epoch from 0 to 1. let config = create_service_parameters(); - let epoch_state = dummy_epoch_state(); let params = create_blend_rewards_params(864_000, 1); - let (rewards_tracker, _) = - Rewards::::new(¶ms, &epoch_state) - .add_income(1000) - .update_session( - &create_test_session_state( - &[provider1, provider2, provider3, provider4], - ServiceType::BlendNetwork, - 0, - ), - &epoch_state, - &config, - ¶ms, - ); + let epoch0 = create_epoch_state( + &[provider1, provider2, provider3, provider4], + ServiceType::BlendNetwork, + 0.into(), + Fr::ZERO, + ¶ms, + ); + let epoch1 = EpochState { + epoch: 1.into(), + nonce: Fr::ONE, + ..epoch0.clone() + }; + let (rewards_tracker, _) = Rewards::::new(¶ms, &epoch0) + .add_income(1000) + .update_epoch(&epoch0, &epoch1, &config, ¶ms); // provider1 submits an activity proof let rewards_tracker = rewards_tracker .update_active( provider1, &ActivityMetadata::Blend(Box::new(blend::ActivityProof { - session: 0, + epoch: 0.into(), proof_of_quota: new_proof_of_quota_unchecked(1), signing_key: new_signing_key(1), proof_of_selection: new_proof_of_selection_unchecked(1), })), - config.session_duration, ¶ms, ) .unwrap(); @@ -434,12 +411,11 @@ mod tests { .update_active( provider2, &ActivityMetadata::Blend(Box::new(blend::ActivityProof { - session: 0, + epoch: 0.into(), proof_of_quota: new_proof_of_quota_unchecked(2), signing_key: new_signing_key(2), proof_of_selection: new_proof_of_selection_unchecked(2), })), - config.session_duration, ¶ms, ) .unwrap(); @@ -450,40 +426,34 @@ mod tests { provider3, // Use the same proof as provider1 just for testing &ActivityMetadata::Blend(Box::new(blend::ActivityProof { - session: 0, + epoch: 0.into(), proof_of_quota: new_proof_of_quota_unchecked(1), signing_key: new_signing_key(1), proof_of_selection: new_proof_of_selection_unchecked(1), })), - config.session_duration, ¶ms, ) .unwrap(); // provider4 doesn't submit an activity proof. - // Update session from 1 to 2. - let (_, reward_utxos) = rewards_tracker.update_session( - &create_test_session_state( - &[provider1, provider2, provider3, provider4], - ServiceType::BlendNetwork, - 1, - ), - &epoch_state, - &config, - ¶ms, - ); + // Update epoch from 1 to 2. + let epoch2 = EpochState { + epoch: 2.into(), + nonce: ZkHash::from(2), + ..epoch1.clone() + }; + let (_, reward_utxos) = rewards_tracker.update_epoch(&epoch1, &epoch2, &config, ¶ms); assert_eq!(reward_utxos.len(), 3); // except provider4 - let Rewards::WithTargetSession { - target_session_state, - .. + let Rewards::WithTargetEpoch { + target_epoch_state, .. } = rewards_tracker else { panic!("rewards_tracker should be in Initialized state"); }; - let zk_id_to_provider_id = target_session_state + let zk_id_to_provider_id = target_epoch_state .providers() .map(|(provider_id, (zk_id, _))| (*zk_id, *provider_id)) .collect::>(); @@ -514,102 +484,111 @@ mod tests { fn test_blend_duplicate_active_messages() { let provider1 = create_provider_id(1); - // Create a reward tracker, and update session from 0 to 1. + // Create a reward tracker, and update epoch from 0 to 1. let config = create_service_parameters(); - let epoch_state = dummy_epoch_state(); let params = create_blend_rewards_params(864_000, 1); - let (rewards_tracker, _) = - Rewards::::new(¶ms, &epoch_state).update_session( - &create_test_session_state(&[provider1], ServiceType::BlendNetwork, 0), - &epoch_state, - &config, - ¶ms, - ); + let epoch0 = create_epoch_state( + &[provider1], + ServiceType::BlendNetwork, + 0.into(), + Fr::ZERO, + ¶ms, + ); + let epoch1 = EpochState { + epoch: 1.into(), + nonce: Fr::ONE, + ..epoch0.clone() + }; + let (rewards_tracker, _) = Rewards::::new(¶ms, &epoch0) + .update_epoch(&epoch0, &epoch1, &config, ¶ms); // provider1 submits an activity proof. let rewards_tracker = rewards_tracker .update_active( provider1, &ActivityMetadata::Blend(Box::new(blend::ActivityProof { - session: 0, + epoch: 0.into(), proof_of_quota: new_proof_of_quota_unchecked(1), signing_key: new_signing_key(1), proof_of_selection: new_proof_of_selection_unchecked(1), })), - config.session_duration, ¶ms, ) .unwrap(); - // provider1 submits another activity proof in the same session, + // provider1 submits another activity proof in the same epoch, // which should error. let err = rewards_tracker .update_active( provider1, &ActivityMetadata::Blend(Box::new(blend::ActivityProof { - session: 0, + epoch: 0.into(), proof_of_quota: new_proof_of_quota_unchecked(2), signing_key: new_signing_key(1), proof_of_selection: new_proof_of_selection_unchecked(2), })), - config.session_duration, ¶ms, ) .unwrap_err(); assert_eq!( err, Error::DuplicateActiveMessage { - session: 0, + epoch: 0.into(), provider_id: Box::new(provider1) } ); } #[test] - fn test_blend_invalid_session() { + fn test_blend_invalid_epoch() { let provider1 = create_provider_id(1); - // Create a reward tracker, and update session from 0 to 1. + // Create a reward tracker, and update epoch from 0 to 1. let config = create_service_parameters(); - let epoch_state = dummy_epoch_state(); let params = create_blend_rewards_params(864_000, 1); - let (rewards_tracker, _) = - Rewards::::new(¶ms, &epoch_state).update_session( - &create_test_session_state(&[provider1], ServiceType::BlendNetwork, 0), - &epoch_state, - &config, - ¶ms, - ); + let epoch0 = create_epoch_state( + &[provider1], + ServiceType::BlendNetwork, + 0.into(), + Fr::ZERO, + ¶ms, + ); + let epoch1 = EpochState { + epoch: 1.into(), + nonce: Fr::ONE, + ..epoch0.clone() + }; + let (rewards_tracker, _) = Rewards::::new(¶ms, &epoch0) + .update_epoch(&epoch0, &epoch1, &config, ¶ms); - // provider1 submits an activity proof with invalid session. + // provider1 submits an activity proof with invalid epoch. let err = rewards_tracker .update_active( provider1, &ActivityMetadata::Blend(Box::new(blend::ActivityProof { - session: 99, + epoch: 99.into(), proof_of_quota: new_proof_of_quota_unchecked(1), signing_key: new_signing_key(1), proof_of_selection: new_proof_of_selection_unchecked(1), })), - config.session_duration, ¶ms, ) .unwrap_err(); assert_eq!( err, - Error::InvalidSession { - expected: 0, - got: 99 + Error::InvalidEpoch { + expected: 0.into(), + got: 99.into() } ); - // No reward should be calculated after session 1. - let (_, rewards) = rewards_tracker.update_session( - &create_test_session_state(&[provider1], ServiceType::BlendNetwork, 1), - &epoch_state, - &config, - ¶ms, - ); + // No reward should be calculated after epoch 1. + let epoch2 = EpochState { + epoch: 2.into(), + nonce: ZkHash::from(2), + ..epoch1.clone() + }; + let (_, rewards) = rewards_tracker.update_epoch(&epoch1, &epoch2, &config, ¶ms); assert_eq!(rewards.len(), 0); } @@ -617,18 +596,24 @@ mod tests { fn test_blend_network_too_small() { let provider1 = create_provider_id(1); - // Create a reward tracker, and update session from 0 to 1. + // Create a reward tracker, and update epoch from 0 to 1. let config = create_service_parameters(); - let epoch_state = dummy_epoch_state(); // Set minimum network size to 2 let params = create_blend_rewards_params(864_000, 2); - let (rewards_tracker, _) = - Rewards::::new(¶ms, &epoch_state).update_session( - &create_test_session_state(&[provider1], ServiceType::BlendNetwork, 0), - &epoch_state, - &config, - ¶ms, - ); + let epoch0 = create_epoch_state( + &[provider1], + ServiceType::BlendNetwork, + 0.into(), + Fr::ZERO, + ¶ms, + ); + let epoch1 = EpochState { + epoch: 1.into(), + nonce: Fr::ONE, + ..epoch0.clone() + }; + let (rewards_tracker, _) = Rewards::::new(¶ms, &epoch0) + .update_epoch(&epoch0, &epoch1, &config, ¶ms); // provider1 submits an activity proof, but it should be rejected // since the network is too small. @@ -636,24 +621,23 @@ mod tests { .update_active( provider1, &ActivityMetadata::Blend(Box::new(blend::ActivityProof { - session: 0, + epoch: 0.into(), proof_of_quota: new_proof_of_quota_unchecked(1), signing_key: new_signing_key(1), proof_of_selection: new_proof_of_selection_unchecked(1), })), - config.session_duration, ¶ms, ) .unwrap_err(); - assert_eq!(err, Error::TargetSessionNotSet); + assert_eq!(err, Error::TargetEpochNotSet); - // No reward should be calculated after session 1. - let (_, rewards) = rewards_tracker.update_session( - &create_test_session_state(&[provider1], ServiceType::BlendNetwork, 1), - &epoch_state, - &config, - ¶ms, - ); + // No reward should be calculated after epoch 1. + let epoch2 = EpochState { + epoch: 2.into(), + nonce: ZkHash::from(2), + ..epoch1.clone() + }; + let (_, rewards) = rewards_tracker.update_epoch(&epoch1, &epoch2, &config, ¶ms); assert_eq!(rewards.len(), 0); } @@ -661,41 +645,46 @@ mod tests { fn test_blend_proof_distance_larger_than_activity_threshold() { let provider1 = create_provider_id(1); - // Create a reward tracker, and update session from 0 to 1. + // Create a reward tracker, and update epoch from 0 to 1. let config = create_service_parameters(); - let epoch_state = dummy_epoch_state_with(0, 9999); let params = create_blend_rewards_params(10, 1); - let (rewards_tracker, _) = - Rewards::::new(¶ms, &epoch_state).update_session( - &create_test_session_state(&[provider1], ServiceType::BlendNetwork, 0), - &epoch_state, - &config, - ¶ms, - ); + let epoch0 = create_epoch_state( + &[provider1], + ServiceType::BlendNetwork, + 0.into(), + ZkHash::from(9999), + ¶ms, + ); + let epoch1 = EpochState { + epoch: 1.into(), + nonce: Fr::ONE, + ..epoch0.clone() + }; + let (rewards_tracker, _) = Rewards::::new(¶ms, &epoch0) + .update_epoch(&epoch0, &epoch1, &config, ¶ms); // provider1 submits an activity proof that is larger than activity threshold. let err = rewards_tracker .update_active( provider1, &ActivityMetadata::Blend(Box::new(blend::ActivityProof { - session: 0, + epoch: 0.into(), proof_of_quota: new_proof_of_quota_unchecked(2), signing_key: new_signing_key(2), proof_of_selection: new_proof_of_selection_unchecked(2), })), - config.session_duration, ¶ms, ) .unwrap_err(); assert_eq!(err, Error::HammingDistanceTooLarge); - // No reward should be calculated after session 1. - let (_, rewards) = rewards_tracker.update_session( - &create_test_session_state(&[provider1], ServiceType::BlendNetwork, 1), - &epoch_state, - &config, - ¶ms, - ); + // No reward should be calculated after epoch 1. + let epoch2 = EpochState { + epoch: 2.into(), + nonce: ZkHash::from(2), + ..epoch1.clone() + }; + let (_, rewards) = rewards_tracker.update_epoch(&epoch1, &epoch2, &config, ¶ms); assert_eq!(rewards.len(), 0); } @@ -703,109 +692,109 @@ mod tests { fn test_blend_invalid_proofs() { let provider1 = create_provider_id(1); - // Create a reward tracker, and update session from 0 to 1. + // Create a reward tracker, and update epoch from 0 to 1. let config = create_service_parameters(); - let epoch_state = dummy_epoch_state(); let params = create_blend_rewards_params(1000, 1); - let (rewards_tracker, _) = - Rewards::::new(¶ms, &epoch_state).update_session( - &create_test_session_state(&[provider1], ServiceType::BlendNetwork, 1), - &epoch_state, - &config, - ¶ms, - ); + let epoch0 = create_epoch_state( + &[provider1], + ServiceType::BlendNetwork, + 0.into(), + Fr::ZERO, + ¶ms, + ); + let epoch1 = EpochState { + epoch: 1.into(), + nonce: Fr::ONE, + ..epoch0.clone() + }; + let (rewards_tracker, _) = Rewards::::new(¶ms, &epoch0) + .update_epoch(&epoch0, &epoch1, &config, ¶ms); // provider1 submits an activity proof, but PoQ/PoSel verification fails. let err = rewards_tracker .update_active( provider1, &ActivityMetadata::Blend(Box::new(blend::ActivityProof { - session: 1, + epoch: 0.into(), proof_of_quota: new_proof_of_quota_unchecked(1), signing_key: new_signing_key(1), proof_of_selection: new_proof_of_selection_unchecked(1), })), - config.session_duration, ¶ms, ) .unwrap_err(); assert_eq!(err, Error::InvalidProof); - // No reward should be calculated after session 1. - let (_, rewards) = rewards_tracker.update_session( - &create_test_session_state(&[provider1], ServiceType::BlendNetwork, 2), - &dummy_epoch_state(), - &config, - ¶ms, - ); + // No reward should be calculated after epoch 1. + let epoch2 = EpochState { + epoch: 2.into(), + nonce: ZkHash::from(2), + ..epoch1.clone() + }; + let (_, rewards) = rewards_tracker.update_epoch(&epoch1, &epoch2, &config, ¶ms); assert_eq!(rewards.len(), 0); } #[test] - fn test_blend_epoch_updates() { - // Create a reward tracker, and update session from 0 to 1. - let params = create_blend_rewards_params(1000, 1); - let rewards_tracker = Rewards::::new( - ¶ms, - // Set 0 to epoch nonce, to make a proof verifier that always fails. - &dummy_epoch_state_with(0, 0), - ); - if let Rewards::WithoutTargetSession { - current_session_tracker, - .. - } = &rewards_tracker - { - assert_eq!(current_session_tracker.epoch_count(), 1); - } else { - panic!("Should not be initialized yet") - } - - // A new epoch received before a new session starts. - // Set non-zero to epoch nonce, to make a proof verifier that always succeed. - let new_epoch = dummy_epoch_state_with(1, 1); - let rewards_tracker = rewards_tracker.update_epoch(&new_epoch, ¶ms); - if let Rewards::WithoutTargetSession { - current_session_tracker, - .. - } = &rewards_tracker - { - assert_eq!(current_session_tracker.epoch_count(), 2); - } else { - panic!("Should not be initialized yet") - } - - // Update session from 0 to 1, with the same epoch state as the last one. - let provider1 = create_provider_id(1); + fn test_blend_proof_verifier_after_epoch_update() { + // Create a reward tracker, and update epoch from 0 to 1. + let provider = create_provider_id(1); let config = create_service_parameters(); - let (rewards_tracker, _) = rewards_tracker.update_session( - &create_test_session_state(&[provider1], ServiceType::BlendNetwork, 0), - &new_epoch, - &config, + let params = create_blend_rewards_params(1000, 1); + let epoch0 = create_epoch_state( + &[provider], + ServiceType::BlendNetwork, + 0.into(), + // Set 0 to epoch nonce, to make a proof verifier that always fails. + Fr::ZERO, ¶ms, ); - if let Rewards::WithTargetSession { - current_session_tracker, - .. - } = &rewards_tracker - { - assert_eq!(current_session_tracker.epoch_count(), 1); - } else { - panic!("Should not be uninitialized"); - } + let epoch1 = EpochState { + epoch: 1.into(), + nonce: Fr::ONE, + ..epoch0.clone() + }; + let (rewards_tracker, _) = Rewards::::new(¶ms, &epoch0) + .update_epoch(&epoch0, &epoch1, &config, ¶ms); - rewards_tracker + // provider submits an activity proof, but rejected due to + // ZeroNonceFailureProofsVerifier + let err = rewards_tracker .update_active( - provider1, + provider, &ActivityMetadata::Blend(Box::new(blend::ActivityProof { - session: 0, + epoch: 0.into(), proof_of_quota: new_proof_of_quota_unchecked(1), signing_key: new_signing_key(1), proof_of_selection: new_proof_of_selection_unchecked(1), })), - config.session_duration, ¶ms, ) - .expect("Proofs must be successfully verified"); + .unwrap_err(); + assert_eq!(err, Error::InvalidProof); + + // Update epoch from 1 to 2 + let epoch2 = EpochState { + epoch: 2.into(), + nonce: ZkHash::from(2), + ..epoch1.clone() + }; + let (rewards_tracker, _) = rewards_tracker.update_epoch(&epoch1, &epoch2, &config, ¶ms); + + // provider submits an activity proof again, and it should be accepted since the + // proof verifier should be updated after epoch update. + rewards_tracker + .update_active( + provider, + &ActivityMetadata::Blend(Box::new(blend::ActivityProof { + epoch: 1.into(), + proof_of_quota: new_proof_of_quota_unchecked(1), + signing_key: new_signing_key(1), + proof_of_selection: new_proof_of_selection_unchecked(1), + })), + ¶ms, + ) + .unwrap(); } #[derive(Debug, Clone, PartialEq)] @@ -818,10 +807,6 @@ mod tests { Self } - fn start_epoch_transition(&mut self, _new_pol_inputs: LeaderInputs) {} - - fn complete_epoch_transition(&mut self) {} - fn verify_proof_of_quota( &self, proof: ProofOfQuota, @@ -851,10 +836,6 @@ mod tests { Self } - fn start_epoch_transition(&mut self, _new_pol_inputs: LeaderInputs) {} - - fn complete_epoch_transition(&mut self) {} - fn verify_proof_of_quota( &self, _proof: ProofOfQuota, @@ -880,13 +861,9 @@ mod tests { fn new(public_inputs: PoQVerificationInputsMinusSigningKey) -> Self { // Fail only if pol_epoch_nonce is ZERO - Self(public_inputs.leader.pol_epoch_nonce == ZkHash::ZERO) + Self(public_inputs.leader.pol_epoch_nonce != Fr::ZERO) } - fn start_epoch_transition(&mut self, _new_pol_inputs: LeaderInputs) {} - - fn complete_epoch_transition(&mut self) {} - fn verify_proof_of_quota( &self, proof: ProofOfQuota, diff --git a/ledger/src/mantle/sdp/rewards/blend/target_session.rs b/ledger/src/mantle/sdp/rewards/blend/target_epoch.rs similarity index 65% rename from ledger/src/mantle/sdp/rewards/blend/target_session.rs rename to ledger/src/mantle/sdp/rewards/blend/target_epoch.rs index a8a32eb9a..0254fe81b 100644 --- a/ledger/src/mantle/sdp/rewards/blend/target_session.rs +++ b/ledger/src/mantle/sdp/rewards/blend/target_epoch.rs @@ -6,57 +6,57 @@ use lb_blend_message::{ }; use lb_core::{ mantle::{Utxo, Value}, - sdp::{ProviderId, ServiceType, SessionNumber}, + sdp::{ProviderId, ServiceType}, }; +use lb_cryptarchia_engine::Epoch; use lb_key_management_system_keys::keys::ZkPublicKey; use rpds::{HashTrieMapSync, HashTrieSetSync}; use crate::mantle::sdp::rewards::{ Error, - blend::{RewardsParameters, current_session::CurrentSessionState}, + blend::{RewardsParameters, current_epoch::CurrentEpochState}, distribute_rewards, }; -/// The immutable state of the target session for which rewards are being -/// calculated. The target session is `s-1` if `s` is the current session. +/// The immutable state of the target epoch for which rewards are being +/// calculated. The target epoch is `E-1` if `E` is the current epoch. #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct TargetSessionState { - /// The target session number - session_number: SessionNumber, - /// The providers in the target session (their public keys and indices). +pub struct TargetEpochState { + /// The target epoch + epoch: Epoch, + /// The providers in the target epoch (their public keys and indices). providers: HashTrieMapSync, - /// Parameters for evaluating activity proofs in the target session + /// Parameters for evaluating activity proofs in the target epoch token_evaluation: BlendingTokenEvaluation, - /// Verifiers for `PoQ` and `PoSel`. - /// These are created from epoch states collected in the target session. - proof_verifiers: Vec, - /// Session incomes stabilized from session `s-1` - session_income: Value, + /// Verifier for `PoQ` and `PoSel` in the target epoch. + proof_verifier: ProofsVerifier, + /// Epoch incomes stabilized from epoch `E-1` + epoch_income: Value, } -impl TargetSessionState { +impl TargetEpochState { pub const fn new( - session_number: SessionNumber, + epoch: Epoch, providers: HashTrieMapSync, token_evaluation: BlendingTokenEvaluation, - proof_verifiers: Vec, - session_income: Value, + proof_verifier: ProofsVerifier, + epoch_income: Value, ) -> Self { Self { - session_number, + epoch, providers, token_evaluation, - proof_verifiers, - session_income, + proof_verifier, + epoch_income, } } - pub const fn session_number(&self) -> SessionNumber { - self.session_number + pub const fn epoch(&self) -> Epoch { + self.epoch } - pub const fn session_income(&self) -> Value { - self.session_income + pub const fn epoch_income(&self) -> Value { + self.epoch_income } pub fn providers(&self) -> impl Iterator { @@ -71,7 +71,7 @@ impl TargetSessionState { } } -impl TargetSessionState +impl TargetEpochState where ProofsVerifier: ProofsVerifierTrait, { @@ -79,13 +79,13 @@ where &self, provider_id: &ProviderId, proof: &lb_core::sdp::blend::ActivityProof, - current_session_state: &CurrentSessionState, + current_epoch_state: &CurrentEpochState, settings: &RewardsParameters, ) -> Result<(ZkPublicKey, HammingDistance), Error> { - if proof.session != self.session_number { - return Err(Error::InvalidSession { - expected: self.session_number, - got: proof.session, + if proof.epoch != self.epoch { + return Err(Error::InvalidEpoch { + expected: self.epoch, + got: proof.epoch, }); } @@ -99,29 +99,22 @@ where .get(provider_id) .ok_or_else(|| Error::UnknownProvider(Box::new(*provider_id)))?; - // Try to verify the proof with each of the available verifiers - let verified_proof = self - .proof_verifiers - .iter() - .find_map(|verifier| { - lb_blend_message::reward::ActivityProof::verify_and_build( - proof, - verifier, - index, - num_providers, - ) - .ok() - }) - .ok_or(Error::InvalidProof)?; + let verified_proof = lb_blend_message::reward::ActivityProof::verify_and_build( + proof, + &self.proof_verifier, + index, + num_providers, + ) + .map_err(|_| Error::InvalidProof)?; tracing::trace!( - "Verifying activity proof {:?} with session randomness: {:?}", + "Verifying activity proof {:?} with epoch randomness: {:?}", verified_proof.token().signing_key(), - current_session_state.session_randomness() + current_epoch_state.epoch_randomness() ); let Some(hamming_distance) = self.token_evaluation.evaluate( verified_proof.token(), - current_session_state.session_randomness(), + current_epoch_state.epoch_randomness(), ) else { return Err(Error::HammingDistanceTooLarge); }; @@ -130,23 +123,23 @@ where } } -/// Tracks activity proofs submitted for the target session whose rewards are -/// being calculated. The target session is `s-1` if `s` is the current session. +/// Tracks activity proofs submitted for the target epoch whose rewards are +/// being calculated. The target epoch is `E-1` if `E` is the current epoch. #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct TargetSessionTracker { - /// Collecting proofs submitted by providers in the target session. +pub struct TargetEpochTracker { + /// Collecting proofs submitted by providers in the target epoch. submitted_proofs: HashTrieMapSync, /// Tracking the minimum Hamming distance among submitted proofs. min_hamming_distance: MinHammingDistance, } -impl Default for TargetSessionTracker { +impl Default for TargetEpochTracker { fn default() -> Self { Self::new() } } -impl TargetSessionTracker { +impl TargetEpochTracker { pub fn new() -> Self { Self { submitted_proofs: HashTrieMapSync::new_sync(), @@ -157,13 +150,13 @@ impl TargetSessionTracker { pub fn insert( &self, provider_id: ProviderId, - session: SessionNumber, + epoch: Epoch, zk_id: ZkPublicKey, hamming_distance: HammingDistance, ) -> Result { if self.submitted_proofs.contains_key(&provider_id) { return Err(Error::DuplicateActiveMessage { - session, + epoch, provider_id: Box::new(provider_id), }); } @@ -177,10 +170,9 @@ impl TargetSessionTracker { }) } - pub fn finalize( + pub fn finalize( &self, - session_number: SessionNumber, - session_income: u64, + target_epoch_state: &TargetEpochState, ) -> (Self, Vec) { if self.submitted_proofs.is_empty() { return (Self::new(), vec![]); @@ -190,7 +182,7 @@ impl TargetSessionTracker { let premium_providers = &self.min_hamming_distance.providers; // Calculate base reward - let base_reward = session_income + let base_reward = target_epoch_state.epoch_income() / (self.submitted_proofs.size() as u64 + premium_providers.size() as u64); // Calculate reward for each provider @@ -206,7 +198,11 @@ impl TargetSessionTracker { ( Self::new(), - distribute_rewards(rewards, session_number, ServiceType::BlendNetwork), + distribute_rewards( + rewards, + target_epoch_state.epoch(), + ServiceType::BlendNetwork, + ), ) } } diff --git a/ledger/src/mantle/sdp/rewards/mod.rs b/ledger/src/mantle/sdp/rewards/mod.rs index f036229ef..b70a29e1b 100644 --- a/ledger/src/mantle/sdp/rewards/mod.rs +++ b/ledger/src/mantle/sdp/rewards/mod.rs @@ -5,16 +5,15 @@ mod test_utils; use std::collections::HashMap; use lb_core::{ - block::BlockNumber, codec::SerializeOp as _, crypto::{Digest, Hash, Hasher}, mantle::{Note, Utxo, Value}, - sdp::{ActivityMetadata, ProviderId, ServiceParameters, ServiceType, SessionNumber}, + sdp::{ActivityMetadata, ProviderId, ServiceParameters, ServiceType}, }; +use lb_cryptarchia_engine::Epoch; use lb_key_management_system_keys::keys::ZkPublicKey; use thiserror::Error; -use super::SessionState; use crate::EpochState; pub type RewardAmount = u64; @@ -22,7 +21,7 @@ pub type RewardAmount = u64; /// Generic trait for service-specific reward calculation. /// /// Each service can implement its own rewards logic by implementing this trait. -/// The rewards object is updated with active messages and session transitions, +/// The rewards object is updated with active messages and epoch transitions, /// and can calculate expected rewards for each provider based on the service's /// internal logic. pub trait Rewards: Clone + PartialEq + Send + Sync + std::fmt::Debug { @@ -37,58 +36,43 @@ pub trait Rewards: Clone + PartialEq + Send + Sync + std::fmt::Debug { &self, declaration_id: ProviderId, metadata: &ActivityMetadata, - block_number: BlockNumber, params: &Self::Params, ) -> Result; - /// Update rewards state when sessions transition and calculate rewards to + /// Update rewards state when epoch transition and calculate rewards to /// distribute. /// - /// Called during session boundaries when active, `past_session`, and - /// next sessions are updated. Returns a map of `ProviderId` to - /// reward amounts for providers eligible for rewards in this session + /// Called during epoch boundaries. Returns a map of `ProviderId` to + /// reward amounts for providers eligible for rewards in this epoch /// transition. /// /// The internal calculation logic is opaque to the SDP ledger and /// determined by the service-specific implementation. - /// - /// # Arguments - /// * `last_active` - The state of the session that just ended. - /// * `next_session_first_epoch_state` - The epoch state corresponding to - /// the 1st block of the session `last_active + 1`. - fn update_session( + fn update_epoch( &self, - last_active: &SessionState, - next_session_first_epoch_state: &EpochState, + // The state of epoch that just ended. + last_epoch_state: &EpochState, + // The state of the new epoch + next_epoch_state: &EpochState, config: &ServiceParameters, params: &Self::Params, ) -> (Self, Vec); - /// Update rewards state when a new epoch begins while the session remains - /// unchanged. - /// - /// If the epoch has already been processed previously, this method performs - /// no update and returns the current state unchanged. - #[must_use] - fn update_epoch(&self, epoch_state: &EpochState, params: &Self::Params) -> Self; #[must_use] fn add_income(&self, income: Value) -> Self; } #[derive(Error, Debug, Clone, PartialEq, Eq)] pub enum Error { - #[error("Target session is not set")] - TargetSessionNotSet, - #[error("Invalid session: expected {expected}, got {got}")] - InvalidSession { - expected: SessionNumber, - got: SessionNumber, - }, + #[error("Target epoch is not set")] + TargetEpochNotSet, + #[error("Invalid epoch: expected {expected}, got {got}")] + InvalidEpoch { expected: Epoch, got: Epoch }, #[error("Invalid opinion length: expected {expected}, got {got}")] InvalidOpinionLength { expected: usize, got: usize }, - #[error("Duplicate active message for session {session}, provider {provider_id:?}")] + #[error("Duplicate active message for epoch {epoch}, provider {provider_id:?}")] DuplicateActiveMessage { - session: SessionNumber, + epoch: Epoch, provider_id: Box, }, #[error("Invalid proof type")] @@ -103,18 +87,18 @@ pub enum Error { /// Creates a deterministic transaction hash for reward distribution. /// -/// The hash is computed from a version constant, session number, and service +/// The hash is computed from a version constant, epoch number, and service /// type, ensuring all nodes produce identical transaction hashes for reward /// notes. -fn create_reward_op_id(session_n: SessionNumber, service_type: ServiceType) -> Hash { +fn create_reward_op_id(epoch: Epoch, service_type: ServiceType) -> Hash { let mut hasher = Hasher::default(); - let session_u8 = session_n.to_le_bytes().to_vec(); + let epoch_u8 = epoch.into_inner().to_le_bytes().to_vec(); let service_type_u8 = service_type .to_bytes() .expect("conversion to bytes should succeed") .to_vec(); ::update(&mut hasher, &service_type_u8); - ::update(&mut hasher, &session_u8); + ::update(&mut hasher, &epoch_u8); hasher.finalize().into() } @@ -127,7 +111,7 @@ fn create_reward_op_id(session_n: SessionNumber, service_type: ServiceType) -> H /// - Filters out 0-value rewards fn distribute_rewards( rewards: HashMap, - session_n: SessionNumber, + epoch: Epoch, service_type: ServiceType, ) -> Vec { let mut sorted_rewards: Vec<(ZkPublicKey, RewardAmount)> = rewards @@ -136,7 +120,7 @@ fn distribute_rewards( .collect(); sorted_rewards.sort_by_key(|(zk_id, _)| *zk_id); - let op_id = create_reward_op_id(session_n, service_type); + let op_id = create_reward_op_id(epoch, service_type); sorted_rewards .into_iter() diff --git a/ledger/src/mantle/sdp/rewards/test_utils.rs b/ledger/src/mantle/sdp/rewards/test_utils.rs index 53d96b5b1..cb28e44e6 100644 --- a/ledger/src/mantle/sdp/rewards/test_utils.rs +++ b/ledger/src/mantle/sdp/rewards/test_utils.rs @@ -1,40 +1,86 @@ +use std::marker::PhantomData; + use lb_core::{ - crypto::ZkHash, + mantle::Note, sdp::{ - Declaration, DeclarationId, Locator, ProviderId, ServiceParameters, ServiceType, - SessionNumber, + Declaration, DeclarationId, Locator, MinStake, ProviderId, ServiceParameters, ServiceType, + locked_notes::LockedNotes, }, }; +use lb_cryptarchia_engine::Epoch; use lb_groth16::{Field as _, Fr}; use lb_key_management_system_keys::keys::{Ed25519Key, ZkPublicKey}; use num_bigint::BigUint; +use rpds::HashTrieMapSync; -use crate::{EpochState, UtxoTree, mantle::sdp::SessionState}; +use crate::{ + EpochState, UtxoTree, + mantle::sdp::{SdpLedger, Service, ServiceState, rewards::blend}, +}; -pub fn create_test_session_state( +pub fn create_epoch_state( provider_ids: &[ProviderId], service_type: ServiceType, - session_n: SessionNumber, -) -> SessionState { + epoch: Epoch, + nonce: Fr, + _params: &blend::RewardsParameters, +) -> EpochState { let mut declarations = rpds::RedBlackTreeMapSync::new_sync(); + let mut locked_notes = LockedNotes::new(); for (i, provider_id) in provider_ids.iter().enumerate() { + let note = Note::new(100, ZkPublicKey::zero()); + let note_id = Fr::from(i as u64).into(); let declaration = Declaration { service_type, provider_id: *provider_id, - locked_note_id: Fr::from(i as u64).into(), + locked_note_id: note_id, locators: "/ip4/1.1.1.1/udp/0".parse::().unwrap().into(), zk_id: ZkPublicKey::new(BigUint::from(i as u64).into()), - created: 0, - active: 0, + created: 0.into(), + active: 0.into(), withdrawn: None, nonce: 0, }; declarations = declarations.insert(DeclarationId([i as u8; 32]), declaration); + locked_notes = locked_notes + .lock( + &MinStake { + threshold: 1, + timestamp: 0, + }, + service_type, + note, + ¬e_id, + ) + .unwrap(); } - SessionState { - declarations, - session_n, - } + + let mut epoch_state = EpochState { + epoch, + nonce, + utxos: UtxoTree::default(), + total_stake: 0, + lottery_0: Fr::ZERO, + lottery_1: Fr::ZERO, + sdp: SdpLedger::new(epoch), + }; + + let ledger = SdpLedger { + services: HashTrieMapSync::new_sync().insert( + ServiceType::BlendNetwork, + Service::BlendNetwork(ServiceState { + declarations, + // TODO: enable after making `rewards` module stable + // rewards: blend::Rewards::new(params, &epoch_state), + _phantom: PhantomData, + }), + ), + locked_notes, + epoch, + }; + + epoch_state.sdp = ledger; + epoch_state } pub fn create_provider_id(byte: u8) -> ProviderId { @@ -46,25 +92,9 @@ pub fn create_provider_id(byte: u8) -> ProviderId { pub fn create_service_parameters() -> ServiceParameters { ServiceParameters { - lock_period: 10, - inactivity_period: 1, - retention_period: 1, - timestamp: 0, - session_duration: 10, - } -} - -pub fn dummy_epoch_state() -> EpochState { - dummy_epoch_state_with(0, 0) -} - -pub fn dummy_epoch_state_with(epoch: u32, nonce: u64) -> EpochState { - EpochState { - epoch: epoch.into(), - nonce: ZkHash::from(BigUint::from(nonce)), - utxos: UtxoTree::default(), - total_stake: 0, - lottery_0: Fr::ZERO, - lottery_1: Fr::ZERO, + lock_period: 10.into(), + inactivity_period: 1.into(), + retention_period: 1.into(), + epoch: 0.into(), } } diff --git a/nodes/node/binary/src/config/blend/deployment.rs b/nodes/node/binary/src/config/blend/deployment.rs index 42eeaf483..70d9fdf62 100644 --- a/nodes/node/binary/src/config/blend/deployment.rs +++ b/nodes/node/binary/src/config/blend/deployment.rs @@ -24,27 +24,13 @@ impl Settings { *slot_duration } - /// Number of rounds per session, calculated as the number of slots per + /// Number of rounds per epoch, calculated as the number of slots per /// epoch, correctly scaled to account for the slot/round ratio. #[must_use] - pub fn rounds_per_session(&self, slots_per_epoch: u64, slot_duration: &Duration) -> NonZeroU64 { + pub fn rounds_per_epoch(&self, slots_per_epoch: u64, slot_duration: &Duration) -> NonZeroU64 { ((slots_per_epoch * slot_duration.as_secs()) / self.round_duration(slot_duration).as_secs()) .try_into() - .expect("There must be at least one round per session.") - } - - /// Number of rounds per interval, calculated as the average number of slots - /// per block (slot activation threshold), correctly scaled to account for - /// the slot/round ratio. - #[must_use] - pub fn rounds_per_interval( - &self, - slots_per_block: u64, - slot_duration: &Duration, - ) -> NonZeroU64 { - ((slots_per_block * slot_duration.as_secs()) / self.round_duration(slot_duration).as_secs()) - .try_into() - .expect("There must be at least one round per interval.") + .expect("There must be at least one round per epoch.") } /// Number of rounds per observation window. @@ -65,36 +51,17 @@ impl Settings { .unwrap() } - /// Number of rounds per session transition period. - /// - /// The Blend spec defines this as roughly the same as - /// [`rounds_per_interval`]. - #[must_use] - pub fn rounds_per_session_transition_period( - &self, - slots_per_block: u64, - slot_duration: &Duration, - ) -> NonZeroU64 { - self.rounds_per_interval(slots_per_block, slot_duration) - } - - /// Number of rounds per epoch transition period. + /// Duration of the epoch transition period. /// /// The Blend spec defines this as roughly the same time it takes to propose /// a new block. #[must_use] - pub fn slots_per_epoch_transition_period( + pub const fn epoch_transition( &self, slots_per_block: u64, slot_duration: &Duration, - ) -> NonZeroU64 { - let rounds_per_session_transition_period = - self.rounds_per_session_transition_period(slots_per_block, slot_duration); - ((self.round_duration(slot_duration).as_secs() - * rounds_per_session_transition_period.get()) - / slot_duration.as_secs()) - .try_into() - .expect("There must be at least one slot per epoch transition period.") + ) -> Duration { + Duration::from_secs(slot_duration.as_secs() * slots_per_block) } #[must_use] @@ -109,7 +76,7 @@ impl Settings { message_frequency_per_round: self.core.scheduler.cover.message_frequency_per_round, minimum_network_size: self.common.minimum_network_size.into(), num_blend_layers: self.common.num_blend_layers, - rounds_per_session: self.rounds_per_session( + rounds_per_epoch: self.rounds_per_epoch( cryptarchia_deployment.slots_per_epoch(), &time_deployment.slot_duration, ), @@ -161,9 +128,6 @@ pub struct SchedulerSettings { pub struct CoverTrafficSettings { /// `F_c`: frequency at which cover messages are generated per round. pub message_frequency_per_round: NonNegativeF64, - // `max`: safety buffer length, expressed in intervals - // TODO: Can we derive this? - pub intervals_for_safety_buffer: u64, } #[derive(Serialize, Deserialize, Clone, Debug)] @@ -181,12 +145,9 @@ mod tests { #[test] fn blend_devnet() { const EXPECTED_ROUND_DURATION: Duration = Duration::from_secs(1); - const EXPECTED_ROUNDS_PER_SESSION: NonZeroU64 = NonZeroU64::new(6_000).unwrap(); - const EXPECTED_ROUNDS_PER_INTERVAL: NonZeroU64 = NonZeroU64::new(20).unwrap(); + const EXPECTED_ROUNDS_PER_EPOCH: NonZeroU64 = NonZeroU64::new(6_000).unwrap(); const EXPECTED_ROUNDS_PER_OBSERVATION_WINDOW: NonZeroU64 = NonZeroU64::new(10).unwrap(); - const EXPECTED_ROUNDS_PER_SESSION_TRANSITION_PERIOD: NonZeroU64 = - NonZeroU64::new(20).unwrap(); - const EXPECTED_SLOTS_PER_EPOCH_TRANSITION_PERIOD: NonZeroU64 = NonZeroU64::new(20).unwrap(); + const EXPECTED_EPOCH_TRANSITION_PERIOD: Duration = Duration::from_secs(20); let deployment: DeploymentSettings = WellKnownDeployment::Devnet.into(); @@ -199,15 +160,8 @@ mod tests { assert_eq!( deployment .blend - .rounds_per_session(slots_per_epoch, &slot_duration), - EXPECTED_ROUNDS_PER_SESSION - ); - - assert_eq!( - deployment - .blend - .rounds_per_interval(slots_per_block, &slot_duration), - EXPECTED_ROUNDS_PER_INTERVAL + .rounds_per_epoch(slots_per_epoch, &slot_duration), + EXPECTED_ROUNDS_PER_EPOCH ); assert_eq!( @@ -218,15 +172,8 @@ mod tests { assert_eq!( deployment .blend - .rounds_per_session_transition_period(slots_per_block, &slot_duration), - EXPECTED_ROUNDS_PER_SESSION_TRANSITION_PERIOD - ); - - assert_eq!( - deployment - .blend - .slots_per_epoch_transition_period(slots_per_block, &slot_duration), - EXPECTED_SLOTS_PER_EPOCH_TRANSITION_PERIOD + .epoch_transition(slots_per_block, &slot_duration), + EXPECTED_EPOCH_TRANSITION_PERIOD ); } } diff --git a/nodes/node/binary/src/config/blend/mod.rs b/nodes/node/binary/src/config/blend/mod.rs index 642803018..e1012802f 100644 --- a/nodes/node/binary/src/config/blend/mod.rs +++ b/nodes/node/binary/src/config/blend/mod.rs @@ -65,20 +65,14 @@ impl ServiceConfig { minimum_network_size: self.deployment.common.minimum_network_size.into(), recovery_path_prefix, time: TimingSettings { - epoch_transition_period_in_slots: self + epoch_transition_period: self .deployment - .slots_per_epoch_transition_period(slots_per_block, &slot_duration), + .epoch_transition(slots_per_block, &slot_duration), round_duration: self.deployment.round_duration(&slot_duration), - rounds_per_interval: self - .deployment - .rounds_per_interval(slots_per_block, &slot_duration), rounds_per_observation_window: self.deployment.rounds_per_observation_window(), - rounds_per_session: self + rounds_per_epoch: self .deployment - .rounds_per_session(slots_per_epoch, &slot_duration), - rounds_per_session_transition_period: self - .deployment - .rounds_per_session_transition_period(slots_per_block, &slot_duration), + .rounds_per_epoch(slots_per_epoch, &slot_duration), }, data_replication_factor: self.deployment.common.data_replication_factor, }, @@ -103,12 +97,6 @@ impl ServiceConfig { }, scheduler: SchedulerSettings { cover: CoverTrafficSettings { - intervals_for_safety_buffer: self - .deployment - .core - .scheduler - .cover - .intervals_for_safety_buffer, message_frequency_per_round: self .deployment .core diff --git a/nodes/node/binary/src/config/cryptarchia/deployment.rs b/nodes/node/binary/src/config/cryptarchia/deployment.rs index 208d6d25c..76d8cbe30 100644 --- a/nodes/node/binary/src/config/cryptarchia/deployment.rs +++ b/nodes/node/binary/src/config/cryptarchia/deployment.rs @@ -1,9 +1,10 @@ use core::num::{NonZero, NonZeroU32}; use std::collections::HashMap; +use lb_chain_service::Epoch; use lb_core::{ - block::{BlockNumber, genesis::GenesisBlock}, - sdp::{MinStake, ServiceType}, + block::genesis::GenesisBlock, + sdp::{MinStake, NumberOfEpochs, ServiceType}, }; use lb_cryptarchia_engine::{ Config as ConsensusConfig, average_slots_for_blocks, base_period_length, time::epoch_length, @@ -83,13 +84,11 @@ pub struct SdpConfig { pub min_stake: MinStake, } -// The same as `lb_core::sdp::ServiceParameters`, minus the -// `session_duration` values which are calculated from the other values -// provided. +// The same as `lb_core::sdp::ServiceParameters`. #[derive(Serialize, Deserialize, Debug, Clone)] pub struct ServiceParameters { - pub lock_period: u64, - pub inactivity_period: u64, - pub retention_period: u64, - pub timestamp: BlockNumber, + pub lock_period: NumberOfEpochs, + pub inactivity_period: NumberOfEpochs, + pub retention_period: NumberOfEpochs, + pub epoch: Epoch, } diff --git a/nodes/node/binary/src/config/cryptarchia/mod.rs b/nodes/node/binary/src/config/cryptarchia/mod.rs index 57a41284b..143c5d6a7 100644 --- a/nodes/node/binary/src/config/cryptarchia/mod.rs +++ b/nodes/node/binary/src/config/cryptarchia/mod.rs @@ -35,8 +35,6 @@ impl ServiceConfig { lb_chain_network_service::ChainNetworkSettings, lb_chain_leader_service::LeaderSettings<(), Libp2pBroadcastSettings>, ) { - let blocks_per_session = self.deployment.blocks_per_epoch(); - let ledger_config = lb_ledger::Config { consensus_config: self.deployment.consensus_config(), epoch_config: EpochConfig { @@ -62,11 +60,10 @@ impl ServiceConfig { ( service_type, ServiceParameters { - session_duration: blocks_per_session, inactivity_period: service_params.inactivity_period, lock_period: service_params.lock_period, retention_period: service_params.retention_period, - timestamp: service_params.timestamp, + epoch: service_params.epoch, }, ) }) diff --git a/nodes/node/binary/src/config/deployment/devnet/deployment.yaml b/nodes/node/binary/src/config/deployment/devnet/deployment.yaml index 236efe90c..c3c15215c 100644 --- a/nodes/node/binary/src/config/deployment/devnet/deployment.yaml +++ b/nodes/node/binary/src/config/deployment/devnet/deployment.yaml @@ -8,7 +8,6 @@ blend: scheduler: cover: message_frequency_per_round: 1.0 - intervals_for_safety_buffer: 100 delayer: maximum_release_delay_in_rounds: 1 minimum_messages_coefficient: 1 @@ -34,7 +33,7 @@ cryptarchia: lock_period: 10 inactivity_period: 1 retention_period: 1 - timestamp: 0 + epoch: 0 min_stake: threshold: 1 timestamp: 0 diff --git a/nodes/node/binary/src/generic_services/blend/mod.rs b/nodes/node/binary/src/generic_services/blend/mod.rs index be8ffd09c..544302060 100644 --- a/nodes/node/binary/src/generic_services/blend/mod.rs +++ b/nodes/node/binary/src/generic_services/blend/mod.rs @@ -1,10 +1,18 @@ -use lb_blend::scheduling::message_blend::provers::{ - core_and_leader::RealCoreAndLeaderProofsGenerator, leader::RealLeaderProofsGenerator, +use axum::async_trait; +use lb_blend::{ + message::crypto::key_ext::Ed25519SecretKeyExt as _, + proofs::{ + quota::{VerifiedProofOfQuota, inputs::prove::private::ProofOfLeadershipQuotaInputs}, + selection::VerifiedProofOfSelection, + }, + scheduling::message_blend::provers::{ + BlendLayerProof, ProofsGeneratorSettings, + core_and_leader::RealCoreAndLeaderProofsGenerator, + leader::{LeaderProofsGenerator, RealLeaderProofsGenerator}, + }, }; -use lb_blend_service::{ - RealProofsVerifier, core::kms::PreloadKMSBackendCorePoQGenerator, membership::service::Adapter, -}; -use lb_chain_broadcast_service::BlockBroadcastService; +use lb_blend_service::{RealProofsVerifier, core::kms::PreloadKMSBackendCorePoQGenerator}; +use lb_key_management_system_service::keys::UnsecuredEd25519Key; use lb_time_service::backends::NtpTimeBackend; use libp2p::PeerId; @@ -12,13 +20,10 @@ use crate::generic_services::{CryptarchiaService, SdpService, blend::pol::PolInf pub(crate) mod pol; -pub type BlendMembershipAdapter = - Adapter, PeerId>; pub type BlendCoreService = lb_blend_service::core::BlendService< lb_blend_service::core::backends::libp2p::Libp2pBlendBackend, PeerId, lb_blend_service::core::network::libp2p::Libp2pAdapter, - BlendMembershipAdapter, SdpService, RealCoreAndLeaderProofsGenerator>, RealProofsVerifier, @@ -27,11 +32,32 @@ pub type BlendCoreService = lb_blend_service::core::BlendServi PolInfoProvider, RuntimeServiceId, >; + +#[derive(Clone)] +pub struct MockLeaderProofsGenerator; + +#[async_trait] +impl LeaderProofsGenerator for MockLeaderProofsGenerator { + fn new( + _settings: ProofsGeneratorSettings, + _private_inputs: ProofOfLeadershipQuotaInputs, + ) -> Self { + Self + } + + async fn get_next_proof(&mut self) -> BlendLayerProof { + BlendLayerProof { + proof_of_quota: VerifiedProofOfQuota::from_bytes_unchecked([0; _]), + proof_of_selection: VerifiedProofOfSelection::from_bytes_unchecked([0; _]), + ephemeral_signing_key: UnsecuredEd25519Key::generate_with_blake_rng(), + } + } +} + pub type BlendEdgeService = lb_blend_service::edge::BlendService< lb_blend_service::edge::backends::libp2p::Libp2pBlendBackend, PeerId, as lb_blend_service::core::network::NetworkAdapter>::BroadcastSettings, - BlendMembershipAdapter, RealLeaderProofsGenerator, NtpTimeBackend, CryptarchiaService, diff --git a/nodes/node/binary/src/generic_services/blend/pol.rs b/nodes/node/binary/src/generic_services/blend/pol.rs index fe9076e9d..a2e8fc178 100644 --- a/nodes/node/binary/src/generic_services/blend/pol.rs +++ b/nodes/node/binary/src/generic_services/blend/pol.rs @@ -58,47 +58,43 @@ where Some(Box::new( WatchStream::new(pol_winning_slot_receiver) .filter_map(ready) - .scan( - None, - |processed_epoch, (leader_private, leader_public, epoch)| { - let should_yield_new_epoch = - processed_epoch.is_none_or(|processed_epoch| processed_epoch < epoch); - if !should_yield_new_epoch { - return ready(Some(None)); - } + .scan(None, |processed_epoch, (leader_private, _, epoch)| { + let should_yield_new_epoch = + processed_epoch.is_none_or(|processed_epoch| processed_epoch < epoch); + if !should_yield_new_epoch { + return ready(Some(None)); + } - *processed_epoch = Some(epoch); - let PolWitnessInputsData { - wallet: - PolWalletInputsData { - aged_path, - aged_selectors, - note_value, - output_number, - secret_key, - transaction_hash, - .. - }, - chain: PolChainInputsData { slot_number, .. }, - } = leader_private.input(); - - let aged_path_and_selectors = - core::array::from_fn(|i| (aged_path[i], aged_selectors[i])); - - ready(Some(Some(PolEpochInfo { - epoch, - poq_public_inputs: leader_public, - poq_private_inputs: ProofOfLeadershipQuotaInputs { - aged_path_and_selectors, - note_value: *note_value, - output_number: *output_number, - secret_key: *secret_key, - slot: *slot_number, - transaction_hash: *transaction_hash, + *processed_epoch = Some(epoch); + let PolWitnessInputsData { + wallet: + PolWalletInputsData { + aged_path, + aged_selectors, + note_value, + output_number, + secret_key, + transaction_hash, + .. }, - }))) - }, - ) + chain: PolChainInputsData { slot_number, .. }, + } = leader_private.input(); + + let aged_path_and_selectors = + core::array::from_fn(|i| (aged_path[i], aged_selectors[i])); + + ready(Some(Some(PolEpochInfo { + epoch, + poq_private_inputs: ProofOfLeadershipQuotaInputs { + aged_path_and_selectors, + note_value: *note_value, + output_number: *output_number, + secret_key: *secret_key, + slot: *slot_number, + transaction_hash: *transaction_hash, + }, + }))) + }) .filter_map(ready), )) } diff --git a/nodes/node/binary/src/lib.rs b/nodes/node/binary/src/lib.rs index e0032cd5b..15aa446e0 100644 --- a/nodes/node/binary/src/lib.rs +++ b/nodes/node/binary/src/lib.rs @@ -9,12 +9,9 @@ pub mod global_allocators; use std::panic::set_hook; use color_eyre::eyre::{Result, eyre}; -pub use lb_blend_service::{ - core::{ - backends::libp2p::Libp2pBlendBackend as BlendBackend, - network::libp2p::Libp2pAdapter as BlendNetworkAdapter, - }, - membership::service::Adapter as BlendMembershipAdapter, +pub use lb_blend_service::core::{ + backends::libp2p::Libp2pBlendBackend as BlendBackend, + network::libp2p::Libp2pAdapter as BlendNetworkAdapter, }; pub use lb_core::{ codec, diff --git a/nodes/node/standalone-deployment-config.yaml b/nodes/node/standalone-deployment-config.yaml index baa3fc661..f7cbbe8b8 100644 --- a/nodes/node/standalone-deployment-config.yaml +++ b/nodes/node/standalone-deployment-config.yaml @@ -8,7 +8,6 @@ blend: scheduler: cover: message_frequency_per_round: 1.0 - intervals_for_safety_buffer: 100 delayer: maximum_release_delay_in_rounds: 3 minimum_messages_coefficient: 1 @@ -34,7 +33,7 @@ cryptarchia: lock_period: 10 inactivity_period: 1 retention_period: 1 - timestamp: 0 + epoch: 0 min_stake: threshold: 1 timestamp: 0 diff --git a/services/api/src/http/mantle.rs b/services/api/src/http/mantle.rs index df0dc1d9e..3a7923050 100644 --- a/services/api/src/http/mantle.rs +++ b/services/api/src/http/mantle.rs @@ -234,7 +234,8 @@ where + Sync + Display + AsServiceId> - + AsServiceId, + + AsServiceId + + 'static, { let processed_blocks_stream = get_processed_blocks_event_stream::( @@ -743,8 +744,11 @@ where TryFrom> + TryInto>, ::Tx: From + AsRef<[u8]>, ::Events: TryFrom + TryInto, - RuntimeServiceId: - Debug + Sync + Display + AsServiceId>, + RuntimeServiceId: Debug + + Sync + + Display + + AsServiceId> + + 'static, { let header_ids = get_immutable_blocks_header_ids(handle, from_slot, to_slot).await?; @@ -795,8 +799,11 @@ where TryFrom> + TryInto>, ::Tx: From + AsRef<[u8]>, ::Events: TryFrom + TryInto, - RuntimeServiceId: - Debug + Sync + Display + AsServiceId>, + RuntimeServiceId: Debug + + Sync + + Display + + AsServiceId> + + 'static, { let relay = handle.relay().await?; let storage_adapter = StorageAdapter::<_, _, RuntimeServiceId>::new(relay).await; @@ -836,8 +843,11 @@ where TryFrom> + TryInto>, ::Tx: From + AsRef<[u8]>, ::Events: TryFrom + TryInto, - RuntimeServiceId: - Debug + Sync + Display + AsServiceId>, + RuntimeServiceId: Debug + + Sync + + Display + + AsServiceId> + + 'static, { let relay = handle.relay().await?; let storage_adapter = StorageAdapter::<_, _, RuntimeServiceId>::new(relay).await; @@ -875,8 +885,11 @@ where TryFrom> + TryInto>, ::Tx: From + AsRef<[u8]>, ::Events: TryFrom + TryInto, - RuntimeServiceId: - Debug + Sync + Display + AsServiceId>, + RuntimeServiceId: Debug + + Sync + + Display + + AsServiceId> + + 'static, { let mut stream = get_transactions::( handle, @@ -892,8 +905,13 @@ pub async fn get_sdp_declarations( handle: &overwatch::overwatch::handle::OverwatchHandle, ) -> Result, super::DynError> where - RuntimeServiceId: - Debug + Send + Sync + Display + 'static + AsServiceId>, + RuntimeServiceId: Debug + + Send + + Sync + + Display + + 'static + + AsServiceId> + + 'static, { let relay = handle.relay::>().await?; let (sender, receiver) = oneshot::channel(); diff --git a/services/blend/Cargo.toml b/services/blend/Cargo.toml index 94a1fd407..ba90182e4 100644 --- a/services/blend/Cargo.toml +++ b/services/blend/Cargo.toml @@ -18,7 +18,6 @@ fork_stream = { workspace = true } futures = { workspace = true } hex = { workspace = true } lb-blend = { workspace = true } -lb-chain-broadcast-service = { workspace = true } lb-chain-service = { workspace = true } lb-core = { workspace = true } lb-cryptarchia-engine = { workspace = true } diff --git a/services/blend/src/core/backends/libp2p/behaviour.rs b/services/blend/src/core/backends/libp2p/behaviour.rs index 7c3a34d40..725b4ff33 100644 --- a/services/blend/src/core/backends/libp2p/behaviour.rs +++ b/services/blend/src/core/backends/libp2p/behaviour.rs @@ -1,4 +1,5 @@ use lb_blend::scheduling::membership::Membership; +use lb_chain_service::Epoch; use lb_libp2p::NetworkBehaviour; use libp2p::{PeerId, allow_block_list::BlockedPeers}; @@ -21,7 +22,7 @@ where { pub fn new( config: &BlendConfig, - current_membership_info: (Membership, u64), + current_membership_info: (Membership, Epoch), ) -> Self { let observation_window_interval_provider = ObservationWindowProvider::from((config, ¤t_membership_info.0)); diff --git a/services/blend/src/core/backends/libp2p/mod.rs b/services/blend/src/core/backends/libp2p/mod.rs index 174c9c203..148453e65 100644 --- a/services/blend/src/core/backends/libp2p/mod.rs +++ b/services/blend/src/core/backends/libp2p/mod.rs @@ -8,6 +8,7 @@ use futures::{ use lb_blend::message::encap::validated::{ EncapsulatedMessageWithVerifiedPublicHeader, EncapsulatedMessageWithVerifiedSignature, }; +use lb_chain_service::Epoch; use lb_log_targets::blend; use libp2p::PeerId; use overwatch::overwatch::handle::OverwatchHandle; @@ -18,7 +19,7 @@ use tokio_stream::wrappers::BroadcastStream; use crate::{ core::{ backends::{ - BlendBackend, PublicInfo, SessionInfo, + BackendEpochInfo, BlendBackend, libp2p::{ swarm::{BlendSwarm, BlendSwarmMessage, SwarmParams}, tokio_provider::ObservationWindowTokioIntervalProvider, @@ -46,7 +47,7 @@ pub(crate) use self::tests::utils as core_swarm_test_utils; pub struct Libp2pBlendBackend { swarm_task_abort_handle: AbortHandle, swarm_message_sender: mpsc::Sender, - incoming_message_sender: broadcast::Sender<(EncapsulatedMessageWithVerifiedSignature, u64)>, + incoming_message_sender: broadcast::Sender<(EncapsulatedMessageWithVerifiedSignature, Epoch)>, } const CHANNEL_SIZE: usize = 64; @@ -61,7 +62,7 @@ where fn new( config: BlendConfig, overwatch_handle: OverwatchHandle, - current_public_info: PublicInfo, + current_epoch_info: BackendEpochInfo, rng: Rng, ) -> Self { let (swarm_message_sender, swarm_message_receiver) = mpsc::channel(CHANNEL_SIZE); @@ -70,7 +71,7 @@ where let swarm = BlendSwarm::<_, ObservationWindowTokioIntervalProvider>::new(SwarmParams { config: &config, - current_public_info, + current_epoch_info, incoming_message_sender: incoming_message_sender.clone(), minimum_network_size, rng, @@ -96,13 +97,13 @@ where async fn publish( &self, msg: EncapsulatedMessageWithVerifiedPublicHeader, - intended_session: u64, + intended_epoch: Epoch, ) { if let Err(e) = self .swarm_message_sender .send(BlendSwarmMessage::Publish { message: Box::new(msg), - session: intended_session, + epoch: intended_epoch, }) .await { @@ -110,29 +111,29 @@ where } } - async fn rotate_session(&mut self, new_session_info: SessionInfo) { + async fn rotate_epoch(&mut self, new_epoch_info: BackendEpochInfo) { if let Err(e) = self .swarm_message_sender - .send(BlendSwarmMessage::StartNewSession(new_session_info)) + .send(BlendSwarmMessage::StartNewEpoch(new_epoch_info)) .await { - tracing::error!(target: LOG_TARGET, "Failed to send new public session info to BlendSwarm: {e}"); + tracing::error!(target: LOG_TARGET, "Failed to send new public epoch info to BlendSwarm: {e}"); } } - async fn complete_session_transition(&mut self) { + async fn complete_epoch_transition(&mut self) { if let Err(e) = self .swarm_message_sender - .send(BlendSwarmMessage::CompleteSessionTransition) + .send(BlendSwarmMessage::CompleteEpochTransition) .await { - tracing::error!(target: LOG_TARGET, "Failed to send session transition termination command to BlendSwarm: {e}"); + tracing::error!(target: LOG_TARGET, "Failed to send epoch transition termination command to BlendSwarm: {e}"); } } fn listen_to_incoming_messages( &mut self, - ) -> Pin + Send>> { + ) -> Pin + Send>> { Box::pin( BroadcastStream::new(self.incoming_message_sender.subscribe()) .filter_map(async |event| event.ok()), diff --git a/services/blend/src/core/backends/libp2p/swarm.rs b/services/blend/src/core/backends/libp2p/swarm.rs index 7630489be..d26a8191d 100644 --- a/services/blend/src/core/backends/libp2p/swarm.rs +++ b/services/blend/src/core/backends/libp2p/swarm.rs @@ -26,6 +26,7 @@ use lb_blend::{ }, scheduling::membership::Membership, }; +use lb_chain_service::Epoch; use lb_libp2p::{DialOpts, SwarmEvent}; use libp2p::{Multiaddr, PeerId, Swarm, SwarmBuilder, swarm::dial_opts::PeerCondition}; use rand::RngCore; @@ -34,7 +35,7 @@ use tokio::sync::{broadcast, mpsc, oneshot}; use crate::{ core::{ backends::{ - PublicInfo, SessionInfo, + BackendEpochInfo, libp2p::{ LOG_TARGET, Libp2pBlendBackendSettings, behaviour::{BlendBehaviour, BlendBehaviourEvent}, @@ -50,10 +51,10 @@ use crate::{ pub enum BlendSwarmMessage { Publish { message: Box, - session: u64, + epoch: Epoch, }, - StartNewSession(SessionInfo), - CompleteSessionTransition, + StartNewEpoch(BackendEpochInfo), + CompleteEpochTransition, GetNetworkInfo { reply: oneshot::Sender>>, }, @@ -70,11 +71,11 @@ pub struct DialAttempt { failed_peers: HashSet, } -/// [`DialAttempt`] with session information, i.e., whether the attempt was made -/// at this session or the previous one. -pub enum SessionDialAttempt { - OngoingSession(Option), - PreviousSession, +/// [`DialAttempt`] with epoch information, i.e., whether the attempt was made +/// at this epoch or the previous one. +pub enum EpochDialAttempt { + OngoingEpoch(Option), + PreviousEpoch, } #[cfg(test)] @@ -97,8 +98,8 @@ where { swarm: Swarm>, swarm_messages_receiver: mpsc::Receiver, - incoming_message_sender: broadcast::Sender<(EncapsulatedMessageWithVerifiedSignature, u64)>, - public_info: PublicInfo, + incoming_message_sender: broadcast::Sender<(EncapsulatedMessageWithVerifiedSignature, Epoch)>, + current_epoch_info: BackendEpochInfo, rng: Rng, max_dial_attempts_per_connection: NonZeroU64, ongoing_dials: HashMap, @@ -108,10 +109,11 @@ where pub struct SwarmParams<'config, Rng> { pub config: &'config BlendConfig, - pub current_public_info: PublicInfo, + pub current_epoch_info: BackendEpochInfo, pub rng: Rng, pub swarm_message_receiver: mpsc::Receiver, - pub incoming_message_sender: broadcast::Sender<(EncapsulatedMessageWithVerifiedSignature, u64)>, + pub incoming_message_sender: + broadcast::Sender<(EncapsulatedMessageWithVerifiedSignature, Epoch)>, pub minimum_network_size: NonZeroUsize, } @@ -127,7 +129,7 @@ where pub(super) fn new( SwarmParams { config, - current_public_info, + current_epoch_info, rng, swarm_message_receiver: swarm_messages_receiver, incoming_message_sender, @@ -140,15 +142,7 @@ where .with_quic() .with_dns() .expect("DNS transport should be supported") - .with_behaviour(|_| { - BlendBehaviour::new( - config, - ( - current_public_info.session.membership.clone(), - current_public_info.session.session_number, - ), - ) - }) + .with_behaviour(|_| BlendBehaviour::new(config, current_epoch_info.clone())) .expect("Blend Behaviour should be built") .with_swarm_config(|cfg| { // The idle timeout starts ticking once there are no active streams on a @@ -166,7 +160,7 @@ where swarm, swarm_messages_receiver, incoming_message_sender, - public_info: current_public_info, + current_epoch_info, rng, max_dial_attempts_per_connection: config.backend.max_dial_attempts_per_peer, ongoing_dials: HashMap::with_capacity( @@ -189,7 +183,7 @@ where IntervalStreamProvider>, { /// Dial random peers from the membership list, - /// excluding the peers with a negotiated connection in the ongoing session, + /// excluding the peers with a negotiated connection in the ongoing epoch, /// the peers that we are already trying to dial, the blocked peers, and /// any extra peers specified in `except`. fn dial_random_peers_except(&mut self, amount: usize, mut except: HashSet) { @@ -197,7 +191,7 @@ where // We need to clone else we would not be able to call `self.dial` inside which // requires access to `&mut self`. - let current_membership = self.public_info.session.membership.clone(); + let current_membership = self.current_epoch_info.0.clone(); // Membership contains local node, so we need to exclude that from the count. if except.len() == current_membership.size() - 1 { tracing::debug!(target: LOG_TARGET, "All eligible peers have been tried. Clearing failed peers memory and retrying from scratch."); @@ -227,7 +221,7 @@ where fn check_and_dial_new_peers_except(&mut self, except: HashSet) { tracing::trace!(target: LOG_TARGET, ?except, "Checking if we need to dial new peers"); - let membership_size = self.public_info.session.membership.size(); + let membership_size = self.current_epoch_info.0.size(); if membership_size < self.minimum_network_size.get() { tracing::warn!(target: LOG_TARGET, "Not dialing any peers because set of core nodes is smaller than the minimum network size. {membership_size} < {}", self.minimum_network_size.get()); return; @@ -253,17 +247,17 @@ where fn collect_network_info(&self) -> NetworkInfo { let core_behaviour = self.swarm.behaviour().blend.with_core(); - let current_session_peers = core_behaviour + let current_epoch_peers = core_behaviour .negotiated_peers() .iter() .map(|(peer_id, peer_state)| (*peer_id, peer_state.negotiated_state().is_healthy())) .collect(); - let old_session_peers = core_behaviour - .old_session_peer_ids() + let old_epoch_peers = core_behaviour + .old_epoch_peer_ids() .map(|peers| peers.copied().collect()); let core_info = CoreInfo { - current_session_peers, - old_session_peers, + current_epoch_peers, + old_epoch_peers, }; NetworkInfo { node_id: *self.swarm.local_peer_id(), @@ -278,11 +272,11 @@ where fn handle_blend_core_behaviour_event(&mut self, blend_event: CoreToCoreEvent) { match blend_event { - lb_blend::network::core::with_core::behaviour::Event::Message { message, sender, session } => { + lb_blend::network::core::with_core::behaviour::Event::Message { message, sender, epoch } => { // Forward message received from node to all other core nodes. - self.forward_received_core_message(&message, sender, session); + self.forward_received_core_message(&message, sender, epoch); // Bubble up to service for decapsulation and delaying. - self.report_message_to_service(*message, session, metrics::InboundMessageType::Core); + self.report_message_to_service(*message, epoch, metrics::InboundMessageType::Core); } lb_blend::network::core::with_core::behaviour::Event::UnhealthyPeer(peer_id) => { self.handle_unhealthy_peer(peer_id); @@ -299,8 +293,8 @@ where lb_blend::network::core::with_core::behaviour::Event::OutboundConnectionUpgradeFailed { peer, reason } => { match reason { ConnectionUpgradeFailureReason::ConnectionFailure => { - // If we ran out of dial attempts, we try to connect to another random peer that we are not yet connected to, if the dial attempt was performed in the current session. - let SessionDialAttempt::OngoingSession(Some(dial_attempt)) = self.schedule_retry(peer) else { + // If we ran out of dial attempts, we try to connect to another random peer that we are not yet connected to, if the dial attempt was performed in the current epoch. + let EpochDialAttempt::OngoingEpoch(Some(dial_attempt)) = self.schedule_retry(peer) else { return; }; let failed_peers = { @@ -371,10 +365,10 @@ where }; match self.schedule_retry(peer_id) { - SessionDialAttempt::PreviousSession => { - tracing::debug!(target: LOG_TARGET, "Received a dial error for peer {peer_id:?} that is not being tracked. This means that a new session has cleared the map of pending dials. No retry will be performed."); + EpochDialAttempt::PreviousEpoch => { + tracing::debug!(target: LOG_TARGET, "Received a dial error for peer {peer_id:?} that is not being tracked. This means that a new epoch has cleared the map of pending dials. No retry will be performed."); } - SessionDialAttempt::OngoingSession(Some(dial_attempt)) => { + EpochDialAttempt::OngoingEpoch(Some(dial_attempt)) => { let failed_peers = { let mut failed_peers = dial_attempt.failed_peers; failed_peers.insert(peer_id); @@ -383,7 +377,7 @@ where self.check_and_dial_new_peers_except(failed_peers); } // Retry in progress. - SessionDialAttempt::OngoingSession(None) => {} + EpochDialAttempt::OngoingEpoch(None) => {} } } _ => { @@ -394,21 +388,21 @@ where fn handle_swarm_message(&mut self, msg: BlendSwarmMessage) { match msg { - BlendSwarmMessage::Publish { message, session } => { - self.handle_publish_swarm_message(*message, session); + BlendSwarmMessage::Publish { message, epoch } => { + self.handle_publish_swarm_message(*message, epoch); } - BlendSwarmMessage::StartNewSession(new_session_info) => { - self.public_info.session = new_session_info; - self.swarm.behaviour_mut().blend.start_new_session(( - self.public_info.session.membership.clone(), - self.public_info.session.session_number, - )); + BlendSwarmMessage::StartNewEpoch(new_epoch_info) => { + self.current_epoch_info = new_epoch_info; + self.swarm + .behaviour_mut() + .blend + .start_new_epoch(self.current_epoch_info.clone()); self.ongoing_dials.clear(); self.pending_retries.clear(); self.check_and_dial_new_peers_except(HashSet::new()); } - BlendSwarmMessage::CompleteSessionTransition => { - self.swarm.behaviour_mut().blend.finish_session_transition(); + BlendSwarmMessage::CompleteEpochTransition => { + self.swarm.behaviour_mut().blend.finish_epoch_transition(); } BlendSwarmMessage::GetNetworkInfo { reply } => { let info = self.collect_network_info(); @@ -494,7 +488,7 @@ where DialOpts::peer_id(peer_id) .addresses(vec![address]) // We use `Always` since we want to be able to dial a peer even if we already have - // an established connection with it that belongs to the previous session. + // an established connection with it that belongs to the previous epoch. .condition(PeerCondition::Always) .build(), ) { @@ -534,23 +528,23 @@ where /// /// It returns: /// - /// * `SessionDialAttempt::PreviousSession` if the peer is not being tracked - /// in the map of ongoing dials, which means that a new session has been + /// * `EpochDialAttempt::PreviousEpoch` if the peer is not being tracked in + /// the map of ongoing dials, which means that a new epoch has been /// started and the dial attempts have been reset; - /// * `SessionDialAttempt::OngoingSession(None)` if a retry has been - /// scheduled with exponential backoff; - /// * `SessionDialAttempt::OngoingSession(Some)` if the maximum attempts - /// have been reached and the peer has been removed from the map of - /// ongoing dials. - fn schedule_retry(&mut self, peer_id: PeerId) -> SessionDialAttempt { + /// * `EpochDialAttempt::OngoingEpoch(None)` if a retry has been scheduled + /// with exponential backoff; + /// * `EpochDialAttempt::OngoingEpoch(Some)` if the maximum attempts have + /// been reached and the peer has been removed from the map of ongoing + /// dials. + fn schedule_retry(&mut self, peer_id: PeerId) -> EpochDialAttempt { let Some(dial_attempt) = self.ongoing_dials.remove(&peer_id) else { - tracing::debug!(target: LOG_TARGET, "Received a dial error for peer {peer_id:?} that is not being tracked. This means that a new session has cleared the map of pending dials."); - return SessionDialAttempt::PreviousSession; + tracing::debug!(target: LOG_TARGET, "Received a dial error for peer {peer_id:?} that is not being tracked. This means that a new epoch has cleared the map of pending dials."); + return EpochDialAttempt::PreviousEpoch; }; let new_attempt_number = dial_attempt.attempt_number.checked_add(1).unwrap(); if new_attempt_number > self.max_dial_attempts_per_connection { tracing::debug!(target: LOG_TARGET, "Maximum attempts ({}) reached for peer {peer_id:?}. Re-dialing stopped.", self.max_dial_attempts_per_connection); - return SessionDialAttempt::OngoingSession(Some(dial_attempt)); + return EpochDialAttempt::OngoingEpoch(Some(dial_attempt)); } let delay = Duration::from_secs(1 << (new_attempt_number.get() - 1)); tracing::debug!( @@ -568,7 +562,7 @@ where }, ) })); - SessionDialAttempt::OngoingSession(None) + EpochDialAttempt::OngoingEpoch(None) } /// Called when a pending retry fires. Re-checks peering degree before @@ -608,7 +602,7 @@ where .behaviour_mut() .blend .with_core_mut() - .publish_message_with_validated_signature_to_current_session(msg) + .publish_message_with_validated_signature_to_current_epoch(msg) { tracing::error!(target: LOG_TARGET, "Failed to publish message to blend network: {e:?}"); metrics::outbound_publish_err(); @@ -621,14 +615,14 @@ where &mut self, msg: &EncapsulatedMessageWithVerifiedSignature, except: PeerId, - session: u64, + epoch: Epoch, ) { if let Err(e) = self .swarm .behaviour_mut() .blend .with_core_mut() - .forward_message_with_validated_signature(msg, except, session) + .forward_message_with_validated_signature(msg, except, epoch) { // If we have a single connection, then we will always hit the `NoPeers` error. // In this case it's ok not to log such error, since this function is only @@ -646,14 +640,14 @@ where fn report_message_to_service( &self, msg: EncapsulatedMessageWithVerifiedSignature, - session: u64, + epoch: Epoch, message_type: metrics::InboundMessageType, ) { tracing::trace!( - "Received message from a peer: {msg:?} from session {session:?} of type {message_type:?}." + "Received message from a peer: {msg:?} from epoch {epoch:?} of type {message_type:?}." ); - if self.incoming_message_sender.send((msg, session)).is_err() { + if self.incoming_message_sender.send((msg, epoch)).is_err() { tracing::trace!(target: LOG_TARGET, "Failed to send incoming message to channel. No active listeners yet."); metrics::inbound_message_err(message_type); } else { @@ -693,7 +687,7 @@ where // Bubble up to service for decapsulation and delaying. self.report_message_to_service( msg, - self.public_info.session.session_number, + self.current_epoch_info.1, metrics::InboundMessageType::Edge, ); } @@ -703,14 +697,14 @@ where fn handle_publish_swarm_message( &mut self, msg: EncapsulatedMessageWithVerifiedPublicHeader, - intended_session: u64, + intended_epoch: Epoch, ) { if let Err(e) = self .swarm .behaviour_mut() .blend .with_core_mut() - .publish_message_with_validated_header(msg, intended_session) + .publish_message_with_validated_header(msg, intended_epoch) { tracing::error!(target: LOG_TARGET, "Failed to publish message to blend network: {e:?}"); metrics::outbound_publish_err(); @@ -732,8 +726,11 @@ where identity: &libp2p::identity::Keypair, behaviour_constructor: BehaviourConstructor, swarm_messages_receiver: mpsc::Receiver, - incoming_message_sender: broadcast::Sender<(EncapsulatedMessageWithVerifiedSignature, u64)>, - current_public_info: PublicInfo, + incoming_message_sender: broadcast::Sender<( + EncapsulatedMessageWithVerifiedSignature, + Epoch, + )>, + current_epoch_info: BackendEpochInfo, rng: Rng, max_dial_attempts_per_connection: NonZeroU64, minimum_network_size: NonZeroUsize, @@ -744,10 +741,10 @@ where { use crate::test_utils::memory_test_swarm; - let membership = current_public_info.session.membership.clone(); + let membership = current_epoch_info.0.clone(); Self { incoming_message_sender, - public_info: current_public_info, + current_epoch_info, max_dial_attempts_per_connection, ongoing_dials: HashMap::new(), pending_retries: FuturesUnordered::new(), diff --git a/services/blend/src/core/backends/libp2p/tests/message_handling.rs b/services/blend/src/core/backends/libp2p/tests/message_handling.rs index 43d14d13a..cdfe405a8 100644 --- a/services/blend/src/core/backends/libp2p/tests/message_handling.rs +++ b/services/blend/src/core/backends/libp2p/tests/message_handling.rs @@ -51,7 +51,7 @@ async fn core_message_propagation() { swarm_1_message_sender .send(BlendSwarmMessage::Publish { message: Box::new(message.clone()), - session: 1, + epoch: 1.into(), }) .await .unwrap(); @@ -59,7 +59,7 @@ async fn core_message_propagation() { // We test that swarm 1 publishes a message, sending it to swarm 2, the only // swarm it is connected to. Then swarm 2 forwards it to swarm 3, which is not // connected to swarm 1. - let (swarm_3_received_message, session) = swarm_3_message_receiver.recv().await.unwrap(); + let (swarm_3_received_message, epoch) = swarm_3_message_receiver.recv().await.unwrap(); assert_eq!(swarm_3_received_message, message.into_inner().into()); - assert_eq!(session, 1); + assert_eq!(epoch, 1); } diff --git a/services/blend/src/core/backends/libp2p/tests/network_maintenance.rs b/services/blend/src/core/backends/libp2p/tests/network_maintenance.rs index 70c92fcdb..f561414e1 100644 --- a/services/blend/src/core/backends/libp2p/tests/network_maintenance.rs +++ b/services/blend/src/core/backends/libp2p/tests/network_maintenance.rs @@ -15,7 +15,7 @@ use crate::{ test_utils::TestEncapsulatedMessage, }; -#[ignore = "TODO: enable this logic after investigating session/epoch transition issues. Test disabled because we don't let connections turn unhealthy because of too little messages now until we have proper observation window values."] +#[ignore = "TODO: enable this logic after investigating epoch transition issues. Test disabled because we don't let connections turn unhealthy because of too little messages now until we have proper observation window values."] #[test(tokio::test)] async fn on_unhealthy_peer() { let (mut identities, mut nodes) = new_nodes_with_empty_address(3); @@ -93,7 +93,7 @@ async fn on_unhealthy_peer() { assert_eq!(second_swarm_connection_details.role(), Endpoint::Listener); } -#[ignore = "TODO: enable this logic after investigating session/epoch transition issues. Test disabled because we don't let connections turn spammy because of too many messages now until we have proper observation window values."] +#[ignore = "TODO: enable this logic after investigating epoch transition issues. Test disabled because we don't let connections turn spammy because of too many messages now until we have proper observation window values."] #[test(tokio::test)] async fn on_malicious_peer() { let (mut identities, mut nodes) = new_nodes_with_empty_address(3); @@ -168,13 +168,13 @@ async fn on_malicious_peer() { .behaviour_mut() .blend .with_core_mut() - .force_send_message_to_current_session_peer(&message_1, listening_swarm_peer_id) + .force_send_message_to_current_epoch_peer(&message_1, listening_swarm_peer_id) .unwrap(); malicious_swarm .behaviour_mut() .blend .with_core_mut() - .force_send_message_to_current_session_peer(&message_2, listening_swarm_peer_id) + .force_send_message_to_current_epoch_peer(&message_2, listening_swarm_peer_id) .unwrap(); loop { diff --git a/services/blend/src/core/backends/libp2p/tests/redials.rs b/services/blend/src/core/backends/libp2p/tests/redials.rs index 626299d25..836c880d5 100644 --- a/services/blend/src/core/backends/libp2p/tests/redials.rs +++ b/services/blend/src/core/backends/libp2p/tests/redials.rs @@ -2,20 +2,15 @@ use core::time::Duration; use std::collections::HashSet; use lb_blend::scheduling::membership::Membership; -use lb_core::crypto::ZkHash; -use lb_groth16::Field as _; use lb_libp2p::{Protocol, SwarmEvent}; use libp2p::{Multiaddr, PeerId}; use test_log::test; use tokio::{select, time, time::sleep}; -use crate::core::backends::{ - SessionInfo, - libp2p::{ - core_swarm_test_utils::{SwarmExt as _, new_nodes_with_empty_address, update_nodes}, - swarm::BlendSwarmMessage, - tests::utils::{BlendBehaviourBuilder, SwarmBuilder, TestSwarm}, - }, +use crate::core::backends::libp2p::{ + core_swarm_test_utils::{SwarmExt as _, new_nodes_with_empty_address, update_nodes}, + swarm::BlendSwarmMessage, + tests::utils::{BlendBehaviourBuilder, SwarmBuilder, TestSwarm}, }; #[test(tokio::test)] @@ -289,10 +284,10 @@ async fn core_clears_failed_peers_memory_when_all_exhausted() { ); } -/// When a new session rotation occurs, pending backoff retries should be +/// When a new epoch rotation occurs, pending backoff retries should be /// discarded along with ongoing dials. #[test(tokio::test)] -async fn core_session_rotation_clears_pending_retries() { +async fn core_epoch_rotation_clears_pending_retries() { let (mut identities, peer_ids) = new_nodes_with_empty_address(1); let TestSwarm { swarm: mut dialing_swarm, @@ -315,22 +310,15 @@ async fn core_session_rotation_clears_pending_retries() { .await; assert_eq!(dialing_swarm.pending_retries_count(), 1); - // Trigger a new session via the swarm message channel. - let new_session_info = SessionInfo { - membership: Membership::new_without_local(&[]), - session_number: 2, - core_public_inputs: lb_blend::proofs::quota::inputs::prove::public::CoreInputs { - quota: 1, - zk_root: ZkHash::ZERO, - }, - }; + // Trigger a new epoch via the swarm message channel. + let new_epoch_info = (Membership::new_without_local(&[]), 2.into()); swarm_message_sender - .send(BlendSwarmMessage::StartNewSession(new_session_info)) + .send(BlendSwarmMessage::StartNewEpoch(new_epoch_info)) .await .unwrap(); dialing_swarm.poll_next().await; - // Session rotation should have cleared both ongoing dials and pending retries. + // Epoch rotation should have cleared both ongoing dials and pending retries. assert!(dialing_swarm.ongoing_dials().is_empty()); assert_eq!(dialing_swarm.pending_retries_count(), 0); } diff --git a/services/blend/src/core/backends/libp2p/tests/utils.rs b/services/blend/src/core/backends/libp2p/tests/utils.rs index 3908b93ec..14c865f6b 100644 --- a/services/blend/src/core/backends/libp2p/tests/utils.rs +++ b/services/blend/src/core/backends/libp2p/tests/utils.rs @@ -15,6 +15,7 @@ use lb_blend::{ }, scheduling::membership::{Membership, Node}, }; +use lb_chain_service::Epoch; use lb_key_management_system_service::keys::UnsecuredEd25519Key; use lb_libp2p::{Protocol, SwarmEvent}; use lb_utils::blake_rng::BlakeRng; @@ -32,7 +33,7 @@ use tokio_stream::wrappers::IntervalStream; use crate::{ core::{ backends::{ - PublicInfo, + BackendEpochInfo, libp2p::{BlendSwarm, behaviour::BlendBehaviour, swarm::BlendSwarmMessage}, }, settings::StartingBlendConfig as BlendConfig, @@ -46,7 +47,7 @@ pub struct TestSwarm { pub swarm: InnerSwarm, pub swarm_message_sender: mpsc::Sender, pub incoming_message_receiver: - broadcast::Receiver<(EncapsulatedMessageWithVerifiedSignature, u64)>, + broadcast::Receiver<(EncapsulatedMessageWithVerifiedSignature, Epoch)>, } /// Generates `count` nodes with randomly generated identities and empty @@ -92,13 +93,16 @@ pub fn build_membership( pub struct SwarmBuilder { identity: Keypair, - public_info: PublicInfo, + public_info: BackendEpochInfo, max_dial_attempts: Option, } impl SwarmBuilder { pub fn new(identity: Keypair, membership: &[Node]) -> Self { - let public_info = build_membership(membership, Some(identity.public().into())).into(); + let public_info = ( + build_membership(membership, Some(identity.public().into())), + 1.into(), + ); Self { identity, public_info, @@ -188,7 +192,7 @@ impl BlendBehaviourBuilder { expected_message_range: observation_window_values.1, interval: observation_window_values.0, }, - (self.membership, 1), + (self.membership, 1.into()), self.peer_id, PROTOCOL_NAME, ), diff --git a/services/blend/src/core/backends/libp2p/tokio_provider.rs b/services/blend/src/core/backends/libp2p/tokio_provider.rs index 30443c398..479a6d7e9 100644 --- a/services/blend/src/core/backends/libp2p/tokio_provider.rs +++ b/services/blend/src/core/backends/libp2p/tokio_provider.rs @@ -78,7 +78,7 @@ where Self { blending_ops_per_message: config.num_blend_layers.into(), maximal_delay_rounds: config.scheduler.delayer.maximum_release_delay_in_rounds, - // TODO: Replace with a session stream: https://github.com/logos-blockchain/logos-blockchain/issues/1533 + // TODO: Replace with an epoch stream: https://github.com/logos-blockchain/logos-blockchain/issues/1533 membership_size: NonZeroU64::try_from(membership.size() as u64) .expect("Membership size cannot be zero."), minimum_messages_coefficient: config.backend.minimum_messages_coefficient, diff --git a/services/blend/src/core/backends/mod.rs b/services/blend/src/core/backends/mod.rs index 4be64a1c5..2145b8ea4 100644 --- a/services/blend/src/core/backends/mod.rs +++ b/services/blend/src/core/backends/mod.rs @@ -2,121 +2,19 @@ use std::{fmt::Debug, pin::Pin}; use futures::Stream; use lb_blend::{ - message::{ - crypto::proofs::PoQVerificationInputsMinusSigningKey, - encap::validated::{ - EncapsulatedMessageWithVerifiedPublicHeader, EncapsulatedMessageWithVerifiedSignature, - }, + message::encap::validated::{ + EncapsulatedMessageWithVerifiedPublicHeader, EncapsulatedMessageWithVerifiedSignature, }, - proofs::quota::inputs::prove::public::{CoreInputs, LeaderInputs}, - scheduling::{membership::Membership, session::SessionEvent}, + scheduling::membership::Membership, }; +use lb_chain_service::Epoch; use overwatch::overwatch::handle::OverwatchHandle; use crate::{core::settings::RunningBlendConfig as BlendConfig, message::NetworkInfo}; pub mod libp2p; -pub type EpochInfo = LeaderInputs; - -/// The public info for both current session and current epoch. Used to derive -/// `PoQ` verification inputs. -#[derive(Debug, Clone)] -pub struct PublicInfo { - /// Current session public info. - pub session: SessionInfo, - /// Current epoch public info. - pub epoch: EpochInfo, -} - -#[cfg(test)] -impl Default for PublicInfo -where - NodeId: Clone + core::hash::Hash + Eq, -{ - fn default() -> Self { - use lb_core::crypto::ZkHash; - use lb_groth16::{Field as _, Fr}; - - Self { - epoch: LeaderInputs { - message_quota: 1, - pol_epoch_nonce: ZkHash::ZERO, - pol_ledger_aged: ZkHash::ZERO, - lottery_0: Fr::ZERO, - lottery_1: Fr::ZERO, - }, - session: SessionInfo { - membership: Membership::new_without_local(&[]), - session_number: 1, - core_public_inputs: CoreInputs { - zk_root: ZkHash::ZERO, - quota: 1, - }, - }, - } - } -} - -#[cfg(test)] -impl From> for PublicInfo { - fn from(value: Membership) -> Self { - use lb_core::crypto::ZkHash; - use lb_groth16::{Field as _, Fr}; - - Self { - epoch: LeaderInputs { - message_quota: 1, - pol_epoch_nonce: ZkHash::ZERO, - pol_ledger_aged: ZkHash::ZERO, - lottery_0: Fr::ZERO, - lottery_1: Fr::ZERO, - }, - session: SessionInfo { - membership: value, - session_number: 1, - core_public_inputs: CoreInputs { - zk_root: ZkHash::ZERO, - quota: 1, - }, - }, - } - } -} - -impl From> for PoQVerificationInputsMinusSigningKey { - fn from( - PublicInfo { - epoch, - session: - SessionInfo { - core_public_inputs, - session_number: session, - .. - }, - }: PublicInfo, - ) -> Self { - Self { - core: core_public_inputs, - leader: epoch, - session, - } - } -} - -/// The public session-related info. -#[derive(Debug, Clone)] -pub struct SessionInfo { - /// Current session membership. - pub membership: Membership, - /// Current session number. - pub session_number: u64, - /// Current session `PoQ` verification inputs. - pub core_public_inputs: CoreInputs, -} - -pub type SessionStream = - Pin>> + Send>>; +pub type BackendEpochInfo = (Membership, Epoch); /// A trait for blend backends that send messages to the blend network. #[async_trait::async_trait] @@ -126,7 +24,7 @@ pub trait BlendBackend { fn new( service_config: BlendConfig, overwatch_handle: OverwatchHandle, - current_public_info: PublicInfo, + current_epoch_info: BackendEpochInfo, rng: Rng, ) -> Self; fn shutdown(self); @@ -134,16 +32,16 @@ pub trait BlendBackend { async fn publish( &self, msg: EncapsulatedMessageWithVerifiedPublicHeader, - intended_session: u64, + intended_epoch: Epoch, ); - /// Rotate session. - async fn rotate_session(&mut self, new_session_info: SessionInfo); - /// Complete the session transition. - async fn complete_session_transition(&mut self); + /// Rotate epoch. + async fn rotate_epoch(&mut self, new_epoch_info: BackendEpochInfo); + /// Complete the epoch transition. + async fn complete_epoch_transition(&mut self); /// Listen to messages received from the blend network. fn listen_to_incoming_messages( &mut self, - ) -> Pin + Send>>; + ) -> Pin + Send>>; /// Return network info about the current blend peers. /// Returns `None` if the backend does not support this operation. diff --git a/services/blend/src/core/mod.rs b/services/blend/src/core/mod.rs index a5fd58327..cd454598b 100644 --- a/services/blend/src/core/mod.rs +++ b/services/blend/src/core/mod.rs @@ -16,6 +16,7 @@ use lb_blend::{ crypto::random_sized_bytes, message::{ Error as MessageError, PayloadType, + crypto::proofs::PoQVerificationInputsMinusSigningKey, encap::{ ProofsVerifier as ProofsVerifierTrait, encapsulated::EncapsulatedMessage, @@ -25,33 +26,26 @@ use lb_blend::{ }, }, reward::{ - self, ActivityProof, BlendingToken, OldSessionBlendingTokenCollector, - SessionBlendingTokenCollector, + self, ActivityProof, BlendingToken, EpochBlendingTokenCollector, + OldEpochBlendingTokenCollector, }, }, - proofs::quota::inputs::prove::{ - private::ProofOfLeadershipQuotaInputs, - public::{CoreInputs, LeaderInputs}, - }, + proofs::quota::inputs::prove::public::{CoreInputs, LeaderInputs}, scheduling::{ - SessionMessageScheduler, + EpochMessageScheduler, + epoch::{EpochEvent, UninitializedEpochEventStream}, message_blend::{ - crypto::SessionCryptographicProcessorSettings, + crypto::EpochCryptographicProcessorSettings, provers::core_and_leader::CoreAndLeaderProofsGenerator, }, message_scheduler::{ - OldSessionMessageScheduler, ProcessedMessageScheduler, + OldEpochMessageScheduler, ProcessedMessageScheduler, + epoch_info::EpochInfo as SchedulerEpochInfo, round_info::{RoundInfo, RoundReleaseType}, - session_info::SessionInfo as SchedulerSessionInfo, }, - session::{SessionEvent, UninitializedSessionEventStream}, - stream::UninitializedFirstReadyStream, }, }; -use lb_chain_service::{ - Epoch, - api::{CryptarchiaServiceApi, CryptarchiaServiceData}, -}; +use lb_chain_service::{Epoch, api::CryptarchiaServiceData}; use lb_core::{ codec::{DeserializeOp as _, SerializeOp as _}, sdp::ActivityMetadata, @@ -68,7 +62,7 @@ use lb_services_utils::{ overwatch::{JsonFileBackend, RecoveryOperator}, wait_until_services_are_ready, }; -use lb_time_service::{SlotTick, TimeService, TimeServiceMessage}; +use lb_time_service::TimeService; use lb_utils::blake_rng::BlakeRng; use network::NetworkAdapter; use overwatch::{ @@ -83,11 +77,10 @@ use overwatch::{ use rand::{RngCore, SeedableRng as _, seq::SliceRandom as _}; use serde::{Deserialize, Serialize}; use tokio::sync::oneshot; -use tracing::{debug, error, info, trace}; +use tracing::{debug, error, info}; use crate::{ core::{ - backends::{PublicInfo, SessionInfo}, kms::{KmsPoQAdapter, PreloadKMSBackendCorePoQGenerator}, processor::{ CoreCryptographicProcessor, DecapsulatedMessageType, Error, @@ -97,15 +90,11 @@ use crate::{ settings::{RunningBlendConfig, StartingBlendConfig}, state::{RecoveryServiceState, ServiceState, StateUpdater as ServiceStateUpdater}, }, - epoch_info::{ - ChainApi, EpochEvent, EpochHandler, LeaderInputsMinusQuota, PolEpochInfo, - PolInfoProvider as PolInfoProviderTrait, - }, + epoch::{CoreEpochInfo, CoreEpochPublicInfo, MaybeEmptyCoreEpochInfo}, + epoch_info::{PolEpochInfo, PolInfoProvider as PolInfoProviderTrait}, kms::PreloadKmsService, - membership::{self, MembershipInfo, ZkInfo}, + membership::{self, ZkInfo, chain::BlendEpochState}, message::{NetworkMessage, ProcessedMessage, ServiceMessage}, - session::{CoreSessionInfo, CoreSessionPublicInfo, MaybeEmptyCoreSessionInfo}, - settings::FIRST_STREAM_ITEM_READY_TIMEOUT, }; pub mod backends; @@ -135,7 +124,6 @@ pub struct BlendService< Backend, NodeId, Network, - MembershipAdapter, SdpService, ProofsGenerator, ProofsVerifier, @@ -151,7 +139,6 @@ pub struct BlendService< last_saved_state: Option>, _phantom: PhantomData<( Backend, - MembershipAdapter, SdpService, ProofsGenerator, TimeBackend, @@ -164,7 +151,6 @@ impl< Backend, NodeId, Network, - MembershipAdapter, SdpService, ProofsGenerator, ProofsVerifier, @@ -177,7 +163,6 @@ impl< Backend, NodeId, Network, - MembershipAdapter, SdpService, ProofsGenerator, ProofsVerifier, @@ -206,7 +191,6 @@ impl< Backend, NodeId, Network, - MembershipAdapter, SdpService, ProofsGenerator, ProofsVerifier, @@ -219,7 +203,6 @@ impl< Backend, NodeId, Network, - MembershipAdapter, SdpService, ProofsGenerator, ProofsVerifier, @@ -230,10 +213,8 @@ impl< > where Backend: BlendBackend + Send + Sync, - NodeId: Clone + Debug + Send + Eq + Hash + Sync + 'static, + NodeId: membership::node_id::TryFrom + Clone + Debug + Send + Eq + Hash + Sync + 'static, Network: NetworkAdapter + Send + Sync, - MembershipAdapter: membership::Adapter + Send, - membership::ServiceMessage: Send + Sync + 'static, ProofsGenerator: CoreAndLeaderProofsGenerator> + Send + Sync, @@ -243,7 +224,6 @@ where ChainService: CryptarchiaServiceData, PolInfoProvider: PolInfoProviderTrait + Send, RuntimeServiceId: AsServiceId> - + AsServiceId<::Service> + AsServiceId + AsServiceId> + AsServiceId @@ -296,7 +276,6 @@ where Some(Duration::from_mins(1)), NetworkService<_, _>, TimeService<_, _>, - ::Service, SdpService, PreloadKmsService<_> ) @@ -311,20 +290,6 @@ where } .await; - let mut epoch_handler = async { - let chain_service = CryptarchiaServiceApi::::new( - overwatch_handle - .relay::() - .await - .expect("Failed to establish channel with chain service."), - ); - EpochHandler::new( - chain_service, - blend_config.time.epoch_transition_period_in_slots, - ) - } - .await; - let kms_api = async { let kms_outbound_relay = overwatch_handle .relay::>() @@ -359,40 +324,19 @@ where .expect("Failed to retrieve non-ephemeral signing key from KMS.") }; - let membership_stream = MembershipAdapter::new( - overwatch_handle - .relay::<::Service>() - .await - .expect("Failed to get relay channel with membership service."), - non_ephemeral_signing_key.public_key(), - Some(zk_public_key), - ) - .subscribe() - .await - .expect("Failed to get membership stream from membership service."); + let public_epoch_stream = + membership::chain::subscribe::( + overwatch_handle, + non_ephemeral_signing_key.public_key(), + Some(zk_public_key), + ) + .await; let sdp_relay = overwatch_handle .relay::() .await .expect("Relay with SDP service should be available."); - // Initialize clock stream for epoch-related public PoQ inputs. - let clock_stream = async { - let time_relay = overwatch_handle - .relay::>() - .await - .expect("Relay with time service should be available."); - let (sender, receiver) = oneshot::channel(); - time_relay - .send(TimeServiceMessage::Subscribe { sender }) - .await - .expect("Failed to subscribe to slot clock."); - receiver - .await - .expect("Should not fail to receive slot stream from time service.") - } - .await; - // Initialize components for the service. let running_blend_config = RunningBlendConfig { backend: blend_config.backend, @@ -407,10 +351,8 @@ where activity_threshold_sensitivity: blend_config.activity_threshold_sensitivity, }; let ( - mut remaining_session_stream, - mut remaining_clock_stream, + mut remaining_epoch_stream, current_public_info, - current_epoch, crypto_processor, current_recovery_checkpoint, message_scheduler, @@ -420,16 +362,13 @@ where NodeId, Backend, Network, - CryptarchiaServiceApi, ProofsGenerator, ProofsVerifier, KmsServiceApi, RuntimeServiceId>, RuntimeServiceId, >( running_blend_config.clone(), - membership_stream, - clock_stream, - &mut epoch_handler, + public_epoch_stream, overwatch_handle.clone(), kms_api, &sdp_relay, @@ -452,56 +391,46 @@ where let mut blend_messages = backend.listen_to_incoming_messages(); // Run the main event loop while the node is a core node across multiple - // sessions. When the node becomes a non-core node in a new session, the - // old session's components (crypto processor, scheduler, blending token + // epochs. When the node becomes a non-core node in a new epoch, the + // old epoch's components (crypto processor, scheduler, blending token // collector, public info, and epoch) are returned for the retirement phase. let ( - old_session_crypto_processor, - old_session_message_scheduler, - old_session_blending_token_collector, - old_session_public_info, - old_epoch, + old_epoch_crypto_processor, + old_epoch_message_scheduler, + old_epoch_blending_token_collector, ) = run_event_loop( inbound_relay, &mut blend_messages, - &mut remaining_clock_stream, secret_pol_info_stream, - &mut remaining_session_stream, + &mut remaining_epoch_stream, &running_blend_config, &mut backend, &network_adapter, &sdp_relay, - &mut epoch_handler, message_scheduler.into(), &mut rng, crypto_processor, current_public_info, - current_epoch, current_recovery_checkpoint, ) .await; // The main event loop has ended because the node is no longer a core node - // in the new session. - // Before terminating the service, complete the old session during a single - // session transition period. + // in the new epoch. + // Before terminating the service, complete the old epoch during a single + // epoch transition period. retire( - // We don't need session numbers anymore since we know we are dealing with a single, - // past session. + // We don't need epoch numbers anymore since we know we are dealing with a single, + // past epoch. blend_messages.map(|(message, _)| message), - remaining_clock_stream, - remaining_session_stream, - &running_blend_config, + remaining_epoch_stream, backend, network_adapter, sdp_relay, - epoch_handler, - old_session_message_scheduler, + old_epoch_message_scheduler, rng, - old_session_blending_token_collector, - old_session_crypto_processor, - old_session_public_info, - old_epoch, + old_epoch_blending_token_collector, + old_epoch_crypto_processor, ) .await; @@ -511,10 +440,6 @@ where /// Initialize the components for the [`BlendService`]. #[expect(clippy::too_many_lines, reason = "Need to initialize many components")] -#[expect( - clippy::too_many_arguments, - reason = "Need to initialize many components." -)] #[expect( clippy::cognitive_complexity, reason = "TODO: address this in a dedicated refactor" @@ -523,16 +448,13 @@ async fn initialize< NodeId, Backend, NetAdapter, - ChainService, ProofsGenerator, ProofsVerifier, KmsAdapter, RuntimeServiceId, >( blend_config: RunningBlendConfig, - membership_stream: impl Stream> + Send + Unpin + 'static, - clock_stream: impl Stream + Send + Sync + Unpin + 'static, - epoch_handler: &mut EpochHandler, + public_epoch_stream: impl Stream> + Send + Unpin + 'static, overwatch_handle: OverwatchHandle, kms_adapter: KmsAdapter, sdp_relay: &OutboundRelay, @@ -541,13 +463,11 @@ async fn initialize< Option>, >, ) -> ( - impl Stream>> + impl Stream>> + Unpin + Send + 'static, - impl Stream + Unpin + Send + Sync + 'static, - PublicInfo, - Epoch, + CoreEpochPublicInfo, CoreCryptographicProcessor< NodeId, KmsAdapter::CorePoQGenerator, @@ -567,7 +487,6 @@ where NodeId: Clone + Debug + Eq + Hash + Send + 'static, Backend: BlendBackend + Sync, NetAdapter: NetworkAdapter, - ChainService: ChainApi + Sync, ProofsGenerator: CoreAndLeaderProofsGenerator, ProofsVerifier: ProofsVerifierTrait, // To avoid bubbling up generics everywhere in the configs (current Overwatch limitation), we @@ -577,39 +496,50 @@ where + 'static, RuntimeServiceId: Clone + Send + Sync + 'static, { - // Initialize membership stream for session and core-related public PoQ inputs. - let session_stream = async { + // Initialize epoch stream for all public PoQ inputs. + let epoch_stream = async { let config = blend_config.clone(); let zk_sk_id = config.zk.secret_key_kms_id.clone(); - membership_stream.map( - move |MembershipInfo { - membership, - session_number, - zk, + public_epoch_stream.map( + move |BlendEpochState { + aged, + epoch, + lottery_0, + lottery_1, + membership_info, + nonce, }| { // This can be empty in case of an empty membership set. let Some(ZkInfo { root, core_and_path_selectors, - }) = zk + }) = membership_info.zk else { - return MaybeEmptyCoreSessionInfo::Empty { - session: session_number, + return MaybeEmptyCoreEpochInfo::Empty { + epoch, + epoch_nonce: nonce, }; }; - // `None` when the local node is not part of the session membership. This can + // `None` when the local node is not part of the epoch membership. This can // happen when the node transitions from core to edge mode. let core_poq_generator = core_and_path_selectors.map(|selectors| { kms_adapter.core_poq_generator(zk_sk_id.clone(), Box::new(selectors)) }); - CoreSessionInfo { - public: CoreSessionPublicInfo { + CoreEpochInfo { + public: CoreEpochPublicInfo { poq_core_public_inputs: CoreInputs { - quota: config.session_core_quota(membership.size()), + quota: config.epoch_core_quota(membership_info.membership.size()), zk_root: root, }, - membership, - session: session_number, + membership: membership_info.membership, + epoch, + poq_leadership_public_inputs: LeaderInputs { + pol_ledger_aged: aged, + pol_epoch_nonce: nonce, + message_quota: config.epoch_leadership_quota(), + lottery_0, + lottery_1, + }, }, core_poq_generator, } @@ -618,142 +548,103 @@ where ) } .await; - let (current_membership_info, remaining_session_stream) = Box::pin( - UninitializedSessionEventStream::new( - session_stream, - FIRST_STREAM_ITEM_READY_TIMEOUT, - blend_config.time.session_transition_period(), - ) - .await_first_ready(), + let (current_epoch_info, remaining_epoch_stream) = Box::pin( + UninitializedEpochEventStream::new(epoch_stream, blend_config.time.epoch_transition_period) + .await_first_ready(), ) .await - .map(|(membership_info, remaining_session_stream)| { - let MaybeEmptyCoreSessionInfo::NonEmpty(core_session_info) = membership_info else { - panic!("First retrieved session for Blend core startup must be available."); + .map(|(epoch_info, remaining_epoch_stream)| { + let MaybeEmptyCoreEpochInfo::NonEmpty(core_epoch_info) = epoch_info else { + panic!("First retrieved epoch for Blend core startup must be available."); }; - (core_session_info, remaining_session_stream.fork()) + (core_epoch_info, remaining_epoch_stream.fork()) }) - .expect("The current session info must be available."); + .expect("The current epoch info must be available."); - let ( - ( - LeaderInputsMinusQuota { - pol_epoch_nonce, - pol_ledger_aged, - lottery_0, - lottery_1, - }, - current_epoch, - ), - remaining_clock_stream, - ) = async { - let (clock_tick, remaining_clock_stream) = - UninitializedFirstReadyStream::new(clock_stream, Duration::from_secs(5)) - .first() - .await - .expect("The clock system must be available."); - let Some(EpochEvent::NewEpoch(new_epoch_info)) = epoch_handler.tick(clock_tick).await - else { - panic!("First poll result of epoch stream should be a `NewEpoch` event."); - }; - (new_epoch_info, remaining_clock_stream) - } - .await; + let CoreEpochInfo { + public: current_epoch_public_info, + core_poq_generator: current_epoch_core_poq_generator, + } = *current_epoch_info; info!( target: LOG_TARGET, "The current membership is ready: {:?}", - current_membership_info.public + current_epoch_public_info ); - let current_public_info = PublicInfo { - epoch: LeaderInputs { - pol_ledger_aged, - pol_epoch_nonce, - message_quota: blend_config.session_leadership_quota(), - lottery_0, - lottery_1, - }, - session: SessionInfo { - membership: current_membership_info.public.membership.clone(), - session_number: current_membership_info.public.session, - core_public_inputs: current_membership_info.public.poq_core_public_inputs, - }, - }; - - trace!(target: LOG_TARGET, "Current public info: {:?}", current_public_info); - let crypto_processor = CoreCryptographicProcessor::< _, KmsAdapter::CorePoQGenerator, ProofsGenerator, ProofsVerifier, >::try_new_with_core_condition_check( - current_membership_info.public.membership.clone(), + current_epoch_public_info.membership.clone(), blend_config.minimum_network_size, - SessionCryptographicProcessorSettings { + EpochCryptographicProcessorSettings { non_ephemeral_encryption_key: blend_config.non_ephemeral_signing_key.derive_x25519(), num_blend_layers: blend_config.num_blend_layers, }, - current_public_info.clone().into(), - current_membership_info - .core_poq_generator + PoQVerificationInputsMinusSigningKey { + core: current_epoch_public_info.poq_core_public_inputs, + leader: current_epoch_public_info.poq_leadership_public_inputs, + }, + current_epoch_core_poq_generator .expect("Core PoQ generator must be present at startup: the proxy service only launches CoreMode when the node is part of the core membership."), - current_epoch, + current_epoch_public_info.epoch, ) .expect("The initial membership should satisfy the core node condition"); - // Initialize the current session state. If the session matches the stored one, + // Initialize the current epoch state. If the epoch matches the stored one, // retrieves the tracked consumed core quota. Else, fallback to `0`. let current_recovery_checkpoint = if let Some(saved_state) = last_saved_state.take() - && saved_state.last_seen_session() == current_membership_info.public.session + && saved_state.last_seen_epoch() == current_epoch_public_info.epoch { tracing::trace!( target: LOG_TARGET, - "Found recovery state for session {:?}: {saved_state:?}", - current_membership_info.public.session + "Found recovery state for epoch {:?}: {saved_state:?}", + current_epoch_public_info.epoch ); saved_state } else { tracing::trace!( target: LOG_TARGET, - "No recovery state found for session {:?}. Initializing a new one.", - current_membership_info.public.session + "No recovery state found for epoch {:?}. Initializing a new one.", + current_epoch_public_info.epoch ); - ServiceState::with_session( - current_membership_info.public.session, - SessionBlendingTokenCollector::new( - &reward::SessionInfo::new( - current_membership_info.public.session, - &pol_epoch_nonce, - current_membership_info.public.membership.size() as u64, - current_membership_info.public.poq_core_public_inputs.quota, + ServiceState::with_epoch( + current_epoch_public_info.epoch, + EpochBlendingTokenCollector::new( + &reward::EpochInfo::new( + current_epoch_public_info.epoch, + ¤t_epoch_public_info.poq_leadership_public_inputs.pol_epoch_nonce, + current_epoch_public_info.membership.size() as u64, + current_epoch_public_info.poq_core_public_inputs.quota, blend_config.activity_threshold_sensitivity, - ).expect("Reward session info must be created successfully. Panicking since the service cannot continue with this session") + ).expect("Reward epoch info must be created successfully. Panicking since the service cannot continue with this epoch") ), None, state_updater, ).expect("service state should be created successfully") }; - // If there is the old session token collector loaded from `last_saved_state`, + // If there is the old epoch token collector loaded from `last_saved_state`, // compute/submit its activity proof because we won't collect more tokens for - // the old session after this initialization step because we are not - // establishing connections for the old session. + // the old epoch after this initialization step because we are not + // establishing connections for the old epoch. let mut state_updater = current_recovery_checkpoint.start_updating(); - if let Some(old_session_token_collector) = state_updater.clear_old_session_token_collector() { - tracing::debug!(target: LOG_TARGET, "Old session token collector loaded. Computing activity proof"); - compute_and_submit_activity_proof(old_session_token_collector, sdp_relay).await; + if let Some(old_epoch_token_collector) = state_updater.clear_old_epoch_token_collector() { + tracing::debug!(target: LOG_TARGET, "Old epoch token collector loaded. Computing activity proof"); + compute_and_submit_activity_proof(old_epoch_token_collector, sdp_relay).await; } let current_recovery_checkpoint = state_updater.commit_changes(); let message_scheduler = SchedulerWrapper::new_with_initial_messages( - SchedulerSessionInfo { + SchedulerEpochInfo { core_quota: blend_config - .session_core_quota(current_membership_info.public.membership.size()) + .epoch_core_quota(current_epoch_public_info.membership.size()) .saturating_sub(current_recovery_checkpoint.spent_quota()), - session_number: u128::from(current_membership_info.public.session).into(), + epoch: current_epoch_public_info.epoch, }, BlakeRng::from_entropy(), blend_config.scheduler_settings(), @@ -772,7 +663,10 @@ where let backend = Backend::new( blend_config.clone(), overwatch_handle, - current_public_info.clone(), + ( + current_epoch_public_info.membership.clone(), + current_epoch_public_info.epoch, + ), BlakeRng::from_entropy(), ); @@ -780,10 +674,8 @@ where let rng = BlakeRng::from_entropy(); ( - remaining_session_stream, - remaining_clock_stream, - current_public_info, - current_epoch, + remaining_epoch_stream, + current_epoch_public_info, crypto_processor, current_recovery_checkpoint, message_scheduler, @@ -811,24 +703,20 @@ where } // Run the main event loop that persists while the node is a core node. -// This can span across multiple sessions. +// This can span across multiple epochs. // -// The tracked `epoch` is updated by both clock events and secret PoL info -// events (whichever arrives first), and guards against duplicate epoch -// rotations in the cryptographic processor. +// Epoch rotations are driven by the public epoch stream (membership and public +// `PoQ` inputs) through `handle_epoch_event`. The secret `PoL` info stream is +// independent: it only enables leadership-proof generation for the current +// epoch once its info arrives, without driving rotations on its own. // -// Returns the old session components when the node is no longer a core node. +// Returns the old epoch components when the node is no longer a core node. #[expect(clippy::too_many_arguments, reason = "categorize args")] -#[expect( - clippy::cognitive_complexity, - reason = "TODO: address this at some point" -)] async fn run_event_loop< NodeId, Backend, Rng, NetAdapter, - ChainService, ProofsGenerator, ProofsVerifier, CorePoQGenerator, @@ -838,43 +726,39 @@ async fn run_event_loop< + Send + Unpin, blend_messages: &mut ( - impl Stream + Send + Unpin + 'static + impl Stream + + Send + + Unpin + + 'static ), - remaining_clock_stream: &mut (impl Stream + Send + Sync + Unpin + 'static), mut secret_pol_info_stream: impl Stream + Send + Unpin, - remaining_session_stream: &mut ( - impl Stream>> + remaining_epoch_stream: &mut ( + impl Stream>> + Unpin + Send ), - blend_config: &RunningBlendConfig, backend: &mut Backend, network_adapter: &NetAdapter, sdp_relay: &OutboundRelay, - epoch_handler: &mut EpochHandler, - mut message_scheduler: SessionMessageScheduler< + mut message_scheduler: EpochMessageScheduler< Rng, ProcessedMessage, EncapsulatedMessageWithVerifiedPublicHeader, >, rng: &mut Rng, - mut crypto_processor: CoreCryptographicProcessor< NodeId, CorePoQGenerator, ProofsGenerator, ProofsVerifier, >, - mut public_info: PublicInfo, - mut epoch: Epoch, + mut current_epoch_info: CoreEpochPublicInfo, mut recovery_checkpoint: ServiceState, ) -> ( CoreCryptographicProcessor, - OldSessionMessageScheduler>, - OldSessionBlendingTokenCollector, - PublicInfo, - Epoch, + OldEpochMessageScheduler>, + OldEpochBlendingTokenCollector, ) where NodeId: Clone + Eq + Hash + Send + Sync + 'static, @@ -892,21 +776,20 @@ where + Sync + Unpin, > + Sync, - ChainService: ChainApi + Sync + Send, ProofsGenerator: CoreAndLeaderProofsGenerator + Send + Sync, CorePoQGenerator: Send + Sync, ProofsVerifier: ProofsVerifierTrait + Send + Sync, RuntimeServiceId: Sync + Send, { - // An optional crypto processor to handle the old session during transition + // An optional crypto processor to handle the old epoch during transition // period. - let mut old_session_crypto_processor: Option< + let mut old_epoch_crypto_processor: Option< CoreCryptographicProcessor, > = None; - let mut old_session_message_scheduler: Option< - OldSessionMessageScheduler>, + let mut old_epoch_message_scheduler: Option< + OldEpochMessageScheduler>, > = None; - let mut current_secret_pol_info: Option = None; + let mut latest_secret_pol_info: Option = None; loop { tokio::select! { @@ -928,57 +811,54 @@ where } } Some(incoming_message) = blend_messages.next() => { - recovery_checkpoint = handle_incoming_blend_message(incoming_message, &mut message_scheduler, old_session_message_scheduler.as_mut(), &crypto_processor, old_session_crypto_processor.as_ref(), recovery_checkpoint); + recovery_checkpoint = handle_incoming_blend_message(incoming_message, &mut message_scheduler, old_epoch_message_scheduler.as_mut(), &crypto_processor, old_epoch_crypto_processor.as_ref(), recovery_checkpoint); } Some(round_info) = message_scheduler.next() => { recovery_checkpoint = handle_release_round(round_info, &mut crypto_processor, rng, backend, network_adapter, recovery_checkpoint).await; } - Some((Some(processed_messages_to_release), previous_session_number)) = async { - match (&mut old_session_message_scheduler, &old_session_crypto_processor) { + Some((Some(processed_messages_to_release), previous_epoch)) = async { + match (&mut old_epoch_message_scheduler, &old_epoch_crypto_processor) { (Some(old_scheduler), Some(old_crypto_processor)) => { - Some((old_scheduler.next().await, old_crypto_processor.session())) + Some((old_scheduler.next().await, old_crypto_processor.epoch())) }, _ => None } } => { - handle_release_round_for_old_session(processed_messages_to_release, rng, backend, network_adapter, previous_session_number).await; + handle_release_round_for_old_epoch(processed_messages_to_release, rng, backend, network_adapter, previous_epoch).await; } - Some(clock_tick) = remaining_clock_stream.next() => { - (public_info, epoch) = handle_clock_event(clock_tick, blend_config, epoch_handler, &mut crypto_processor, public_info, epoch).await; - } - Some(pol_info) = secret_pol_info_stream.next() => { - if let Some(new_leader_inputs) = handle_new_secret_epoch_info(blend_config, &pol_info, &mut crypto_processor, epoch) { - epoch = pol_info.epoch; - public_info.epoch = new_leader_inputs; + Some(pol_secret_info) = secret_pol_info_stream.next() => { + if current_epoch_info.epoch == pol_secret_info.epoch { + crypto_processor.set_epoch_private(pol_secret_info.poq_private_inputs.clone(), pol_secret_info.epoch); } - current_secret_pol_info = Some(pol_info.poq_private_inputs); + latest_secret_pol_info = Some(pol_secret_info); } - Some(session_event) = remaining_session_stream.next() => { - match handle_session_event(session_event, blend_config, crypto_processor, message_scheduler, public_info, recovery_checkpoint, backend, sdp_relay, epoch, current_secret_pol_info.as_ref()).await { - HandleSessionEventOutput::Transitioning { new_crypto_processor, old_crypto_processor, new_scheduler, old_scheduler, new_public_info, new_recovery_checkpoint } => { + Some(epoch_event) = remaining_epoch_stream.next() => { + match handle_epoch_event(epoch_event, blend_config, crypto_processor, message_scheduler, current_epoch_info, recovery_checkpoint, backend, sdp_relay, latest_secret_pol_info.as_ref()).await { + // Current epoch info updated to new one + HandleEpochEventOutput::Transitioning { new_crypto_processor, old_crypto_processor, new_scheduler, old_scheduler, new_epoch_info, new_recovery_checkpoint } => { crypto_processor = new_crypto_processor; - old_session_crypto_processor = Some(old_crypto_processor); + old_epoch_crypto_processor = Some(old_crypto_processor); message_scheduler = new_scheduler; - old_session_message_scheduler = Some(old_scheduler); - public_info = new_public_info; + old_epoch_message_scheduler = Some(old_scheduler); + current_epoch_info = new_epoch_info; recovery_checkpoint = new_recovery_checkpoint; }, - HandleSessionEventOutput::TransitionCompleted { current_crypto_processor, current_scheduler, current_public_info, new_recovery_checkpoint } => { + // Current epoch info unchanged + HandleEpochEventOutput::TransitionCompleted { current_crypto_processor, current_scheduler, new_recovery_checkpoint, current_epoch_info: same_epoch_info } => { crypto_processor = current_crypto_processor; - old_session_crypto_processor = None; + old_epoch_crypto_processor = None; message_scheduler = current_scheduler; - old_session_message_scheduler = None; - public_info = current_public_info; + old_epoch_message_scheduler = None; + current_epoch_info = same_epoch_info; recovery_checkpoint = new_recovery_checkpoint; }, - HandleSessionEventOutput::Retiring { old_crypto_processor, old_scheduler, old_token_collector, old_public_info } => { + // Current epoch info consumed, not usable anymore + HandleEpochEventOutput::Retiring { old_crypto_processor, old_scheduler, old_token_collector } => { tracing::info!(target: LOG_TARGET, "Exiting from the main event loop"); return ( old_crypto_processor, old_scheduler, old_token_collector, - old_public_info, - epoch, ); }, } @@ -987,7 +867,7 @@ where } } -/// Processes the old session during the session transition period +/// Processes the old epoch during the epoch transition period /// before retiring the core service. #[expect(clippy::too_many_arguments, reason = "categorize args")] async fn retire< @@ -995,43 +875,38 @@ async fn retire< Backend, Rng, NetAdapter, - ChainService, ProofsGenerator, ProofsVerifier, CorePoQGenerator, RuntimeServiceId, >( mut blend_messages: impl Stream - + Send + Unpin + + Send + 'static, - mut remaining_clock_stream: impl Stream + Send + Sync + Unpin + 'static, - mut remaining_session_stream: impl Stream< - Item = SessionEvent>, - > + Unpin, - blend_config: &RunningBlendConfig, + mut remaining_epoch_stream: impl Stream< + Item = EpochEvent>, + > + Send + + Unpin, mut backend: Backend, network_adapter: NetAdapter, sdp_relay: OutboundRelay, - mut epoch_handler: EpochHandler, - mut message_scheduler: OldSessionMessageScheduler< + mut message_scheduler: OldEpochMessageScheduler< Rng, ProcessedMessage, >, mut rng: Rng, - mut blending_token_collector: OldSessionBlendingTokenCollector, - mut crypto_processor: CoreCryptographicProcessor< + mut blending_token_collector: OldEpochBlendingTokenCollector, + crypto_processor: CoreCryptographicProcessor< NodeId, CorePoQGenerator, ProofsGenerator, ProofsVerifier, >, - mut public_info: PublicInfo, - mut epoch: Epoch, ) where - NodeId: Clone + Eq + Hash + Send + 'static, + NodeId: Clone + Eq + Hash + Send + Sync + 'static, Rng: rand::Rng + Clone + Send + Unpin, - Backend: BlendBackend + Sync, + Backend: BlendBackend + Send + Sync, NetAdapter: NetworkAdapter< RuntimeServiceId, BroadcastSettings: Serialize @@ -1043,27 +918,25 @@ async fn retire< + Send + Sync + Unpin, - > + Sync, - ChainService: ChainApi + Sync, - ProofsGenerator: CoreAndLeaderProofsGenerator + Sync, - ProofsVerifier: ProofsVerifierTrait, - RuntimeServiceId: Sync, + > + Send + + Sync, + ProofsGenerator: CoreAndLeaderProofsGenerator + Send + Sync, + CorePoQGenerator: Send + Sync, + ProofsVerifier: ProofsVerifierTrait + Send + Sync, + RuntimeServiceId: Send + Sync, { loop { tokio::select! { Some(incoming_message) = blend_messages.next() => { - handle_incoming_blend_message_from_old_session(incoming_message, &mut message_scheduler, &crypto_processor, &mut blending_token_collector); + handle_incoming_blend_message_from_old_epoch(incoming_message, &mut message_scheduler, &crypto_processor, &mut blending_token_collector); } Some(processed_messages_to_release) = message_scheduler.next() => { - handle_release_round_for_old_session(processed_messages_to_release, &mut rng, &backend, &network_adapter, crypto_processor.session()).await; + handle_release_round_for_old_epoch(processed_messages_to_release, &mut rng, &backend, &network_adapter, crypto_processor.epoch()).await; } - Some(clock_tick) = remaining_clock_stream.next() => { - (public_info, epoch) = handle_clock_event(clock_tick, blend_config, &mut epoch_handler, &mut crypto_processor, public_info, epoch).await; - } - Some(SessionEvent::TransitionPeriodExpired) = remaining_session_stream.next() => { - handle_session_transition_expired(&mut backend, blending_token_collector, &sdp_relay).await; - // Now the core service is no longer needed for the current (new) session, - // and the remaining session transition has been completed, + Some(EpochEvent::TransitionPeriodExpired) = remaining_epoch_stream.next() => { + handle_epoch_transition_expired(&mut backend, blending_token_collector, &sdp_relay).await; + // Now the core service is no longer needed for the current (new) epoch, + // and the remaining epoch transition has been completed, // so finishing the retirement process. return; } @@ -1071,19 +944,18 @@ async fn retire< } } -/// Handles a [`SessionEvent`]. +/// Handles an [`EpochEvent`]. /// -/// It consumes the previous cryptographic processor and creates a new one -/// on a new session with its new membership. It also creates new public inputs -/// for `PoQ` verification in this new session. It ignores the transition period -/// expiration event and returns the previous cryptographic processor as is. -#[expect(clippy::too_many_arguments, reason = "necessary for session handling")] -#[expect(clippy::too_many_lines, reason = "necessary for session handling")] -#[expect( - clippy::cognitive_complexity, - reason = "necessary for session handling" -)] -async fn handle_session_event< +/// On a new epoch it consumes the previous cryptographic processor and creates +/// a new one for the new epoch with its new membership and public `PoQ` +/// verification inputs. If secret `PoL` info for the new epoch is already +/// available, leadership-proof generation is enabled on the new processor right +/// away. It ignores the transition period expiration event and returns the +/// previous cryptographic processor as is. +#[expect(clippy::too_many_arguments, reason = "necessary for epoch handling")] +#[expect(clippy::too_many_lines, reason = "necessary for epoch handling")] +#[expect(clippy::cognitive_complexity, reason = "necessary for epoch handling")] +async fn handle_epoch_event< NodeId, ProofsGenerator, ProofsVerifier, @@ -1093,7 +965,7 @@ async fn handle_session_event< CorePoQGenerator, RuntimeServiceId, >( - event: SessionEvent>, + event: EpochEvent>, settings: &RunningBlendConfig, current_cryptographic_processor: CoreCryptographicProcessor< NodeId, @@ -1101,18 +973,17 @@ async fn handle_session_event< ProofsGenerator, ProofsVerifier, >, - current_scheduler: SessionMessageScheduler< + current_scheduler: EpochMessageScheduler< Rng, ProcessedMessage, EncapsulatedMessageWithVerifiedPublicHeader, >, - current_public_info: PublicInfo, + current_epoch_info: CoreEpochPublicInfo, current_recovery_checkpoint: ServiceState, backend: &mut Backend, sdp_relay: &OutboundRelay, - current_epoch: Epoch, - current_secret_info: Option<&ProofOfLeadershipQuotaInputs>, -) -> HandleSessionEventOutput< + current_secret_info: Option<&PolEpochInfo>, +) -> HandleEpochEventOutput< NodeId, Rng, ProofsGenerator, @@ -1130,178 +1001,165 @@ where Backend: BlendBackend, { match event { - SessionEvent::NewSession(MaybeEmptyCoreSessionInfo::NonEmpty(CoreSessionInfo { - core_poq_generator, - public: - CoreSessionPublicInfo { - poq_core_public_inputs: new_core_public_inputs, - session: new_session, - membership: new_membership, - }, - })) => { - let (_, _, _, _, current_session_blending_token_collector, _, state_updater) = + EpochEvent::NewEpoch(MaybeEmptyCoreEpochInfo::NonEmpty(core_epoch_info)) => { + let CoreEpochInfo { + core_poq_generator: new_core_poq_generator, + public: new_epoch_info, + } = *core_epoch_info; + let (_, _, _, _, current_epoch_blending_token_collector, _, state_updater) = current_recovery_checkpoint.into_components(); - let new_reward_session_info = reward::SessionInfo::new( - new_session, - ¤t_public_info.epoch.pol_epoch_nonce, - new_membership.size() as u64, - new_core_public_inputs.quota, + let new_reward_epoch_info = reward::EpochInfo::new( + new_epoch_info.epoch, + &new_epoch_info.poq_leadership_public_inputs.pol_epoch_nonce, + new_epoch_info.membership.size() as u64, + new_epoch_info.poq_core_public_inputs.quota, settings.activity_threshold_sensitivity, ) - .expect("Reward session info must be created successfully. Panicking since the service cannot continue with this session"); - let (new_session_blending_token_collector, old_session_blending_token_collector) = - current_session_blending_token_collector.rotate_session(&new_reward_session_info); + .expect("Reward epoch info must be created successfully. Panicking since the service cannot continue with this epoch"); + let (new_epoch_blending_token_collector, old_epoch_blending_token_collector) = + current_epoch_blending_token_collector.rotate_epoch(&new_reward_epoch_info); - let new_session_info = SessionInfo { - membership: new_membership.clone(), - session_number: new_session, - core_public_inputs: new_core_public_inputs, - }; - backend.rotate_session(new_session_info.clone()).await; + backend + .rotate_epoch((new_epoch_info.membership.clone(), new_epoch_info.epoch)) + .await; - let new_scheduler_session_info = SchedulerSessionInfo { - core_quota: settings.session_core_quota(new_session_info.membership.size()), - session_number: u128::from(new_session).into(), + let new_scheduler_epoch_info = SchedulerEpochInfo { + core_quota: settings.epoch_core_quota(new_epoch_info.membership.size()), + epoch: new_epoch_info.epoch, }; - let new_public_info = PublicInfo { - session: new_session_info.clone(), - ..current_public_info - }; - let Some(core_poq_generator) = core_poq_generator else { + let Some(core_poq_generator) = new_core_poq_generator else { tracing::info!(target: LOG_TARGET, "Local node is not part of new membership. Retiring from core."); - return HandleSessionEventOutput::Retiring { + return HandleEpochEventOutput::Retiring { old_crypto_processor: current_cryptographic_processor, old_scheduler: current_scheduler - .rotate_session(new_scheduler_session_info, settings.scheduler_settings()) + .rotate_epoch(new_scheduler_epoch_info, settings.scheduler_settings()) .1, - old_token_collector: old_session_blending_token_collector, - old_public_info: current_public_info, + old_token_collector: old_epoch_blending_token_collector, }; }; let new_processor = match CoreCryptographicProcessor::try_new_with_core_condition_check( - new_membership, + new_epoch_info.membership.clone(), settings.minimum_network_size, - SessionCryptographicProcessorSettings { + EpochCryptographicProcessorSettings { non_ephemeral_encryption_key: settings .non_ephemeral_signing_key .derive_x25519(), num_blend_layers: settings.num_blend_layers, }, - new_public_info.clone().into(), + PoQVerificationInputsMinusSigningKey { + core: new_epoch_info.poq_core_public_inputs, + leader: new_epoch_info.poq_leadership_public_inputs, + }, core_poq_generator, - current_epoch, + new_epoch_info.epoch, ) { Ok(mut new_processor) => { - if let Some(current_secret_info) = current_secret_info { + if let Some(current_secret_info) = current_secret_info + && current_secret_info.epoch == new_epoch_info.epoch + { new_processor.set_epoch_private( - current_secret_info.clone(), - current_public_info.epoch, - current_epoch, + current_secret_info.poq_private_inputs.clone(), + new_epoch_info.epoch, ); } new_processor } Err(e @ (Error::LocalIsNotCoreNode | Error::NetworkIsTooSmall(_))) => { tracing::info!(target: LOG_TARGET, "New membership does not satisfy the core node condition: {e:?}"); - return HandleSessionEventOutput::Retiring { + return HandleEpochEventOutput::Retiring { old_crypto_processor: current_cryptographic_processor, old_scheduler: current_scheduler - .rotate_session( - new_scheduler_session_info, - settings.scheduler_settings(), - ) + .rotate_epoch(new_scheduler_epoch_info, settings.scheduler_settings()) .1, - old_token_collector: old_session_blending_token_collector, - old_public_info: current_public_info, + old_token_collector: old_epoch_blending_token_collector, }; } }; let (new_scheduler, old_scheduler) = current_scheduler - .rotate_session(new_scheduler_session_info, settings.scheduler_settings()); - HandleSessionEventOutput::Transitioning { + .rotate_epoch(new_scheduler_epoch_info, settings.scheduler_settings()); + HandleEpochEventOutput::Transitioning { new_crypto_processor: new_processor, old_crypto_processor: current_cryptographic_processor, new_scheduler, old_scheduler, - new_public_info, - new_recovery_checkpoint: ServiceState::with_session( - new_session, - new_session_blending_token_collector, - Some(old_session_blending_token_collector), + new_recovery_checkpoint: ServiceState::with_epoch( + new_epoch_info.epoch, + new_epoch_blending_token_collector, + Some(old_epoch_blending_token_collector), state_updater, ) .expect("service state should be created successfully"), + new_epoch_info, } } - SessionEvent::NewSession(MaybeEmptyCoreSessionInfo::Empty { session }) => { - tracing::info!(target: LOG_TARGET, "New session event received, but no session info is available due to empty membership set."); - let (_, _, _, _, current_session_blending_token_collector, _, _) = + EpochEvent::NewEpoch(MaybeEmptyCoreEpochInfo::Empty { epoch, epoch_nonce }) => { + tracing::info!(target: LOG_TARGET, "New epoch event received, but no epoch info is available due to empty membership set."); + let (_, _, _, _, current_epoch_blending_token_collector, _, _) = current_recovery_checkpoint.into_components(); - let new_reward_session_info = reward::SessionInfo::new( - session, - ¤t_public_info.epoch.pol_epoch_nonce, + let new_reward_epoch_info = reward::EpochInfo::new( + epoch, + &epoch_nonce, 0, 0, settings.activity_threshold_sensitivity, ) - .expect("Reward session info must be created successfully. Panicking since the service cannot continue with this session"); - let (_, old_session_blending_token_collector) = - current_session_blending_token_collector.rotate_session(&new_reward_session_info); - HandleSessionEventOutput::Retiring { + .expect("Reward epoch info must be created successfully. Panicking since the service cannot continue with this epoch"); + let (_, old_epoch_blending_token_collector) = + current_epoch_blending_token_collector.rotate_epoch(&new_reward_epoch_info); + HandleEpochEventOutput::Retiring { old_crypto_processor: current_cryptographic_processor, old_scheduler: current_scheduler.consume(), - old_token_collector: old_session_blending_token_collector, - old_public_info: current_public_info, + old_token_collector: old_epoch_blending_token_collector, } } - SessionEvent::TransitionPeriodExpired => { + EpochEvent::TransitionPeriodExpired => { let mut state_updater = current_recovery_checkpoint.start_updating(); - if let Some(old_token_collector) = state_updater.clear_old_session_token_collector() { - handle_session_transition_expired(backend, old_token_collector, sdp_relay).await; + if let Some(old_token_collector) = state_updater.clear_old_epoch_token_collector() { + handle_epoch_transition_expired(backend, old_token_collector, sdp_relay).await; } - HandleSessionEventOutput::TransitionCompleted { + HandleEpochEventOutput::TransitionCompleted { current_crypto_processor: current_cryptographic_processor, current_scheduler, - current_public_info, + current_epoch_info, new_recovery_checkpoint: state_updater.commit_changes(), } } } } -/// Handles [`SessionEvent::TransitionPeriodExpired`]. -async fn handle_session_transition_expired( +/// Handles [`EpochEvent::TransitionPeriodExpired`]. +async fn handle_epoch_transition_expired( backend: &mut Backend, - blending_token_collector: OldSessionBlendingTokenCollector, + blending_token_collector: OldEpochBlendingTokenCollector, sdp_relay: &OutboundRelay, ) where Backend: BlendBackend, NodeId: Eq + Hash + Clone + Send, { compute_and_submit_activity_proof(blending_token_collector, sdp_relay).await; - backend.complete_session_transition().await; + backend.complete_epoch_transition().await; } async fn compute_and_submit_activity_proof( - blending_token_collector: OldSessionBlendingTokenCollector, + blending_token_collector: OldEpochBlendingTokenCollector, sdp_relay: &OutboundRelay, ) { if let Some(activity_proof) = blending_token_collector.compute_activity_proof() { if let Err(e) = submit_activity_proof(activity_proof, sdp_relay).await { - error!(target: LOG_TARGET, "Failed to submit activity proof for the old session: {e:?}"); + error!(target: LOG_TARGET, "Failed to submit activity proof for the old epoch: {e:?}"); } } else { - debug!(target: LOG_TARGET, "No activity proof generated for the old session"); + debug!(target: LOG_TARGET, "No activity proof generated for the old epoch"); } } -enum HandleSessionEventOutput< +enum HandleEpochEventOutput< NodeId, Rng, ProofsGenerator, @@ -1315,32 +1173,31 @@ enum HandleSessionEventOutput< CoreCryptographicProcessor, old_crypto_processor: CoreCryptographicProcessor, - new_scheduler: SessionMessageScheduler< + new_scheduler: EpochMessageScheduler< Rng, ProcessedMessage, EncapsulatedMessageWithVerifiedPublicHeader, >, - old_scheduler: OldSessionMessageScheduler>, - new_public_info: PublicInfo, + old_scheduler: OldEpochMessageScheduler>, + new_epoch_info: CoreEpochPublicInfo, new_recovery_checkpoint: ServiceState, }, TransitionCompleted { current_crypto_processor: CoreCryptographicProcessor, - current_scheduler: SessionMessageScheduler< + current_scheduler: EpochMessageScheduler< Rng, ProcessedMessage, EncapsulatedMessageWithVerifiedPublicHeader, >, - current_public_info: PublicInfo, + current_epoch_info: CoreEpochPublicInfo, new_recovery_checkpoint: ServiceState, }, Retiring { old_crypto_processor: CoreCryptographicProcessor, - old_scheduler: OldSessionMessageScheduler>, - old_token_collector: OldSessionBlendingTokenCollector, - old_public_info: PublicInfo, + old_scheduler: OldEpochMessageScheduler>, + old_token_collector: OldEpochBlendingTokenCollector, }, } @@ -1370,7 +1227,7 @@ async fn handle_serialized_local_data_message< ProofsGenerator, ProofsVerifier, >, - scheduler: &mut SessionMessageScheduler< + scheduler: &mut EpochMessageScheduler< Rng, ProcessedMessage, EncapsulatedMessageWithVerifiedPublicHeader, @@ -1445,7 +1302,7 @@ where ) } }; - state_updater.collect_current_session_tokens(blending_tokens.into_iter()); + state_updater.collect_current_epoch_tokens(blending_tokens.into_iter()); // We treat a partially or fully decapsulated message as a processed message, // and we schedule for its release at the next release round. @@ -1461,8 +1318,8 @@ where /// Processes an incoming Blend message (with verified signature) received /// from a core or edge peer. /// -/// Decapsulation is attempted with the current or old session's cryptographic -/// processor depending on the session the message is coming from. +/// Decapsulation is attempted with the current or old epoch's cryptographic +/// processor depending on the epoch the message is coming from. fn handle_incoming_blend_message< NodeId, Rng, @@ -1472,14 +1329,14 @@ fn handle_incoming_blend_message< ProofsVerifier, CorePoQGenerator, >( - (validated_encapsulated_message, session): (EncapsulatedMessageWithVerifiedSignature, u64), - scheduler: &mut SessionMessageScheduler< + (validated_encapsulated_message, epoch): (EncapsulatedMessageWithVerifiedSignature, Epoch), + scheduler: &mut EpochMessageScheduler< Rng, ProcessedMessage, EncapsulatedMessageWithVerifiedPublicHeader, >, - old_session_scheduler: Option< - &mut OldSessionMessageScheduler>, + old_epoch_scheduler: Option< + &mut OldEpochMessageScheduler>, >, cryptographic_processor: &CoreCryptographicProcessor< NodeId, @@ -1487,7 +1344,7 @@ fn handle_incoming_blend_message< ProofsGenerator, ProofsVerifier, >, - old_session_cryptographic_processor: Option< + old_epoch_cryptographic_processor: Option< &CoreCryptographicProcessor, >, current_recovery_checkpoint: ServiceState, @@ -1499,39 +1356,39 @@ where BackendSettings: Clone, ProofsVerifier: ProofsVerifierTrait, { - if session == cryptographic_processor.session() { + if epoch == cryptographic_processor.epoch() { let Some(output) = try_validate_and_decapsulate( validated_encapsulated_message, cryptographic_processor, - session, + epoch, ) else { return current_recovery_checkpoint; }; - handle_decapsulated_incoming_message_from_current_session( + handle_decapsulated_incoming_message_from_current_epoch( output, scheduler, current_recovery_checkpoint, cryptographic_processor, ) - } else if let Some(old_cryptographic_processor) = old_session_cryptographic_processor - && session == old_cryptographic_processor.session() + } else if let Some(old_cryptographic_processor) = old_epoch_cryptographic_processor + && epoch == old_cryptographic_processor.epoch() { let Some(output) = try_validate_and_decapsulate( validated_encapsulated_message, old_cryptographic_processor, - session, + epoch, ) else { return current_recovery_checkpoint; }; - handle_decapsulated_incoming_message_from_old_session( + handle_decapsulated_incoming_message_from_old_epoch( output, - old_session_scheduler - .expect("Old session scheduler should be available when old session crypto processor is available"), + old_epoch_scheduler + .expect("Old epoch scheduler should be available when old epoch crypto processor is available"), current_recovery_checkpoint, old_cryptographic_processor, ) } else { - tracing::debug!(target: LOG_TARGET, "Received message for session {session} that is not currently handled. Ignoring..."); + tracing::debug!(target: LOG_TARGET, "Received message for epoch {epoch} that is not currently handled. Ignoring..."); current_recovery_checkpoint } } @@ -1547,22 +1404,22 @@ fn try_validate_and_decapsulate, - session: u64, + epoch: Epoch, ) -> Option where ProofsVerifier: ProofsVerifierTrait, { let Ok(validated_message) = processor.validate_message_poq(message) else { - tracing::debug!(target: LOG_TARGET, "Received message for session {session} failed PoQ validation. Ignoring..."); + tracing::debug!(target: LOG_TARGET, "Received message for epoch {epoch} failed PoQ validation. Ignoring..."); return None; }; match processor.decapsulate_message_recursive(validated_message) { Ok(output) => Some(output), Err(e) => { if matches!(e, MessageError::PrivateHeaderDeserializationFailed) { - tracing::trace!(target: LOG_TARGET, "Failed to decapsulate received message for session {session} due to deserialization error. This can happen when the message was intended for another node or when the message is malformed. Ignoring..."); + tracing::trace!(target: LOG_TARGET, "Failed to decapsulate received message for epoch {epoch} due to deserialization error. This can happen when the message was intended for another node or when the message is malformed. Ignoring..."); } else { - tracing::debug!(target: LOG_TARGET, "Failed to decapsulate received message for session {session}: {e:?}."); + tracing::debug!(target: LOG_TARGET, "Failed to decapsulate received message for epoch {epoch}: {e:?}."); } None } @@ -1570,8 +1427,8 @@ where } /// Same as [`handle_incoming_blend_message`] but only tries with -/// the old session crypto processor. -fn handle_incoming_blend_message_from_old_session< +/// the old epoch crypto processor. +fn handle_incoming_blend_message_from_old_epoch< Rng, NodeId, BroadcastSettings, @@ -1580,14 +1437,14 @@ fn handle_incoming_blend_message_from_old_session< CorePoQGenerator, >( validated_encapsulated_message: EncapsulatedMessageWithVerifiedSignature, - scheduler: &mut OldSessionMessageScheduler>, + scheduler: &mut OldEpochMessageScheduler>, cryptographic_processor: &CoreCryptographicProcessor< NodeId, CorePoQGenerator, ProofsGenerator, ProofsVerifier, >, - blending_token_collector: &mut OldSessionBlendingTokenCollector, + blending_token_collector: &mut OldEpochBlendingTokenCollector, ) where NodeId: 'static, BroadcastSettings: Serialize + for<'de> Deserialize<'de> + Debug + Eq + Hash + Clone + Send, @@ -1607,20 +1464,20 @@ fn handle_incoming_blend_message_from_old_session< } Err(e) => { if matches!(e, MessageError::PrivateHeaderDeserializationFailed) { - tracing::trace!(target: LOG_TARGET, "Failed to decapsulate received message from old session due to deserialization error. This can happen when the message was intended for another node or when the message is malformed. Ignoring..."); + tracing::trace!(target: LOG_TARGET, "Failed to decapsulate received message from old epoch due to deserialization error. This can happen when the message was intended for another node or when the message is malformed. Ignoring..."); } else { - tracing::debug!(target: LOG_TARGET, "Failed to decapsulate received message from old session: {e:?}"); + tracing::debug!(target: LOG_TARGET, "Failed to decapsulate received message from old epoch: {e:?}"); } } } } -/// Schedules a decapsulated incoming message from the current session, +/// Schedules a decapsulated incoming message from the current epoch, /// and collects the blending tokens obtained from the decapsulation. /// /// It updates the recovery checkpoint by storing the scheduled message /// and the collected tokens. -fn handle_decapsulated_incoming_message_from_current_session< +fn handle_decapsulated_incoming_message_from_current_epoch< Rng, BroadcastSettings, BackendSettings, @@ -1630,7 +1487,7 @@ fn handle_decapsulated_incoming_message_from_current_session< ProofsVerifier, >( multi_layer_decapsulation_output: MultiLayerDecapsulationOutput, - scheduler: &mut SessionMessageScheduler< + scheduler: &mut EpochMessageScheduler< Rng, ProcessedMessage, EncapsulatedMessageWithVerifiedPublicHeader, @@ -1662,15 +1519,15 @@ where .expect("Swarm should bubble up unique messages only."); } - state_updater.collect_current_session_tokens(blending_tokens); + state_updater.collect_current_epoch_tokens(blending_tokens); state_updater.commit_changes() } -/// Schedules a decapsulated incoming message from the old session, +/// Schedules a decapsulated incoming message from the old epoch, /// and collects the blending tokens obtained from the decapsulation. /// /// It updates the recovery checkpoint by storing the collected tokens. -fn handle_decapsulated_incoming_message_from_old_session< +fn handle_decapsulated_incoming_message_from_old_epoch< Rng, BroadcastSettings, BackendSettings, @@ -1680,7 +1537,7 @@ fn handle_decapsulated_incoming_message_from_old_session< ProofsVerifier, >( multi_layer_decapsulation_output: MultiLayerDecapsulationOutput, - scheduler: &mut OldSessionMessageScheduler>, + scheduler: &mut OldEpochMessageScheduler>, recovery_checkpoint: ServiceState, old_cryptographic_processor: &CoreCryptographicProcessor< NodeId, @@ -1702,7 +1559,7 @@ where let mut state_updater = recovery_checkpoint.start_updating(); state_updater - .collect_old_session_tokens(blending_tokens) + .collect_old_epoch_tokens(blending_tokens) .expect("token collector in the state should be updated successfully"); state_updater.commit_changes() } @@ -1847,7 +1704,7 @@ where usize::from(should_generate_cover_message), ); let mut state_updater = current_recovery_checkpoint.start_updating(); - let current_session = cryptographic_processor.session(); + let current_epoch = cryptographic_processor.epoch(); let data_messages_relay_futures = data_messages.into_iter() // While we iterate and map the messages to the sending futures, we update the recovery state to remove each message. @@ -1859,7 +1716,7 @@ where state_updater.consume_core_quota(1); }).map( |data_message_to_blend| -> BoxFuture<'_, ()> { - backend.publish(data_message_to_blend, current_session).boxed() + backend.publish(data_message_to_blend, current_epoch).boxed() }, ).collect::>(); @@ -1868,7 +1725,7 @@ where backend, network_adapter, Some(&mut state_updater), - current_session, + current_epoch, ); let mut message_futures = data_messages_relay_futures @@ -1891,7 +1748,7 @@ where EncapsulatedMessageWithVerifiedPublicHeader::from_message_unchecked( encapsulated_cover_message, ), - current_session, + current_epoch, ) .boxed(), ); @@ -1906,12 +1763,12 @@ where state_updater.commit_changes() } -async fn handle_release_round_for_old_session( +async fn handle_release_round_for_old_epoch( processed_messages_to_release: Vec>, rng: &mut Rng, backend: &Backend, network_adapter: &NetAdapter, - session_number: u64, + epoch: Epoch, ) where NodeId: Eq + Hash + 'static, Rng: RngCore + Send, @@ -1923,14 +1780,14 @@ async fn handle_release_round_for_old_session 0 { tracing::debug!( target: LOG_TARGET, - "Sent out {num_futures} processed messages at this release window for the old session" + "Sent out {num_futures} processed messages at this release window for the old epoch" ); } else { tracing::trace!( target: LOG_TARGET, - "Sent out {num_futures} processed messages at this release window for the old session" + "Sent out {num_futures} processed messages at this release window for the old epoch" ); } } @@ -1974,7 +1831,7 @@ fn build_futures_to_release_processed_messages< mut state_updater: Option< &mut ServiceStateUpdater, >, - session_number: u64, + epoch: Epoch, ) -> Vec> where NodeId: Eq + Hash + 'static, @@ -1997,7 +1854,7 @@ where message, }) => network_adapter.broadcast(message, broadcast_settings).boxed(), ProcessedMessage::Encapsulated(encapsulated_message) => { - backend.publish(*encapsulated_message, session_number).boxed() + backend.publish(*encapsulated_message, epoch).boxed() } } }, @@ -2048,7 +1905,7 @@ where }; let (blending_tokens, message_type) = multi_layer_decapsulation_output.into_components(); - state_updater.collect_current_session_tokens(blending_tokens.into_iter()); + state_updater.collect_current_epoch_tokens(blending_tokens.into_iter()); match message_type { // This is the initial message that was encapsulated, since we fully @@ -2060,195 +1917,12 @@ where } } -/// Handle a clock event by calling into the epoch handler and process the -/// resulting epoch event, if any. -/// -/// On a new epoch, it updates the public info and conditionally rotates both -/// the cryptographic processor and the backend verifier. Both rotations are -/// guarded by `new_epoch > current_epoch` to avoid duplicates when the `PoL` -/// info handler in the event loop has already advanced to this epoch (and -/// already called `backend.rotate_epoch`). At the end of an epoch transition -/// period, it notifies the Blend components that the old epoch transition is -/// complete. -/// -/// Returns the updated public info and the new tracked epoch. -async fn handle_clock_event< - NodeId, - ProofsGenerator, - ProofsVerifier, - ChainService, - BackendSettings, - CorePoQGenerator, - RuntimeServiceId, ->( - slot_tick: SlotTick, - settings: &RunningBlendConfig, - epoch_handler: &mut EpochHandler, - cryptographic_processor: &mut CoreCryptographicProcessor< - NodeId, - CorePoQGenerator, - ProofsGenerator, - ProofsVerifier, - >, - current_public_info: PublicInfo, - current_epoch: Epoch, -) -> (PublicInfo, Epoch) -where - BackendSettings: Sync, - ProofsGenerator: CoreAndLeaderProofsGenerator, - ProofsVerifier: ProofsVerifierTrait, - ChainService: ChainApi + Sync, - RuntimeServiceId: Sync, -{ - let Some(epoch_event) = epoch_handler.tick(slot_tick).await else { - return (current_public_info, current_epoch); - }; - - match epoch_event { - EpochEvent::NewEpoch(( - LeaderInputsMinusQuota { - pol_epoch_nonce, - pol_ledger_aged, - lottery_0, - lottery_1, - }, - new_epoch, - )) => { - tracing::debug!(target: LOG_TARGET, "New epoch {new_epoch:?} with nonce {pol_epoch_nonce:?} started"); - if new_epoch <= current_epoch { - return (current_public_info, current_epoch); - } - - // Only rotate if the PoL info handler hasn't already advanced - // the crypto processor and backend verifier to this epoch. - let new_leader_inputs = LeaderInputs { - message_quota: settings.session_leadership_quota(), - pol_epoch_nonce, - pol_ledger_aged, - lottery_0, - lottery_1, - }; - let new_public_info = PublicInfo { - epoch: new_leader_inputs, - ..current_public_info - }; - - // Only rotate if the PoL info handler hasn't already advanced - // the crypto processor and backend verifier to this epoch. - cryptographic_processor.rotate_epoch(new_leader_inputs, new_epoch); - - (new_public_info, new_epoch) - } - EpochEvent::OldEpochTransitionPeriodExpired => { - tracing::debug!(target: LOG_TARGET, "Old epoch transition period expired."); - cryptographic_processor.complete_epoch_transition(); - - (current_public_info, current_epoch) - } - EpochEvent::NewEpochAndOldEpochTransitionExpired(( - LeaderInputsMinusQuota { - pol_epoch_nonce, - pol_ledger_aged, - lottery_0, - lottery_1, - }, - new_epoch, - )) => { - tracing::debug!(target: LOG_TARGET, "New epoch {new_epoch:?} with nonce {pol_epoch_nonce:?} started and old epoch transition period expired."); - if new_epoch <= current_epoch { - return (current_public_info, current_epoch); - } - - let new_leader_inputs = LeaderInputs { - message_quota: settings.session_leadership_quota(), - pol_epoch_nonce, - pol_ledger_aged, - lottery_0, - lottery_1, - }; - let new_public_inputs = PublicInfo { - epoch: new_leader_inputs, - ..current_public_info - }; - - // Complete the previous epoch's transition first, then rotate to - // the new epoch (only if the PoL info handler hasn't already - // advanced the crypto processor and backend verifier to this epoch). - cryptographic_processor.complete_epoch_transition(); - cryptographic_processor.rotate_epoch(new_leader_inputs, new_epoch); - - (new_public_inputs, new_epoch) - } - } -} - -/// Handle the availability of new secret `PoL` info by updating the -/// cryptographic processor. -/// -/// If the secret info is for a new epoch that the clock handler hasn't -/// processed yet, the core proof generator and verifier are updated first -/// via [`CoreCryptographicProcessor::rotate_epoch`]. Then the leadership -/// proof generator is set with the received private inputs. -fn handle_new_secret_epoch_info< - NodeId, - ProofsGenerator, - BackendSettings, - ProofsVerifier, - CorePoQGenerator, ->( - settings: &RunningBlendConfig, - new_pol_info: &PolEpochInfo, - cryptographic_processor: &mut CoreCryptographicProcessor< - NodeId, - CorePoQGenerator, - ProofsGenerator, - ProofsVerifier, - >, - current_epoch: Epoch, -) -> Option -where - BackendSettings: Sync, - ProofsGenerator: CoreAndLeaderProofsGenerator, - ProofsVerifier: ProofsVerifierTrait, -{ - tracing::debug!( - target: LOG_TARGET, - current_epoch = ?current_epoch, - slot = ?new_pol_info.poq_public_inputs.slot, - "Received new secret PoL info; updating cryptographic processor" - ); - let new_leader_inputs = LeaderInputs { - pol_ledger_aged: new_pol_info.poq_public_inputs.aged_root, - pol_epoch_nonce: new_pol_info.poq_public_inputs.epoch_nonce, - message_quota: settings.session_leadership_quota(), - lottery_0: new_pol_info.poq_public_inputs.lottery_0, - lottery_1: new_pol_info.poq_public_inputs.lottery_1, - }; - - cryptographic_processor.set_epoch_private( - new_pol_info.poq_private_inputs.clone(), - new_leader_inputs, - new_pol_info.epoch, - ); - - // If we've already processed the public epoch inputs, do not return anything. - if new_pol_info.epoch <= current_epoch { - return None; - } - - // If the secret info is for a new epoch not yet seen via the clock - // handler, update the core proof generator and proof verifier first. - cryptographic_processor.rotate_epoch(new_leader_inputs, new_pol_info.epoch); - - Some(new_leader_inputs) -} - /// Submits an activity proof to the SDP service. async fn submit_activity_proof( proof: ActivityProof, sdp_relay: &OutboundRelay, ) -> Result<(), RelayError> { - debug!(target: LOG_TARGET, "Submitting activity proof for the old session"); + debug!(target: LOG_TARGET, "Submitting activity proof for the old epoch"); sdp_relay .send(SdpMessage::PostActivity { metadata: ActivityMetadata::Blend(Box::new((&proof).into())), diff --git a/services/blend/src/core/processor.rs b/services/blend/src/core/processor.rs index 0424b2377..051e34318 100644 --- a/services/blend/src/core/processor.rs +++ b/services/blend/src/core/processor.rs @@ -23,8 +23,8 @@ use lb_blend::{ membership::Membership, message_blend::{ crypto::{ - SessionCryptographicProcessorSettings, - core_and_leader::send_and_receive::SessionCryptographicProcessor, + EpochCryptographicProcessorSettings, + core_and_leader::send_and_receive::EpochCryptographicProcessor, }, provers::core_and_leader::CoreAndLeaderProofsGenerator, }, @@ -33,9 +33,17 @@ use lb_blend::{ use lb_chain_service::Epoch; pub struct CoreCryptographicProcessor( - SessionCryptographicProcessor, + EpochCryptographicProcessor, ); +impl + CoreCryptographicProcessor +{ + pub const fn epoch(&self) -> Epoch { + self.0.epoch() + } +} + impl CoreCryptographicProcessor where @@ -45,7 +53,7 @@ where pub fn try_new_with_core_condition_check( membership: Membership, minimum_network_size: NonZeroU64, - settings: SessionCryptographicProcessorSettings, + settings: EpochCryptographicProcessorSettings, public_info: PoQVerificationInputsMinusSigningKey, core_proof_of_quota_generator: CorePoQGenerator, epoch: Epoch, @@ -70,12 +78,12 @@ where fn new( membership: Membership, - settings: SessionCryptographicProcessorSettings, + settings: EpochCryptographicProcessorSettings, public_info: PoQVerificationInputsMinusSigningKey, core_proof_of_quota_generator: CorePoQGenerator, epoch: Epoch, ) -> Self { - Self(SessionCryptographicProcessor::new( + Self(EpochCryptographicProcessor::new( settings, membership, public_info, @@ -148,13 +156,13 @@ where } /// Semantically similar to the underlying - /// [`SessionCryptographicProcessor::decapsulate_message`], but it does not + /// [`EpochCryptographicProcessor::decapsulate_message`], but it does not /// stop after decapsulating the outermost layer. It stops only when a layer /// cannot be decapsulated or when the decapsulation is completed. /// /// If no layer (`Err`) or at most one layer (`Ok`) can be decapsulated, /// this is semantically equivalent to - /// calling [`SessionCryptographicProcessor::decapsulate_message`]. + /// calling [`EpochCryptographicProcessor::decapsulate_message`]. /// /// If more than a single layer can be decapsulated, then the decapsulation /// happens recursively until the first layer that cannot be decapsulated is @@ -224,7 +232,7 @@ impl Deref for CoreCryptographicProcessor { type Target = - SessionCryptographicProcessor; + EpochCryptographicProcessor; fn deref(&self) -> &Self::Target { &self.0 @@ -267,7 +275,7 @@ mod tests { }, selection::{self, VerifiedProofOfSelection}, }, - scheduling::message_blend::crypto::SessionCryptographicProcessorSettings, + scheduling::message_blend::crypto::EpochCryptographicProcessorSettings, }; use lb_chain_service::Epoch; use lb_core::crypto::ZkHash; @@ -286,7 +294,6 @@ mod tests { use lb_groth16::Field as _; PoQVerificationInputsMinusSigningKey { - session: 1, core: CoreInputs { quota: 1, zk_root: ZkHash::ZERO, @@ -514,8 +521,8 @@ mod tests { .unwrap() } - fn settings(local_id: NodeId) -> SessionCryptographicProcessorSettings { - SessionCryptographicProcessorSettings { + fn settings(local_id: NodeId) -> EpochCryptographicProcessorSettings { + EpochCryptographicProcessorSettings { non_ephemeral_encryption_key: key(local_id).0.derive_x25519(), num_blend_layers: NonZeroU64::new(1).unwrap(), } diff --git a/services/blend/src/core/scheduler.rs b/services/blend/src/core/scheduler.rs index 0058f3c66..04fc0ad33 100644 --- a/services/blend/src/core/scheduler.rs +++ b/services/blend/src/core/scheduler.rs @@ -1,15 +1,15 @@ use std::fmt::Debug; use lb_blend::scheduling::{ - SessionMessageScheduler, - message_scheduler::{ProcessedMessageScheduler as _, Settings, session_info::SessionInfo}, + EpochMessageScheduler, + message_scheduler::{ProcessedMessageScheduler as _, Settings, epoch_info::EpochInfo}, }; /// A wrapper around a [`MessageScheduler`] that allows creation with a set of /// initial messages. pub struct SchedulerWrapper { /// The inner message scheduler. - scheduler: SessionMessageScheduler, + scheduler: EpochMessageScheduler, } impl SchedulerWrapper @@ -19,7 +19,7 @@ where DataMessage: Debug + Unpin, { pub fn new_with_initial_messages( - session_info: SessionInfo, + epoch_info: EpochInfo, rng: Rng, settings: Settings, processed_messages: ProcessedMessages, @@ -29,7 +29,7 @@ where ProcessedMessages: Iterator, DataMessages: Iterator, { - let mut scheduler = SessionMessageScheduler::new(session_info, rng, settings); + let mut scheduler = EpochMessageScheduler::new(epoch_info, rng, settings); processed_messages.for_each(|m| scheduler.schedule_processed_message(m)); data_messages.for_each(|m| scheduler.queue_data_message(m)); Self { scheduler } @@ -37,7 +37,7 @@ where } impl From> - for SessionMessageScheduler + for EpochMessageScheduler { fn from(wrapper: SchedulerWrapper) -> Self { wrapper.scheduler diff --git a/services/blend/src/core/service_components.rs b/services/blend/src/core/service_components.rs index a74d827bf..0994b6dc9 100644 --- a/services/blend/src/core/service_components.rs +++ b/services/blend/src/core/service_components.rs @@ -21,7 +21,6 @@ impl< Backend, NodeId, Network, - MembershipAdapter, SdpAdapter, ProofsGenerator, ProofsVerifier, @@ -34,7 +33,6 @@ impl< Backend, NodeId, Network, - MembershipAdapter, SdpAdapter, ProofsGenerator, ProofsVerifier, diff --git a/services/blend/src/core/settings.rs b/services/blend/src/core/settings.rs index 098fe297a..4d73cb822 100644 --- a/services/blend/src/core/settings.rs +++ b/services/blend/src/core/settings.rs @@ -40,13 +40,13 @@ pub struct RunningBlendConfig { } impl RunningBlendConfig { - pub fn session_core_quota(&self, membership_size: usize) -> u64 { + pub fn epoch_core_quota(&self, membership_size: usize) -> u64 { self.scheduler .cover - .session_core_quota(self.num_blend_layers, &self.time, membership_size) + .epoch_core_quota(self.num_blend_layers, &self.time, membership_size) } - pub const fn session_leadership_quota(&self) -> u64 { + pub const fn epoch_leadership_quota(&self) -> u64 { let num_blend_layers = self.num_blend_layers.get(); let additional_encapsulations = num_blend_layers .checked_mul(self.data_replication_factor) @@ -56,13 +56,13 @@ impl RunningBlendConfig { .expect("Overflow when computing leadership quota.") } - pub(super) fn scheduler_settings(&self) -> lb_blend::scheduling::message_scheduler::Settings { + pub(super) const fn scheduler_settings( + &self, + ) -> lb_blend::scheduling::message_scheduler::Settings { lb_blend::scheduling::message_scheduler::Settings { - additional_safety_intervals: self.scheduler.cover.intervals_for_safety_buffer, - expected_intervals_per_session: self.time.intervals_per_session(), maximum_release_delay_in_rounds: self.scheduler.delayer.maximum_release_delay_in_rounds, round_duration: self.time.round_duration, - rounds_per_interval: self.time.rounds_per_interval, + rounds_per_epoch: self.time.rounds_per_epoch, num_blend_layers: self.num_blend_layers, } } @@ -84,15 +84,12 @@ pub struct SchedulerSettings { pub struct CoverTrafficSettings { /// `F_c`: frequency at which cover messages are generated per round. pub message_frequency_per_round: NonNegativeF64, - // `max`: safety buffer length, expressed in intervals - pub intervals_for_safety_buffer: u64, } #[cfg(test)] impl Default for CoverTrafficSettings { fn default() -> Self { Self { - intervals_for_safety_buffer: 1, message_frequency_per_round: 1.try_into().unwrap(), } } @@ -100,14 +97,14 @@ impl Default for CoverTrafficSettings { impl CoverTrafficSettings { #[must_use] - pub(crate) fn session_core_quota( + pub(crate) fn epoch_core_quota( &self, num_blend_layers: NonZeroU64, timings: &TimingSettings, membership_size: usize, ) -> u64 { core_quota( - timings.rounds_per_session, + timings.rounds_per_epoch, self.message_frequency_per_round, num_blend_layers, membership_size, diff --git a/services/blend/src/core/state.rs b/services/blend/src/core/state.rs index ce5bae3e7..0b1a80c7a 100644 --- a/services/blend/src/core/state.rs +++ b/services/blend/src/core/state.rs @@ -3,8 +3,9 @@ mod serde { use lb_blend::message::{ encap::validated::EncapsulatedMessageWithVerifiedPublicHeader, - reward::{OldSessionBlendingTokenCollector, SessionBlendingTokenCollector}, + reward::{EpochBlendingTokenCollector, OldEpochBlendingTokenCollector}, }; + use lb_chain_service::Epoch; use serde::{Deserialize, Serialize}; use crate::{ @@ -17,15 +18,15 @@ mod serde { /// /// For details about its fields, check [`ServiceState`]. pub struct SerializableServiceState { - last_seen_session: u64, + last_seen_epoch: Epoch, spent_core_quota: u64, #[serde(bound( deserialize = "BroadcastSettings: Deserialize<'de> + Eq + core::hash::Hash" ))] unsent_processed_messages: HashSet>, unsent_data_messages: HashSet, - current_session_token_collector: SessionBlendingTokenCollector, - old_session_token_collector: Option, + current_epoch_token_collector: EpochBlendingTokenCollector, + old_epoch_token_collector: Option, } impl SerializableServiceState { @@ -37,18 +38,18 @@ mod serde { state_updater: overwatch::services::state::StateUpdater< Option>, >, - ) -> Result, error::SessionMismatch> + ) -> Result, error::EpochMismatch> where BackendSettings: Clone, BroadcastSettings: Clone, { ServiceState::new( - self.last_seen_session, + self.last_seen_epoch, self.spent_core_quota, self.unsent_processed_messages, self.unsent_data_messages, - self.current_session_token_collector, - self.old_session_token_collector, + self.current_epoch_token_collector, + self.old_epoch_token_collector, state_updater, ) } @@ -59,21 +60,21 @@ mod serde { { fn from(value: ServiceState) -> Self { let ( - last_seen_session, + last_seen_epoch, spent_core_quota, unsent_processed_messages, unsent_data_messages, - current_session_token_collector, - old_session_token_collector, + current_epoch_token_collector, + old_epoch_token_collector, _, ) = value.into_components(); Self { - last_seen_session, + last_seen_epoch, spent_core_quota, unsent_processed_messages, unsent_data_messages, - current_session_token_collector, - old_session_token_collector, + current_epoch_token_collector, + old_epoch_token_collector, } } } @@ -89,8 +90,9 @@ mod service { use lb_blend::message::{ encap::validated::EncapsulatedMessageWithVerifiedPublicHeader, - reward::{BlendingToken, OldSessionBlendingTokenCollector, SessionBlendingTokenCollector}, + reward::{BlendingToken, EpochBlendingTokenCollector, OldEpochBlendingTokenCollector}, }; + use lb_chain_service::Epoch; use crate::{ core::state::{error, recovery_state::RecoveryServiceState, state_updater::StateUpdater}, @@ -100,15 +102,15 @@ mod service { #[derive(Clone)] /// Recovery state for Blend core service. pub struct ServiceState { - /// The last session that was saved. - last_seen_session: u64, - /// The last value for the core quota allowance for the session that is + /// The last epoch that was saved. + last_seen_epoch: Epoch, + /// The last value for the core quota allowance for the epoch that is /// tracked. spent_core_quota: u64, unsent_processed_messages: HashSet>, unsent_data_messages: HashSet, - current_session_token_collector: SessionBlendingTokenCollector, - old_session_token_collector: Option, + current_epoch_token_collector: EpochBlendingTokenCollector, + old_epoch_token_collector: Option, state_updater: overwatch::services::state::StateUpdater< Option>, >, @@ -120,18 +122,15 @@ mod service { { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.debug_struct("ServiceState") - .field("last_seen_session", &self.last_seen_session) + .field("last_seen_epoch", &self.last_seen_epoch) .field("spent_core_quota", &self.spent_core_quota) .field("unsent_processed_messages", &self.unsent_processed_messages) .field("unsent_data_messages", &self.unsent_data_messages) .field( - "current_session_token_collector", - &self.current_session_token_collector, - ) - .field( - "old_session_token_collector", - &self.old_session_token_collector, + "current_epoch_token_collector", + &self.current_epoch_token_collector, ) + .field("old_epoch_token_collector", &self.old_epoch_token_collector) .finish_non_exhaustive() } } @@ -144,73 +143,72 @@ mod service { // Creates a new instance with the provided fields, and saves it using // `state_updater`. pub(super) fn new( - last_seen_session: u64, + last_seen_epoch: Epoch, spent_core_quota: u64, unsent_processed_messages: HashSet>, unsent_data_messages: HashSet, - current_session_token_collector: SessionBlendingTokenCollector, - old_session_token_collector: Option, + current_epoch_token_collector: EpochBlendingTokenCollector, + old_epoch_token_collector: Option, state_updater: overwatch::services::state::StateUpdater< Option>, >, - ) -> Result { - // Check if `current_session_token_collector` has the correct session number. - let provided_current_session = current_session_token_collector.session_number(); - if provided_current_session != last_seen_session { - return Err(error::SessionMismatch { - last_seen: last_seen_session, - provided: provided_current_session, + ) -> Result { + // Check if `current_epoch_token_collector` has the correct epoch number. + let provided_current_epoch = current_epoch_token_collector.epoch(); + if provided_current_epoch != last_seen_epoch { + return Err(error::EpochMismatch { + last_seen: last_seen_epoch, + provided: provided_current_epoch, }); } - // Check if `old_session_token_collector` has the correct session number. - if let Some(old_session_token_collector) = &old_session_token_collector { - let provided_current_session = old_session_token_collector - .session_number() - .saturating_add(1); - if provided_current_session != last_seen_session { - return Err(error::SessionMismatch { - last_seen: last_seen_session, - provided: provided_current_session, + // Check if `old_epoch_token_collector` has the correct epoch number. + if let Some(old_epoch_token_collector) = &old_epoch_token_collector { + let provided_current_epoch = + old_epoch_token_collector.epoch().saturating_add(1.into()); + if provided_current_epoch != last_seen_epoch { + return Err(error::EpochMismatch { + last_seen: last_seen_epoch, + provided: provided_current_epoch, }); } } let this = Self { - last_seen_session, + last_seen_epoch, spent_core_quota, unsent_processed_messages, unsent_data_messages, - current_session_token_collector, - old_session_token_collector, + current_epoch_token_collector, + old_epoch_token_collector, state_updater, }; this.save(); Ok(this) } - /// Create a new instance with the provided session, and empty state for + /// Create a new instance with the provided epoch, and empty state for /// the rest. /// /// The new instance is saved immediately using `state_updater`. /// - /// This is typically used on session rotations or when no previous + /// This is typically used on epoch rotations or when no previous /// state was recovered. - pub fn with_session( - session: u64, - current_session_token_collector: SessionBlendingTokenCollector, - old_session_token_collector: Option, + pub fn with_epoch( + epoch: Epoch, + current_epoch_token_collector: EpochBlendingTokenCollector, + old_epoch_token_collector: Option, state_updater: overwatch::services::state::StateUpdater< Option>, >, - ) -> Result { + ) -> Result { Self::new( - session, + epoch, 0, HashSet::new(), HashSet::new(), - current_session_token_collector, - old_session_token_collector, + current_epoch_token_collector, + old_epoch_token_collector, state_updater, ) } @@ -228,8 +226,8 @@ mod service { StateUpdater::new(self) } - pub const fn last_seen_session(&self) -> u64 { - self.last_seen_session + pub const fn last_seen_epoch(&self) -> Epoch { + self.last_seen_epoch } pub(super) const fn spend_quota(&mut self, quota: u64) { @@ -243,21 +241,21 @@ mod service { self.spent_core_quota } - pub(super) fn collect_current_session_tokens( + pub(super) fn collect_current_epoch_tokens( &mut self, tokens: impl Iterator, ) { for token in tokens { - self.current_session_token_collector.collect(token); + self.current_epoch_token_collector.collect(token); } } - pub(super) fn collect_old_session_tokens( + pub(super) fn collect_old_epoch_tokens( &mut self, tokens: impl Iterator, - ) -> Result<(), error::OldSessionTokenCollectorNotExist> { - self.old_session_token_collector.as_mut().map_or( - Err(error::OldSessionTokenCollectorNotExist), + ) -> Result<(), error::OldEpochTokenCollectorNotExist> { + self.old_epoch_token_collector.as_mut().map_or( + Err(error::OldEpochTokenCollectorNotExist), |collector| { for token in tokens { collector.collect(token); @@ -267,17 +265,15 @@ mod service { ) } - pub(super) const fn clear_old_session_token_collector( + pub(super) const fn clear_old_epoch_token_collector( &mut self, - ) -> Option { - self.old_session_token_collector.take() + ) -> Option { + self.old_epoch_token_collector.take() } #[cfg(test)] - pub(crate) const fn current_session_token_collector( - &self, - ) -> &SessionBlendingTokenCollector { - &self.current_session_token_collector + pub(crate) const fn current_epoch_token_collector(&self) -> &EpochBlendingTokenCollector { + &self.current_epoch_token_collector } #[expect( @@ -287,23 +283,23 @@ mod service { pub fn into_components( self, ) -> ( - u64, + Epoch, u64, HashSet>, HashSet, - SessionBlendingTokenCollector, - Option, + EpochBlendingTokenCollector, + Option, overwatch::services::state::StateUpdater< Option>, >, ) { ( - self.last_seen_session, + self.last_seen_epoch, self.spent_core_quota, self.unsent_processed_messages, self.unsent_data_messages, - self.current_session_token_collector, - self.old_session_token_collector, + self.current_epoch_token_collector, + self.old_epoch_token_collector, self.state_updater, ) } @@ -378,7 +374,7 @@ mod state_updater { use lb_blend::message::{ encap::validated::EncapsulatedMessageWithVerifiedPublicHeader, - reward::{BlendingToken, OldSessionBlendingTokenCollector}, + reward::{BlendingToken, OldEpochBlendingTokenCollector}, }; use crate::{ @@ -428,27 +424,27 @@ mod state_updater { self.inner } - pub fn collect_current_session_tokens( + pub fn collect_current_epoch_tokens( &mut self, tokens: impl Iterator, ) { self.changed = true; - self.inner.collect_current_session_tokens(tokens); + self.inner.collect_current_epoch_tokens(tokens); } - pub fn collect_old_session_tokens( + pub fn collect_old_epoch_tokens( &mut self, tokens: impl Iterator, - ) -> Result<(), error::OldSessionTokenCollectorNotExist> { + ) -> Result<(), error::OldEpochTokenCollectorNotExist> { self.changed = true; - self.inner.collect_old_session_tokens(tokens) + self.inner.collect_old_epoch_tokens(tokens) } - pub const fn clear_old_session_token_collector( + pub const fn clear_old_epoch_token_collector( &mut self, - ) -> Option { + ) -> Option { self.changed = true; - self.inner.clear_old_session_token_collector() + self.inner.clear_old_epoch_token_collector() } } @@ -583,14 +579,14 @@ mod recovery_state { } pub mod error { - use lb_core::sdp::SessionNumber; + use lb_chain_service::Epoch; #[derive(Debug)] - pub struct SessionMismatch { - pub last_seen: SessionNumber, - pub provided: SessionNumber, + pub struct EpochMismatch { + pub last_seen: Epoch, + pub provided: Epoch, } #[derive(Debug)] - pub struct OldSessionTokenCollectorNotExist; + pub struct OldEpochTokenCollectorNotExist; } diff --git a/services/blend/src/core/tests/mod.rs b/services/blend/src/core/tests/mod.rs index 022dc329e..f167ae245 100644 --- a/services/blend/src/core/tests/mod.rs +++ b/services/blend/src/core/tests/mod.rs @@ -2,11 +2,11 @@ mod utils; use futures::StreamExt as _; use lb_blend::{ - message::reward::{ActivityProof, BlendingToken, SessionBlendingTokenCollector}, + message::reward::{ActivityProof, BlendingToken, EpochBlendingTokenCollector}, proofs::{quota::VerifiedProofOfQuota, selection::VerifiedProofOfSelection}, scheduling::{ - SessionMessageScheduler, message_blend::crypto::SessionCryptographicProcessorSettings, - session::SessionEvent, + EpochMessageScheduler, epoch::EpochEvent, + message_blend::crypto::EpochCryptographicProcessorSettings, }, }; use lb_chain_service::Epoch; @@ -14,37 +14,48 @@ use lb_core::{codec::SerializeOp as _, crypto::ZkHash, sdp::ActivityMetadata}; use lb_groth16::Field as _; use lb_key_management_system_service::keys::Ed25519Key; use lb_poq::CORE_MERKLE_TREE_HEIGHT; -use lb_time_service::SlotTick; use lb_utils::blake_rng::BlakeRng; use rand::SeedableRng as _; use crate::{ core::{ - HandleSessionEventOutput, + HandleEpochEventOutput, backends::BlendBackend, - handle_clock_event, handle_incoming_blend_message, handle_new_secret_epoch_info, - handle_session_event, handle_session_transition_expired, initialize, post_initialize, - retire, run_event_loop, + handle_epoch_event, handle_epoch_transition_expired, handle_incoming_blend_message, + initialize, post_initialize, retire, run_event_loop, state::ServiceState, tests::utils::{ MockKmsAdapter, MockProofsVerifier, NodeId, TestBlendBackend, TestBlendBackendEvent, - TestNetworkAdapter, dummy_overwatch_resources, new_crypto_processor, new_membership, - new_public_info, new_stream, reward_session_info, scheduler_session_info, - scheduler_settings, sdp_relay, settings, timing_settings, wait_for_blend_backend_event, + TestNetworkAdapter, dummy_overwatch_resources, dummy_pol_private_inputs, + new_crypto_processor, new_epoch_info, new_membership, new_stream, + recorded_set_epoch_private_calls, reset_set_epoch_private_calls, reward_epoch_info, + scheduler_epoch_info, scheduler_settings, sdp_relay, settings, timing_settings, + wait_for_blend_backend_event, }, }, - epoch_info::{EpochHandler, PolEpochInfo}, - membership::{MembershipInfo, ZkInfo}, + epoch::{CoreEpochInfo, CoreEpochPublicInfo}, + epoch_info::PolEpochInfo, + membership::{MembershipInfo, ZkInfo, chain::BlendEpochState}, message::NetworkMessage, - session::{CoreSessionInfo, CoreSessionPublicInfo}, - test_utils::{ - crypto::MockCoreAndLeaderProofsGenerator, - epoch::{OncePolStreamProvider, TestChainService}, - }, + test_utils::{crypto::MockCoreAndLeaderProofsGenerator, epoch::OncePolStreamProvider}, }; type RuntimeServiceId = (); +fn test_blend_epoch_state( + epoch: u32, + membership_info: MembershipInfo, +) -> BlendEpochState { + BlendEpochState { + epoch: epoch.into(), + nonce: ZkHash::ZERO, + aged: ZkHash::ZERO, + lottery_0: ZkHash::ZERO, + lottery_1: ZkHash::ZERO, + membership_info, + } +} + /// Check if incoming encapsulated messages are properly decapsulated and /// scheduled by [`handle_incoming_blend_message`]. #[test_log::test(tokio::test)] @@ -53,7 +64,7 @@ async fn test_handle_incoming_blend_message() { dummy_overwatch_resources::<(), (), RuntimeServiceId>(); // Prepare a encapsulated message. - let mut session = 0; + let mut epoch = 0.into(); let minimal_network_size = 1; let (membership, local_private_key) = new_membership(minimal_network_size); let (settings, _recovery_file) = settings( @@ -62,9 +73,9 @@ async fn test_handle_incoming_blend_message() { (), 0, ); - let public_info = new_public_info(session, membership.clone(), &settings); + let public_info = new_epoch_info(epoch, membership.clone(), &settings); let mut processor = new_crypto_processor( - SessionCryptographicProcessorSettings { + EpochCryptographicProcessorSettings { non_ephemeral_encryption_key: settings.non_ephemeral_signing_key.derive_x25519(), num_blend_layers: settings.num_blend_layers, }, @@ -84,20 +95,20 @@ async fn test_handle_incoming_blend_message() { // Check that the message is successfully decapsulated and scheduled. let scheduler_settings = scheduler_settings(&timing_settings(), settings.num_blend_layers); - let mut scheduler = SessionMessageScheduler::new( - scheduler_session_info(&public_info), + let mut scheduler = EpochMessageScheduler::new( + scheduler_epoch_info(&public_info), BlakeRng::from_entropy(), scheduler_settings, ); - let recovery_checkpoint = ServiceState::with_session( - session, - SessionBlendingTokenCollector::new(&reward_session_info(&public_info)), + let recovery_checkpoint = ServiceState::with_epoch( + epoch, + EpochBlendingTokenCollector::new(&reward_epoch_info(&public_info)), None, state_updater, ) .unwrap(); let recovery_checkpoint = handle_incoming_blend_message( - (msg.clone().into(), 0), + (msg.clone().into(), 0.into()), &mut scheduler, None, &processor, @@ -107,18 +118,18 @@ async fn test_handle_incoming_blend_message() { assert_eq!(scheduler.release_delayer().unreleased_messages().len(), 1); assert_eq!( recovery_checkpoint - .current_session_token_collector() + .current_epoch_token_collector() .tokens() .len(), 1 ); - // Creates a new processor/scheduler/token_collector with the new session + // Creates a new processor/scheduler/token_collector with the new epoch // number. - session += 1; - let public_info = new_public_info(session, membership.clone(), &settings); + epoch += 1; + let public_info = new_epoch_info(epoch, membership.clone(), &settings); let mut new_processor = new_crypto_processor( - SessionCryptographicProcessorSettings { + EpochCryptographicProcessorSettings { non_ephemeral_encryption_key: settings.non_ephemeral_signing_key.derive_x25519(), num_blend_layers: settings.num_blend_layers, }, @@ -126,24 +137,24 @@ async fn test_handle_incoming_blend_message() { (), ); let (mut new_scheduler, mut scheduler) = - scheduler.rotate_session(scheduler_session_info(&public_info), scheduler_settings); + scheduler.rotate_epoch(scheduler_epoch_info(&public_info), scheduler_settings); let (_, _, _, _, current_token_collector, _, state_updater) = recovery_checkpoint.into_components(); let (new_token_collector, old_token_collector) = - current_token_collector.rotate_session(&reward_session_info(&public_info)); + current_token_collector.rotate_epoch(&reward_epoch_info(&public_info)); // Check that decapsulating the same message fails with the new processor // but succeeds with the old one. Also, it should be scheduled in the old // scheduler. - let recovery_checkpoint = ServiceState::with_session( - session, + let recovery_checkpoint = ServiceState::with_epoch( + epoch, new_token_collector, Some(old_token_collector), state_updater, ) .unwrap(); let recovery_checkpoint = handle_incoming_blend_message( - (msg.clone().into(), 0), + (msg.clone().into(), 0.into()), &mut new_scheduler, Some(&mut scheduler), &new_processor, @@ -157,7 +168,7 @@ async fn test_handle_incoming_blend_message() { assert_eq!(scheduler.release_delayer().unreleased_messages().len(), 2); assert_eq!( recovery_checkpoint - .current_session_token_collector() + .current_epoch_token_collector() .tokens() .len(), 0 @@ -167,7 +178,7 @@ async fn test_handle_incoming_blend_message() { recovery_checkpoint .clone() .start_updating() - .clear_old_session_token_collector() + .clear_old_epoch_token_collector() .unwrap() .tokens() .len(), @@ -181,7 +192,7 @@ async fn test_handle_incoming_blend_message() { .await .expect("encapsulation must succeed"); let recovery_checkpoint = handle_incoming_blend_message( - (msg.into(), 1), + (msg.into(), 1.into()), &mut new_scheduler, Some(&mut scheduler), &new_processor, @@ -195,7 +206,7 @@ async fn test_handle_incoming_blend_message() { assert_eq!(scheduler.release_delayer().unreleased_messages().len(), 2); assert_eq!( recovery_checkpoint - .current_session_token_collector() + .current_epoch_token_collector() .tokens() .len(), 1 @@ -204,22 +215,22 @@ async fn test_handle_incoming_blend_message() { recovery_checkpoint .clone() .start_updating() - .clear_old_session_token_collector() + .clear_old_epoch_token_collector() .unwrap() .tokens() .len(), 1 ); - // Check that a message built with a future session cannot be + // Check that a message built with a future epoch cannot be // decapsulated by either processor, and thus not scheduled. - session += 1; + epoch += 1; let mut future_processor = new_crypto_processor( - SessionCryptographicProcessorSettings { + EpochCryptographicProcessorSettings { non_ephemeral_encryption_key: settings.non_ephemeral_signing_key.derive_x25519(), num_blend_layers: settings.num_blend_layers, }, - &new_public_info(session, membership, &settings), + &new_epoch_info(epoch, membership, &settings), (), ); let msg = future_processor @@ -227,7 +238,7 @@ async fn test_handle_incoming_blend_message() { .await .expect("encapsulation must succeed"); let recovery_checkpoint = handle_incoming_blend_message( - (msg.into(), 2), + (msg.into(), 2.into()), &mut new_scheduler, Some(&mut scheduler), &new_processor, @@ -242,7 +253,7 @@ async fn test_handle_incoming_blend_message() { assert_eq!(scheduler.release_delayer().unreleased_messages().len(), 2); assert_eq!( recovery_checkpoint - .current_session_token_collector() + .current_epoch_token_collector() .tokens() .len(), 1 @@ -250,7 +261,7 @@ async fn test_handle_incoming_blend_message() { assert_eq!( recovery_checkpoint .start_updating() - .clear_old_session_token_collector() + .clear_old_epoch_token_collector() .unwrap() .tokens() .len(), @@ -272,11 +283,11 @@ async fn test_handle_incoming_blend_message_with_invalid_poq() { 0, ); - // Create session 0 processor and build a message with session 0 proofs. - let session_0 = 0; - let public_info_0 = new_public_info(session_0, membership.clone(), &settings); + // Create epoch 0 processor and build a message with epoch 0 proofs. + let epoch_0 = 0.into(); + let public_info_0 = new_epoch_info(epoch_0, membership.clone(), &settings); let mut processor_0 = new_crypto_processor( - SessionCryptographicProcessorSettings { + EpochCryptographicProcessorSettings { non_ephemeral_encryption_key: settings.non_ephemeral_signing_key.derive_x25519(), num_blend_layers: settings.num_blend_layers, }, @@ -295,12 +306,12 @@ async fn test_handle_incoming_blend_message_with_invalid_poq() { .await .expect("encapsulation must succeed"); - // Create session 1 processor - its MockProofsVerifier expects session 1 + // Create epoch 1 processor - its MockProofsVerifier expects epoch 1 // proofs. - let session_1 = 1; - let public_info_1 = new_public_info(session_1, membership, &settings); + let epoch_1 = 1.into(); + let public_info_1 = new_epoch_info(epoch_1, membership, &settings); let processor_1 = new_crypto_processor( - SessionCryptographicProcessorSettings { + EpochCryptographicProcessorSettings { non_ephemeral_encryption_key: settings.non_ephemeral_signing_key.derive_x25519(), num_blend_layers: settings.num_blend_layers, }, @@ -309,24 +320,24 @@ async fn test_handle_incoming_blend_message_with_invalid_poq() { ); let scheduler_settings = scheduler_settings(&timing_settings(), settings.num_blend_layers); - let mut scheduler = SessionMessageScheduler::new( - scheduler_session_info(&public_info_1), + let mut scheduler = EpochMessageScheduler::new( + scheduler_epoch_info(&public_info_1), BlakeRng::from_entropy(), scheduler_settings, ); - let recovery_checkpoint = ServiceState::with_session( - session_1, - SessionBlendingTokenCollector::new(&reward_session_info(&public_info_1)), + let recovery_checkpoint = ServiceState::with_epoch( + epoch_1, + EpochBlendingTokenCollector::new(&reward_epoch_info(&public_info_1)), None, state_updater, ) .unwrap(); - // Send session 0 message claiming to be for session 1. + // Send epoch 0 message claiming to be for epoch 1. // Signature is valid (built correctly) but PoQ will fail because the - // MockProofsVerifier for session 1 expects session 1 proofs. + // MockProofsVerifier for epoch 1 expects epoch 1 proofs. drop(handle_incoming_blend_message( - (msg.into(), session_1), + (msg.into(), epoch_1), &mut scheduler, None, &processor_1, @@ -343,11 +354,11 @@ async fn test_handle_incoming_blend_message_with_invalid_poq() { } #[test_log::test(tokio::test)] -async fn test_handle_session_transition_expired() { +async fn test_handle_epoch_transition_expired() { let (overwatch_handle, _, _, _) = dummy_overwatch_resources::<(), (), RuntimeServiceId>(); // Prepare settings. - let session = 0; + let epoch = 0.into(); let minimal_network_size = 1; let (membership, local_private_key) = new_membership(minimal_network_size); let (mut settings, _recovery_file) = settings( @@ -356,29 +367,28 @@ async fn test_handle_session_transition_expired() { (), 0, ); - // Set a long rounds_per_session to make the core quota large enough, + // Set a long rounds_per_epoch to make the core quota large enough, // since we want the activity threshold to be sufficiently high. - settings.time.rounds_per_session = 648_000.try_into().unwrap(); + settings.time.rounds_per_epoch = 648_000.try_into().unwrap(); // Create backend. - let public_info = new_public_info(session, membership.clone(), &settings); + let public_info = new_epoch_info(epoch, membership.clone(), &settings); let mut backend = >::new( settings.clone(), overwatch_handle.clone(), - public_info.clone(), + (public_info.membership.clone(), public_info.epoch), BlakeRng::from_entropy(), ); let mut backend_event_receiver = backend.subscribe_to_events(); // Create token collector and collect a token. - let mut token_collector = - SessionBlendingTokenCollector::new(&reward_session_info(&public_info)) - .rotate_session(&reward_session_info(&new_public_info( - session + 1, - membership.clone(), - &settings, - ))) - .1; + let mut token_collector = EpochBlendingTokenCollector::new(&reward_epoch_info(&public_info)) + .rotate_epoch(&reward_epoch_info(&new_epoch_info( + epoch + 1, + membership.clone(), + &settings, + ))) + .1; let token = BlendingToken::new( Ed25519Key::from_bytes(&[0; _]).public_key(), VerifiedProofOfQuota::from_bytes_unchecked([0; _]), @@ -389,8 +399,8 @@ async fn test_handle_session_transition_expired() { // Create SDP relay. let (sdp_relay, mut sdp_relay_receiver) = sdp_relay(); - // Call `handle_session_transition_expired`. - handle_session_transition_expired::<_, NodeId, BlakeRng, _>( + // Call `handle_epoch_transition_expired`. + handle_epoch_transition_expired::<_, NodeId, BlakeRng, _>( &mut backend, token_collector, &sdp_relay, @@ -400,7 +410,7 @@ async fn test_handle_session_transition_expired() { // Check that the backend handled the transition completion. wait_for_blend_backend_event( &mut backend_event_receiver, - TestBlendBackendEvent::SessionTransitionCompleted, + TestBlendBackendEvent::EpochTransitionCompleted, ) .await; @@ -413,21 +423,16 @@ async fn test_handle_session_transition_expired() { else { panic!("expected PostActivity with ActivityMetadata::Blend"); }; - assert_eq!( - *activity_proof, - (&ActivityProof::new(session, token)).into() - ); + assert_eq!(*activity_proof, (&ActivityProof::new(epoch, token)).into()); } #[test_log::test(tokio::test)] -async fn test_handle_session_event() { - use lb_chain_service::Epoch; - +async fn test_handle_epoch_event() { let (overwatch_handle, _overwatch_cmd_receiver, state_updater, _state_receiver) = dummy_overwatch_resources::<(), (), RuntimeServiceId>(); - // Prepare components for session event handling. - let session = 0; + // Prepare components for epoch event handling. + let epoch = 0.into(); let minimal_network_size = 2; let (membership, local_private_key) = new_membership(minimal_network_size); let (settings, _recovery_file) = settings( @@ -436,38 +441,37 @@ async fn test_handle_session_event() { (), 0, ); - let public_info = new_public_info(session, membership.clone(), &settings); + let public_info = new_epoch_info(epoch, membership.clone(), &settings); let crypto_processor = new_crypto_processor( - SessionCryptographicProcessorSettings { + EpochCryptographicProcessorSettings { non_ephemeral_encryption_key: settings.non_ephemeral_signing_key.derive_x25519(), num_blend_layers: settings.num_blend_layers, }, &public_info, (), ); - let scheduler = SessionMessageScheduler::new( - scheduler_session_info(&public_info), + let scheduler = EpochMessageScheduler::new( + scheduler_epoch_info(&public_info), BlakeRng::from_entropy(), scheduler_settings(&settings.time, settings.num_blend_layers), ); - let token_collector = SessionBlendingTokenCollector::new(&reward_session_info(&public_info)); + let token_collector = EpochBlendingTokenCollector::new(&reward_epoch_info(&public_info)); let mut backend = >::new( settings.clone(), overwatch_handle.clone(), - public_info.clone(), + (public_info.membership.clone(), public_info.epoch), BlakeRng::from_entropy(), ); let mut backend_event_receiver = backend.subscribe_to_events(); let (sdp_relay, _sdp_relay_receiver) = sdp_relay(); - // Handle a NewSession event, expecting Transitioning output. - let output = handle_session_event( - SessionEvent::NewSession( - CoreSessionInfo { - public: CoreSessionPublicInfo { - membership: membership.clone(), - session: session + 1, - poq_core_public_inputs: public_info.session.core_public_inputs, + // Handle a NewEpoch event, expecting Transitioning output. + let output = handle_epoch_event( + EpochEvent::NewEpoch( + CoreEpochInfo { + public: CoreEpochPublicInfo { + epoch: epoch + 1, + ..public_info.clone() }, core_poq_generator: Some(()), } @@ -477,29 +481,25 @@ async fn test_handle_session_event() { crypto_processor, scheduler, public_info, - ServiceState::with_session(session, token_collector, None, state_updater.clone()).unwrap(), + ServiceState::with_epoch(epoch, token_collector, None, state_updater.clone()).unwrap(), &mut backend, &sdp_relay, - Epoch::new(0), None, ) .await; - let HandleSessionEventOutput::Transitioning { + let HandleEpochEventOutput::Transitioning { new_crypto_processor, - old_crypto_processor, new_scheduler, old_scheduler, - new_public_info, + new_epoch_info, new_recovery_checkpoint, + old_crypto_processor, } = output else { panic!("expected Transitioning output"); }; - assert_eq!( - new_crypto_processor.verifier().session_number(), - session + 1 - ); - assert_eq!(old_crypto_processor.verifier().session_number(), session); + assert_eq!(new_crypto_processor.epoch(), epoch + 1); + assert_eq!(old_crypto_processor.epoch(), epoch); assert_eq!( new_scheduler.release_delayer().unreleased_messages().len(), 0 @@ -508,65 +508,61 @@ async fn test_handle_session_event() { old_scheduler.release_delayer().unreleased_messages().len(), 0 ); - assert_eq!(new_public_info.session.session_number, session + 1); + assert_eq!(new_epoch_info.epoch, epoch + 1); assert!( new_recovery_checkpoint .clone() .start_updating() - .clear_old_session_token_collector() + .clear_old_epoch_token_collector() .is_some() ); // Handle a TransitionExpired event, expecting TransitionCompleted output. - let output = handle_session_event( - SessionEvent::TransitionPeriodExpired, + let output = handle_epoch_event( + EpochEvent::TransitionPeriodExpired, &settings, new_crypto_processor, new_scheduler, - new_public_info, + new_epoch_info, new_recovery_checkpoint, &mut backend, &sdp_relay, - Epoch::new(0), None, ) .await; - let HandleSessionEventOutput::TransitionCompleted { + let HandleEpochEventOutput::TransitionCompleted { current_crypto_processor, current_scheduler, - current_public_info, + current_epoch_info, new_recovery_checkpoint, } = output else { panic!("expected TransitionCompleted output"); }; - assert_eq!( - current_crypto_processor.verifier().session_number(), - session + 1 - ); - assert_eq!(current_public_info.session.session_number, session + 1); + assert_eq!(current_crypto_processor.epoch(), epoch + 1); + assert_eq!(current_epoch_info.epoch, epoch + 1); assert!( new_recovery_checkpoint .clone() .start_updating() - .clear_old_session_token_collector() + .clear_old_epoch_token_collector() .is_none() ); wait_for_blend_backend_event( &mut backend_event_receiver, - TestBlendBackendEvent::SessionTransitionCompleted, + TestBlendBackendEvent::EpochTransitionCompleted, ) .await; - // Handle a NewSession event with a new too small membership, + // Handle a NewEpoch event with a new too small membership, // expecting Retiring output. - let output = handle_session_event( - SessionEvent::NewSession( - CoreSessionInfo { - public: CoreSessionPublicInfo { + let output = handle_epoch_event( + EpochEvent::NewEpoch( + CoreEpochInfo { + public: CoreEpochPublicInfo { membership: new_membership(minimal_network_size - 1).0, - session: session + 2, - poq_core_public_inputs: current_public_info.session.core_public_inputs, + epoch: epoch + 2, + ..current_epoch_info.clone() }, core_poq_generator: Some(()), } @@ -575,38 +571,34 @@ async fn test_handle_session_event() { &settings, current_crypto_processor, current_scheduler, - current_public_info, + current_epoch_info, new_recovery_checkpoint, &mut backend, &sdp_relay, - Epoch::new(0), None, ) .await; - let HandleSessionEventOutput::Retiring { + let HandleEpochEventOutput::Retiring { old_crypto_processor, - old_public_info, .. } = output else { panic!("expected Retiring output"); }; - assert_eq!( - old_crypto_processor.verifier().session_number(), - session + 1 - ); - assert_eq!(old_public_info.session.session_number, session + 1); + assert_eq!(old_crypto_processor.epoch(), epoch + 1); } -/// Handle a `NewSession(Empty)` event (empty membership), expecting `Retiring` -/// output. This exercises the `MaybeEmptyCoreSessionInfo::Empty` branch of -/// `handle_session_event` directly. +/// On an epoch change where the membership actually changes (and the local node +/// remains part of the core), the service must transition: build a new +/// cryptographic generator bound to the new epoch, retain the old one for the +/// old epoch, and propagate the *new* membership to the backend via +/// `rotate_epoch`. #[test_log::test(tokio::test)] -async fn test_handle_session_event_empty_session_retires() { +async fn test_handle_epoch_event_membership_change_rewires_backend_and_generators() { let (overwatch_handle, _overwatch_cmd_receiver, state_updater, _state_receiver) = dummy_overwatch_resources::<(), (), RuntimeServiceId>(); - let session = 0; + let epoch = 0.into(); let minimal_network_size = 2; let (membership, local_private_key) = new_membership(minimal_network_size); let (settings, _recovery_file) = settings( @@ -615,67 +607,253 @@ async fn test_handle_session_event_empty_session_retires() { (), 0, ); - let public_info = new_public_info(session, membership.clone(), &settings); + let public_info = new_epoch_info(epoch, membership.clone(), &settings); let crypto_processor = new_crypto_processor( - SessionCryptographicProcessorSettings { + EpochCryptographicProcessorSettings { non_ephemeral_encryption_key: settings.non_ephemeral_signing_key.derive_x25519(), num_blend_layers: settings.num_blend_layers, }, &public_info, (), ); - let scheduler = SessionMessageScheduler::new( - scheduler_session_info(&public_info), + let scheduler = EpochMessageScheduler::new( + scheduler_epoch_info(&public_info), BlakeRng::from_entropy(), scheduler_settings(&settings.time, settings.num_blend_layers), ); - let token_collector = SessionBlendingTokenCollector::new(&reward_session_info(&public_info)); + let token_collector = EpochBlendingTokenCollector::new(&reward_epoch_info(&public_info)); let mut backend = >::new( settings.clone(), overwatch_handle.clone(), - public_info.clone(), + (public_info.membership.clone(), public_info.epoch), + BlakeRng::from_entropy(), + ); + let mut backend_event_receiver = backend.subscribe_to_events(); + let (sdp_relay, _sdp_relay_receiver) = sdp_relay(); + + // The new epoch has a *different* (larger) membership; `new_membership` + // always includes the local node, so the node stays part of the core. + let new_epoch = epoch + 1; + let new_membership = new_membership(minimal_network_size + 1).0; + assert_ne!( + new_membership.size(), + membership.size(), + "the test must exercise an actual membership change" + ); + let new_public_info = new_epoch_info(new_epoch, new_membership.clone(), &settings); + + let output = handle_epoch_event( + EpochEvent::NewEpoch( + CoreEpochInfo { + public: new_public_info.clone(), + core_poq_generator: Some(()), + } + .into(), + ), + &settings, + crypto_processor, + scheduler, + public_info, + ServiceState::with_epoch(epoch, token_collector, None, state_updater.clone()).unwrap(), + &mut backend, + &sdp_relay, + None, + ) + .await; + + let HandleEpochEventOutput::Transitioning { + new_crypto_processor, + old_crypto_processor, + new_epoch_info: returned_epoch_info, + .. + } = output + else { + panic!("expected Transitioning output"); + }; + + // A fresh generator is built for the new epoch, and the previous one is + // retained for the old epoch. + assert_eq!(new_crypto_processor.epoch(), new_epoch); + assert_eq!(old_crypto_processor.epoch(), epoch); + // The returned public info carries the new membership. + assert_eq!(returned_epoch_info.epoch, new_epoch); + assert_eq!(returned_epoch_info.membership.size(), new_membership.size()); + + // The backend was rotated to the new epoch, carrying the *new* membership. + wait_for_blend_backend_event( + &mut backend_event_receiver, + TestBlendBackendEvent::EpochRotated { + epoch: new_epoch, + membership_size: new_membership.size(), + }, + ) + .await; +} + +/// On an epoch change, if secret `PoL` info for the *new* epoch is already +/// available (`current_secret_info`), it must be applied to the *new* +/// cryptographic generator via `set_epoch_private`. If the available secret +/// info is for a different epoch, it must not be applied. This is the +/// public-stream side of the public/secret out-of-order coordination. +#[test_log::test(tokio::test)] +async fn test_handle_epoch_event_applies_matching_secret_to_new_generator() { + async fn transition_to_new_epoch_with_secret(secret_epoch: Epoch) -> Vec { + let (overwatch_handle, _overwatch_cmd_receiver, state_updater, _state_receiver) = + dummy_overwatch_resources::<(), (), RuntimeServiceId>(); + let epoch = 0.into(); + let minimal_network_size = 2; + let (membership, local_private_key) = new_membership(minimal_network_size); + let (settings, _recovery_file) = settings( + local_private_key.clone(), + u64::from(minimal_network_size).try_into().unwrap(), + (), + 0, + ); + let public_info = new_epoch_info(epoch, membership.clone(), &settings); + let crypto_processor = new_crypto_processor( + EpochCryptographicProcessorSettings { + non_ephemeral_encryption_key: settings.non_ephemeral_signing_key.derive_x25519(), + num_blend_layers: settings.num_blend_layers, + }, + &public_info, + (), + ); + let scheduler = EpochMessageScheduler::new( + scheduler_epoch_info(&public_info), + BlakeRng::from_entropy(), + scheduler_settings(&settings.time, settings.num_blend_layers), + ); + let token_collector = EpochBlendingTokenCollector::new(&reward_epoch_info(&public_info)); + let mut backend = >::new( + settings.clone(), + overwatch_handle.clone(), + (public_info.membership.clone(), public_info.epoch), + BlakeRng::from_entropy(), + ); + let (sdp_relay, _sdp_relay_receiver) = sdp_relay(); + + let secret_info = PolEpochInfo { + epoch: secret_epoch, + poq_private_inputs: dummy_pol_private_inputs(), + }; + + // Isolate the `set_epoch_private` calls made by `handle_epoch_event`. + reset_set_epoch_private_calls(); + let _output = handle_epoch_event( + EpochEvent::NewEpoch( + CoreEpochInfo { + public: CoreEpochPublicInfo { + epoch: epoch + 1, + ..public_info.clone() + }, + core_poq_generator: Some(()), + } + .into(), + ), + &settings, + crypto_processor, + scheduler, + public_info, + ServiceState::with_epoch(epoch, token_collector, None, state_updater.clone()).unwrap(), + &mut backend, + &sdp_relay, + Some(&secret_info), + ) + .await; + recorded_set_epoch_private_calls() + } + + // Secret for the new epoch (1) is applied to the new generator. + assert_eq!( + transition_to_new_epoch_with_secret(1.into()).await, + vec![Epoch::new(1)], + "secret matching the new epoch must be applied to the new generator" + ); + + // Secret for a non-matching epoch (5) must not be applied. + assert!( + transition_to_new_epoch_with_secret(5.into()) + .await + .is_empty(), + "secret for a non-matching epoch must not be applied to the new generator" + ); +} + +/// Handle a `NewEpoch(Empty)` event (empty membership), expecting `Retiring` +/// output. This exercises the `MaybeEmptyCoreEpochInfo::Empty` branch of +/// `handle_epoch_event` directly. +#[test_log::test(tokio::test)] +async fn test_handle_epoch_event_empty_epoch_retires() { + let (overwatch_handle, _overwatch_cmd_receiver, state_updater, _state_receiver) = + dummy_overwatch_resources::<(), (), RuntimeServiceId>(); + + let epoch = 0.into(); + let minimal_network_size = 2; + let (membership, local_private_key) = new_membership(minimal_network_size); + let (settings, _recovery_file) = settings( + local_private_key.clone(), + u64::from(minimal_network_size).try_into().unwrap(), + (), + 0, + ); + let public_info = new_epoch_info(epoch, membership.clone(), &settings); + let crypto_processor = new_crypto_processor( + EpochCryptographicProcessorSettings { + non_ephemeral_encryption_key: settings.non_ephemeral_signing_key.derive_x25519(), + num_blend_layers: settings.num_blend_layers, + }, + &public_info, + (), + ); + let scheduler = EpochMessageScheduler::new( + scheduler_epoch_info(&public_info), + BlakeRng::from_entropy(), + scheduler_settings(&settings.time, settings.num_blend_layers), + ); + let token_collector = EpochBlendingTokenCollector::new(&reward_epoch_info(&public_info)); + let mut backend = >::new( + settings.clone(), + overwatch_handle.clone(), + (public_info.membership.clone(), public_info.epoch), BlakeRng::from_entropy(), ); let (sdp_relay, _sdp_relay_receiver) = sdp_relay(); - // Handle a NewSession(Empty) event - empty membership triggers Retiring. - let empty_session: u64 = session + 1; - let output = handle_session_event( - SessionEvent::NewSession(empty_session.into()), + // Handle a NewEpoch(Empty) event - empty membership triggers Retiring. + let empty_epoch = epoch + 1; + let output = handle_epoch_event( + EpochEvent::NewEpoch((empty_epoch, ZkHash::from(1)).into()), &settings, crypto_processor, scheduler, public_info.clone(), - ServiceState::with_session(session, token_collector, None, state_updater.clone()).unwrap(), + ServiceState::with_epoch(epoch, token_collector, None, state_updater.clone()).unwrap(), &mut backend, &sdp_relay, - Epoch::new(0), None, ) .await; - let HandleSessionEventOutput::Retiring { + let HandleEpochEventOutput::Retiring { old_crypto_processor, - old_public_info, .. } = output else { - panic!("expected Retiring output for Empty session"); + panic!("expected Retiring output for Empty epoch"); }; - // The old processor/info should be from the session we were on before - // the empty session arrived. - assert_eq!(old_crypto_processor.verifier().session_number(), session); - assert_eq!(old_public_info.session.session_number, session); + // The old processor/info should be from the epoch we were on before + // the empty epoch arrived. + assert_eq!(old_crypto_processor.epoch(), epoch); } -/// Handle a `NewSession(NonEmpty)` event where membership exists but the local +/// Handle a `NewEpoch(NonEmpty)` event where membership exists but the local /// node is not part of it (`core_poq_generator = None`), expecting `Retiring` /// output. #[test_log::test(tokio::test)] -async fn test_handle_session_event_non_empty_without_local_core_path_retires() { +async fn test_handle_epoch_event_non_empty_without_local_core_path_retires() { let (overwatch_handle, _overwatch_cmd_receiver, state_updater, _state_receiver) = dummy_overwatch_resources::<(), (), RuntimeServiceId>(); - let session = 0; + let epoch = 0.into(); let minimal_network_size = 2; let (membership, local_private_key) = new_membership(minimal_network_size); let (settings, _recovery_file) = settings( @@ -684,36 +862,35 @@ async fn test_handle_session_event_non_empty_without_local_core_path_retires() { (), 0, ); - let public_info = new_public_info(session, membership.clone(), &settings); + let public_info = new_epoch_info(epoch, membership.clone(), &settings); let crypto_processor = new_crypto_processor( - SessionCryptographicProcessorSettings { + EpochCryptographicProcessorSettings { non_ephemeral_encryption_key: settings.non_ephemeral_signing_key.derive_x25519(), num_blend_layers: settings.num_blend_layers, }, &public_info, (), ); - let scheduler = SessionMessageScheduler::new( - scheduler_session_info(&public_info), + let scheduler = EpochMessageScheduler::new( + scheduler_epoch_info(&public_info), BlakeRng::from_entropy(), scheduler_settings(&settings.time, settings.num_blend_layers), ); - let token_collector = SessionBlendingTokenCollector::new(&reward_session_info(&public_info)); + let token_collector = EpochBlendingTokenCollector::new(&reward_epoch_info(&public_info)); let mut backend = >::new( settings.clone(), overwatch_handle.clone(), - public_info.clone(), + (public_info.membership.clone(), public_info.epoch), BlakeRng::from_entropy(), ); let (sdp_relay, _sdp_relay_receiver) = sdp_relay(); - let output = handle_session_event( - SessionEvent::NewSession( - CoreSessionInfo { - public: CoreSessionPublicInfo { - membership, - session: session + 1, - poq_core_public_inputs: public_info.session.core_public_inputs, + let output = handle_epoch_event( + EpochEvent::NewEpoch( + CoreEpochInfo { + public: CoreEpochPublicInfo { + epoch: epoch + 1, + ..public_info.clone() }, core_poq_generator: None, } @@ -723,33 +900,30 @@ async fn test_handle_session_event_non_empty_without_local_core_path_retires() { crypto_processor, scheduler, public_info.clone(), - ServiceState::with_session(session, token_collector, None, state_updater.clone()).unwrap(), + ServiceState::with_epoch(epoch, token_collector, None, state_updater.clone()).unwrap(), &mut backend, &sdp_relay, - Epoch::new(0), None, ) .await; - let HandleSessionEventOutput::Retiring { + let HandleEpochEventOutput::Retiring { old_crypto_processor, - old_public_info, .. } = output else { - panic!("expected Retiring output for NonEmpty session without local core path"); + panic!("expected Retiring output for NonEmpty epoch without local core path"); }; - assert_eq!(old_crypto_processor.verifier().session_number(), session); - assert_eq!(old_public_info.session.session_number, session); + assert_eq!(old_crypto_processor.epoch(), epoch); } -/// Check if the service keeps running after it receives a new session where -/// it's still core. Also, check if it stops after the session transition period -/// if it receives another new session that doesn't meet the core node +/// Check if the service keeps running after it receives a new epoch where +/// it's still core. Also, check if it stops after the epoch transition period +/// if it receives another new epoch that doesn't meet the core node /// conditions. #[test_log::test(tokio::test)] -async fn complete_old_session_after_main_loop_done() { +async fn complete_old_epoch_after_main_loop_done() { let minimal_network_size = 2; let (membership, local_private_key) = new_membership(minimal_network_size); @@ -765,53 +939,31 @@ async fn complete_old_session_after_main_loop_done() { let (inbound_relay, _inbound_message_sender) = new_stream(); let (mut blend_message_stream, _blend_message_sender) = new_stream(); let (membership_stream, membership_sender) = new_stream(); - let (clock_stream, clock_sender) = new_stream(); // Send the initial membership info that the service will expect to receive // immediately. - let initial_session = 0; let mut membership_info = MembershipInfo { membership: membership.clone(), zk: Some(ZkInfo { root: ZkHash::ZERO, core_and_path_selectors: Some([(ZkHash::ZERO, false); CORE_MERKLE_TREE_HEIGHT]), }), - session_number: initial_session, }; membership_sender - .send(membership_info.clone()) + .send(test_blend_epoch_state(0, membership_info.clone())) .await .unwrap(); let (sdp_relay, _sdp_relay_receiver) = sdp_relay(); - // Send the initial slot tick that the service will expect to receive - // immediately. - clock_sender - .send(SlotTick { - epoch: 0.into(), - slot: 0.into(), - }) - .await - .unwrap(); - - // Prepare an epoch handler with the mock chain service that always returns the - // same epoch state. - let mut epoch_handler = EpochHandler::new( - TestChainService, - settings.time.epoch_transition_period_in_slots, - ); - // Prepare dummy Overwatch resources. let (overwatch_handle, _overwatch_cmd_receiver, state_updater, _state_receiver) = dummy_overwatch_resources(); // Initialize the service. let ( - mut remaining_session_stream, - mut remaining_clock_stream, + mut remaining_epoch_stream, current_public_info, - _, crypto_processor, current_recovery_checkpoint, message_scheduler, @@ -821,7 +973,6 @@ async fn complete_old_session_after_main_loop_done() { NodeId, TestBlendBackend, TestNetworkAdapter, - TestChainService, MockCoreAndLeaderProofsGenerator, MockProofsVerifier, MockKmsAdapter, @@ -829,8 +980,6 @@ async fn complete_old_session_after_main_loop_done() { >( settings.clone(), membership_stream, - clock_stream, - &mut epoch_handler, overwatch_handle.clone(), MockKmsAdapter, &sdp_relay, @@ -847,76 +996,69 @@ async fn complete_old_session_after_main_loop_done() { post_initialize::(&overwatch_handle).await; let ( - old_session_crypto_processor, - old_session_message_scheduler, - old_session_blending_token_collector, - old_session_public_info, - _, + old_epoch_crypto_processor, + old_epoch_message_scheduler, + old_epoch_blending_token_collector, ) = run_event_loop( inbound_relay, &mut blend_message_stream, - &mut remaining_clock_stream, secret_pol_info_stream, - &mut remaining_session_stream, + &mut remaining_epoch_stream, &settings_cloned, &mut backend, &TestNetworkAdapter, &sdp_relay, - &mut epoch_handler, message_scheduler.into(), &mut rng, crypto_processor, current_public_info, - Epoch::new(0), current_recovery_checkpoint, ) .await; retire( blend_message_stream.map(|(msg, _)| msg), - remaining_clock_stream, - remaining_session_stream, - &settings_cloned, + remaining_epoch_stream, backend, TestNetworkAdapter, sdp_relay, - epoch_handler, - old_session_message_scheduler, + old_epoch_message_scheduler, rng, - old_session_blending_token_collector, - old_session_crypto_processor, - old_session_public_info, - Epoch::new(0), + old_epoch_blending_token_collector, + old_epoch_crypto_processor, ) .await; }); - // Send a new session with the same membership. - membership_info.session_number += 1; + // Send a new epoch with the same membership. + membership_sender - .send(membership_info.clone()) + .send(test_blend_epoch_state(1, membership_info.clone())) .await .unwrap(); - // Since the node is still core in the new session, - // the service must keep running even after a session transition period. + // Since the node is still core in the new epoch, + // the service must keep running even after a epoch transition period. wait_for_blend_backend_event( &mut backend_event_receiver, - TestBlendBackendEvent::SessionTransitionCompleted, + TestBlendBackendEvent::EpochTransitionCompleted, ) .await; assert!(!join_handle.is_finished()); - // Send a new session with a new membership smaller than minimal size + // Send a new epoch with a new membership smaller than minimal size membership_info.membership = new_membership(minimal_network_size.checked_sub(1).unwrap()).0; - membership_info.session_number += 1; - membership_sender.send(membership_info).await.unwrap(); + + membership_sender + .send(test_blend_epoch_state(2, membership_info)) + .await + .unwrap(); // Since the network is smaller than the minimal size, - // the service must stop after a session transition period. + // the service must stop after a epoch transition period. wait_for_blend_backend_event( &mut backend_event_receiver, - TestBlendBackendEvent::SessionTransitionCompleted, + TestBlendBackendEvent::EpochTransitionCompleted, ) .await; join_handle @@ -924,10 +1066,10 @@ async fn complete_old_session_after_main_loop_done() { .expect("the service should stop without error"); } -/// Check that the service handles a new session with empty providers (zk: None) +/// Check that the service handles a new epoch with empty providers (zk: None) /// without panicking. It should retire gracefully. #[test_log::test(tokio::test)] -async fn stop_on_empty_session() { +async fn stop_on_empty_epoch() { let minimal_network_size = 2; let (membership, local_private_key) = new_membership(minimal_network_size); @@ -943,53 +1085,31 @@ async fn stop_on_empty_session() { let (inbound_relay, _inbound_message_sender) = new_stream(); let (mut blend_message_stream, _blend_message_sender) = new_stream(); let (membership_stream, membership_sender) = new_stream(); - let (clock_stream, clock_sender) = new_stream(); // Send the initial membership info that the service will expect to receive // immediately. - let initial_session = 0; let membership_info = MembershipInfo { membership: membership.clone(), zk: Some(ZkInfo { root: ZkHash::ZERO, core_and_path_selectors: Some([(ZkHash::ZERO, false); CORE_MERKLE_TREE_HEIGHT]), }), - session_number: initial_session, }; membership_sender - .send(membership_info.clone()) + .send(test_blend_epoch_state(0, membership_info.clone())) .await .unwrap(); let (sdp_relay, _sdp_relay_receiver) = sdp_relay(); - // Send the initial slot tick that the service will expect to receive - // immediately. - clock_sender - .send(SlotTick { - epoch: 0.into(), - slot: 0.into(), - }) - .await - .unwrap(); - - // Prepare an epoch handler with the mock chain service that always returns the - // same epoch state. - let mut epoch_handler = EpochHandler::new( - TestChainService, - settings.time.epoch_transition_period_in_slots, - ); - // Prepare dummy Overwatch resources. let (overwatch_handle, _overwatch_cmd_receiver, state_updater, _state_receiver) = dummy_overwatch_resources(); // Initialize the service. let ( - mut remaining_session_stream, - mut remaining_clock_stream, + mut remaining_epoch_stream, current_public_info, - _, crypto_processor, current_recovery_checkpoint, message_scheduler, @@ -999,7 +1119,6 @@ async fn stop_on_empty_session() { NodeId, TestBlendBackend, TestNetworkAdapter, - TestChainService, MockCoreAndLeaderProofsGenerator, MockProofsVerifier, MockKmsAdapter, @@ -1007,8 +1126,6 @@ async fn stop_on_empty_session() { >( settings.clone(), membership_stream, - clock_stream, - &mut epoch_handler, overwatch_handle.clone(), MockKmsAdapter, &sdp_relay, @@ -1025,77 +1142,69 @@ async fn stop_on_empty_session() { post_initialize::(&overwatch_handle).await; let ( - old_session_crypto_processor, - old_session_message_scheduler, - old_session_blending_token_collector, - old_session_public_info, - _, + old_epoch_crypto_processor, + old_epoch_message_scheduler, + old_epoch_blending_token_collector, ) = run_event_loop( inbound_relay, &mut blend_message_stream, - &mut remaining_clock_stream, secret_pol_info_stream, - &mut remaining_session_stream, + &mut remaining_epoch_stream, &settings_cloned, &mut backend, &TestNetworkAdapter, &sdp_relay, - &mut epoch_handler, message_scheduler.into(), &mut rng, crypto_processor, current_public_info, - Epoch::new(0), current_recovery_checkpoint, ) .await; retire( blend_message_stream.map(|(msg, _)| msg), - remaining_clock_stream, - remaining_session_stream, - &settings_cloned, + remaining_epoch_stream, backend, TestNetworkAdapter, sdp_relay, - epoch_handler, - old_session_message_scheduler, + old_epoch_message_scheduler, rng, - old_session_blending_token_collector, - old_session_crypto_processor, - old_session_public_info, - Epoch::new(0), + old_epoch_blending_token_collector, + old_epoch_crypto_processor, ) .await; }); - // Send a new session with empty providers (zk: None). - // This simulates a session where no providers are available. + // Send a new epoch with empty providers (zk: None). + // This simulates an epoch where no providers are available. membership_sender - .send(MembershipInfo { - membership: membership.clone(), - zk: None, - session_number: initial_session + 1, - }) + .send(test_blend_epoch_state( + 1, + MembershipInfo { + membership: membership.clone(), + zk: None, + }, + )) .await .unwrap(); wait_for_blend_backend_event( &mut backend_event_receiver, - TestBlendBackendEvent::SessionTransitionCompleted, + TestBlendBackendEvent::EpochTransitionCompleted, ) .await; // The service should stop without panicking. join_handle .await - .expect("the service should stop without panic on empty session"); + .expect("the service should stop without panic on empty epoch"); } -/// Check that the service handles a non-empty new session where the local node +/// Check that the service handles a non-empty new epoch where the local node /// has no core path (`core_poq_generator = None`) without panicking. It should /// retire gracefully. #[test_log::test(tokio::test)] -async fn stop_on_non_empty_session_without_local_core_path() { +async fn stop_on_non_empty_epoch_without_local_core_path() { let minimal_network_size = 2; let (membership, local_private_key) = new_membership(minimal_network_size); @@ -1111,53 +1220,31 @@ async fn stop_on_non_empty_session_without_local_core_path() { let (inbound_relay, _inbound_message_sender) = new_stream(); let (mut blend_message_stream, _blend_message_sender) = new_stream(); let (membership_stream, membership_sender) = new_stream(); - let (clock_stream, clock_sender) = new_stream(); // Send the initial membership info that the service will expect to receive // immediately. - let initial_session = 0; let membership_info = MembershipInfo { membership: membership.clone(), zk: Some(ZkInfo { root: ZkHash::ZERO, core_and_path_selectors: Some([(ZkHash::ZERO, false); CORE_MERKLE_TREE_HEIGHT]), }), - session_number: initial_session, }; membership_sender - .send(membership_info.clone()) + .send(test_blend_epoch_state(0, membership_info.clone())) .await .unwrap(); let (sdp_relay, _sdp_relay_receiver) = sdp_relay(); - // Send the initial slot tick that the service will expect to receive - // immediately. - clock_sender - .send(SlotTick { - epoch: 0.into(), - slot: 0.into(), - }) - .await - .unwrap(); - - // Prepare an epoch handler with the mock chain service that always returns the - // same epoch state. - let mut epoch_handler = EpochHandler::new( - TestChainService, - settings.time.epoch_transition_period_in_slots, - ); - // Prepare dummy Overwatch resources. let (overwatch_handle, _overwatch_cmd_receiver, state_updater, _state_receiver) = dummy_overwatch_resources(); // Initialize the service. let ( - mut remaining_session_stream, - mut remaining_clock_stream, + mut remaining_epoch_stream, current_public_info, - _, crypto_processor, current_recovery_checkpoint, message_scheduler, @@ -1167,7 +1254,6 @@ async fn stop_on_non_empty_session_without_local_core_path() { NodeId, TestBlendBackend, TestNetworkAdapter, - TestChainService, MockCoreAndLeaderProofsGenerator, MockProofsVerifier, MockKmsAdapter, @@ -1175,8 +1261,6 @@ async fn stop_on_non_empty_session_without_local_core_path() { >( settings.clone(), membership_stream, - clock_stream, - &mut epoch_handler, overwatch_handle.clone(), MockKmsAdapter, &sdp_relay, @@ -1193,66 +1277,58 @@ async fn stop_on_non_empty_session_without_local_core_path() { post_initialize::(&overwatch_handle).await; let ( - old_session_crypto_processor, - old_session_message_scheduler, - old_session_blending_token_collector, - old_session_public_info, - _, + old_epoch_crypto_processor, + old_epoch_message_scheduler, + old_epoch_blending_token_collector, ) = run_event_loop( inbound_relay, &mut blend_message_stream, - &mut remaining_clock_stream, secret_pol_info_stream, - &mut remaining_session_stream, + &mut remaining_epoch_stream, &settings_cloned, &mut backend, &TestNetworkAdapter, &sdp_relay, - &mut epoch_handler, message_scheduler.into(), &mut rng, crypto_processor, current_public_info, - Epoch::new(0), current_recovery_checkpoint, ) .await; retire( blend_message_stream.map(|(msg, _)| msg), - remaining_clock_stream, - remaining_session_stream, - &settings_cloned, + remaining_epoch_stream, backend, TestNetworkAdapter, sdp_relay, - epoch_handler, - old_session_message_scheduler, + old_epoch_message_scheduler, rng, - old_session_blending_token_collector, - old_session_crypto_processor, - old_session_public_info, - Epoch::new(0), + old_epoch_blending_token_collector, + old_epoch_crypto_processor, ) .await; }); - // Send a new non-empty session without local core path. + // Send a new non-empty epoch without local core path. membership_sender - .send(MembershipInfo { - membership, - zk: Some(ZkInfo { - root: ZkHash::ZERO, - core_and_path_selectors: None, - }), - session_number: initial_session + 1, - }) + .send(test_blend_epoch_state( + 1, + MembershipInfo { + membership, + zk: Some(ZkInfo { + root: ZkHash::ZERO, + core_and_path_selectors: None, + }), + }, + )) .await .unwrap(); wait_for_blend_backend_event( &mut backend_event_receiver, - TestBlendBackendEvent::SessionTransitionCompleted, + TestBlendBackendEvent::EpochTransitionCompleted, ) .await; // The service should stop without panicking. @@ -1261,12 +1337,12 @@ async fn stop_on_non_empty_session_without_local_core_path() { .expect("the service should stop without panic when local core path is missing"); } -/// Verify that the proof generator produces proofs for the correct session, -/// and that those proofs are only accepted by a verifier for the same session. +/// Verify that the proof generator produces proofs for the correct epoch, +/// and that those proofs are only accepted by a verifier for the same epoch. #[test_log::test(tokio::test)] -async fn test_proof_generator_session_binding() { - let session_0 = 0u64; - let session_1 = 1u64; +async fn test_proof_generator_epoch_binding() { + let epoch_0 = 0.into(); + let epoch_1 = 1.into(); let minimal_network_size = 1; let (membership, local_private_key) = new_membership(minimal_network_size); let (settings, _recovery_file) = settings( @@ -1276,12 +1352,12 @@ async fn test_proof_generator_session_binding() { 0, ); - // Create proof generators for session 0 and session 1. - let public_info_0 = new_public_info(session_0, membership.clone(), &settings); - let public_info_1 = new_public_info(session_1, membership.clone(), &settings); + // Create proof generators for epoch 0 and epoch 1. + let public_info_0 = new_epoch_info(epoch_0, membership.clone(), &settings); + let public_info_1 = new_epoch_info(epoch_1, membership.clone(), &settings); let mut generator_0 = new_crypto_processor( - SessionCryptographicProcessorSettings { + EpochCryptographicProcessorSettings { non_ephemeral_encryption_key: settings.non_ephemeral_signing_key.derive_x25519(), num_blend_layers: settings.num_blend_layers, }, @@ -1290,7 +1366,7 @@ async fn test_proof_generator_session_binding() { ); let mut generator_1 = new_crypto_processor( - SessionCryptographicProcessorSettings { + EpochCryptographicProcessorSettings { non_ephemeral_encryption_key: settings.non_ephemeral_signing_key.derive_x25519(), num_blend_layers: settings.num_blend_layers, }, @@ -1298,7 +1374,7 @@ async fn test_proof_generator_session_binding() { (), ); - // Build a message with session 0 proofs. + // Build a message with epoch 0 proofs. let payload = NetworkMessage { message: vec![], broadcast_settings: (), @@ -1308,32 +1384,32 @@ async fn test_proof_generator_session_binding() { let msg_0 = generator_0 .encapsulate_data_payload(&payload) .await - .expect("encapsulation with session 0 must succeed"); + .expect("encapsulation with epoch 0 must succeed"); - // Build a message with session 1 proofs. + // Build a message with epoch 1 proofs. let msg_1 = generator_1 .encapsulate_data_payload(&payload) .await - .expect("encapsulation with session 1 must succeed"); + .expect("encapsulation with epoch 1 must succeed"); - // Session 0 message should be decapsulable by session 0 processor. + // Epoch 0 message should be decapsulable by epoch 0 processor. let (_, _, state_updater, _state_receiver) = dummy_overwatch_resources::<(), (), RuntimeServiceId>(); let scheduler_settings = scheduler_settings(&timing_settings(), settings.num_blend_layers); - let mut scheduler_0 = SessionMessageScheduler::new( - scheduler_session_info(&public_info_0), + let mut scheduler_0 = EpochMessageScheduler::new( + scheduler_epoch_info(&public_info_0), BlakeRng::from_entropy(), scheduler_settings, ); - let recovery_checkpoint = ServiceState::with_session( - session_0, - SessionBlendingTokenCollector::new(&reward_session_info(&public_info_0)), + let recovery_checkpoint = ServiceState::with_epoch( + epoch_0, + EpochBlendingTokenCollector::new(&reward_epoch_info(&public_info_0)), None, state_updater, ) .unwrap(); drop(handle_incoming_blend_message( - (msg_0.clone().into(), session_0), + (msg_0.clone().into(), epoch_0), &mut scheduler_0, None, &generator_0, @@ -1343,27 +1419,27 @@ async fn test_proof_generator_session_binding() { assert_eq!( scheduler_0.release_delayer().unreleased_messages().len(), 1, - "Session 0 message must be scheduled by session 0 processor" + "Epoch 0 message must be scheduled by epoch 0 processor" ); - // Session 1 message should NOT be decapsulable by session 0 processor - // (wrong PoQ proofs for session 0). + // Epoch 1 message should NOT be decapsulable by epoch 0 processor + // (wrong PoQ proofs for epoch 0). let (_, _, state_updater, _state_receiver) = dummy_overwatch_resources::<(), (), RuntimeServiceId>(); - let mut scheduler_0_only = SessionMessageScheduler::new( - scheduler_session_info(&public_info_0), + let mut scheduler_0_only = EpochMessageScheduler::new( + scheduler_epoch_info(&public_info_0), BlakeRng::from_entropy(), scheduler_settings, ); - let recovery_checkpoint = ServiceState::with_session( - session_0, - SessionBlendingTokenCollector::new(&reward_session_info(&public_info_0)), + let recovery_checkpoint = ServiceState::with_epoch( + epoch_0, + EpochBlendingTokenCollector::new(&reward_epoch_info(&public_info_0)), None, state_updater, ) .unwrap(); drop(handle_incoming_blend_message( - (msg_1.clone().into(), session_0), + (msg_1.clone().into(), epoch_0), &mut scheduler_0_only, None, &generator_0, @@ -1376,26 +1452,26 @@ async fn test_proof_generator_session_binding() { .unreleased_messages() .len(), 0, - "Session 1 message must NOT be scheduled by session 0 processor" + "Epoch 1 message must NOT be scheduled by epoch 0 processor" ); - // Session 1 message should be decapsulable by session 1 processor. + // Epoch 1 message should be decapsulable by epoch 1 processor. let (_, _, state_updater, _state_receiver) = dummy_overwatch_resources::<(), (), RuntimeServiceId>(); - let mut scheduler_1 = SessionMessageScheduler::new( - scheduler_session_info(&public_info_1), + let mut scheduler_1 = EpochMessageScheduler::new( + scheduler_epoch_info(&public_info_1), BlakeRng::from_entropy(), scheduler_settings, ); - let recovery_checkpoint = ServiceState::with_session( - session_1, - SessionBlendingTokenCollector::new(&reward_session_info(&public_info_1)), + let recovery_checkpoint = ServiceState::with_epoch( + epoch_1, + EpochBlendingTokenCollector::new(&reward_epoch_info(&public_info_1)), None, state_updater, ) .unwrap(); drop(handle_incoming_blend_message( - (msg_1.into(), session_1), + (msg_1.into(), epoch_1), &mut scheduler_1, None, &generator_1, @@ -1405,185 +1481,13 @@ async fn test_proof_generator_session_binding() { assert_eq!( scheduler_1.release_delayer().unreleased_messages().len(), 1, - "Session 1 message must be scheduled by session 1 processor" + "Epoch 1 message must be scheduled by epoch 1 processor" ); } -/// Verify that `handle_clock_event` correctly updates the public info and -/// epoch number when the `EpochHandler` emits a `NewEpoch` event. -#[test_log::test(tokio::test)] -async fn test_handle_clock_event_new_epoch() { - let minimal_network_size = 1; - let (membership, local_private_key) = new_membership(minimal_network_size); - let (settings, _recovery_file) = settings( - local_private_key.clone(), - u64::from(minimal_network_size).try_into().unwrap(), - (), - 0, - ); - let session = 0; - let public_info = new_public_info(session, membership.clone(), &settings); - let mut processor = new_crypto_processor( - SessionCryptographicProcessorSettings { - non_ephemeral_encryption_key: settings.non_ephemeral_signing_key.derive_x25519(), - num_blend_layers: settings.num_blend_layers, - }, - &public_info, - (), - ); - - let initial_epoch = Epoch::new(0); - - // Create an EpochHandler with a transition period of 1 slot. - let mut epoch_handler: EpochHandler<_, RuntimeServiceId> = - EpochHandler::new(TestChainService, 1.try_into().unwrap()); - - // First tick initializes the epoch handler. - let (updated_info, updated_epoch) = handle_clock_event( - SlotTick { - epoch: 1.into(), - slot: 1.into(), - }, - &settings, - &mut epoch_handler, - &mut processor, - public_info.clone(), - initial_epoch, - ) - .await; - assert_eq!( - updated_epoch, - Epoch::new(1), - "Epoch must advance to 1 after first tick in epoch 1" - ); - // Public info should be updated with new leader inputs derived from chain - // state. - assert_ne!( - updated_info.epoch, public_info.epoch, - "Leader inputs should be updated from chain epoch state" - ); - - // Tick in the same epoch should not change epoch. - let (unchanged_info, unchanged_epoch) = handle_clock_event( - SlotTick { - epoch: 1.into(), - slot: 2.into(), - }, - &settings, - &mut epoch_handler, - &mut processor, - updated_info.clone(), - updated_epoch, - ) - .await; - assert_eq!(unchanged_epoch, Epoch::new(1)); - assert_eq!(unchanged_info.epoch, updated_info.epoch); - - // Tick in a new epoch should advance again. - let (final_info, final_epoch) = handle_clock_event( - SlotTick { - epoch: 2.into(), - slot: 3.into(), - }, - &settings, - &mut epoch_handler, - &mut processor, - unchanged_info.clone(), - unchanged_epoch, - ) - .await; - assert_eq!( - final_epoch, - Epoch::new(2), - "Epoch must advance to 2 after tick in epoch 2" - ); - // Since epoch_transition_period is 1 slot and slot 2 was the last in epoch 1, - // this triggers NewEpochAndOldEpochTransitionExpired which both completes the - // old transition and rotates to epoch 2. - assert_eq!(final_info.session.session_number, session); -} - -/// Verify that `handle_new_secret_epoch_info` returns updated leader inputs -/// when the `PoL` info epoch is newer than the current epoch, and returns -/// `None` when the epoch has already been processed. -#[test_log::test(tokio::test)] -async fn test_handle_new_secret_epoch_info() { - let minimal_network_size = 1; - let (membership, local_private_key) = new_membership(minimal_network_size); - let (settings, _recovery_file) = settings( - local_private_key, - u64::from(minimal_network_size).try_into().unwrap(), - (), - 0, - ); - let session = 0; - let public_info = new_public_info(session, membership, &settings); - let mut processor = new_crypto_processor( - SessionCryptographicProcessorSettings { - non_ephemeral_encryption_key: settings.non_ephemeral_signing_key.derive_x25519(), - num_blend_layers: settings.num_blend_layers, - }, - &public_info, - (), - ); - - let current_epoch = Epoch::new(0); - - // PoL info for a new epoch (epoch 1 > current 0): should return Some. - let pol_info = PolEpochInfo { - epoch: Epoch::new(1), - poq_public_inputs: lb_core::proofs::leader_proof::LeaderPublic { - slot: 1, - latest_root: lb_groth16::Fr::ZERO, - lottery_0: lb_groth16::Fr::ONE, - lottery_1: lb_groth16::Fr::ONE, - epoch_nonce: ZkHash::ONE, - aged_root: ZkHash::ONE, - }, - poq_private_inputs: - lb_blend::proofs::quota::inputs::prove::private::ProofOfLeadershipQuotaInputs { - slot: 1, - note_value: 1, - transaction_hash: ZkHash::ZERO, - output_number: 1, - aged_path_and_selectors: [(ZkHash::ZERO, false); _], - secret_key: ZkHash::ZERO, - }, - }; - let result = handle_new_secret_epoch_info(&settings, &pol_info, &mut processor, current_epoch); - assert!( - result.is_some(), - "Should return Some(LeaderInputs) when PoL epoch > current epoch" - ); - let new_leader = result.unwrap(); - assert_eq!(new_leader.pol_epoch_nonce, ZkHash::ONE); - assert_eq!(new_leader.pol_ledger_aged, ZkHash::ONE); - - // PoL info for the same epoch (epoch 1 == current 1): should return None. - let already_processed_epoch = Epoch::new(1); - let result = handle_new_secret_epoch_info( - &settings, - &pol_info, - &mut processor, - already_processed_epoch, - ); - assert!( - result.is_none(), - "Should return None when PoL epoch <= current epoch" - ); - - // PoL info for an older epoch: should return None. - let future_epoch = Epoch::new(5); - let result = handle_new_secret_epoch_info(&settings, &pol_info, &mut processor, future_epoch); - assert!( - result.is_none(), - "Should return None when PoL epoch < current epoch" - ); -} - -/// When `initialize` receives a `last_saved_state` whose session matches the -/// current membership session, the saved state is restored (e.g. `spent_quota` -/// is preserved). When the session does not match, a fresh state is created. +/// When `initialize` receives a `last_saved_state` whose epoch matches the +/// current membership epoch, the saved state is restored (e.g. `spent_quota` +/// is preserved). When the epoch does not match, a fresh state is created. #[test_log::test(tokio::test)] async fn test_initialize_recovers_matching_saved_state() { let minimal_network_size = 2; @@ -1595,58 +1499,42 @@ async fn test_initialize_recovers_matching_saved_state() { 0, ); - let initial_session = 0; + let initial_epoch = 0.into(); - // Matching session: saved state should be restored + // Matching epoch: saved state should be restored let (membership_stream, membership_sender) = new_stream(); - let (clock_stream, clock_sender) = new_stream(); membership_sender - .send(MembershipInfo { - membership: membership.clone(), - zk: Some(ZkInfo { - root: ZkHash::ZERO, - core_and_path_selectors: Some([(ZkHash::ZERO, false); CORE_MERKLE_TREE_HEIGHT]), - }), - session_number: initial_session, - }) - .await - .unwrap(); - clock_sender - .send(SlotTick { - epoch: 0.into(), - slot: 0.into(), - }) + .send(test_blend_epoch_state( + 0, + MembershipInfo { + membership: membership.clone(), + zk: Some(ZkInfo { + root: ZkHash::ZERO, + core_and_path_selectors: Some([(ZkHash::ZERO, false); CORE_MERKLE_TREE_HEIGHT]), + }), + }, + )) .await .unwrap(); - let mut epoch_handler = EpochHandler::new( - TestChainService, - settings.time.epoch_transition_period_in_slots, - ); let (overwatch_handle, _overwatch_cmd_receiver, state_updater, _state_receiver) = dummy_overwatch_resources(); let (sdp_relay_1, _sdp_relay_receiver) = sdp_relay(); - // Build a pre-populated saved state with matching session and some spent quota. - let public_info = new_public_info(initial_session, membership.clone(), &settings); - let token_collector = SessionBlendingTokenCollector::new(&reward_session_info(&public_info)); - let saved_state = ServiceState::with_session( - initial_session, - token_collector, - None, - state_updater.clone(), - ) - .unwrap(); + // Build a pre-populated saved state with matching epoch and some spent quota. + let public_info = new_epoch_info(initial_epoch, membership.clone(), &settings); + let token_collector = EpochBlendingTokenCollector::new(&reward_epoch_info(&public_info)); + let saved_state = + ServiceState::with_epoch(initial_epoch, token_collector, None, state_updater.clone()) + .unwrap(); let mut updater = saved_state.start_updating(); updater.consume_core_quota(5); let saved_state = updater.commit_changes(); let ( - _remaining_session_stream, - _remaining_clock_stream, + _remaining_epoch_stream, _current_public_info, - _current_epoch, _crypto_processor, recovered_checkpoint, _message_scheduler, @@ -1656,7 +1544,6 @@ async fn test_initialize_recovers_matching_saved_state() { NodeId, TestBlendBackend, TestNetworkAdapter, - TestChainService, MockCoreAndLeaderProofsGenerator, MockProofsVerifier, MockKmsAdapter, @@ -1664,8 +1551,6 @@ async fn test_initialize_recovers_matching_saved_state() { >( settings.clone(), membership_stream, - clock_stream, - &mut epoch_handler, overwatch_handle, MockKmsAdapter, &sdp_relay_1, @@ -1677,57 +1562,49 @@ async fn test_initialize_recovers_matching_saved_state() { assert_eq!( recovered_checkpoint.spent_quota(), 5, - "Matching session: spent_quota should be restored from saved state" + "Matching epoch: spent_quota should be restored from saved state" ); - assert_eq!(recovered_checkpoint.last_seen_session(), initial_session); + assert_eq!(recovered_checkpoint.last_seen_epoch(), initial_epoch); - // Mismatched session: fresh state should be created + // Mismatched epoch: fresh state should be created let (membership_stream2, membership_sender2) = new_stream(); - let (clock_stream2, clock_sender2) = new_stream(); membership_sender2 - .send(MembershipInfo { - membership: membership.clone(), - zk: Some(ZkInfo { - root: ZkHash::ZERO, - core_and_path_selectors: Some([(ZkHash::ZERO, false); CORE_MERKLE_TREE_HEIGHT]), - }), - session_number: initial_session, - }) - .await - .unwrap(); - clock_sender2 - .send(SlotTick { - epoch: 0.into(), - slot: 1.into(), - }) + .send(test_blend_epoch_state( + 0, + MembershipInfo { + membership: membership.clone(), + zk: Some(ZkInfo { + root: ZkHash::ZERO, + core_and_path_selectors: Some([(ZkHash::ZERO, false); CORE_MERKLE_TREE_HEIGHT]), + }), + }, + )) .await .unwrap(); - let mut epoch_handler2 = EpochHandler::new( - TestChainService, - settings.time.epoch_transition_period_in_slots, - ); let (overwatch_handle2, _overwatch_cmd_receiver2, state_updater2, _state_receiver2) = dummy_overwatch_resources(); let (sdp_relay2, _sdp_relay_receiver2) = sdp_relay(); - // Build a saved state for a *different* session (session 99) with spent quota. - let stale_public_info = new_public_info(99, membership.clone(), &settings); + // Build a saved state for a *different* epoch (epoch 99) with spent quota. + let stale_public_info = new_epoch_info(99.into(), membership.clone(), &settings); let stale_token_collector = - SessionBlendingTokenCollector::new(&reward_session_info(&stale_public_info)); - let stale_state = - ServiceState::with_session(99, stale_token_collector, None, state_updater2.clone()) - .unwrap(); + EpochBlendingTokenCollector::new(&reward_epoch_info(&stale_public_info)); + let stale_state = ServiceState::with_epoch( + 99.into(), + stale_token_collector, + None, + state_updater2.clone(), + ) + .unwrap(); let mut updater = stale_state.start_updating(); updater.consume_core_quota(42); let stale_state = updater.commit_changes(); let ( - _remaining_session_stream2, - _remaining_clock_stream2, + _remaining_epoch_stream2, _current_public_info2, - _current_epoch2, _crypto_processor2, recovered_checkpoint2, _message_scheduler2, @@ -1737,7 +1614,6 @@ async fn test_initialize_recovers_matching_saved_state() { NodeId, TestBlendBackend, TestNetworkAdapter, - TestChainService, MockCoreAndLeaderProofsGenerator, MockProofsVerifier, MockKmsAdapter, @@ -1745,8 +1621,6 @@ async fn test_initialize_recovers_matching_saved_state() { >( settings.clone(), membership_stream2, - clock_stream2, - &mut epoch_handler2, overwatch_handle2, MockKmsAdapter, &sdp_relay2, @@ -1758,11 +1632,11 @@ async fn test_initialize_recovers_matching_saved_state() { assert_eq!( recovered_checkpoint2.spent_quota(), 0, - "Mismatched session: spent_quota should be 0 for fresh state" + "Mismatched epoch: spent_quota should be 0 for fresh state" ); assert_eq!( - recovered_checkpoint2.last_seen_session(), - initial_session, - "Mismatched session: should track the current session, not the stale one" + recovered_checkpoint2.last_seen_epoch(), + initial_epoch, + "Mismatched epoch: should track the current epoch, not the stale one" ); } diff --git a/services/blend/src/core/tests/utils.rs b/services/blend/src/core/tests/utils.rs index 7ae9df297..4895dd743 100644 --- a/services/blend/src/core/tests/utils.rs +++ b/services/blend/src/core/tests/utils.rs @@ -1,3 +1,4 @@ +use core::cell::RefCell; use std::{num::NonZeroU64, pin::Pin, sync::Arc, time::Duration}; use async_trait::async_trait; @@ -27,18 +28,18 @@ use lb_blend::{ scheduling::{ membership::Membership, message_blend::{ - crypto::SessionCryptographicProcessorSettings, + crypto::EpochCryptographicProcessorSettings, provers::{ BlendLayerProof, ProofsGeneratorSettings, core_and_leader::CoreAndLeaderProofsGenerator, }, }, - message_scheduler::{self, session_info::SessionInfo as SchedulerSessionInfo}, + message_scheduler::{self, epoch_info::EpochInfo as SchedulerEpochInfo}, }, }; use lb_chain_service::Epoch; -use lb_core::{crypto::ZkHash, sdp::SessionNumber}; -use lb_groth16::{Field as _, Fr}; +use lb_core::crypto::ZkHash; +use lb_groth16::{Field as _, Fr, fr_from_bytes_unchecked, fr_to_bytes}; use lb_key_management_system_service::keys::{Ed25519PublicKey, UnsecuredEd25519Key}; use lb_network_service::{NetworkService, backends::NetworkBackend}; use lb_poq::CorePathAndSelectors; @@ -56,7 +57,7 @@ use tokio_stream::wrappers::{BroadcastStream, ReceiverStream}; use crate::{ core::{ - backends::{BlendBackend, PublicInfo, SessionInfo}, + backends::{BackendEpochInfo, BlendBackend}, kms::KmsPoQAdapter, network::NetworkAdapter, processor::CoreCryptographicProcessor, @@ -67,6 +68,7 @@ use crate::{ state::RecoveryServiceState, tests::RuntimeServiceId, }, + epoch::CoreEpochPublicInfo, message::NetworkInfo, settings::TimingSettings, test_utils, @@ -102,7 +104,6 @@ pub fn settings( scheduler: SchedulerSettings { cover: CoverTrafficSettings { message_frequency_per_round: 1.0.try_into().unwrap(), - intervals_for_safety_buffer: 0, }, delayer: MessageDelayerSettings { maximum_release_delay_in_rounds: 1.try_into().unwrap(), @@ -124,12 +125,10 @@ pub fn settings( pub fn timing_settings() -> TimingSettings { TimingSettings { - rounds_per_session: 10.try_into().unwrap(), - rounds_per_interval: 10.try_into().unwrap(), + rounds_per_epoch: 10.try_into().unwrap(), round_duration: Duration::from_secs(1), rounds_per_observation_window: 5.try_into().unwrap(), - rounds_per_session_transition_period: 2.try_into().unwrap(), - epoch_transition_period_in_slots: 1.try_into().unwrap(), + epoch_transition_period: Duration::from_secs(1), } } @@ -138,11 +137,9 @@ pub fn scheduler_settings( num_blend_layers: NonZeroU64, ) -> message_scheduler::Settings { message_scheduler::Settings { - additional_safety_intervals: 0, - expected_intervals_per_session: NonZeroU64::try_from(1).unwrap(), maximum_release_delay_in_rounds: NonZeroU64::try_from(1).unwrap(), round_duration: timing_settings.round_duration, - rounds_per_interval: timing_settings.rounds_per_interval, + rounds_per_epoch: timing_settings.rounds_per_epoch, num_blend_layers, } } @@ -169,7 +166,7 @@ where fn new( _service_config: BlendConfig, _overwatch_handle: OverwatchHandle, - _current_public_info: PublicInfo, + _current_epoch_info: BackendEpochInfo, _rng: Rng, ) -> Self { let (event_sender, _) = broadcast::channel(CHANNEL_SIZE); @@ -180,21 +177,32 @@ where async fn publish( &self, _msg: EncapsulatedMessageWithVerifiedPublicHeader, - _intended_session: u64, + _intended_epoch: Epoch, ) { } - async fn rotate_session(&mut self, _new_session_info: SessionInfo) {} + async fn rotate_epoch(&mut self, new_epoch_info: BackendEpochInfo) { + // Notify tests that the backend rotated to a new epoch, carrying the new + // epoch and membership size so tests can assert the new membership was + // propagated to the backend. + let (membership, epoch) = new_epoch_info; + // Ignore send errors: not all tests subscribe to backend events, and + // `rotate_epoch` is also called right before a retirement (no subscriber). + let _ = self.event_sender.send(TestBlendBackendEvent::EpochRotated { + epoch, + membership_size: membership.size(), + }); + } - async fn complete_session_transition(&mut self) { - // Notify tests that the backend completed the session transition. + async fn complete_epoch_transition(&mut self) { + // Notify tests that the backend completed the epoch transition. self.event_sender - .send(TestBlendBackendEvent::SessionTransitionCompleted) + .send(TestBlendBackendEvent::EpochTransitionCompleted) .unwrap(); } fn listen_to_incoming_messages( &mut self, - ) -> Pin + Send>> { + ) -> Pin + Send>> { unimplemented!() } @@ -212,7 +220,12 @@ impl TestBlendBackend { #[derive(Debug, Clone, PartialEq, Eq)] pub enum TestBlendBackendEvent { - SessionTransitionCompleted, + EpochTransitionCompleted, + /// Emitted when the backend is rotated to a new epoch. + EpochRotated { + epoch: Epoch, + membership_size: usize, + }, } /// Waits for the given event to be received on the provided channel. @@ -303,8 +316,8 @@ pub fn dummy_overwatch_resources( - settings: SessionCryptographicProcessorSettings, - public_info: &PublicInfo, + settings: EpochCryptographicProcessorSettings, + epoch_info: &CoreEpochPublicInfo, core_poq_generator: CorePoQGenerator, ) -> CoreCryptographicProcessor< NodeId, @@ -312,74 +325,103 @@ pub fn new_crypto_processor( MockCoreAndLeaderProofsGenerator, MockProofsVerifier, > { - let minimum_network_size = u64::try_from(public_info.session.membership.size()) + let minimum_network_size = u64::try_from(epoch_info.membership.size()) .expect("membership size must fit into u64") .try_into() .expect("minimum_network_size must be non-zero"); CoreCryptographicProcessor::try_new_with_core_condition_check( - public_info.session.membership.clone(), + epoch_info.membership.clone(), minimum_network_size, settings, PoQVerificationInputsMinusSigningKey { - session: public_info.session.session_number, - core: public_info.session.core_public_inputs, - leader: public_info.epoch, + core: epoch_info.poq_core_public_inputs, + leader: epoch_info.poq_leadership_public_inputs, }, core_poq_generator, - Epoch::new(0), + epoch_info.epoch, ) .expect("crypto processor must be created successfully") } -pub fn new_public_info( - session: u64, +pub fn new_epoch_info( + epoch: Epoch, membership: Membership, settings: &BlendConfig, -) -> PublicInfo { - let core_quota = settings.session_core_quota(membership.size()); - PublicInfo { - session: SessionInfo { - session_number: session, - membership, - core_public_inputs: CoreInputs { - zk_root: ZkHash::ZERO, - quota: core_quota, - }, +) -> CoreEpochPublicInfo { + let core_quota = settings.epoch_core_quota(membership.size()); + CoreEpochPublicInfo { + epoch, + membership, + poq_core_public_inputs: CoreInputs { + zk_root: ZkHash::ZERO, + quota: core_quota, }, - epoch: LeaderInputs { + poq_leadership_public_inputs: LeaderInputs { pol_ledger_aged: ZkHash::ZERO, - pol_epoch_nonce: ZkHash::ZERO, - message_quota: settings.session_leadership_quota(), + pol_epoch_nonce: fr_from_bytes_unchecked(&epoch.into_inner().to_le_bytes()), + message_quota: settings.epoch_leadership_quota(), lottery_0: Fr::ZERO, lottery_1: Fr::ZERO, }, } } -pub fn scheduler_session_info(public_info: &PublicInfo) -> SchedulerSessionInfo { - SchedulerSessionInfo { - core_quota: public_info.session.core_public_inputs.quota, - session_number: u128::from(public_info.session.session_number).into(), +/// Dummy secret `PoL` leadership inputs, for tests that exercise the +/// secret/public epoch-info coordination without needing valid proofs. +pub fn dummy_pol_private_inputs() -> ProofOfLeadershipQuotaInputs { + ProofOfLeadershipQuotaInputs { + slot: 1, + note_value: 1, + transaction_hash: ZkHash::ZERO, + output_number: 1, + aged_path_and_selectors: [(ZkHash::ZERO, false); _], + secret_key: ZkHash::ZERO, } } -pub fn reward_session_info(public_info: &PublicInfo) -> reward::SessionInfo { - reward::SessionInfo::new( - public_info.session.session_number, - &public_info.epoch.pol_epoch_nonce, +pub fn scheduler_epoch_info(public_info: &CoreEpochPublicInfo) -> SchedulerEpochInfo { + SchedulerEpochInfo { + core_quota: public_info.poq_core_public_inputs.quota, + epoch: public_info.epoch, + } +} + +pub fn reward_epoch_info(public_info: &CoreEpochPublicInfo) -> reward::EpochInfo { + reward::EpochInfo::new( + public_info.epoch, + &public_info.poq_leadership_public_inputs.pol_epoch_nonce, public_info - .session .membership .size() .try_into() .expect("num_core_nodes must fit into u64"), - public_info.session.core_public_inputs.quota, + public_info.poq_core_public_inputs.quota, 1, ) - .expect("session info must be created successfully") + .expect("epoch info must be created successfully") } -pub struct MockCoreAndLeaderProofsGenerator(SessionNumber); +thread_local! { + /// Records the epochs for which [`MockCoreAndLeaderProofsGenerator::set_epoch_private`] + /// was called, so tests can assert that the secret `PoL` info was applied to + /// the expected generator. Reliable because `#[tokio::test]` uses a + /// single-threaded runtime, so the value is test-isolated. + static SET_EPOCH_PRIVATE_CALLS: RefCell> = const { RefCell::new(Vec::new()) }; +} + +/// Clears the record of `set_epoch_private` calls. Call before the code under +/// test to isolate the calls of interest. +pub fn reset_set_epoch_private_calls() { + SET_EPOCH_PRIVATE_CALLS.with(|calls| calls.borrow_mut().clear()); +} + +/// Returns the epochs for which `set_epoch_private` has been called since the +/// last reset, in call order. +pub fn recorded_set_epoch_private_calls() -> Vec { + SET_EPOCH_PRIVATE_CALLS.with(|calls| calls.borrow().clone()) +} + +pub struct MockCoreAndLeaderProofsGenerator(ZkHash); #[async_trait] impl CoreAndLeaderProofsGenerator @@ -389,41 +431,38 @@ impl CoreAndLeaderProofsGenerator settings: ProofsGeneratorSettings, _core_proof_of_quota_generator: CorePoQGenerator, ) -> Self { - Self(settings.public_inputs.session) + Self(settings.public_inputs.leader.pol_epoch_nonce) } - fn rotate_epoch(&mut self, _: LeaderInputs, _: Epoch) {} - fn set_epoch_private(&mut self, _: ProofOfLeadershipQuotaInputs, _: LeaderInputs, _: Epoch) {} + fn set_epoch_private(&mut self, _: ProofOfLeadershipQuotaInputs, target_epoch: Epoch) { + SET_EPOCH_PRIVATE_CALLS.with(|calls| calls.borrow_mut().push(target_epoch)); + } async fn get_next_core_proof(&mut self) -> Option { - Some(session_based_dummy_proofs(self.0)) + Some(epoch_based_dummy_proofs(self.0)) } async fn get_next_leader_proof(&mut self) -> Option { - Some(session_based_dummy_proofs(self.0)) + Some(epoch_based_dummy_proofs(self.0)) } } #[derive(Debug, Clone)] -pub struct MockProofsVerifier(SessionNumber); +pub struct MockProofsVerifier(ZkHash); impl ProofsVerifier for MockProofsVerifier { type Error = (); fn new(public_inputs: PoQVerificationInputsMinusSigningKey) -> Self { - Self(public_inputs.session) + Self(public_inputs.leader.pol_epoch_nonce) } - fn start_epoch_transition(&mut self, _new_pol_inputs: LeaderInputs) {} - - fn complete_epoch_transition(&mut self) {} - fn verify_proof_of_quota( &self, proof: ProofOfQuota, _signing_key: &Ed25519PublicKey, ) -> Result { - let expected_proof = session_based_dummy_proofs(self.0).proof_of_quota; + let expected_proof = epoch_based_dummy_proofs(self.0).proof_of_quota; if proof == expected_proof { Ok(expected_proof) } else { @@ -436,7 +475,7 @@ impl ProofsVerifier for MockProofsVerifier { proof: ProofOfSelection, _inputs: &VerifyInputs, ) -> Result { - let expected_proof = session_based_dummy_proofs(self.0).proof_of_selection; + let expected_proof = epoch_based_dummy_proofs(self.0).proof_of_selection; if proof == expected_proof { Ok(expected_proof) } else { @@ -445,29 +484,23 @@ impl ProofsVerifier for MockProofsVerifier { } } -fn session_based_dummy_proofs(session: SessionNumber) -> BlendLayerProof { - let session_bytes = session.to_le_bytes(); +fn epoch_based_dummy_proofs(epoch: ZkHash) -> BlendLayerProof { + let epoch_bytes = fr_to_bytes(&epoch); BlendLayerProof { proof_of_quota: VerifiedProofOfQuota::from_bytes_unchecked({ let mut bytes = [0u8; _]; - bytes[..session_bytes.len()].copy_from_slice(&session_bytes); + bytes[..epoch_bytes.len()].copy_from_slice(&epoch_bytes); bytes }), proof_of_selection: VerifiedProofOfSelection::from_bytes_unchecked({ let mut bytes = [0u8; _]; - bytes[..session_bytes.len()].copy_from_slice(&session_bytes); + bytes[..epoch_bytes.len()].copy_from_slice(&epoch_bytes); bytes }), ephemeral_signing_key: UnsecuredEd25519Key::generate_with_blake_rng(), } } -impl MockProofsVerifier { - pub fn session_number(&self) -> SessionNumber { - self.0 - } -} - pub struct MockKmsAdapter; impl KmsPoQAdapter for MockKmsAdapter { diff --git a/services/blend/src/edge/backends/libp2p/tests/message_handling.rs b/services/blend/src/edge/backends/libp2p/tests/message_handling.rs index c6d386d31..67cda2a74 100644 --- a/services/blend/src/edge/backends/libp2p/tests/message_handling.rs +++ b/services/blend/src/edge/backends/libp2p/tests/message_handling.rs @@ -65,15 +65,15 @@ async fn edge_message_propagation() { // Verify that both peers receive the message, even though the edge swarm is // connected to only one of them. - let (swarm_1_received_message, swarm_1_message_session) = + let (swarm_1_received_message, swarm_1_message_epoch) = core_swarm_1_incoming_message_receiver.recv().await.unwrap(); - let (swarm_2_received_message, swarm_2_message_session) = + let (swarm_2_received_message, swarm_2_message_epoch) = core_swarm_2_incoming_message_receiver.recv().await.unwrap(); assert_eq!(swarm_1_received_message, message.clone().into()); - assert_eq!(swarm_1_message_session, 1); + assert_eq!(swarm_1_message_epoch, 1); assert_eq!(swarm_2_received_message, message.clone().into()); - assert_eq!(swarm_2_message_session, 1); + assert_eq!(swarm_2_message_epoch, 1); } #[test(tokio::test)] diff --git a/services/blend/src/edge/backends/libp2p/tests/redials.rs b/services/blend/src/edge/backends/libp2p/tests/redials.rs index b00e7463e..87d89af86 100644 --- a/services/blend/src/edge/backends/libp2p/tests/redials.rs +++ b/services/blend/src/edge/backends/libp2p/tests/redials.rs @@ -125,10 +125,10 @@ async fn edge_redial_different_peer_after_redial_limit() { // Verify the message is anyway received by the core swarm after the maximum // number of dial attempts have been performed with the unreachable address. - let (received_message, received_message_session) = + let (received_message, received_message_epoch) = core_swarm_incoming_message_receiver.recv().await.unwrap(); assert_eq!(received_message, message.into_inner().into()); - assert_eq!(received_message_session, 1); + assert_eq!(received_message_epoch, 1); } /// Verifies that when a peer fails all dial attempts, it is added to the failed diff --git a/services/blend/src/edge/handlers.rs b/services/blend/src/edge/handlers.rs index 886123d30..517cc4d8b 100644 --- a/services/blend/src/edge/handlers.rs +++ b/services/blend/src/edge/handlers.rs @@ -6,7 +6,7 @@ use lb_blend::{ scheduling::{ membership::Membership, message_blend::{ - crypto::leader::send::SessionCryptographicProcessor, + crypto::leader::send::EpochCryptographicProcessor, provers::leader::LeaderProofsGenerator, }, }, @@ -19,11 +19,24 @@ use rand::SeedableRng as _; use crate::edge::{LOG_TARGET, RunningSettings as Settings, backends::BlendBackend}; pub struct MessageHandler { - cryptographic_processor: SessionCryptographicProcessor, + cryptographic_processor: EpochCryptographicProcessor, backend: Backend, _phantom: PhantomData, } +impl + MessageHandler +where + Backend: BlendBackend, + NodeId: Clone, + ProofsGenerator: LeaderProofsGenerator, +{ + #[cfg(test)] + pub const fn epoch(&self) -> Epoch { + self.cryptographic_processor.epoch() + } +} + impl MessageHandler where @@ -73,7 +86,7 @@ where overwatch_handle: OverwatchHandle, epoch: Epoch, ) -> Self { - let cryptographic_processor = SessionCryptographicProcessor::new( + let cryptographic_processor = EpochCryptographicProcessor::new( settings.num_blend_layers, membership.clone(), public_info, diff --git a/services/blend/src/edge/mod.rs b/services/blend/src/edge/mod.rs index 075839c2e..a20a03dea 100644 --- a/services/blend/src/edge/mod.rs +++ b/services/blend/src/edge/mod.rs @@ -18,11 +18,11 @@ use lb_blend::{ message::crypto::proofs::PoQVerificationInputsMinusSigningKey, proofs::quota::inputs::prove::public::{CoreInputs, LeaderInputs}, scheduling::{ + epoch::{EpochEvent, UninitializedEpochEventStream}, message_blend::provers::leader::LeaderProofsGenerator, - session::{SessionEvent, UninitializedSessionEventStream}, }, }; -use lb_chain_service::api::{CryptarchiaServiceApi, CryptarchiaServiceData}; +use lb_chain_service::api::CryptarchiaServiceData; use lb_core::codec::SerializeOp as _; use lb_key_management_system_service::{ api::KmsServiceApi, keys::KeyOperators, @@ -30,7 +30,7 @@ use lb_key_management_system_service::{ }; use lb_log_targets::blend; use lb_services_utils::wait_until_services_are_ready; -use lb_time_service::{SlotTick, TimeService, TimeServiceMessage}; +use lb_time_service::TimeService; use overwatch::{ OpaqueServiceResourcesHandle, overwatch::OverwatchHandle, @@ -41,7 +41,6 @@ use overwatch::{ }, }; use serde::{Serialize, de::DeserializeOwned}; -pub(crate) use service_components::ServiceComponents; use settings::StartingBlendConfig; use tokio::sync::oneshot; use tracing::{debug, error, info}; @@ -51,13 +50,10 @@ use crate::{ handlers::{Error, MessageHandler}, settings::RunningBlendConfig, }, - epoch_info::{ - ChainApi, EpochEvent, EpochHandler, PolEpochInfo, PolInfoProvider as PolInfoProviderTrait, - }, + epoch_info::{PolEpochInfo, PolInfoProvider as PolInfoProviderTrait}, kms::PreloadKmsService, - membership::{self, MembershipInfo, node_id}, + membership::{self, chain::BlendEpochState, node_id}, message::{NetworkInfo, NetworkMessage, ServiceMessage}, - settings::FIRST_STREAM_ITEM_READY_TIMEOUT, }; const LOG_TARGET: &str = blend::service::EDGE; @@ -65,16 +61,10 @@ const LOG_TARGET: &str = blend::service::EDGE; type RunningSettings = RunningBlendConfig<>::Settings>; -type EpochInfoAndHandler = ( - PolEpochInfo, - MessageHandler, -); - pub struct BlendService< Backend, NodeId, BroadcastSettings, - MembershipAdapter, ProofsGenerator, TimeBackend, ChainService, @@ -85,20 +75,13 @@ pub struct BlendService< NodeId: Clone, { service_resources_handle: OpaqueServiceResourcesHandle, - _phantom: PhantomData<( - MembershipAdapter, - ProofsGenerator, - TimeBackend, - ChainService, - PolInfoProvider, - )>, + _phantom: PhantomData<(ProofsGenerator, TimeBackend, ChainService, PolInfoProvider)>, } impl< Backend, NodeId, BroadcastSettings, - MembershipAdapter, ProofsGenerator, TimeBackend, ChainService, @@ -109,7 +92,6 @@ impl< Backend, NodeId, BroadcastSettings, - MembershipAdapter, ProofsGenerator, TimeBackend, ChainService, @@ -126,13 +108,11 @@ where type Message = ServiceMessage; } -#[expect(clippy::too_many_lines, reason = "TODO: Address this at some point.")] #[async_trait::async_trait] impl< Backend, NodeId, BroadcastSettings, - MembershipAdapter, ProofsGenerator, TimeBackend, ChainService, @@ -143,7 +123,6 @@ impl< Backend, NodeId, BroadcastSettings, - MembershipAdapter, ProofsGenerator, TimeBackend, ChainService, @@ -154,14 +133,11 @@ where Backend: BlendBackend + Send + Sync, NodeId: Clone + Debug + Eq + Hash + Send + Sync + node_id::TryFrom + 'static, BroadcastSettings: Serialize + DeserializeOwned + Send, - MembershipAdapter: membership::Adapter + Send, - membership::ServiceMessage: Send + Sync + 'static, ProofsGenerator: LeaderProofsGenerator + Send, TimeBackend: lb_time_service::backends::TimeBackend + Send, ChainService: CryptarchiaServiceData, PolInfoProvider: PolInfoProviderTrait + Send, - RuntimeServiceId: AsServiceId<::Service> - + AsServiceId + RuntimeServiceId: AsServiceId + AsServiceId> + AsServiceId + AsServiceId> @@ -202,7 +178,6 @@ where &overwatch_handle, Some(Duration::from_mins(1)), TimeService<_, _>, - ::Service, PreloadKmsService<_> ) .await?; @@ -229,36 +204,14 @@ where NodeId::try_from_provider_id(&non_ephemeral_signing_key.public_key().to_bytes()) .expect("non-ephemeral signing key should decode into a valid node id"); - // Initialize membership stream for session and core-related public PoQ inputs. - let session_stream = MembershipAdapter::new( - overwatch_handle - .relay::<::Service>() - .await - .expect("Failed to get relay channel with membership service."), - non_ephemeral_signing_key.public_key(), - // No ZK stuff needs to be computed by edge nodes, so no ZK key is specified here. - None, - ) - .subscribe() - .await - .expect("Failed to get membership stream from membership service."); - - // Initialize clock stream for detecting epoch transitions. - let clock_stream = async { - let time_relay = overwatch_handle - .relay::>() - .await - .expect("Relay with time service should be available."); - let (sender, receiver) = oneshot::channel(); - time_relay - .send(TimeServiceMessage::Subscribe { sender }) - .await - .expect("Failed to subscribe to slot clock."); - receiver - .await - .expect("Should not fail to receive slot stream from time service.") - } - .await; + let public_epoch_stream = + membership::chain::subscribe::( + &overwatch_handle, + non_ephemeral_signing_key.public_key(), + // No ZK stuff needs to be computed by edge nodes, so no ZK key is specified here. + None, + ) + .await; let messages_to_blend_stream = Box::pin(inbound_relay.filter_map(async |msg| { match msg { @@ -277,29 +230,12 @@ where } })); - let epoch_handler = async { - let chain_service = CryptarchiaServiceApi::::new( - overwatch_handle - .relay::() - .await - .expect("Failed to establish channel with chain service."), - ); - EpochHandler::new( - chain_service, - settings.time.epoch_transition_period_in_slots, - ) - } - .await; - - run::( - UninitializedSessionEventStream::new( - session_stream, - FIRST_STREAM_ITEM_READY_TIMEOUT, - settings.time.session_transition_period(), + run::( + UninitializedEpochEventStream::new( + public_epoch_stream, + settings.time.epoch_transition_period, ), - clock_stream, messages_to_blend_stream, - epoch_handler, RunningSettings:: { backend: settings.backend, cover: settings.cover, @@ -329,33 +265,33 @@ where /// Run the event loop of the service. /// -/// The event loop handles three types of events: -/// - **Session changes**: resets the message handler with the new session but -/// the current epoch info. If the handler was shut down (waiting for secret -/// epoch info), it stays shut down. -/// - **Clock ticks (epoch transitions)**: on a new epoch, shuts down the -/// message handler until secret `PoL` info for that epoch is received. If -/// secret info was already provided for the new epoch, the handler is kept. -/// - **Secret `PoL` info**: always (re)creates the message handler with the new -/// epoch's public and private inputs, preserving the current session. +/// The event loop keeps track of the latest public epoch info and the latest +/// secret `PoL` info independently and rebuilds the message handler whenever +/// the two line up on the same epoch. It handles three types of events: +/// - **New public epoch info** (chain-derived membership + leader inputs): +/// becomes the current public info; the handler is rebuilt if the latest +/// secret info is for the same epoch, otherwise it stays down. +/// - **New secret `PoL` info**: becomes the current secret info; the handler is +/// rebuilt if it matches the current public info's epoch. +/// - **Incoming messages to blend**: forwarded to the current message handler; +/// dropped with a warning if no handler is active (secret `PoL` info for the +/// current epoch has not yet arrived). /// /// Returns an [`Error`] if a new membership does not satisfy the edge node /// condition. /// /// # Panics -/// - If the initial membership is not yielded immediately from the session -/// stream. +/// - If the initial public epoch info is not yielded immediately from the +/// public epoch stream. #[expect( clippy::cognitive_complexity, reason = "TODO: address this in a dedicated refactor" )] -async fn run( - session_stream: UninitializedSessionEventStream< - impl Stream> + Unpin, +async fn run( + public_epoch_stream: UninitializedEpochEventStream< + impl Stream> + Unpin, >, - mut clock_stream: impl Stream + Unpin, mut incoming_message_stream: impl Stream> + Send + Unpin, - mut epoch_handler: EpochHandler, settings: RunningSettings, overwatch_handle: &OverwatchHandle, notify_ready: impl Fn(), @@ -364,21 +300,19 @@ where Backend: BlendBackend + Sync + Send, NodeId: Clone + Debug + Eq + Hash + Send + Sync + 'static, ProofsGenerator: LeaderProofsGenerator + Send, - ChainService: ChainApi + Send + Sync, PolInfoProvider: PolInfoProviderTrait, RuntimeServiceId: Clone + Send + Sync, { - let (mut current_membership_info, mut remaining_session_stream) = session_stream + let (mut current_epoch_info, mut remaining_public_epoch_stream) = public_epoch_stream .await_first_ready() .await - .expect("The current session info must be available."); + .expect("The current epoch info must be available."); info!( target: LOG_TARGET, - session = current_membership_info.session_number, - members = current_membership_info.membership.size(), - local_node_index = current_membership_info.membership.local_index(), - has_zk = current_membership_info.zk.is_some(), + members = current_epoch_info.membership_info.membership.size(), + local_node_index = current_epoch_info.membership_info.membership.local_index(), + has_zk = current_epoch_info.membership_info.zk.is_some(), "current membership is ready" ); @@ -391,32 +325,44 @@ where .await .expect("Should not fail to subscribe to secret PoL info stream."); - let mut current_pol_info_and_message_handler: Option<( - PolEpochInfo, + let mut current_secret_epoch_info: Option = None; + let mut current_epoch_message_handler: Option< MessageHandler, - )> = None; + > = None; loop { tokio::select! { - Some(SessionEvent::NewSession(new_session_info)) = remaining_session_stream.next() => { - match handle_new_session(&new_session_info, settings.clone(), &mut current_pol_info_and_message_handler, overwatch_handle.clone()) { + Some(EpochEvent::NewEpoch(new_public_epoch_info)) = remaining_public_epoch_stream.next() => { + current_epoch_info = new_public_epoch_info; + match handle_new_epoch_event(¤t_epoch_info, current_secret_epoch_info.as_ref(), &mut current_epoch_message_handler, settings.clone(), overwatch_handle.clone()) { Err(Error::NetworkIsTooSmall(_)) => { info!(target: LOG_TARGET, "New membership does not satisfy edge node condition, edge service shutting down."); return Ok(()); } Err(e) => { - error!(target: LOG_TARGET, "Error when handling new session: {e:?}, edge service shutting down."); + error!(target: LOG_TARGET, "Error when handling new public epoch: {e:?}, edge service shutting down."); return Err(e); } - Ok(()) => { - // We need to keep track of this for now because message handlers are initialized with a membership info. Exposing a simple `rotate_epoch` will allow us to avoid tracking this value here. - current_membership_info = new_session_info; + Ok(()) => {} + } + } + Some(new_secret_pol_info) = secret_pol_info_stream.next() => { + current_secret_epoch_info = Some(new_secret_pol_info); + match handle_new_epoch_event(¤t_epoch_info, current_secret_epoch_info.as_ref(), &mut current_epoch_message_handler, settings.clone(), overwatch_handle.clone()) { + Err(Error::NetworkIsTooSmall(_)) => { + info!(target: LOG_TARGET, "New membership does not satisfy edge node condition, edge service shutting down."); + return Ok(()); } + Err(e) => { + error!(target: LOG_TARGET, "Error when handling new secret epoch: {e:?}, edge service shutting down."); + return Err(e); + } + Ok(()) => {} } } Some(message) = incoming_message_stream.next() => { // TODO: Investigate why secret PoL info at times arrives after the block proposal. - let Some(handler) = current_pol_info_and_message_handler.as_mut().map(|(_, handler)| handler) else { + let Some(handler) = current_epoch_message_handler.as_mut() else { tracing::warn!(target: LOG_TARGET, "Received a message to blend, but no active message handler is available to process it because the secret PoL info for the current epoch is not yet available. Ignoring the message."); continue; }; @@ -425,200 +371,90 @@ where handler.handle_message_to_blend(message.clone()).await; } } - Some(clock_tick) = clock_stream.next() => { - handle_clock_event(clock_tick, &mut epoch_handler, &mut current_pol_info_and_message_handler).await; - } - Some(new_secret_pol_info) = secret_pol_info_stream.next() => { - handle_new_secret_epoch_info(&new_secret_pol_info, settings.clone(), overwatch_handle, ¤t_membership_info, &mut current_pol_info_and_message_handler); - } } } } -/// Handle a new session. -/// -/// If the message handler was active, it is recreated with the new session's -/// membership and core info, preserving the current epoch's leader inputs and -/// private inputs. If it was `None` (no secret epoch info yet, or shut down -/// after an epoch transition), it stays `None` — only the membership info -/// tracked by the caller is updated for when the handler is later recreated -/// by [`handle_new_secret_epoch_info`]. -/// -/// Returns [`Error`] if the new membership does not satisfy the edge node -/// condition. -#[expect( - clippy::type_complexity, - reason = "There are too many generics. Any type alias would be as complicated." -)] -fn handle_new_session( - new_membership_info: &MembershipInfo, - settings: RunningSettings, - current_epoch_info_and_message_handler: &mut Option<( - PolEpochInfo, +fn handle_new_epoch_event( + current_public_epoch_info: &BlendEpochState, + current_secret_epoch_info: Option<&PolEpochInfo>, + current_epoch_message_handler: &mut Option< MessageHandler, - )>, + >, + settings: RunningSettings, overwatch_handle: OverwatchHandle, ) -> Result<(), Error> where Backend: BlendBackend, - NodeId: Clone + Eq + Hash + Send + 'static, + NodeId: Clone + Send + Eq + Hash + 'static, ProofsGenerator: LeaderProofsGenerator, - RuntimeServiceId: Clone, { - let Some(zk_info) = &new_membership_info.zk else { + // Whatever happens on a new epoch, we shut down the previous handler. + // It will be rebuilt below if the current public and secret info line up + // on the same epoch. + drop(current_epoch_message_handler.take()); + + let Some(zk_info) = ¤t_public_epoch_info.membership_info.zk else { return Err(Error::NetworkIsTooSmall(0)); }; // Validate the edge node condition up front so the service shuts down on - // an invalid membership regardless of whether secret PoL info has arrived - // yet. Without this check, an invalid membership would silently update - // `current_membership_info` and surface later as a panic in - // `handle_new_secret_epoch_info`. - let membership_size = new_membership_info.membership.size(); + // an invalid membership regardless of whether secret PoL info is available + // yet for the current epoch. + let membership_size = current_public_epoch_info.membership_info.membership.size(); if membership_size < settings.minimum_network_size.get() as usize { return Err(Error::NetworkIsTooSmall(membership_size)); } - if new_membership_info.membership.contains_local() { + if current_public_epoch_info + .membership_info + .membership + .contains_local() + { return Err(Error::LocalIsCoreNode); } - debug!(target: LOG_TARGET, "New session received, trying to create a new message handler"); - - // Update session and core public inputs, preserving the current epoch's - // leader inputs. - let Some((current_epoch_private_info, _)) = current_epoch_info_and_message_handler.take() - else { - debug!(target: LOG_TARGET, "No current epoch private info available. Ignoring new session event to create a new message handler."); + let Some(current_secret_epoch_info) = current_secret_epoch_info else { + assert!( + current_epoch_message_handler.is_none(), + "If there is no secret PoL info, there should not be an active message handler." + ); + debug!(target: LOG_TARGET, "No secret PoL info available for the new epoch, cannot create message handler until it arrives."); return Ok(()); }; + if current_public_epoch_info.epoch != current_secret_epoch_info.epoch { + debug!(target: LOG_TARGET, "Public and secret epoch info on different epochs (public: {:?}, secret: {:?}). Cannot create new handler.", current_public_epoch_info.epoch, current_secret_epoch_info.epoch); + return Ok(()); + } + let new_public_inputs = PoQVerificationInputsMinusSigningKey { - session: new_membership_info.session_number, core: CoreInputs { - quota: settings.cover.session_core_quota( + quota: settings.cover.epoch_core_quota( settings.num_blend_layers, &settings.time, - new_membership_info.membership.size(), + current_public_epoch_info.membership_info.membership.size(), ), zk_root: zk_info.root, }, leader: LeaderInputs { - lottery_0: current_epoch_private_info.poq_public_inputs.lottery_0, - lottery_1: current_epoch_private_info.poq_public_inputs.lottery_1, - pol_epoch_nonce: current_epoch_private_info.poq_public_inputs.epoch_nonce, - pol_ledger_aged: current_epoch_private_info.poq_public_inputs.aged_root, - message_quota: settings.session_leadership_quota(), + lottery_0: current_public_epoch_info.lottery_0, + lottery_1: current_public_epoch_info.lottery_1, + pol_epoch_nonce: current_public_epoch_info.nonce, + pol_ledger_aged: current_public_epoch_info.aged, + message_quota: settings.epoch_leadership_quota(), }, }; + debug!(target: LOG_TARGET, "Creating new handler for epoch {:?}", current_public_epoch_info.epoch); let new_handler = MessageHandler::try_new_with_edge_condition_check( settings, - new_membership_info.membership.clone(), + current_public_epoch_info.membership_info.membership.clone(), new_public_inputs, - current_epoch_private_info.poq_private_inputs.clone(), + current_secret_epoch_info.poq_private_inputs.clone(), overwatch_handle, - current_epoch_private_info.epoch, + current_public_epoch_info.epoch, )?; - *current_epoch_info_and_message_handler = Some((current_epoch_private_info, new_handler)); - + *current_epoch_message_handler = Some(new_handler); Ok(()) } - -/// Handles a clock tick by forwarding it to the epoch handler. -/// -/// If the tick reveals a new epoch that is ahead of the last received secret -/// `PoL` info (`current_epoch`), the message handler is shut down until -/// [`handle_new_secret_epoch_info`] provides the secret info for the new epoch. -/// If secret info was already received for the new epoch, or if the handler was -/// already `None`, it is left unchanged. -async fn handle_clock_event( - slot_tick: SlotTick, - epoch_handler: &mut EpochHandler, - current_epoch_info_and_message_handler: &mut Option< - EpochInfoAndHandler, - >, -) where - ChainService: ChainApi + Send + Sync, - RuntimeServiceId: Clone + Send + Sync, -{ - let Some(epoch_event) = epoch_handler.tick(slot_tick).await else { - return; - }; - - let Some(current_epoch) = current_epoch_info_and_message_handler - .as_ref() - .map(|(epoch_info, _)| epoch_info.epoch) - else { - return; - }; - - // Shut down the message handler if a new epoch is detected for which we - // have not yet received secret `PoL` info. - match epoch_event { - EpochEvent::NewEpoch((_, new_epoch)) - | EpochEvent::NewEpochAndOldEpochTransitionExpired((_, new_epoch)) - if new_epoch > current_epoch => - { - debug!(target: LOG_TARGET, "New epoch detected: {epoch_event:?}, shutting down message handler until new secret PoL info is available."); - *current_epoch_info_and_message_handler = None; - } - // If it's not a new epoch event, or if the new epoch has already been processed when the - // secret info was received, keep the current message handler. - _ => {} - } -} - -/// Processes new secret `PoL` info. -/// -/// Always creates a new message handler using the new epoch's public and -/// private inputs from the `PoL` info, while preserving the current session. -fn handle_new_secret_epoch_info( - new_pol_epoch_info: &PolEpochInfo, - settings: RunningSettings, - overwatch_handle: &OverwatchHandle, - current_membership_info: &MembershipInfo, - current_epoch_info_and_message_handler: &mut Option< - EpochInfoAndHandler, - >, -) where - Backend: BlendBackend, - NodeId: Clone + Eq + Hash + Send + 'static, - ProofsGenerator: LeaderProofsGenerator, - RuntimeServiceId: Clone, -{ - let Some(zk_root) = current_membership_info.zk.as_ref().map(|zk| zk.root) else { - *current_epoch_info_and_message_handler = None; - return; - }; - - let current_membership = current_membership_info.membership.clone(); - let new_public_inputs = PoQVerificationInputsMinusSigningKey { - leader: LeaderInputs { - lottery_0: new_pol_epoch_info.poq_public_inputs.lottery_0, - lottery_1: new_pol_epoch_info.poq_public_inputs.lottery_1, - pol_epoch_nonce: new_pol_epoch_info.poq_public_inputs.epoch_nonce, - pol_ledger_aged: new_pol_epoch_info.poq_public_inputs.aged_root, - message_quota: settings.session_leadership_quota(), - }, - core: CoreInputs { - quota: settings.cover.session_core_quota( - settings.num_blend_layers, - &settings.time, - current_membership.size(), - ), - zk_root, - }, - session: current_membership_info.session_number, - }; - let new_handler = MessageHandler::try_new_with_edge_condition_check( - settings, - current_membership, - new_public_inputs, - new_pol_epoch_info.poq_private_inputs.clone(), - overwatch_handle.clone(), - new_pol_epoch_info.epoch, - ).expect("Should not fail to re-create message handler on epoch rotation after private inputs are set."); - - *current_epoch_info_and_message_handler = Some((new_pol_epoch_info.clone(), new_handler)); -} diff --git a/services/blend/src/edge/service_components.rs b/services/blend/src/edge/service_components.rs index 08c0e27bf..1cc43077e 100644 --- a/services/blend/src/edge/service_components.rs +++ b/services/blend/src/edge/service_components.rs @@ -6,17 +6,18 @@ pub trait ServiceComponents { /// Settings for broadcasting messages that have passed through the blend /// network. type BroadcastSettings; - /// Adapter for membership service. - type MembershipAdapter; type ProofsGenerator; type BackendSettings; + /// Chain service, used by the proxy to derive membership from the chain. + type ChainService; + /// Time backend, used by the proxy to subscribe to slot ticks. + type TimeBackend; } impl< Backend, NodeId, BroadcastSettings, - MembershipAdapter, ProofsGenerator, TimeBackend, ChainService, @@ -27,7 +28,6 @@ impl< Backend, NodeId, BroadcastSettings, - MembershipAdapter, ProofsGenerator, TimeBackend, ChainService, @@ -40,6 +40,7 @@ where { type BackendSettings = Backend::Settings; type BroadcastSettings = BroadcastSettings; - type MembershipAdapter = MembershipAdapter; type ProofsGenerator = ProofsGenerator; + type ChainService = ChainService; + type TimeBackend = TimeBackend; } diff --git a/services/blend/src/edge/settings.rs b/services/blend/src/edge/settings.rs index 4e57190f7..30ffbc620 100644 --- a/services/blend/src/edge/settings.rs +++ b/services/blend/src/edge/settings.rs @@ -30,7 +30,7 @@ pub struct RunningBlendConfig { } impl RunningBlendConfig { - pub const fn session_leadership_quota(&self) -> u64 { + pub const fn epoch_leadership_quota(&self) -> u64 { let num_blend_layers = self.num_blend_layers.get(); let additional_encapsulations = num_blend_layers .checked_mul(self.data_replication_factor) diff --git a/services/blend/src/edge/tests/mod.rs b/services/blend/src/edge/tests/mod.rs index b70d48b8d..5d93637d5 100644 --- a/services/blend/src/edge/tests/mod.rs +++ b/services/blend/src/edge/tests/mod.rs @@ -1,16 +1,12 @@ use core::time::Duration; use lb_blend::{ - message::crypto::proofs::PoQVerificationInputsMinusSigningKey, - proofs::quota::inputs::prove::{ - private::ProofOfLeadershipQuotaInputs, - public::{CoreInputs, LeaderInputs}, - }, + proofs::quota::inputs::prove::private::ProofOfLeadershipQuotaInputs, + scheduling::membership::Membership, }; use lb_chain_service::Epoch; -use lb_core::{crypto::ZkHash, proofs::leader_proof::LeaderPublic}; +use lb_core::crypto::ZkHash; use lb_groth16::{Field as _, Fr}; -use lb_time_service::SlotTick; use tokio::time::sleep; use crate::{ @@ -20,9 +16,9 @@ use crate::{ MockLeaderProofsGenerator, NodeId, TestBackend, overwatch_handle, settings, spawn_run, }, }, - epoch_info::{EpochHandler, PolEpochInfo}, - membership::MembershipInfo, - test_utils::{epoch::TestChainService, membership::membership}, + epoch_info::PolEpochInfo, + membership::chain::BlendEpochState, + test_utils::membership::membership, }; pub mod utils; @@ -30,11 +26,11 @@ pub mod utils; /// [`run`] forwards messages to the core nodes in the updated membership. #[test_log::test(tokio::test)] #[ignore = "We need a different test setup since we are not blocking the edge tokio task until the secret PoL info is fetched, which makes this test flaky."] -async fn run_with_session_transition() { +async fn run_with_epoch_transition() { let local_node = NodeId(99); let mut core_node = NodeId(0); let minimal_network_size = 1; - let (_, session_sender, msg_sender, mut node_id_receiver) = spawn_run( + let (_, epoch_sender, msg_sender, mut node_id_receiver) = spawn_run( local_node, minimal_network_size, Some(membership(&[core_node], local_node)), @@ -48,9 +44,9 @@ async fn run_with_session_transition() { core_node ); - // Send a new session with another core node 1. + // Send a new epoch with another core node 1. core_node = NodeId(1); - session_sender + epoch_sender .send(membership(&[core_node], local_node)) .await .expect("channel opened"); @@ -71,15 +67,15 @@ async fn run_shuts_down_if_new_membership_is_small() { let local_node = NodeId(99); let core_node = NodeId(0); let minimal_network_size = 1; - let (join_handle, session_sender, _, _) = spawn_run( + let (join_handle, epoch_sender, _, _) = spawn_run( local_node, minimal_network_size, Some(membership(&[core_node], local_node)), ) .await; - // Send a new session with an empty membership (smaller than the min size). - session_sender + // Send a new epoch with an empty membership (smaller than the min size). + epoch_sender .send(membership(&[], local_node)) .await .expect("channel opened"); @@ -92,15 +88,15 @@ async fn run_fails_if_local_is_core_in_new_membership() { let local_node = NodeId(99); let core_node = NodeId(0); let minimal_network_size = 1; - let (join_handle, session_sender, _, _) = spawn_run( + let (join_handle, epoch_sender, _, _) = spawn_run( local_node, minimal_network_size, Some(membership(&[core_node], local_node)), ) .await; - // Send a new session with a membership where the local node is core. - session_sender + // Send a new epoch with a membership where the local node is core. + epoch_sender .send(membership(&[local_node], local_node)) .await .expect("channel opened"); @@ -113,14 +109,6 @@ async fn run_fails_if_local_is_core_in_new_membership() { fn test_pol_epoch_info(epoch: Epoch) -> PolEpochInfo { PolEpochInfo { epoch, - poq_public_inputs: LeaderPublic { - slot: 1, - latest_root: Fr::ZERO, - lottery_0: Fr::ZERO, - lottery_1: Fr::ZERO, - epoch_nonce: ZkHash::ZERO, - aged_root: ZkHash::ZERO, - }, poq_private_inputs: ProofOfLeadershipQuotaInputs { slot: 1, note_value: 1, @@ -132,206 +120,197 @@ fn test_pol_epoch_info(epoch: Epoch) -> PolEpochInfo { } } -fn poq_verification_inputs() -> PoQVerificationInputsMinusSigningKey { - PoQVerificationInputsMinusSigningKey { - leader: LeaderInputs { - pol_ledger_aged: ZkHash::ZERO, - pol_epoch_nonce: ZkHash::ZERO, - message_quota: 10, - lottery_0: Fr::ZERO, - lottery_1: Fr::ZERO, - }, - core: CoreInputs { - quota: 1, - zk_root: Fr::ZERO, - }, - session: 1, - } -} - -/// `handle_clock_event` shuts down the message handler when a new epoch is -/// detected ahead of the current handler's epoch. -#[test_log::test(tokio::test)] -async fn handle_clock_event_new_epoch_shuts_down_handler() { - let local_node = NodeId(99); - let core_node = NodeId(0); - let (node_id_sender, _node_id_receiver) = tokio::sync::mpsc::channel(1); - - let edge_membership = membership(&[core_node], local_node); - let pol_info = test_pol_epoch_info(Epoch::new(1)); - - let handler = super::handlers::MessageHandler::< - TestBackend, - NodeId, - MockLeaderProofsGenerator, - usize, - >::try_new_with_edge_condition_check( - settings(local_node, 1, node_id_sender), - edge_membership, - poq_verification_inputs(), - pol_info.poq_private_inputs.clone(), - overwatch_handle(), - Epoch::new(1), - ) - .unwrap(); - - let mut handler_state = Some((pol_info, handler)); - - // Create an EpochHandler and initialize it with an epoch-1 tick. - let mut epoch_handler = EpochHandler::new(TestChainService, 1.try_into().unwrap()); - super::handle_clock_event::( - SlotTick { - epoch: Epoch::new(1), - slot: 1.into(), - }, - &mut epoch_handler, - &mut handler_state, - ) - .await; - // After the first tick (matching handler's epoch), handler should still be up. - assert!( - handler_state.is_some(), - "Handler should remain active for a tick within the same epoch" - ); - - // Send a tick for epoch 2, which is ahead of the handler's epoch (1). - super::handle_clock_event::( - SlotTick { - epoch: Epoch::new(2), - slot: 2.into(), - }, - &mut epoch_handler, - &mut handler_state, - ) - .await; - // The handler should be shut down because epoch 2 > handler's epoch 1. - assert!( - handler_state.is_none(), - "Handler should be shut down when clock tick reveals a newer epoch" - ); -} - -/// `handle_new_secret_epoch_info` creates a new message handler with the -/// provided epoch's public and private inputs. +/// `handle_new_epoch_event` creates a new message handler with the provided +/// epoch's public and private inputs, and replaces it on the next epoch. #[test_log::test(tokio::test)] async fn handle_new_secret_epoch_info_recreates_handler() { let local_node = NodeId(99); let core_node = NodeId(0); let (node_id_sender, _node_id_receiver) = tokio::sync::mpsc::channel(1); - let edge_membership = membership(&[core_node], local_node); - let membership_info = MembershipInfo::from_membership_and_session_number(edge_membership, 1); - let settings = settings(local_node, 1, node_id_sender); let overwatch = overwatch_handle(); - // Start with no handler (e.g. after an epoch transition shut it down). - let mut handler_state: Option<( - PolEpochInfo, + let mut handler_state: Option< super::handlers::MessageHandler, - )> = None; + > = None; - // Provide secret PoL info for epoch 2. - let new_pol_info = test_pol_epoch_info(Epoch::new(2)); - super::handle_new_secret_epoch_info( - &new_pol_info, - settings.clone(), - &overwatch, - &membership_info, + // Public + secret for epoch 2 -> handler is created. + let public_2 = test_blend_epoch_state(Epoch::new(2), membership(&[core_node], local_node)); + let secret_2 = test_pol_epoch_info(Epoch::new(2)); + super::handle_new_epoch_event( + &public_2, + Some(&secret_2), &mut handler_state, - ); + settings.clone(), + overwatch.clone(), + ) + .unwrap(); assert!( handler_state.is_some(), - "Handler should be created after secret PoL info is provided" + "Handler should be created when public and secret info for the same epoch are present" ); - assert_eq!(handler_state.as_ref().unwrap().0.epoch, Epoch::new(2)); + assert_eq!(handler_state.as_ref().unwrap().epoch(), Epoch::new(2)); - // Provide secret PoL info for epoch 3 - handler should be replaced. - let newer_pol_info = test_pol_epoch_info(Epoch::new(3)); - super::handle_new_secret_epoch_info( - &newer_pol_info, - settings, - &overwatch, - &membership_info, + // Public + secret for epoch 3 -> handler is replaced. + let public_3 = test_blend_epoch_state(Epoch::new(3), membership(&[core_node], local_node)); + let secret_3 = test_pol_epoch_info(Epoch::new(3)); + super::handle_new_epoch_event( + &public_3, + Some(&secret_3), &mut handler_state, - ); + settings, + overwatch, + ) + .unwrap(); assert!(handler_state.is_some()); - assert_eq!(handler_state.as_ref().unwrap().0.epoch, Epoch::new(3)); + assert_eq!(handler_state.as_ref().unwrap().epoch(), Epoch::new(3)); } -/// Full epoch lifecycle: handler active → clock advances epoch → handler shut -/// down → new secret `PoL` info → handler recreated. +fn test_blend_epoch_state(epoch: Epoch, membership: Membership) -> BlendEpochState { + BlendEpochState { + epoch, + nonce: Fr::ZERO, + aged: Fr::ZERO, + lottery_0: Fr::ZERO, + lottery_1: Fr::ZERO, + membership_info: membership.into(), + } +} + +/// Two consecutive public epoch infos with no private in between (e.g. the +/// node had no winning slot in the first epoch). The handler must stay down +/// as long as no secret `PoL` info is available. #[test_log::test(tokio::test)] -async fn epoch_transition_full_lifecycle() { +async fn two_publics_without_private_in_between() { let local_node = NodeId(99); let core_node = NodeId(0); let (node_id_sender, _node_id_receiver) = tokio::sync::mpsc::channel(1); - let edge_membership = membership(&[core_node], local_node); - let membership_info = - MembershipInfo::from_membership_and_session_number(edge_membership.clone(), 1); let settings = settings(local_node, 1, node_id_sender); let overwatch = overwatch_handle(); - // Start with handler active at epoch 1. - let pol_info = test_pol_epoch_info(Epoch::new(1)); - let handler = super::handlers::MessageHandler::< - TestBackend, - NodeId, - MockLeaderProofsGenerator, - usize, - >::try_new_with_edge_condition_check( + let mut handler_state: Option< + super::handlers::MessageHandler, + > = None; + + // First public, no secret yet -> handler must stay down. + super::handle_new_epoch_event( + &test_blend_epoch_state(Epoch::new(1), membership(&[core_node], local_node)), + None, + &mut handler_state, settings.clone(), - edge_membership, - poq_verification_inputs(), - pol_info.poq_private_inputs.clone(), overwatch.clone(), - Epoch::new(1), ) .unwrap(); - let mut handler_state = Some((pol_info, handler)); - - let mut epoch_handler = EpochHandler::new(TestChainService, 1.try_into().unwrap()); - - // Initialize epoch handler with epoch-1 tick. - super::handle_clock_event::( - SlotTick { - epoch: Epoch::new(1), - slot: 1.into(), - }, - &mut epoch_handler, - &mut handler_state, - ) - .await; - assert!(handler_state.is_some()); - - // Clock advances to epoch 2 - handler shut down. - super::handle_clock_event::( - SlotTick { - epoch: Epoch::new(2), - slot: 2.into(), - }, - &mut epoch_handler, - &mut handler_state, - ) - .await; assert!( handler_state.is_none(), - "Handler should be shut down on epoch advance" + "Handler must not be created without the private info" ); - // Secret PoL info arrives for epoch 2 - handler recreated. - let new_pol_info = test_pol_epoch_info(Epoch::new(2)); - super::handle_new_secret_epoch_info( - &new_pol_info, - settings, - &overwatch, - &membership_info, + // Second public for a later epoch, still no secret -> handler stays down. + super::handle_new_epoch_event( + &test_blend_epoch_state(Epoch::new(2), membership(&[core_node], local_node)), + None, &mut handler_state, - ); + settings, + overwatch, + ) + .unwrap(); assert!( - handler_state.is_some(), - "Handler should be recreated after secret PoL info" + handler_state.is_none(), + "Handler must remain down: no private info has been received" + ); +} + +/// Public arrives first, then private for the same epoch: handler is created +/// on the second call once both sides line up on the same epoch. +#[test_log::test(tokio::test)] +async fn public_then_private_same_epoch_creates_handler() { + let local_node = NodeId(99); + let core_node = NodeId(0); + let (node_id_sender, _node_id_receiver) = tokio::sync::mpsc::channel(1); + + let settings = settings(local_node, 1, node_id_sender); + let overwatch = overwatch_handle(); + + let mut handler_state: Option< + super::handlers::MessageHandler, + > = None; + + let public_1 = test_blend_epoch_state(Epoch::new(1), membership(&[core_node], local_node)); + + // Public for epoch 1, no secret yet -> no handler. + super::handle_new_epoch_event( + &public_1, + None, + &mut handler_state, + settings.clone(), + overwatch.clone(), + ) + .unwrap(); + assert!(handler_state.is_none()); + + // Secret for epoch 1 arrives, same public -> handler is created. + super::handle_new_epoch_event( + &public_1, + Some(&test_pol_epoch_info(Epoch::new(1))), + &mut handler_state, + settings, + overwatch, + ) + .unwrap(); + assert_eq!( + handler_state + .as_ref() + .expect("Handler must be created") + .epoch(), + Epoch::new(1) + ); +} + +/// Secret arrives for an epoch ahead of the current public (mismatch), then +/// public catches up to the same epoch: handler is created on the match. +#[test_log::test(tokio::test)] +async fn private_then_public_same_epoch_creates_handler() { + let local_node = NodeId(99); + let core_node = NodeId(0); + let (node_id_sender, _node_id_receiver) = tokio::sync::mpsc::channel(1); + + let settings = settings(local_node, 1, node_id_sender); + let overwatch = overwatch_handle(); + + let mut handler_state: Option< + super::handlers::MessageHandler, + > = None; + + // Secret for epoch 1 while public is still on epoch 0 -> epochs mismatch, + // no handler. + let secret_1 = test_pol_epoch_info(Epoch::new(1)); + super::handle_new_epoch_event( + &test_blend_epoch_state(Epoch::new(0), membership(&[core_node], local_node)), + Some(&secret_1), + &mut handler_state, + settings.clone(), + overwatch.clone(), + ) + .unwrap(); + assert!(handler_state.is_none()); + + // Public catches up to epoch 1 -> handler is created. + super::handle_new_epoch_event( + &test_blend_epoch_state(Epoch::new(1), membership(&[core_node], local_node)), + Some(&secret_1), + &mut handler_state, + settings, + overwatch, + ) + .unwrap(); + assert_eq!( + handler_state + .as_ref() + .expect("Handler must be created") + .epoch(), + Epoch::new(1) ); - assert_eq!(handler_state.as_ref().unwrap().0.epoch, Epoch::new(2)); } diff --git a/services/blend/src/edge/tests/utils.rs b/services/blend/src/edge/tests/utils.rs index f40cec716..a2247e9ff 100644 --- a/services/blend/src/edge/tests/utils.rs +++ b/services/blend/src/edge/tests/utils.rs @@ -2,21 +2,19 @@ use core::{num::NonZeroU64, time::Duration}; use std::fmt::{Debug, Display}; use async_trait::async_trait; -use futures::{StreamExt as _, future::ready, stream::once}; +use futures::StreamExt as _; use lb_blend::{ message::encap::validated::EncapsulatedMessageWithVerifiedPublicHeader, - proofs::quota::inputs::prove::{private::ProofOfLeadershipQuotaInputs, public::LeaderInputs}, + proofs::quota::inputs::prove::private::ProofOfLeadershipQuotaInputs, scheduling::{ + epoch::UninitializedEpochEventStream, membership::Membership, message_blend::provers::{ BlendLayerProof, ProofsGeneratorSettings, leader::LeaderProofsGenerator, }, - session::UninitializedSessionEventStream, }, }; -use lb_chain_service::Epoch; use lb_key_management_system_service::keys::UnsecuredEd25519Key; -use lb_time_service::SlotTick; use overwatch::overwatch::{OverwatchHandle, commands::OverwatchCommand}; use rand::{RngCore, rngs::OsRng}; use tokio::{sync::mpsc, task::JoinHandle}; @@ -26,15 +24,10 @@ use crate::{ core::settings::CoverTrafficSettings, edge::{ backends::BlendBackend, handlers::Error, run, settings::RunningBlendConfig as BlendConfig, + tests::test_blend_epoch_state, }, - epoch_info::EpochHandler, - membership::MembershipInfo, - settings::{FIRST_STREAM_ITEM_READY_TIMEOUT, TimingSettings}, - test_utils::{ - crypto::mock_blend_proof, - epoch::{OncePolStreamProvider, TestChainService}, - membership::key, - }, + settings::TimingSettings, + test_utils::{crypto::mock_blend_proof, epoch::OncePolStreamProvider, membership::key}, }; pub struct MockLeaderProofsGenerator; @@ -48,14 +41,6 @@ impl LeaderProofsGenerator for MockLeaderProofsGenerator { Self } - fn rotate_epoch( - &mut self, - _new_epoch_public: LeaderInputs, - _new_private_inputs: ProofOfLeadershipQuotaInputs, - _new_epoch: Epoch, - ) { - } - async fn get_next_proof(&mut self) -> BlendLayerProof { mock_blend_proof() } @@ -71,19 +56,19 @@ pub async fn spawn_run( mpsc::Sender>, mpsc::Receiver, ) { - let (session_sender, session_receiver) = mpsc::channel(1); + let (epoch_sender, epoch_receiver) = mpsc::channel(1); let (msg_sender, msg_receiver) = mpsc::channel(1); let (node_id_sender, node_id_receiver) = mpsc::channel(1); if let Some(initial_membership) = initial_membership { - session_sender + epoch_sender .send(initial_membership) .await .expect("channel opened"); } - let session_stream = ReceiverStream::new(session_receiver) - .map(|membership| MembershipInfo::from_membership_and_session_number(membership, 1)); + let epoch_stream = ReceiverStream::new(epoch_receiver) + .map(|membership| test_blend_epoch_state(0.into(), membership)); let settings = settings(local_node, minimal_network_size, node_id_sender); let join_handle = tokio::spawn(async move { @@ -91,21 +76,11 @@ pub async fn spawn_run( TestBackend, _, MockLeaderProofsGenerator, - _, OncePolStreamProvider, _, >( - UninitializedSessionEventStream::new( - session_stream, - FIRST_STREAM_ITEM_READY_TIMEOUT, - Duration::ZERO, - ), - once(ready(SlotTick { - epoch: 1.into(), - slot: 1.into(), - })), + UninitializedEpochEventStream::new(epoch_stream, Duration::ZERO), ReceiverStream::new(msg_receiver), - EpochHandler::new(TestChainService, 1.try_into().unwrap()), settings, &overwatch_handle(), || {}, @@ -113,7 +88,7 @@ pub async fn spawn_run( .await }); - (join_handle, session_sender, msg_sender, node_id_receiver) + (join_handle, epoch_sender, msg_sender, node_id_receiver) } pub fn settings( @@ -123,12 +98,10 @@ pub fn settings( ) -> BlendConfig { BlendConfig { time: TimingSettings { - rounds_per_session: NonZeroU64::new(1).unwrap(), - rounds_per_interval: NonZeroU64::new(1).unwrap(), + rounds_per_epoch: NonZeroU64::new(1).unwrap(), round_duration: Duration::from_secs(1), rounds_per_observation_window: NonZeroU64::new(1).unwrap(), - rounds_per_session_transition_period: NonZeroU64::new(1).unwrap(), - epoch_transition_period_in_slots: NonZeroU64::new(1).unwrap(), + epoch_transition_period: Duration::from_secs(1), }, non_ephemeral_signing_key: key(local_id).0, num_blend_layers: NonZeroU64::new(1).unwrap(), diff --git a/services/blend/src/epoch.rs b/services/blend/src/epoch.rs new file mode 100644 index 000000000..3d1f6d65c --- /dev/null +++ b/services/blend/src/epoch.rs @@ -0,0 +1,50 @@ +use lb_blend::{ + proofs::quota::inputs::prove::public::{CoreInputs, LeaderInputs}, + scheduling::membership::Membership, +}; +use lb_chain_service::Epoch; +use lb_core::crypto::ZkHash; + +#[derive(Clone, Debug)] +// TODO: Refactor this so that it's a struct with the common fields, and +// everything case-specific is an enum. +pub enum MaybeEmptyCoreEpochInfo { + Empty { epoch: Epoch, epoch_nonce: ZkHash }, + NonEmpty(Box>), +} + +impl From<(Epoch, ZkHash)> + for MaybeEmptyCoreEpochInfo +{ + fn from((epoch, epoch_nonce): (Epoch, ZkHash)) -> Self { + Self::Empty { epoch, epoch_nonce } + } +} + +impl From> + for MaybeEmptyCoreEpochInfo +{ + fn from(core_epoch_info: CoreEpochInfo) -> Self { + Self::NonEmpty(Box::new(core_epoch_info)) + } +} + +#[derive(Clone, Debug)] +/// All info that Blend services need to be available on new epochs. +pub struct CoreEpochInfo { + /// The epoch info available to all nodes. + pub public: CoreEpochPublicInfo, + /// The core `PoQ` generator component. `None` when Blend is running + /// (network large enough), but local node is not part of the core + /// membership. + pub core_poq_generator: Option, +} + +#[derive(Clone, Debug)] +/// All public info that Blend services need to be available on new epochs. +pub struct CoreEpochPublicInfo { + pub epoch: Epoch, + pub poq_leadership_public_inputs: LeaderInputs, + pub poq_core_public_inputs: CoreInputs, + pub membership: Membership, +} diff --git a/services/blend/src/epoch_info.rs b/services/blend/src/epoch_info.rs index 5d29bcbc1..74985e8fa 100644 --- a/services/blend/src/epoch_info.rs +++ b/services/blend/src/epoch_info.rs @@ -1,20 +1,9 @@ -use core::{ - fmt::{self, Debug, Formatter}, - marker::PhantomData, - num::NonZeroU64, - ops::Deref, -}; +use core::fmt::{self, Debug, Formatter}; use async_trait::async_trait; use futures::Stream; use lb_blend::proofs::quota::inputs::prove::private::ProofOfLeadershipQuotaInputs; -use lb_chain_service::api::{CryptarchiaServiceApi, CryptarchiaServiceData}; -use lb_core::{crypto::ZkHash, proofs::leader_proof::LeaderPublic}; -use lb_cryptarchia_engine::{Epoch, Slot}; -use lb_groth16::Fr; -use lb_ledger::EpochState; -use lb_log_targets::blend; -use lb_time_service::SlotTick; +use lb_cryptarchia_engine::Epoch; use overwatch::overwatch::OverwatchHandle; /// Secret `PoL` info associated to an epoch, as returned by the `PoL` info @@ -22,7 +11,6 @@ use overwatch::overwatch::OverwatchHandle; #[derive(Clone)] pub struct PolEpochInfo { pub epoch: Epoch, - pub poq_public_inputs: LeaderPublic, /// The `PoL` secret inputs that are found to be winning at least one slot /// in the current epoch. pub poq_private_inputs: ProofOfLeadershipQuotaInputs, @@ -32,7 +20,6 @@ impl Debug for PolEpochInfo { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.debug_struct("PolEpochInfo") .field("epoch", &self.epoch) - .field("poq_public_inputs", &self.poq_public_inputs) .field("poq_private_inputs", &"") .finish() } @@ -46,821 +33,3 @@ pub trait PolInfoProvider { overwatch_handle: &OverwatchHandle, ) -> Option; } - -const LOG_TARGET: &str = blend::service::EPOCH; - -/// A trait that provides the needed functionalities for the epoch stream to -/// fetch the epoch state for a given slot. -#[async_trait] -pub trait ChainApi { - async fn get_epoch_state_for_slot(&self, slot: Slot) -> Option; -} - -#[async_trait] -impl ChainApi - for CryptarchiaServiceApi -where - Cryptarchia: CryptarchiaServiceData, - RuntimeServiceId: Send + Sync, -{ - async fn get_epoch_state_for_slot(&self, slot: Slot) -> Option { - match self.get_epoch_state(slot).await { - Ok(Ok(val)) => Some(val), - Ok(Err(e)) => { - tracing::error!(target: LOG_TARGET, "Get epoch state error: {e:?}."); - None - } - Err(e) => { - tracing::error!(target: LOG_TARGET, "Get epoch state API error: {e:?}."); - None - } - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(test, derive(Default))] -pub struct LeaderInputsMinusQuota { - pub pol_ledger_aged: ZkHash, - pub pol_epoch_nonce: ZkHash, - pub lottery_0: Fr, - pub lottery_1: Fr, -} - -impl From for LeaderInputsMinusQuota { - fn from( - EpochState { - nonce, - utxos, - lottery_0, - lottery_1, - .. - }: EpochState, - ) -> Self { - Self { - pol_epoch_nonce: nonce, - pol_ledger_aged: utxos.root(), - lottery_0, - lottery_1, - } - } -} - -/// Event related to a given epoch. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum EpochEvent { - /// A new epoch is available, which is either ongoing (if the handler is - /// started mid-epoch) or has just started. - NewEpoch((LeaderInputsMinusQuota, Epoch)), - /// The information about the previous epoch the handler was tracking can - /// now be discarded since its transition period has elapsed. - OldEpochTransitionPeriodExpired, - /// A new epoch comes just as the previous epoch transition is over. This - /// can happen in one of two cases: - /// * The epoch transition period is set to 1s, which means that an epoch - /// can be discarded as soon as it is over, or - /// * The epoch transition period has the same duration as an epoch, which - /// means that when a new epoch starts, the epoch before the one that just - /// elapsed can be discarded, with the rotate one that will be remembered - /// until a new epoch starts. - /// - /// Consumers of this event will need to consider that any calls to - /// `terminate_epoch_transition` should precede any calls to - /// `rotate_epoch`, where `terminate_epoch_transition` would invalidate - /// the epoch that can now be discarded, while `rotate_epoch` - /// would move from the previous epoch to the new one that is notified about - /// in this event. - NewEpochAndOldEpochTransitionExpired((LeaderInputsMinusQuota, Epoch)), -} - -/// A slot tick whose values against the previous tick have been validated. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct ValidatedSlotTick(SlotTick); - -impl ValidatedSlotTick { - fn transition_to(self, slot_tick: SlotTick) -> Result { - if self.epoch > slot_tick.epoch || self.slot >= slot_tick.slot { - return Err(()); - } - Ok(slot_tick.into()) - } -} - -impl From for ValidatedSlotTick { - fn from(value: SlotTick) -> Self { - Self(value) - } -} - -impl Deref for ValidatedSlotTick { - type Target = SlotTick; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -#[derive(Debug, PartialEq, Eq)] -/// Internal tracking state for epoch information. -enum EpochTrackingState { - /// The handler (which was most likely just started) has received the first - /// epoch information from the chain API. - FirstEpoch { current_epoch: Epoch }, - /// The handler has witnessed an epoch change, and it keeps minimal info - /// about the previous epoch, to notify consumer when the configured - /// epoch transition period has elapsed. - SecondOrLaterEpoch { - /// The last slot of the previous epoch. `None` when the consumers have - /// been notified of the previous epoch, and `Some` for the time between - /// a new epoch and the old epoch transition period. - previous_epoch_last_slot: Option, - /// The current epoch number. - current_epoch: Epoch, - }, -} - -impl EpochTrackingState { - const fn new(ValidatedSlotTick(SlotTick { epoch, .. }): ValidatedSlotTick) -> Self { - Self::FirstEpoch { - current_epoch: epoch, - } - } - - /// Use the slot before the provided one to mark the currently tracked epoch - /// as passed, and takes the new provided epoch as the new ongoing one. - #[expect(clippy::unused_self, reason = "We want to consume the input.")] - fn transition_to_new_epoch( - self, - ValidatedSlotTick(SlotTick { epoch, slot }): ValidatedSlotTick, - ) -> Self { - Self::SecondOrLaterEpoch { - previous_epoch_last_slot: Some(slot.into_inner().saturating_sub(1).into()), - current_epoch: epoch, - } - } - - /// Return the currently tracked epoch. - const fn last_processed_epoch(&self) -> Epoch { - match self { - Self::FirstEpoch { current_epoch } | Self::SecondOrLaterEpoch { current_epoch, .. } => { - *current_epoch - } - } - } -} - -/// A stream that listens to slot ticks, and on the first slot tick received as -/// well as the first slot tick of each new epoch, fetches the epoch state from -/// the provided chain service adapter. -/// -/// Then, when the configured transition period elapses for the past epoch, it -/// notifies consumers about it, once for each epoch. -pub struct EpochHandler { - /// A tracked of the last processed tick, to perform input tick sanitation, - /// i.e., ensuring slots are always increasing and epochs are not - /// decreasing. - last_processed_tick: Option, - /// Information about the current epoch, and whether the past epoch epoch - /// transition has already elapsed. - epoch_tracking_state: Option, - /// The chain service API providing epoch state for a given slot. - chain_service: ChainService, - /// Configured epoch transition period in slots. - epoch_transition_period_in_slots: NonZeroU64, - _phantom: PhantomData, -} - -impl EpochHandler { - pub const fn new( - chain_service: ChainService, - epoch_transition_period_in_slots: NonZeroU64, - ) -> Self { - Self { - last_processed_tick: None, - epoch_tracking_state: None, - chain_service, - epoch_transition_period_in_slots, - _phantom: PhantomData, - } - } -} - -impl EpochHandler -where - ChainService: ChainApi + Sync, - RuntimeServiceId: Sync, -{ - #[expect( - clippy::cognitive_complexity, - reason = "TODO: address this in a dedicated refactor" - )] - pub async fn tick(&mut self, new_tick: SlotTick) -> Option { - // We try to validate the new tick, else we restore the last valid one that we - // had. - let validated_slot_tick = if let Some(last_slot_tick) = self.last_processed_tick.take() { - last_slot_tick.transition_to(new_tick).inspect_err(|()| { - tracing::error!(target: LOG_TARGET, "Slot ticks are assumed to be always increasing, and epoch ticks are assumed to never be decreasing."); - self.last_processed_tick = Some(last_slot_tick); - }).ok()? - } else { - new_tick.into() - }; - self.last_processed_tick = Some(validated_slot_tick); - - if let Some(last_processed_epoch) = self - .epoch_tracking_state - .as_ref() - .map(EpochTrackingState::last_processed_epoch) - && last_processed_epoch == new_tick.epoch - { - tracing::trace!(target: LOG_TARGET, "New slot for same epoch. Skipping..."); - // We know we're in the same epoch, so we only check if the previous epoch is - // expired, and notify consumers about it. - if self.check_and_consume_past_epoch_transition_period(validated_slot_tick) { - return Some(EpochEvent::OldEpochTransitionPeriodExpired); - } - return None; - } - - tracing::debug!(target: LOG_TARGET, "Found epoch unseen before. Retrieving its state..."); - let Some(epoch_state) = self - .chain_service - .get_epoch_state_for_slot(new_tick.slot) - .await - else { - tracing::warn!(target: LOG_TARGET, "Failed to get epoch state for slot. Skipping..."); - return None; - }; - tracing::debug!( - target: LOG_TARGET, - "Retrieved epoch state for unseen epoch {:?}", - epoch_state.epoch() - ); - - // This is true if epochs are shorter than transition periods. It's not likely - // to happen in production, but we must still account for this - // possibility. In this case, we consider an old epoch expired if another one - // after it was replaced with a new, current one. - let should_notify_about_two_epochs_back = - if let Some(EpochTrackingState::SecondOrLaterEpoch { - previous_epoch_last_slot, - .. - }) = self.epoch_tracking_state - && previous_epoch_last_slot.is_some() - { - true - } else { - false - }; - - // If we have already previously processed an epoch, keep track of its last - // slot, so we can signal consumers when the transition period is over. - if let Some(current_epoch_state) = self.epoch_tracking_state.take() { - self.epoch_tracking_state = - Some(current_epoch_state.transition_to_new_epoch(validated_slot_tick)); - } else { - self.epoch_tracking_state = Some(EpochTrackingState::new(validated_slot_tick)); - } - - // We need to notify about epoch transitioncheck if the previous epoch is - // expired only if two epochs in the past are not, since the two - // conditions cannot exist at the same time. - let epoch_event = if should_notify_about_two_epochs_back - || self.check_and_consume_past_epoch_transition_period(validated_slot_tick) - { - EpochEvent::NewEpochAndOldEpochTransitionExpired((epoch_state.into(), new_tick.epoch)) - } else { - EpochEvent::NewEpoch((epoch_state.into(), new_tick.epoch)) - }; - - Some(epoch_event) - } - - // If we have witnessed a new epoch being transitioned and we have passed its - // transition period, notify the consumers. - const fn check_and_consume_past_epoch_transition_period( - &mut self, - ValidatedSlotTick(SlotTick { slot, .. }): ValidatedSlotTick, - ) -> bool { - if let Some(EpochTrackingState::SecondOrLaterEpoch { - previous_epoch_last_slot, - .. - }) = &mut self.epoch_tracking_state - && let Some(some_previous_epoch_last_slot) = previous_epoch_last_slot - && slot.into_inner() - some_previous_epoch_last_slot.into_inner() - >= self.epoch_transition_period_in_slots.get() - { - *previous_epoch_last_slot = None; - return true; - } - - false - } -} - -#[cfg(test)] -mod tests { - - use lb_time_service::SlotTick; - use test_log::test; - - use crate::{ - epoch_info::{EpochEvent, EpochHandler, EpochTrackingState, LeaderInputsMinusQuota}, - test_utils::epoch::{TestChainService, default_epoch_state}, - }; - - type TestEpochHandler = EpochHandler; - - #[test(tokio::test)] - async fn epoch_ticks() { - let ticks = vec![ - SlotTick { - epoch: 1.into(), - slot: 1.into(), - }, - // New slot same epoch - SlotTick { - epoch: 1.into(), - slot: 2.into(), - }, - // New slot new epoch - SlotTick { - epoch: 2.into(), - slot: 3.into(), - }, - ]; - let mut ticks_iter = ticks.into_iter(); - let mut stream = TestEpochHandler::new(TestChainService, u64::MAX.try_into().unwrap()); - - // First poll of the stream will set the epoch info and return the retrieved - // state. - let next_tick = stream.tick(ticks_iter.next().unwrap()).await; - assert_eq!( - stream.last_processed_tick, - Some( - SlotTick { - epoch: 1.into(), - slot: 1.into() - } - .into() - ) - ); - assert_eq!( - next_tick, - Some(EpochEvent::NewEpoch(( - LeaderInputsMinusQuota::from(default_epoch_state()), - 1.into() - ))) - ); - assert_eq!( - stream.epoch_tracking_state, - Some(EpochTrackingState::FirstEpoch { - current_epoch: 1.into() - }) - ); - - // Second poll of the stream will not return anything since it's in the same - // epoch. - let next_tick = stream.tick(ticks_iter.next().unwrap()).await; - assert_eq!( - stream.last_processed_tick, - Some( - SlotTick { - epoch: 1.into(), - slot: 2.into() - } - .into() - ) - ); - assert!(next_tick.is_none()); - assert_eq!( - stream.epoch_tracking_state, - Some(EpochTrackingState::FirstEpoch { - current_epoch: 1.into() - }) - ); - - // Third poll of the stream will yield a new element since we're in a new epoch. - let next_tick = stream.tick(ticks_iter.next().unwrap()).await; - assert_eq!( - stream.last_processed_tick, - Some( - SlotTick { - epoch: 2.into(), - slot: 3.into() - } - .into() - ) - ); - assert_eq!( - next_tick, - Some(EpochEvent::NewEpoch(( - LeaderInputsMinusQuota::from(default_epoch_state()), - 2.into() - ))) - ); - assert_eq!( - stream.epoch_tracking_state, - Some(EpochTrackingState::SecondOrLaterEpoch { - current_epoch: 2.into(), - previous_epoch_last_slot: Some(2.into()) - }) - ); - } - - #[test(tokio::test)] - async fn epoch_transition_1_slot() { - let ticks = vec![ - // Ongoing epoch - SlotTick { - epoch: 1.into(), - slot: 1.into(), - }, - // New slot same epoch - SlotTick { - epoch: 1.into(), - slot: 2.into(), - }, - // New slot new epoch (will trigger epoch transition and new epoch event because of 1s - // transition period) - SlotTick { - epoch: 2.into(), - slot: 3.into(), - }, - // New slot new epoch (will trigger epoch transition and new epoch event because of 1s - // transition period) - SlotTick { - epoch: 3.into(), - slot: 4.into(), - }, - ]; - let mut ticks_iter = ticks.into_iter(); - let mut stream = TestEpochHandler::new(TestChainService, 1.try_into().unwrap()); - - // First tick: we yield the retrieved data and mark the epoch as the first seen. - let next_tick = stream.tick(ticks_iter.next().unwrap()).await; - assert_eq!( - next_tick, - Some(EpochEvent::NewEpoch(( - LeaderInputsMinusQuota::from(default_epoch_state()), - 1.into() - ))) - ); - assert_eq!( - stream.epoch_tracking_state, - Some(EpochTrackingState::FirstEpoch { - current_epoch: 1.into() - }) - ); - - // Second tick: same epoch new slot, nothing interesting happens. - let next_tick = stream.tick(ticks_iter.next().unwrap()).await; - assert!(next_tick.is_none()); - assert_eq!( - stream.epoch_tracking_state, - Some(EpochTrackingState::FirstEpoch { - current_epoch: 1.into() - }) - ); - - // Third tick: new epoch new slot, old epoch transition expired - let next_tick = stream.tick(ticks_iter.next().unwrap()).await; - assert_eq!( - next_tick, - Some(EpochEvent::NewEpochAndOldEpochTransitionExpired(( - LeaderInputsMinusQuota::from(default_epoch_state()), - 2.into() - ))) - ); - assert_eq!( - stream.epoch_tracking_state, - Some(EpochTrackingState::SecondOrLaterEpoch { - current_epoch: 2.into(), - previous_epoch_last_slot: None - }) - ); - - // Fourth tick: new epoch same slot, old epoch transition expired - let next_tick = stream.tick(ticks_iter.next().unwrap()).await; - assert_eq!( - next_tick, - Some(EpochEvent::NewEpochAndOldEpochTransitionExpired(( - LeaderInputsMinusQuota::from(default_epoch_state()), - 3.into() - ))) - ); - assert_eq!( - stream.epoch_tracking_state, - Some(EpochTrackingState::SecondOrLaterEpoch { - current_epoch: 3.into(), - // `None` since we are notifying in this tick about its expiration. - previous_epoch_last_slot: None - }) - ); - } - - #[test(tokio::test)] - async fn epoch_transition_more_than_1_slot() { - let ticks = vec![ - // Ongoing epoch - SlotTick { - epoch: 1.into(), - slot: 1.into(), - }, - // New slot same epoch - SlotTick { - epoch: 1.into(), - slot: 2.into(), - }, - // New slot new epoch (will trigger a new epoch event only) - SlotTick { - epoch: 2.into(), - slot: 3.into(), - }, - // New slot same epoch (will trigger an epoch transition only) - SlotTick { - epoch: 2.into(), - slot: 4.into(), - }, - // New slot new epoch (will trigger a new epoch event only) - SlotTick { - epoch: 3.into(), - slot: 5.into(), - }, - // New slot new epoch (will trigger a new epoch event and a epoch transition - // together) - SlotTick { - epoch: 4.into(), - slot: 6.into(), - }, - // New slot same epoch ((will trigger an epoch transition only) - SlotTick { - epoch: 4.into(), - slot: 7.into(), - }, - ]; - let mut ticks_iter = ticks.into_iter(); - let mut stream = TestEpochHandler::new(TestChainService, 2.try_into().unwrap()); - - // We yield the retrieved data and mark the epoch as the first seen. - let next_tick = stream.tick(ticks_iter.next().unwrap()).await; - assert_eq!( - next_tick, - Some(EpochEvent::NewEpoch(( - LeaderInputsMinusQuota::from(default_epoch_state()), - 1.into() - ))) - ); - assert_eq!( - stream.epoch_tracking_state, - Some(EpochTrackingState::FirstEpoch { - current_epoch: 1.into() - }) - ); - - // Second tick: same epoch new slot, nothing interesting happens. - let next_tick = stream.tick(ticks_iter.next().unwrap()).await; - assert!(next_tick.is_none()); - assert_eq!( - stream.epoch_tracking_state, - Some(EpochTrackingState::FirstEpoch { - current_epoch: 1.into() - }) - ); - - // Third tick: new epoch new slot, new epoch event generated - let next_tick = stream.tick(ticks_iter.next().unwrap()).await; - assert_eq!( - next_tick, - Some(EpochEvent::NewEpoch(( - LeaderInputsMinusQuota::from(default_epoch_state()), - 2.into() - ))) - ); - assert_eq!( - stream.epoch_tracking_state, - Some(EpochTrackingState::SecondOrLaterEpoch { - current_epoch: 2.into(), - previous_epoch_last_slot: Some(2.into()) - }) - ); - - // Fourth tick: new slot same epoch, previous epoch now expires - let next_tick = stream.tick(ticks_iter.next().unwrap()).await; - assert_eq!(next_tick, Some(EpochEvent::OldEpochTransitionPeriodExpired)); - assert_eq!( - stream.epoch_tracking_state, - Some(EpochTrackingState::SecondOrLaterEpoch { - current_epoch: 2.into(), - // `None` since we are notifying in this tick about its expiration. - previous_epoch_last_slot: None - }) - ); - - // Fifth tick: new slot new epoch, new epoch event generated - let next_tick = stream.tick(ticks_iter.next().unwrap()).await; - assert_eq!( - next_tick, - Some(EpochEvent::NewEpoch(( - LeaderInputsMinusQuota::from(default_epoch_state()), - 3.into() - ))) - ); - assert_eq!( - stream.epoch_tracking_state, - Some(EpochTrackingState::SecondOrLaterEpoch { - current_epoch: 3.into(), - previous_epoch_last_slot: Some(4.into()) - }) - ); - - // Sixth tick: new slot new epoch, new epoch and old epoch expiration are - // generated - let next_tick = stream.tick(ticks_iter.next().unwrap()).await; - assert_eq!( - next_tick, - Some(EpochEvent::NewEpochAndOldEpochTransitionExpired(( - LeaderInputsMinusQuota::from(default_epoch_state()), - 4.into() - ))) - ); - assert_eq!( - stream.epoch_tracking_state, - Some(EpochTrackingState::SecondOrLaterEpoch { - current_epoch: 4.into(), - // Not `None` because we notified consumers about the epoch before the one that was - // just rotated, so this one will be notified next. - previous_epoch_last_slot: Some(5.into()) - }) - ); - - // Seventh tick: new slot same epoch, the past epoch can now be discarded. - let next_tick = stream.tick(ticks_iter.next().unwrap()).await; - assert_eq!(next_tick, Some(EpochEvent::OldEpochTransitionPeriodExpired)); - assert_eq!( - stream.epoch_tracking_state, - Some(EpochTrackingState::SecondOrLaterEpoch { - current_epoch: 4.into(), - previous_epoch_last_slot: None - }) - ); - } - - #[test(tokio::test)] - async fn slot_not_increasing() { - let mut stream = TestEpochHandler::new(TestChainService, u64::MAX.try_into().unwrap()); - stream - .tick(SlotTick { - epoch: 2.into(), - slot: 2.into(), - }) - .await; - assert_eq!( - stream.last_processed_tick, - Some( - SlotTick { - epoch: 2.into(), - slot: 2.into() - } - .into() - ) - ); - assert!( - stream - .tick(SlotTick { - epoch: 3.into(), - slot: 2.into(), - }) - .await - .is_none() - ); - assert_eq!( - stream.last_processed_tick, - Some( - SlotTick { - epoch: 2.into(), - slot: 2.into() - } - .into() - ) - ); - assert_eq!( - stream.last_processed_tick, - Some( - SlotTick { - epoch: 2.into(), - slot: 2.into(), - } - .into() - ) - ); - assert_eq!( - stream.last_processed_tick, - Some( - SlotTick { - epoch: 2.into(), - slot: 2.into() - } - .into() - ) - ); - assert!( - stream - .tick(SlotTick { - epoch: 3.into(), - slot: 1.into(), - }) - .await - .is_none() - ); - assert_eq!( - stream.last_processed_tick, - Some( - SlotTick { - epoch: 2.into(), - slot: 2.into() - } - .into() - ) - ); - assert_eq!( - stream.last_processed_tick, - Some( - SlotTick { - epoch: 2.into(), - slot: 2.into(), - } - .into() - ) - ); - assert_eq!( - stream.last_processed_tick, - Some( - SlotTick { - epoch: 2.into(), - slot: 2.into() - } - .into() - ) - ); - } - - #[test(tokio::test)] - async fn epoch_not_increasing() { - let mut stream = TestEpochHandler::new(TestChainService, u64::MAX.try_into().unwrap()); - stream - .tick(SlotTick { - epoch: 2.into(), - slot: 2.into(), - }) - .await; - assert_eq!( - stream.last_processed_tick, - Some( - SlotTick { - epoch: 2.into(), - slot: 2.into() - } - .into() - ) - ); - assert!( - stream - .tick(SlotTick { - epoch: 2.into(), - slot: 3.into(), - }) - .await - .is_none() - ); - assert_eq!( - stream.last_processed_tick, - Some( - SlotTick { - epoch: 2.into(), - slot: 3.into() - } - .into() - ) - ); - assert!( - stream - .tick(SlotTick { - epoch: 1.into(), - slot: 3.into(), - }) - .await - .is_none() - ); - assert_eq!( - stream.last_processed_tick, - Some( - SlotTick { - epoch: 2.into(), - slot: 3.into() - } - .into() - ) - ); - } -} diff --git a/services/blend/src/instance.rs b/services/blend/src/instance.rs index 92631a065..398a78911 100644 --- a/services/blend/src/instance.rs +++ b/services/blend/src/instance.rs @@ -3,7 +3,7 @@ use std::{ hash::Hash, }; -use lb_blend::scheduling::{membership::Membership, session::SessionEvent}; +use lb_blend::scheduling::{epoch::EpochEvent, membership::Membership}; use lb_network_service::NetworkService; use overwatch::{ overwatch::OverwatchHandle, @@ -33,13 +33,13 @@ where Edge(EdgeMode), EdgeAfterCore { mode: EdgeMode, - // Keep the previous core mode for the session transition period. + // Keep the previous core mode for the epoch transition period. prev: CoreMode, }, Broadcast(BroadcastMode), BroadcastAfterCore { mode: BroadcastMode, - // Keep the previous core mode for the session transition period. + // Keep the previous core mode for the epoch transition period. prev: CoreMode, }, } @@ -139,16 +139,16 @@ where } } - /// Handles a session event, potentially causing a mode transition. - pub async fn handle_session_event( + /// Handles an epoch event, potentially causing a mode transition. + pub async fn handle_epoch_event( self, - event: SessionEvent>, + event: EpochEvent>, overwatch_handle: &OverwatchHandle, minimal_network_size: usize, local_node_id: CoreService::NodeId, ) -> Result { match event { - SessionEvent::NewSession(MembershipInfo { membership, .. }) => { + EpochEvent::NewEpoch(MembershipInfo { membership, .. }) => { self.transition( Mode::choose(&membership, minimal_network_size), overwatch_handle, @@ -156,7 +156,7 @@ where ) .await } - SessionEvent::TransitionPeriodExpired => { + EpochEvent::TransitionPeriodExpired => { Ok(self.handle_transition_period_expired().await) } } @@ -605,9 +605,9 @@ mod tests { }); } - /// Check if the instance handles session events correctly. + /// Check if the instance handles epoch events correctly. #[test] - fn test_handle_session_event() { + fn test_handle_epoch_event() { let app = OverwatchRunner::::run(settings(), None).unwrap(); app.runtime().handle().block_on(async { let handle = app.handle(); @@ -625,12 +625,9 @@ mod tests { let local_node = LOCAL_NODE_ID; let minimal_network_size = 1; let instance = instance - .handle_session_event( + .handle_epoch_event( // With an empty membership smaller than the minimal size. - SessionEvent::NewSession(MembershipInfo::from_membership_and_session_number( - membership(&[], local_node), - 1, - )), + EpochEvent::NewEpoch(membership(&[], local_node).into()), handle, minimal_network_size, LOCAL_NODE_ID, @@ -641,8 +638,8 @@ mod tests { // BroadcastAfterCore -> Broadcast, after the transition period expires. let instance = instance - .handle_session_event( - SessionEvent::TransitionPeriodExpired, + .handle_epoch_event( + EpochEvent::TransitionPeriodExpired, handle, minimal_network_size, LOCAL_NODE_ID, @@ -653,11 +650,8 @@ mod tests { // Broadcast -> Edge let instance = instance - .handle_session_event( - SessionEvent::NewSession(MembershipInfo::from_membership_and_session_number( - membership(&[1], local_node), - 1, - )), + .handle_epoch_event( + EpochEvent::NewEpoch(membership(&[1], local_node).into()), handle, minimal_network_size, LOCAL_NODE_ID, @@ -668,11 +662,8 @@ mod tests { // Edge -> Edge (stay) let instance = instance - .handle_session_event( - SessionEvent::NewSession(MembershipInfo::from_membership_and_session_number( - membership(&[1], local_node), - 1, - )), + .handle_epoch_event( + EpochEvent::NewEpoch(membership(&[1], local_node).into()), handle, minimal_network_size, LOCAL_NODE_ID, @@ -683,11 +674,8 @@ mod tests { // Edge -> Core let instance = instance - .handle_session_event( - SessionEvent::NewSession(MembershipInfo::from_membership_and_session_number( - membership(&[1], 1), - 1, - )), + .handle_epoch_event( + EpochEvent::NewEpoch(membership(&[1], 1).into()), handle, minimal_network_size, LOCAL_NODE_ID, diff --git a/services/blend/src/lib.rs b/services/blend/src/lib.rs index 46f345350..150d49837 100644 --- a/services/blend/src/lib.rs +++ b/services/blend/src/lib.rs @@ -8,11 +8,13 @@ use std::{ use async_trait::async_trait; use futures::StreamExt as _; pub use lb_blend::message::{crypto::proofs::RealProofsVerifier, encap::ProofsVerifier}; -use lb_blend::scheduling::session::UninitializedSessionEventStream; +use lb_blend::scheduling::epoch::UninitializedEpochEventStream; +use lb_chain_service::api::CryptarchiaServiceData; use lb_key_management_system_service::{api::KmsServiceApi, keys::PublicKeyEncoding}; use lb_log_targets::blend; use lb_network_service::NetworkService; use lb_services_utils::wait_until_services_are_ready; +use lb_time_service::TimeService; use overwatch::{ DynError, OpaqueServiceResourcesHandle, services::{ @@ -34,19 +36,20 @@ use crate::{ instance::{Instance, Mode}, kms::PreloadKmsService, membership::{ - Adapter as _, MembershipInfo, + MembershipInfo, + chain::BlendEpochState, node_id::{self, TryFrom as _}, }, - settings::{FIRST_STREAM_ITEM_READY_TIMEOUT, Settings}, + settings::Settings, }; pub mod core; pub mod edge; +pub mod epoch; pub mod epoch_info; pub mod membership; pub mod message; pub(crate) mod metrics; -pub mod session; pub mod settings; mod instance; @@ -108,17 +111,19 @@ where EdgeService: ServiceData // We tie the core and edge proofs generator to be the same type, to avoid mistakes in the // node configuration where the two services use different verification logic - + EdgeServiceComponents - + Send + + EdgeServiceComponents< + BackendSettings: Clone + Send + Sync, + ChainService: CryptarchiaServiceData, + TimeBackend: lb_time_service::backends::TimeBackend + Send, + > + Send + 'static, - EdgeService::MembershipAdapter: - membership::Adapter + Send, - membership::ServiceMessage: Send + Sync + 'static, RuntimeServiceId: AsServiceId + AsServiceId + AsServiceId - + AsServiceId> - + AsServiceId> + + AsServiceId<::ChainService> + + AsServiceId< + TimeService<::TimeBackend, RuntimeServiceId>, + > + AsServiceId> + AsServiceId< NetworkService< NetworkBackendOfService, @@ -129,6 +134,7 @@ where + Clone + Send + Sync + + Unpin + 'static, { fn init( @@ -160,7 +166,6 @@ where wait_until_services_are_ready!( &overwatch_handle, Some(Duration::from_mins(1)), - MembershipService, PreloadKmsService<_> ) .await?; @@ -180,27 +185,35 @@ where CoreService::NodeId::try_from_provider_id(non_ephemeral_signing_key_public.as_bytes()) .expect("non-ephemeral signing public key should decode into a valid node id"); - let membership_stream = as membership::Adapter>::new( - overwatch_handle - .relay::>() - .await?, + let membership_stream = membership::chain::subscribe::< + ::ChainService, + CoreService::NodeId, + ::TimeBackend, + RuntimeServiceId, + >( + overwatch_handle, non_ephemeral_signing_key_public, // We don't need to generate secret zk info in the proxy service, so we ignore the // secret key at this level. None, ) - .subscribe() - .await?; + .await + // We take only the membership info from the epoch stream since the proxy service does not + // need anything else. + .map( + |BlendEpochState { + membership_info, .. + }| membership_info, + ); - let (MembershipInfo { membership, .. }, mut remaining_session_stream) = - UninitializedSessionEventStream::new( + let (MembershipInfo { membership, .. }, mut remaining_membership_stream) = + UninitializedEpochEventStream::new( membership_stream, - FIRST_STREAM_ITEM_READY_TIMEOUT, - settings.common.time.session_transition_period(), + settings.common.time.epoch_transition_period, ) .await_first_ready() .await - .expect("The current session must be ready"); + .expect("The current epoch state must be ready"); info!( target: LOG_TARGET, @@ -224,11 +237,11 @@ where loop { tokio::select! { - Some(session_event) = remaining_session_stream.next() => { - debug!(target: LOG_TARGET, ?session_event, "received session event"); + Some(epoch_event) = remaining_membership_stream.next() => { + debug!(target: LOG_TARGET, ?epoch_event, "received epoch event"); instance = instance - .handle_session_event( - session_event, + .handle_epoch_event( + epoch_event, overwatch_handle, minimal_network_size, local_node_id.clone(), @@ -249,8 +262,3 @@ type BroadcastSettings = <::Message as MessageComponents< >::NodeId, >>::BroadcastSettings; - -type MembershipAdapter = ::MembershipAdapter; - -type MembershipService = - as membership::Adapter>::Service; diff --git a/services/blend/src/membership/chain.rs b/services/blend/src/membership/chain.rs new file mode 100644 index 000000000..1f85e636a --- /dev/null +++ b/services/blend/src/membership/chain.rs @@ -0,0 +1,135 @@ +//! Chain-derived per-epoch state. +//! +//! On every slot tick the chain is queried for the current epoch's +//! [`EpochState`](lb_ledger::EpochState); on each new epoch the membership and +//! leader inputs frozen into its SDP snapshot are yielded together as a +//! [`BlendEpochState`]. Both halves come from the same chain query, so they +//! cannot drift — replacing the pushed `ActiveProviders` broadcast. + +use core::{hash::Hash, pin::Pin}; +use std::fmt::{Debug, Display}; + +use futures::{Stream, StreamExt as _, stream::unfold}; +use lb_chain_service::{ + Epoch, + api::{CryptarchiaServiceApi, CryptarchiaServiceData}, +}; +use lb_groth16::Fr; +use lb_key_management_system_service::keys::{Ed25519PublicKey, ZkPublicKey}; +use lb_time_service::{SlotTick, TimeService, TimeServiceMessage, backends::TimeBackend}; +use overwatch::{overwatch::OverwatchHandle, services::AsServiceId}; +use tokio::sync::oneshot; + +use crate::{ + LOG_TARGET, + membership::{MembershipInfo, node_id, service::membership_info_from_epoch_state}, +}; + +#[derive(Clone, Debug)] +pub struct BlendEpochState { + pub epoch: Epoch, + pub nonce: Fr, + pub aged: Fr, + pub lottery_0: Fr, + pub lottery_1: Fr, + pub membership_info: MembershipInfo, +} + +/// A chain-derived per-epoch state stream. +/// +/// Not `Sync`, since producing each item awaits a chain query; consumers only +/// require `Send + Unpin`. +pub type BlendEpochStateStream = + Pin> + Send + 'static>>; + +/// Subscribe to a chain-derived stream of [`BlendEpochState`]. +/// +/// One item is yielded per epoch — the first slot of the epoch whose chain +/// query succeeds. Slot ticks within an already-yielded epoch are ignored; +/// failed chain queries do not advance the tracked epoch, so the next slot of +/// the same epoch is retried. +pub async fn subscribe( + overwatch_handle: &OverwatchHandle, + signing_public_key: Ed25519PublicKey, + zk_public_key: Option, +) -> BlendEpochStateStream +where + ChainService: CryptarchiaServiceData, + NodeId: node_id::TryFrom + Clone + Hash + Eq + Send + Sync + 'static, + TimeRuntimeBackend: TimeBackend + Send, + RuntimeServiceId: AsServiceId + + AsServiceId> + + Clone + + Debug + + Display + + Sync + + Send + + Unpin + + 'static, +{ + let chain_service = CryptarchiaServiceApi::::new( + overwatch_handle + .relay::() + .await + .expect("Relay with chain service should be available."), + ); + + let slot_ticks = { + let time_relay = overwatch_handle + .relay::>() + .await + .expect("Relay with time service should be available."); + let (sender, receiver) = oneshot::channel(); + time_relay + .send(TimeServiceMessage::Subscribe { sender }) + .await + .expect("Failed to subscribe to slot clock."); + receiver + .await + .expect("Should not fail to receive slot stream from time service.") + }; + + // TODO: Refactor into a function or own type that replaces `EpochHandler`. + Box::pin(unfold( + ( + slot_ticks, + None::, + chain_service, + signing_public_key, + zk_public_key, + ), + async move |(mut ticks, mut last_epoch, chain_api, signing_pk, zk_pk)| { + loop { + let SlotTick { epoch, slot } = ticks.next().await?; + if Some(epoch) == last_epoch { + continue; + } + match chain_api.get_epoch_state(slot).await { + Ok(Ok(epoch_state)) => { + last_epoch = Some(epoch); + let membership_info = membership_info_from_epoch_state::( + &epoch_state, + &signing_pk, + zk_pk, + ); + let item = BlendEpochState { + epoch, + nonce: epoch_state.nonce, + aged: epoch_state.utxo_merkle_root(), + lottery_0: epoch_state.lottery_0, + lottery_1: epoch_state.lottery_1, + membership_info, + }; + return Some((item, (ticks, last_epoch, chain_api, signing_pk, zk_pk))); + } + Ok(Err(e)) => { + tracing::warn!(target: LOG_TARGET, "Chain service returned error for epoch state at slot {slot:?}: {e:?}; will retry on next slot of epoch {epoch:?}"); + } + Err(e) => { + tracing::warn!(target: LOG_TARGET, "Failed to query epoch state at slot {slot:?}: {e:?}; will retry on next slot of epoch {epoch:?}"); + } + } + } + }, + )) +} diff --git a/services/blend/src/membership/mod.rs b/services/blend/src/membership/mod.rs index fdd482571..62853d55c 100644 --- a/services/blend/src/membership/mod.rs +++ b/services/blend/src/membership/mod.rs @@ -1,53 +1,42 @@ +pub mod chain; pub mod node_id; pub mod service; use core::fmt::{self, Debug, Formatter}; -use std::pin::Pin; -use futures::Stream; use lb_blend::scheduling::membership::Membership; use lb_core::crypto::ZkHash; use lb_groth16::fr_to_bytes; -use lb_key_management_system_service::keys::{Ed25519PublicKey, ZkPublicKey}; use lb_poq::CorePathAndSelectors; -use overwatch::services::{ServiceData, relay::OutboundRelay}; #[derive(Clone, Debug)] pub struct MembershipInfo { pub membership: Membership, // `None` if membership is empty. pub zk: Option, - pub session_number: u64, } -impl MembershipInfo { - #[cfg(test)] - #[must_use] - pub fn from_membership_and_session_number( - membership: Membership, - session_number: u64, - ) -> Self { +#[cfg(test)] +impl From> for MembershipInfo { + fn from(membership: Membership) -> Self { let zk = if membership.is_empty() { None } else { Some(ZkInfo::default()) }; - Self { - membership, - zk, - session_number, - } + + Self { membership, zk } } } #[derive(Clone)] #[cfg_attr(test, derive(Default))] -/// ZK info for a new session. +/// ZK info for a new epoch. pub struct ZkInfo { /// The merkle root of the ZK public keys of all core nodes. pub root: ZkHash, /// The merkle path (and selectors) proving the node's ZK public key is part - /// of the session merkle tree. This is `None` for edge nodes. + /// of the epoch merkle tree. This is `None` for edge nodes. pub core_and_path_selectors: Option, } @@ -59,26 +48,3 @@ impl Debug for ZkInfo { .finish() } } - -pub type MembershipStream = - Pin> + Send + Sync + 'static>>; - -pub type ServiceMessage = - <::Service as ServiceData>::Message; - -/// An adapter for the membership service. -#[async_trait::async_trait] -pub trait Adapter { - type Service: ServiceData; - type NodeId; - type Error: std::error::Error; - - fn new( - relay: OutboundRelay>, - signing_public_key: Ed25519PublicKey, - zk_public_key: Option, - ) -> Self; - - /// Subscribe to membership updates. - async fn subscribe(&self) -> Result, Self::Error>; -} diff --git a/services/blend/src/membership/service.rs b/services/blend/src/membership/service.rs index 5c67fcdaa..db45351f7 100644 --- a/services/blend/src/membership/service.rs +++ b/services/blend/src/membership/service.rs @@ -1,21 +1,16 @@ -use std::{hash::Hash, marker::PhantomData}; +use std::hash::Hash; -use futures::StreamExt as _; use lb_blend::{ crypto::merkle::sort_nodes_and_build_merkle_tree, scheduling::membership::{Membership, Node}, }; -use lb_chain_broadcast_service::{BlockBroadcastMsg, SessionSubscription, SessionUpdate}; -use lb_core::sdp::{ProviderId, ProviderInfo}; +use lb_core::sdp::{ProviderId, ProviderInfo, ServiceType}; use lb_key_management_system_service::keys::{Ed25519PublicKey, ZkPublicKey}; -use overwatch::{ - DynError, - services::{ServiceData, relay::OutboundRelay}, -}; -use tokio::sync::oneshot; +use lb_ledger::EpochState; +use overwatch::DynError; use tracing::{debug, warn}; -use crate::membership::{MembershipInfo, MembershipStream, ServiceMessage, ZkInfo, node_id}; +use crate::membership::{MembershipInfo, ZkInfo, node_id}; /// Wrapper around [`Node`] that includes its ZK public key. #[derive(Debug, Clone)] @@ -24,132 +19,69 @@ struct ZkNode { pub zk_key: ZkPublicKey, } -pub struct Adapter +/// Build [`MembershipInfo`] from the SDP membership snapshot frozen into +/// `epoch_state`. +/// +/// This is the chain-derived replacement for the pushed [`ActiveProviders`] +/// broadcast: the membership for the epoch is read from `EpochState.sdp` +/// instead of a broadcast stream. +/// +/// Note: this intentionally duplicates the node/Merkle construction in +/// `Adapter::subscribe` rather than sharing it, because the broadcast +/// `subscribe` path is slated for removal once this becomes the membership +/// source; sharing logic with code about to be deleted is not worth the churn. +#[must_use] +pub fn membership_info_from_epoch_state( + epoch_state: &EpochState, + signing_public_key: &Ed25519PublicKey, + maybe_zk_public_key: Option, +) -> MembershipInfo where - Service: ServiceData, + NodeId: node_id::TryFrom + Clone + Hash + Eq, { - /// A relay to send messages to the membership service. - relay: OutboundRelay<::Message>, - /// A signing public key of the local node, required to - /// build a [`Membership`] instance. - signing_public_key: Ed25519PublicKey, - zk_public_key: Option, - _phantom: PhantomData, -} + let declarations = epoch_state.sdp.declarations(); + let mut nodes: Vec> = declarations + .iter() + .filter(|(service_type, _)| matches!(service_type, ServiceType::BlendNetwork)) + .flat_map(|(_, declarations)| declarations.values()) + .filter_map(|declaration| { + let provider_info = ProviderInfo { + locators: declaration.locators.clone(), + zk_id: declaration.zk_id, + }; + node_from_provider::(&declaration.provider_id, &provider_info) + }) + .collect(); -#[async_trait::async_trait] -impl super::Adapter for Adapter -where - Service: ServiceData, - NodeId: node_id::TryFrom + Clone + Hash + Eq + Sync, -{ - type Service = Service; - type NodeId = NodeId; - type Error = Error; - - fn new( - relay: OutboundRelay>, - signing_public_key: Ed25519PublicKey, - zk_public_key: Option, - ) -> Self { - Self { - relay, - signing_public_key, - zk_public_key, - _phantom: PhantomData, - } - } - - /// Subscribe to membership updates. - /// - /// It returns a stream of [`Membership`] instances, - async fn subscribe(&self) -> Result, Self::Error> { - let signing_public_key = self.signing_public_key; - let maybe_zk_public_key = self.zk_public_key; - - let session_stream = self.subscribe_stream().await?; - - Ok(Box::pin( - session_stream - .map( - |SessionUpdate { - providers, - session_number, - }| { - ( - providers - .iter() - .filter_map(|(provider_id, provider_info)| { - node_from_provider::(provider_id, provider_info) - }) - .collect::>(), - session_number, - ) - }, - ) - // Sort nodes (if any) by their ZK public key to build a Merkle tree, since the - // returned `HashMap` from the chain broadcast service is - // non-deterministic across different machines. - .map(move |(mut nodes, session_number)| { - let zk_info = if nodes.is_empty() { - None - } else { - let zk_tree = sort_nodes_and_build_merkle_tree( - &mut nodes, - |ZkNode { zk_key, .. }| zk_key.into_inner(), - ) - .expect( - "Should not fail to build Merkle tree of core nodes' zk public keys.", - ); - let core_and_path_selectors = - maybe_zk_public_key.and_then(|zk_public_key| { - let Some(proof) = - zk_tree.get_proof_for_key(zk_public_key.as_fr()) - else { - debug!( - "Local node's ZK public key not found in membership Merkle tree: node is not a core member." - ); - return None; - }; - Some(proof) - }); - Some(ZkInfo { - core_and_path_selectors, - root: zk_tree.root(), - }) - }; - let membership_nodes = nodes - .into_iter() - .map(|ZkNode { node, .. }| node) - .collect::>(); - let membership = Membership::new(&membership_nodes, &signing_public_key); - MembershipInfo { - membership, - zk: zk_info, - session_number, - } - }), - )) - } -} - -impl Adapter -where - Service: ServiceData, - NodeId: Sync, -{ - /// Subscribe to membership updates for the given service type. - async fn subscribe_stream(&self) -> Result { - let (sender, receiver) = oneshot::channel(); - - self.relay - .send(BlockBroadcastMsg::SubscribeBlendSession { - result_sender: sender, - }) - .await - .map_err(|(e, _)| Error::Other(e.into()))?; - - receiver.await.map_err(|e| Error::Other(e.into())) + let zk_info = if nodes.is_empty() { + None + } else { + let zk_tree = sort_nodes_and_build_merkle_tree(&mut nodes, |ZkNode { zk_key, .. }| { + zk_key.into_inner() + }) + .expect("Should not fail to build Merkle tree of core nodes' zk public keys."); + let core_and_path_selectors = maybe_zk_public_key.and_then(|zk_public_key| { + let Some(proof) = zk_tree.get_proof_for_key(zk_public_key.as_fr()) else { + debug!( + "Local node's ZK public key not found in membership Merkle tree: node is not a core member." + ); + return None; + }; + Some(proof) + }); + Some(ZkInfo { + core_and_path_selectors, + root: zk_tree.root(), + }) + }; + let membership_nodes = nodes + .into_iter() + .map(|ZkNode { node, .. }| node) + .collect::>(); + let membership = Membership::new(&membership_nodes, signing_public_key); + MembershipInfo { + membership, + zk: zk_info, } } diff --git a/services/blend/src/message.rs b/services/blend/src/message.rs index 56f8db230..4da45b4bb 100644 --- a/services/blend/src/message.rs +++ b/services/blend/src/message.rs @@ -13,12 +13,12 @@ pub struct NetworkInfo { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CoreInfo { - /// Negotiated peers for the current session, with a flag indicating whether + /// Negotiated peers for the current epoch, with a flag indicating whether /// they are healthy (`true`) or not (`false`). - pub current_session_peers: Vec<(NodeId, bool)>, - /// Negotiated peers for the old session, if a session transition is in + pub current_epoch_peers: Vec<(NodeId, bool)>, + /// Negotiated peers for the old epoch, if an epoch transition is in /// progress. - pub old_session_peers: Option>, + pub old_epoch_peers: Option>, } /// A message that is handled by [`BlendService`]. diff --git a/services/blend/src/session.rs b/services/blend/src/session.rs deleted file mode 100644 index 3541b6f0c..000000000 --- a/services/blend/src/session.rs +++ /dev/null @@ -1,47 +0,0 @@ -use lb_blend::{ - proofs::quota::inputs::prove::public::CoreInputs, scheduling::membership::Membership, -}; - -#[derive(Clone, Debug)] -// TODO: Refactor this so that it's a struct with the common fields, and -// everything case-specific is an enum. -pub enum MaybeEmptyCoreSessionInfo { - Empty { session: u64 }, - NonEmpty(CoreSessionInfo), -} - -impl From for MaybeEmptyCoreSessionInfo { - fn from(session: u64) -> Self { - Self::Empty { session } - } -} - -impl From> - for MaybeEmptyCoreSessionInfo -{ - fn from(core_session_info: CoreSessionInfo) -> Self { - Self::NonEmpty(core_session_info) - } -} - -#[derive(Clone, Debug)] -/// All info that Blend services need to be available on new sessions. -pub struct CoreSessionInfo { - /// The session info available to all nodes. - pub public: CoreSessionPublicInfo, - /// The core `PoQ` generator component. `None` when Blend is running - /// (network large enough), but local node is not part of the core - /// membership. - pub core_poq_generator: Option, -} - -#[derive(Clone, Debug)] -/// All public info that Blend services need to be available on new sessions. -pub struct CoreSessionPublicInfo { - /// The list of core Blend nodes for the new session. - pub membership: Membership, - /// The session number. - pub session: u64, - /// The set of public inputs to verify core `PoQ`s. - pub poq_core_public_inputs: CoreInputs, -} diff --git a/services/blend/src/settings/mod.rs b/services/blend/src/settings/mod.rs index f8f95a160..edc369005 100644 --- a/services/blend/src/settings/mod.rs +++ b/services/blend/src/settings/mod.rs @@ -1,5 +1,3 @@ -use std::time::Duration; - use serde::{Deserialize, Serialize}; use crate::{ @@ -16,8 +14,6 @@ pub use self::edge::EdgeSettings; mod timing; pub use self::timing::TimingSettings; -pub(crate) const FIRST_STREAM_ITEM_READY_TIMEOUT: Duration = Duration::from_secs(5); - #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Settings { pub common: CommonSettings, diff --git a/services/blend/src/settings/timing.rs b/services/blend/src/settings/timing.rs index f5501155b..24646c107 100644 --- a/services/blend/src/settings/timing.rs +++ b/services/blend/src/settings/timing.rs @@ -2,48 +2,16 @@ use core::{num::NonZeroU64, time::Duration}; use serde::{Deserialize, Serialize}; -use crate::epoch_info::EpochHandler; - #[serde_with::serde_as] #[derive(Serialize, Deserialize, Clone, Debug)] pub struct TimingSettings { - /// `S`: length of a session in terms of expected rounds (on average). - pub rounds_per_session: NonZeroU64, - /// `|I|`: length of an interval in terms of rounds. - pub rounds_per_interval: NonZeroU64, + /// `S`: length of an epoch in terms of rounds. + pub rounds_per_epoch: NonZeroU64, #[serde_as( as = "lb_utils::bounded_duration::MinimalBoundedDuration<1, lb_utils::bounded_duration::SECOND>" )] /// Duration of a round. pub round_duration: Duration, pub rounds_per_observation_window: NonZeroU64, - /// Session transition period in rounds. - pub rounds_per_session_transition_period: NonZeroU64, - pub epoch_transition_period_in_slots: NonZeroU64, -} - -impl TimingSettings { - #[must_use] - pub const fn session_duration(&self) -> Duration { - Duration::from_secs(self.rounds_per_session.get() * self.round_duration.as_secs()) - } - - #[must_use] - pub fn intervals_per_session(&self) -> NonZeroU64 { - NonZeroU64::try_from(self.rounds_per_session.get() / self.rounds_per_interval.get()).expect("Obtained `0` when calculating the number of intervals per session, which is not allowed.") - } - - #[must_use] - pub const fn session_transition_period(&self) -> Duration { - Duration::from_secs( - self.rounds_per_session_transition_period.get() * self.round_duration.as_secs(), - ) - } - - pub const fn epoch_handler( - &self, - chain_service: ChainService, - ) -> EpochHandler { - EpochHandler::new(chain_service, self.epoch_transition_period_in_slots) - } + pub epoch_transition_period: Duration, } diff --git a/services/blend/src/test_utils/crypto.rs b/services/blend/src/test_utils/crypto.rs index fa024e29d..d96096d2c 100644 --- a/services/blend/src/test_utils/crypto.rs +++ b/services/blend/src/test_utils/crypto.rs @@ -9,7 +9,7 @@ use lb_blend::{ proofs::{ quota::{ ProofOfQuota, VerifiedProofOfQuota, - inputs::prove::{private::ProofOfLeadershipQuotaInputs, public::LeaderInputs}, + inputs::prove::private::ProofOfLeadershipQuotaInputs, }, selection::{ProofOfSelection, VerifiedProofOfSelection, inputs::VerifyInputs}, }, @@ -33,13 +33,10 @@ impl CoreAndLeaderProofsGenerator Self } - fn rotate_epoch(&mut self, _new_epoch_public: LeaderInputs, _new_epoch: Epoch) {} - fn set_epoch_private( &mut self, _new_epoch_private: ProofOfLeadershipQuotaInputs, - _new_epoch_public: LeaderInputs, - _new_epoch: Epoch, + _target_epoch: Epoch, ) { } @@ -62,10 +59,6 @@ impl ProofsVerifier for MockProofsVerifier { Self } - fn start_epoch_transition(&mut self, _new_pol_inputs: LeaderInputs) {} - - fn complete_epoch_transition(&mut self) {} - fn verify_proof_of_quota( &self, proof: ProofOfQuota, @@ -110,10 +103,6 @@ impl ProofsVerifier for StaticFetchVerifier { Self } - fn start_epoch_transition(&mut self, _new_pol_inputs: LeaderInputs) {} - - fn complete_epoch_transition(&mut self) {} - fn verify_proof_of_quota( &self, proof: ProofOfQuota, diff --git a/services/blend/src/test_utils/epoch.rs b/services/blend/src/test_utils/epoch.rs index 1a5915b5c..2dacd07a1 100644 --- a/services/blend/src/test_utils/epoch.rs +++ b/services/blend/src/test_utils/epoch.rs @@ -1,36 +1,12 @@ use async_trait::async_trait; use futures::{Stream, future::ready, stream::once}; use lb_blend::proofs::quota::inputs::prove::private::ProofOfLeadershipQuotaInputs; -use lb_chain_service::{Epoch, Slot}; -use lb_core::{crypto::ZkHash, proofs::leader_proof::LeaderPublic}; -use lb_groth16::{Field as _, Fr}; -use lb_ledger::EpochState; +use lb_chain_service::Epoch; +use lb_core::crypto::ZkHash; +use lb_groth16::Field as _; use overwatch::overwatch::OverwatchHandle; -use crate::epoch_info::{ChainApi, PolEpochInfo, PolInfoProvider}; - -pub fn default_epoch_state() -> EpochState { - use lb_ledger::UtxoTree; - - EpochState { - epoch: 1.into(), - nonce: ZkHash::ZERO, - total_stake: 1_000, - utxos: UtxoTree::new(), - lottery_0: Fr::ZERO, - lottery_1: Fr::ZERO, - } -} - -#[derive(Clone)] -pub struct TestChainService; - -#[async_trait] -impl ChainApi for TestChainService { - async fn get_epoch_state_for_slot(&self, _slot: Slot) -> Option { - Some(default_epoch_state()) - } -} +use crate::epoch_info::{PolEpochInfo, PolInfoProvider}; pub struct OncePolStreamProvider; @@ -43,14 +19,6 @@ impl PolInfoProvider for OncePolStreamProvid ) -> Option { Some(Box::new(once(ready(PolEpochInfo { epoch: Epoch::new(0), - poq_public_inputs: LeaderPublic { - slot: 1, - latest_root: Fr::ZERO, - lottery_0: Fr::ZERO, - lottery_1: Fr::ZERO, - epoch_nonce: ZkHash::ZERO, - aged_root: ZkHash::ZERO, - }, poq_private_inputs: ProofOfLeadershipQuotaInputs { slot: 1, note_value: 1, diff --git a/services/chain/broadcast-service/Cargo.toml b/services/chain/broadcast-service/Cargo.toml index f952ef11f..1a4504a07 100644 --- a/services/chain/broadcast-service/Cargo.toml +++ b/services/chain/broadcast-service/Cargo.toml @@ -13,12 +13,10 @@ version = { workspace = true } workspace = true [dependencies] -async-trait = { workspace = true } -derivative = { workspace = true } -futures = { workspace = true } -lb-core = { workspace = true } -overwatch = { workspace = true } -serde = { features = ["derive"], workspace = true } -tokio = { workspace = true } -tokio-stream = { workspace = true } -tracing = { workspace = true } +async-trait = { workspace = true } +derivative = { workspace = true } +lb-core = { workspace = true } +overwatch = { workspace = true } +serde = { features = ["derive"], workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } diff --git a/services/chain/broadcast-service/src/lib.rs b/services/chain/broadcast-service/src/lib.rs index ac13ef986..e4d6f4217 100644 --- a/services/chain/broadcast-service/src/lib.rs +++ b/services/chain/broadcast-service/src/lib.rs @@ -1,13 +1,9 @@ -use core::{fmt::Debug, pin::Pin}; -use std::{collections::HashMap, fmt::Display}; +use core::fmt::Debug; +use std::fmt::Display; use async_trait::async_trait; use derivative::Derivative; -use futures::{Stream, StreamExt as _, future::ready, stream::iter}; -use lb_core::{ - header::HeaderId, - sdp::{ProviderId, ProviderInfo, SessionNumber}, -}; +use lb_core::header::HeaderId; use overwatch::{ OpaqueServiceResourcesHandle, services::{ @@ -17,19 +13,10 @@ use overwatch::{ }; use serde::{Deserialize, Serialize}; use tokio::sync::{broadcast, oneshot}; -use tokio_stream::wrappers::BroadcastStream; use tracing::{error, info, trace}; const BROADCAST_CHANNEL_SIZE: usize = 128; -pub type SessionSubscription = Pin + Send + Sync>>; - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct SessionUpdate { - pub session_number: SessionNumber, - pub providers: HashMap, -} - #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct BlockInfo { pub height: u64, @@ -40,22 +27,14 @@ pub struct BlockInfo { #[derivative(Debug)] pub enum BlockBroadcastMsg { BroadcastFinalizedBlock(BlockInfo), - BroadcastBlendSession(SessionUpdate), SubscribeToFinalizedBlocks { result_sender: oneshot::Sender>, }, - SubscribeBlendSession { - #[derivative(Debug = "ignore")] - result_sender: oneshot::Sender, - }, } pub struct BlockBroadcastService { service_resources_handle: OpaqueServiceResourcesHandle, finalized_blocks: broadcast::Sender, - blend_session: broadcast::Sender, - // For sending latest session on subscription. - last_blend_session: Option, } impl ServiceData for BlockBroadcastService { @@ -75,13 +54,10 @@ where _initial_state: Self::State, ) -> Result { let (finalized_blocks, _) = broadcast::channel(BROADCAST_CHANNEL_SIZE); - let (blend_session, _) = broadcast::channel(BROADCAST_CHANNEL_SIZE); Ok(Self { service_resources_handle, finalized_blocks, - blend_session, - last_blend_session: None, }) } @@ -99,12 +75,6 @@ where trace!("No listener for finalized blocks. Not broadcasting. "); } } - BlockBroadcastMsg::BroadcastBlendSession(session) => { - self.last_blend_session = Some(session.clone()); - if self.blend_session.send(session).is_err() { - trace!("No listener for blend sessions. Not broadcasting. "); - } - } BlockBroadcastMsg::SubscribeToFinalizedBlocks { result_sender } => { // TODO: This naively broadcast what was sent from the chain service. In case // of LIB branch change (might happend during bootstrapping), blocks should be @@ -113,36 +83,9 @@ where error!("Could not subscribe to new blocks channel: {err:?}"); } } - BlockBroadcastMsg::SubscribeBlendSession { result_sender } => { - if result_sender - .send(create_session_stream( - self.last_blend_session.clone(), - &self.blend_session, - )) - .is_err() - { - error!("Could not subscribe to blend session channel."); - } - } } } Ok(()) } } - -/// Create a stream from the current optional, last-processed value and the -/// broadcast sender. -/// -/// The stream immediately yields the current value if `Some`, else it will wait -/// for the first `Ok` value as returned by the broadcast channel wrapper -/// stream. -fn create_session_stream( - current_value: Option, - sender: &broadcast::Sender, -) -> SessionSubscription { - Box::pin( - iter(current_value) - .chain(BroadcastStream::new(sender.subscribe()).filter_map(|item| ready(item.ok()))), - ) -} diff --git a/services/chain/chain-leader/src/leadership.rs b/services/chain/chain-leader/src/leadership.rs index 02597c9d2..ea596d62e 100644 --- a/services/chain/chain-leader/src/leadership.rs +++ b/services/chain/chain-leader/src/leadership.rs @@ -422,6 +422,7 @@ mod pol_tests { total_stake, lottery_0, lottery_1, + sdp: lb_ledger::mantle::sdp::SdpLedger::new(1.into()), }; // Create notifier channel (not used in this test) @@ -505,18 +506,17 @@ mod pol_tests { [( ServiceType::BlendNetwork, ServiceParameters { - lock_period: 10, - inactivity_period: 20, - retention_period: 100, - timestamp: 0, - session_duration: 10, + lock_period: 10.into(), + inactivity_period: 20.into(), + retention_period: 100.into(), + epoch: 0.into(), }, )] .into(), ), service_rewards_params: ServiceRewardsParameters { blend: RewardsParameters { - rounds_per_session: NonZero::new(10u64).unwrap(), + rounds_per_epoch: NonZero::new(10u64).unwrap(), message_frequency_per_round: NonNegativeF64::try_from(1.0).unwrap(), num_blend_layers: NonZero::new(3u64).unwrap(), minimum_network_size: NonZero::new(1u64).unwrap(), diff --git a/services/chain/chain-leader/src/lib.rs b/services/chain/chain-leader/src/lib.rs index cb33cb21a..dfc2aa1e8 100644 --- a/services/chain/chain-leader/src/lib.rs +++ b/services/chain/chain-leader/src/lib.rs @@ -11,7 +11,7 @@ use core::fmt::Debug; use std::{fmt::Display, iter, pin::Pin, time::Duration}; use futures::{StreamExt as _, stream}; -use lb_chain_network_service::api::ChainNetworkServiceData; +use lb_chain_network_service::api::{ChainNetworkServiceApi, ChainNetworkServiceData}; use lb_chain_service::{ Epoch, api::{CryptarchiaServiceApi, CryptarchiaServiceData}, @@ -44,7 +44,7 @@ use overwatch::{ use serde::{Deserialize, Serialize, de::DeserializeOwned}; use thiserror::Error; use tokio::sync::{oneshot, watch}; -use tracing::{Level, debug, error, info, instrument, span, trace}; +use tracing::{Level, error, info, instrument, span, trace}; use tracing_futures::Instrument as _; pub use crate::wallet::LeaderWalletConfig; @@ -315,6 +315,14 @@ where .expect("Failed to estabilish connection with Cryptarchia"), ); + let chain_network_api = ChainNetworkServiceApi::::new( + self.service_resources_handle + .overwatch_handle + .relay::() + .await + .expect("Failed to estabilish connection with ChainNetwork"), + ); + let LeaderSettings { config: ledger_config, transaction_selector_settings, @@ -459,7 +467,7 @@ where .await { Ok(block) => { - Self::publish_block_proposal(block, &blend_adapter).await; + Self::apply_and_publish_block_proposal(block, &chain_network_api, &blend_adapter).await; } Err(e) => { metrics::consensus_proposals_create_failed(); @@ -645,18 +653,20 @@ where Ok(block) } - /// Publish our own proposed block to the blend network. - async fn publish_block_proposal( + /// Apply our own proposed block to the chain and publish it to the blend + /// network. + async fn apply_and_publish_block_proposal( block: Block, + chain_network_api: &ChainNetworkServiceApi, blend_adapter: &BlendAdapter, ) { - // TODO: enable this once we elimnate sessions from Blend and so on - // Now we're disabling this to avoid a case which a proposing node - // transitions to a new session much earlier than other nodes. - debug!( - target: LOG_TARGET, header_id = ?block.header().id(), - "skipping self-applying block and just publishing it", - ); + if let Err(e) = chain_network_api + .apply_block_and_reconcile_mempool(block.clone()) + .await + { + error!(target: LOG_TARGET, "Failed to apply our own proposed block {:?}: {e:?}", block.header().id()); + return; + } blend_adapter.publish_proposal(block.to_proposal()).await; metrics::consensus_proposals_created_local(); diff --git a/services/chain/chain-network/src/bootstrap/ibd.rs b/services/chain/chain-network/src/bootstrap/ibd.rs index e82eeee7d..ca4c5ba1b 100644 --- a/services/chain/chain-network/src/bootstrap/ibd.rs +++ b/services/chain/chain-network/src/bootstrap/ibd.rs @@ -1120,34 +1120,37 @@ mod tests { #[must_use] fn ledger_config() -> lb_ledger::Config { + let epoch_config = EpochConfig { + epoch_stake_distribution_stabilization: NonZero::new(1).unwrap(), + epoch_period_nonce_buffer: NonZero::new(1).unwrap(), + epoch_period_nonce_stabilization: NonZero::new(1).unwrap(), + }; + let consensus_config = lb_cryptarchia_engine::Config::new( + NonZero::new(1).unwrap(), + NonNegativeRatio::new(1, 10.try_into().unwrap()), + 1f64.try_into().expect("1 > 0"), + ); + let epoch_length = epoch_config.epoch_length(consensus_config.base_period_length()); + lb_ledger::Config { - epoch_config: EpochConfig { - epoch_stake_distribution_stabilization: NonZero::new(1).unwrap(), - epoch_period_nonce_buffer: NonZero::new(1).unwrap(), - epoch_period_nonce_stabilization: NonZero::new(1).unwrap(), - }, - consensus_config: lb_cryptarchia_engine::Config::new( - NonZero::new(1).unwrap(), - NonNegativeRatio::new(1, 10.try_into().unwrap()), - 1f64.try_into().expect("1 > 0"), - ), + epoch_config, + consensus_config, sdp_config: lb_ledger::mantle::sdp::Config { service_params: Arc::new( [( ServiceType::BlendNetwork, ServiceParameters { - lock_period: 10, - inactivity_period: 20, - retention_period: 100, - timestamp: 0, - session_duration: 10, + lock_period: 10.into(), + inactivity_period: 20.into(), + retention_period: 100.into(), + epoch: 0.into(), }, )] .into(), ), service_rewards_params: ServiceRewardsParameters { blend: rewards::blend::RewardsParameters { - rounds_per_session: NonZeroU64::new(10).unwrap(), + rounds_per_epoch: epoch_length.try_into().unwrap(), message_frequency_per_round: NonNegativeF64::try_from(1.0).unwrap(), num_blend_layers: NonZeroU64::new(3).unwrap(), minimum_network_size: NonZeroU64::new(1).unwrap(), diff --git a/services/chain/chain-network/src/lib.rs b/services/chain/chain-network/src/lib.rs index f1400addc..52610c2f2 100644 --- a/services/chain/chain-network/src/lib.rs +++ b/services/chain/chain-network/src/lib.rs @@ -20,7 +20,6 @@ use lb_core::{ block::{Block, Proposal}, header::HeaderId, mantle::{AuthenticatedMantleTx, Transaction, TxHash}, - sdp::ServiceType, }; pub use lb_cryptarchia_engine::{Epoch, Slot}; pub use lb_ledger::EpochState; @@ -76,8 +75,6 @@ pub enum Error { Mempool(String), #[error("Block header id not found: {0}")] HeaderIdNotFound(HeaderId), - #[error("Service session not found: {0:?}")] - ServiceSessionNotFound(ServiceType), } #[derive(Debug)] diff --git a/services/chain/chain-service/Cargo.toml b/services/chain/chain-service/Cargo.toml index da7153c44..29174a4a5 100644 --- a/services/chain/chain-service/Cargo.toml +++ b/services/chain/chain-service/Cargo.toml @@ -32,7 +32,6 @@ lb-utils = { workspace = true } overwatch = { workspace = true } serde = { workspace = true } serde_with = { workspace = true } -strum = { workspace = true } thiserror = { workspace = true } time = { workspace = true } tokio = { workspace = true } diff --git a/services/chain/chain-service/src/lib.rs b/services/chain/chain-service/src/lib.rs index f229f5ebe..98fe01210 100644 --- a/services/chain/chain-service/src/lib.rs +++ b/services/chain/chain-service/src/lib.rs @@ -11,7 +11,7 @@ mod tests; use core::fmt::Debug; use std::{ - collections::{BTreeMap, HashMap, HashSet}, + collections::{BTreeMap, HashSet}, fmt::Display, path::PathBuf, pin::Pin, @@ -20,12 +20,8 @@ use std::{ use bytes::Bytes; use derivative::Derivative; -use futures::{ - FutureExt as _, Stream, StreamExt as _, TryStreamExt as _, future::join_all, stream, -}; -use lb_chain_broadcast_service::{ - BlockBroadcastMsg, BlockBroadcastService, BlockInfo, SessionUpdate, -}; +use futures::{Stream, StreamExt as _, TryStreamExt as _, future::join_all, stream}; +use lb_chain_broadcast_service::{BlockBroadcastMsg, BlockBroadcastService, BlockInfo}; use lb_core::{ block::{Block, genesis::GenesisBlock}, events::Events, @@ -34,7 +30,7 @@ use lb_core::{ AuthenticatedMantleTx, GenesisTx as _, Transaction, TxHash, gas::MainnetGasConstants, tx::GasPrices, }, - sdp::{Declaration, DeclarationId, ProviderId, ProviderInfo, ServiceType}, + sdp::{Declaration, DeclarationId}, }; use lb_cryptarchia_engine::{Branch, PrunedBlocks, ReorgedBlocks}; pub use lb_cryptarchia_engine::{Epoch, Slot, State}; @@ -55,7 +51,6 @@ use overwatch::{ use relays::BroadcastRelay; use serde::{Deserialize, Serialize, de::DeserializeOwned}; use serde_with::serde_as; -use strum::IntoEnumIterator as _; use thiserror::Error; use time::OffsetDateTime; use tokio::{ @@ -108,8 +103,6 @@ pub enum Error { HeaderIdNotFound(HeaderId), #[error("Parent header ID not found for child={0}")] ParentIdNotFound(HeaderId), - #[error("Service session not found: {0:?}")] - ServiceSessionNotFound(ServiceType), } #[derive(Derivative)] @@ -474,33 +467,6 @@ impl Cryptarchia { pub fn has_block(&self, block_id: &HeaderId) -> bool { self.consensus.branches().get(block_id).is_some() } - - fn active_session_providers( - &self, - block_id: &HeaderId, - service_type: ServiceType, - ) -> Result, Error> { - let ledger = self - .ledger - .state(block_id) - .ok_or(Error::HeaderIdNotFound(*block_id))?; - - ledger - .active_session_providers(service_type) - .ok_or(Error::ServiceSessionNotFound(service_type)) - } - - fn active_sessions_numbers( - &self, - block_id: &HeaderId, - ) -> Result, Error> { - let ledger = self - .ledger - .state(block_id) - .ok_or(Error::HeaderIdNotFound(*block_id))?; - - Ok(ledger.active_sessions()) - } } #[derive(Debug, Deserialize, Serialize, Clone)] @@ -649,6 +615,7 @@ where current_slot, ) .await; + // These are blocks that have been pruned by the cryptarchia engine but have not // yet been deleted from the storage layer. let mut storage_blocks_to_remove = Self::delete_stale_blocks_from_storage( @@ -789,8 +756,8 @@ where } } - Some(lb_time_service::SlotTick { slot, .. }) = slot_timer.next() => { - current_slot = slot; + Some(lb_time_service::SlotTick { slot: new_slot, .. }) = slot_timer.next() => { + current_slot = new_slot; } _ = state_recording_timer.tick() => { @@ -876,6 +843,7 @@ where Ok((current_slot, slot_timer)) } + #[expect(clippy::too_many_lines, reason = "TODO: refactor into funcs")] async fn process_message( cryptarchia: &Cryptarchia, new_block_channel: &broadcast::Sender, @@ -931,9 +899,15 @@ where let declarations = cryptarchia .ledger .state(&tip) - .map(LedgerState::sdp_declarations) - .unwrap_or_default(); - + .map(|ledger_state| ledger_state.mantle_ledger().sdp.declarations()) + .unwrap_or_default() + .iter() + .flat_map(|(_, declarations)| { + declarations + .iter() + .map(|(id, declaration)| (*id, declaration.clone())) + }) + .collect(); reply_channel.send(declarations).unwrap_or_else(|_| { error!("Could not send SDP declarations through channel"); }); @@ -1071,14 +1045,6 @@ where let header = block.header(); let prev_lib = cryptarchia.lib(); - let previous_session_numbers = match cryptarchia.active_sessions_numbers(&prev_lib) { - Ok(session_numbers) => session_numbers, - Err(e) => { - warn!("Error getting previous session numbers: {e}"); - ServiceType::iter().map(|s| (s, 0)).collect() - } - }; - let (pruned_blocks, reorged_blocks, events) = cryptarchia.try_apply_block(&block, current_slot)?; let new_lib = cryptarchia.lib(); @@ -1150,14 +1116,6 @@ where if let Err(e) = lib_broadcaster.send(lib_update) { warn!("No LIB-update subscribers to notify: {e}"); } - - Self::broadcast_session_updates_for_block( - cryptarchia, - &new_lib, - relays, - Some(&previous_session_numbers), - ) - .await; } let reorged_txs: Vec<_> = join_all( @@ -1338,7 +1296,6 @@ where if let Err(e) = self.new_block_subscription_sender.send(init_event) { warn!("No new-block subscribers to notify: {e}"); } - Self::broadcast_session_updates_for_block(&cryptarchia, &init_tip.id(), relays, None).await; // Phase 1: Collect only block IDs in (LIB, tip]. info!( @@ -1585,74 +1542,6 @@ where (cryptarchia, storage_blocks_to_remove) } - - async fn broadcast_session_updates_for_block( - cryptarchia: &Cryptarchia, - block_id: &HeaderId, - relays: &CryptarchiaConsensusRelays, - previous_sessions: Option<&HashMap>, - ) { - let Ok(new_sessions) = cryptarchia.active_sessions_numbers(block_id) else { - error!("Could not get active session numbers for block {block_id:?}"); - return; - }; - - for (service, new_session_number) in &new_sessions { - Self::handle_service_update( - cryptarchia, - block_id, - relays, - previous_sessions, - service, - new_session_number, - ) - .await; - } - } - - async fn handle_service_update( - cryptarchia: &Cryptarchia, - block_id: &HeaderId, - relays: &CryptarchiaConsensusRelays, - previous_sessions: Option<&HashMap>, - service: &ServiceType, - new_session_number: &u64, - ) { - // If `previous_sessions` is provided, check if the session number has changed. - // Otherwise, always broadcast (for initialization). - if previous_sessions.is_some_and(|prev| { - prev.get(service) - .copied() - .expect("previous session number is set") - == *new_session_number - }) { - return; - } - - match cryptarchia.active_session_providers(block_id, *service) { - Ok(providers) => { - let update = SessionUpdate { - session_number: *new_session_number, - providers, - }; - - let broadcast_relay = relays.broadcast_relay(); - - let broadcast_future = match service { - ServiceType::BlendNetwork => { - broadcast_blend_session(broadcast_relay, update).boxed() - } - }; - - if let Err(e) = broadcast_future.await { - error!("Failed to broadcast session update for {service:?}: {e}"); - } - } - Err(e) => { - error!("Could not get session providers for service {service:?}: {e}"); - } - } - } } async fn broadcast_finalized_block( @@ -1664,13 +1553,3 @@ async fn broadcast_finalized_block( .await .map_err(|(error, _)| Box::new(error) as DynError) } - -async fn broadcast_blend_session( - broadcast_relay: &BroadcastRelay, - session: SessionUpdate, -) -> Result<(), DynError> { - broadcast_relay - .send(BlockBroadcastMsg::BroadcastBlendSession(session)) - .await - .map_err(|(error, _)| Box::new(error) as DynError) -} diff --git a/services/chain/chain-service/src/relays.rs b/services/chain/chain-service/src/relays.rs index b1602da94..50f65ad52 100644 --- a/services/chain/chain-service/src/relays.rs +++ b/services/chain/chain-service/src/relays.rs @@ -55,6 +55,7 @@ where ::Tx: From + AsRef<[u8]>, ::Block: TryFrom> + TryInto>, ::Events: TryFrom + TryInto, + RuntimeServiceId: 'static, { pub async fn new( broadcast_relay: BroadcastRelay, diff --git a/services/chain/chain-service/src/states.rs b/services/chain/chain-service/src/states.rs index 65828ccc3..f6efb704a 100644 --- a/services/chain/chain-service/src/states.rs +++ b/services/chain/chain-service/src/states.rs @@ -78,7 +78,6 @@ impl ServiceState for CryptarchiaConsensusState { &settings.config, genesis_tx.cryptarchia_parameter().epoch_nonce, )?; - // TODO: store genesis block and events to DB: https://github.com/logos-blockchain/logos-blockchain/issues/2747 (lib_id, lib_id, ledger) } StartingState::Lib { @@ -132,30 +131,33 @@ mod tests { NonNegativeRatio::new(1, 10.try_into().unwrap()), 1f64.try_into().expect("1 > 0"), ); + let epoch_config = lb_cryptarchia_engine::EpochConfig { + epoch_stake_distribution_stabilization: 1.try_into().unwrap(), + epoch_period_nonce_buffer: 1.try_into().unwrap(), + epoch_period_nonce_stabilization: 1.try_into().unwrap(), + }; + let epoch_length = + epoch_config.epoch_length(cryptarchia_engine_config.base_period_length()); + let ledger_config = lb_ledger::Config { - epoch_config: lb_cryptarchia_engine::EpochConfig { - epoch_stake_distribution_stabilization: 1.try_into().unwrap(), - epoch_period_nonce_buffer: 1.try_into().unwrap(), - epoch_period_nonce_stabilization: 1.try_into().unwrap(), - }, + epoch_config, consensus_config: cryptarchia_engine_config.clone(), sdp_config: lb_ledger::mantle::sdp::Config { service_params: Arc::new( [( ServiceType::BlendNetwork, ServiceParameters { - lock_period: 10, - inactivity_period: 20, - retention_period: 100, - timestamp: 0, - session_duration: 10, + lock_period: 10.into(), + inactivity_period: 20.into(), + retention_period: 100.into(), + epoch: 0.into(), }, )] .into(), ), service_rewards_params: ServiceRewardsParameters { blend: rewards::blend::RewardsParameters { - rounds_per_session: NonZeroU64::new(10).unwrap(), + rounds_per_epoch: epoch_length.try_into().unwrap(), message_frequency_per_round: NonNegativeF64::try_from(1.0).unwrap(), num_blend_layers: NonZeroU64::new(3).unwrap(), minimum_network_size: NonZeroU64::new(1).unwrap(), @@ -256,36 +258,40 @@ mod tests { #[expect(clippy::too_many_lines, reason = "Test function")] fn restore_preserves_info() { let genesis_header_id: HeaderId = [0; 32].into(); + let security_param: NonZero = 2.try_into().unwrap(); let cryptarchia_engine_config = lb_cryptarchia_engine::Config::new( security_param, NonNegativeRatio::new(1, 10.try_into().unwrap()), 1f64.try_into().expect("1 > 0"), ); + let epoch_config = lb_cryptarchia_engine::EpochConfig { + epoch_stake_distribution_stabilization: 1.try_into().unwrap(), + epoch_period_nonce_buffer: 1.try_into().unwrap(), + epoch_period_nonce_stabilization: 1.try_into().unwrap(), + }; + let epoch_length = + epoch_config.epoch_length(cryptarchia_engine_config.base_period_length()); + let ledger_config = lb_ledger::Config { - epoch_config: lb_cryptarchia_engine::EpochConfig { - epoch_stake_distribution_stabilization: 1.try_into().unwrap(), - epoch_period_nonce_buffer: 1.try_into().unwrap(), - epoch_period_nonce_stabilization: 1.try_into().unwrap(), - }, + epoch_config, consensus_config: cryptarchia_engine_config.clone(), sdp_config: lb_ledger::mantle::sdp::Config { service_params: Arc::new( [( ServiceType::BlendNetwork, ServiceParameters { - lock_period: 10, - inactivity_period: 20, - retention_period: 100, - timestamp: 0, - session_duration: 10, + lock_period: 10.into(), + inactivity_period: 20.into(), + retention_period: 100.into(), + epoch: 0.into(), }, )] .into(), ), service_rewards_params: ServiceRewardsParameters { blend: rewards::blend::RewardsParameters { - rounds_per_session: NonZeroU64::new(10).unwrap(), + rounds_per_epoch: epoch_length.try_into().unwrap(), message_frequency_per_round: NonNegativeF64::try_from(1.0).unwrap(), num_blend_layers: NonZeroU64::new(3).unwrap(), minimum_network_size: NonZeroU64::new(1).unwrap(), diff --git a/services/chain/chain-service/src/tests/mod.rs b/services/chain/chain-service/src/tests/mod.rs index d0baa0d63..235a2535d 100644 --- a/services/chain/chain-service/src/tests/mod.rs +++ b/services/chain/chain-service/src/tests/mod.rs @@ -240,35 +240,37 @@ async fn get_block_ids() { } #[must_use] -fn ledger_config(security_param: NonZero) -> lb_ledger::Config { +pub fn ledger_config(security_param: NonZero) -> lb_ledger::Config { let mut service_params = HashMap::new(); service_params.insert( lb_core::sdp::ServiceType::BlendNetwork, ServiceParameters { - lock_period: 10, - inactivity_period: 1, - retention_period: 1, - timestamp: 0, - session_duration: 10, + lock_period: 10.into(), + inactivity_period: 1.into(), + retention_period: 1.into(), + epoch: 0.into(), }, ); + let epoch_config = EpochConfig { + epoch_stake_distribution_stabilization: 3.try_into().unwrap(), + epoch_period_nonce_buffer: 3.try_into().unwrap(), + epoch_period_nonce_stabilization: 4.try_into().unwrap(), + }; + let consensus_config = lb_cryptarchia_engine::Config::new( + security_param, + NonNegativeRatio::new(1, 10.try_into().unwrap()), + 1.0.try_into().unwrap(), + ); + let epoch_length = epoch_config.epoch_length(consensus_config.base_period_length()); lb_ledger::Config { - epoch_config: EpochConfig { - epoch_stake_distribution_stabilization: 3.try_into().unwrap(), - epoch_period_nonce_buffer: 3.try_into().unwrap(), - epoch_period_nonce_stabilization: 4.try_into().unwrap(), - }, - consensus_config: lb_cryptarchia_engine::Config::new( - security_param, - NonNegativeRatio::new(1, 10.try_into().unwrap()), - 1.0.try_into().unwrap(), - ), + epoch_config, + consensus_config, sdp_config: lb_ledger::mantle::sdp::Config { service_params: Arc::new(service_params), service_rewards_params: ServiceRewardsParameters { blend: rewards::blend::RewardsParameters { - rounds_per_session: 10.try_into().unwrap(), + rounds_per_epoch: epoch_length.try_into().unwrap(), message_frequency_per_round: 1.0.try_into().unwrap(), num_blend_layers: 3.try_into().unwrap(), minimum_network_size: 1.try_into().unwrap(), @@ -286,7 +288,7 @@ fn ledger_config(security_param: NonZero) -> lb_ledger::Config { } /// Builds a block by grinding through slots -fn try_build_block( +pub fn try_build_block( cryptarchia: &Cryptarchia, parent: HeaderId, utxo: Utxo, @@ -340,7 +342,7 @@ fn try_build_block( None } -fn utxo() -> (ZkKey, Utxo) { +pub fn utxo() -> (ZkKey, Utxo) { let mut op_id = [0u8; 32]; thread_rng().fill_bytes(&mut op_id); let zk_sk = ZkKey::from(Fr::ZERO); @@ -352,7 +354,7 @@ fn utxo() -> (ZkKey, Utxo) { (zk_sk, utxo) } -fn spawn_storage_service( +pub fn spawn_storage_service( mut rx: mpsc::Receiver>, ) -> (JoinHandle<()>, TempDir) { let db_dir = TempDir::new().unwrap(); @@ -376,7 +378,7 @@ fn spawn_storage_service( (handle, db_dir) } -struct TestRuntimeServiceId; +pub struct TestRuntimeServiceId; impl AsServiceId> for TestRuntimeServiceId diff --git a/services/network/src/backends/libp2p/swarm/gossipsub.rs b/services/network/src/backends/libp2p/swarm/gossipsub.rs index 2badb4fb9..de4e66242 100644 --- a/services/network/src/backends/libp2p/swarm/gossipsub.rs +++ b/services/network/src/backends/libp2p/swarm/gossipsub.rs @@ -70,14 +70,8 @@ impl SwarmHandler { target: LOG_TARGET, "Broadcasted message with id: {id} to topic: {topic}" ); - // self-notification because libp2p doesn't do it. - // Only do this on first attempt; if a previous attempt already - // injected the message locally due to InsufficientPeers, avoid - // notifying local subscribers twice. - // TODO: Remove this logic once we start re-applying blocks produced locally. In - // that case, we don't need to bubble this up since we have already applied the - // block before broadcasting it. - if retry_count == 0 && self.swarm.is_subscribed(&topic) { + // self-notification because libp2p doesn't do it + if self.swarm.is_subscribed(&topic) { log_error!(self.pubsub_messages_tx.send(gossipsub::Message { source: None, data: message.into(), @@ -87,21 +81,6 @@ impl SwarmHandler { } } Err(gossipsub::PublishError::InsufficientPeers) if retry_count < MAX_RETRY => { - // TODO: Remove this logic once we start re-applying blocks produced locally. In - // that case, we don't need to bubble this up since we have already applied the - // block before broadcasting it. In single-node or transiently isolated setups, - // publish can fail with InsufficientPeers. Still surface the - // message locally once so higher layers can progress while - // retries continue for eventual dissemination. - if retry_count == 0 && self.swarm.is_subscribed(&topic) { - log_error!(self.pubsub_messages_tx.send(gossipsub::Message { - source: None, - data: message.clone().into(), - sequence_number: None, - topic: topic_hash(&topic), - })); - } - let wait = exp_backoff(retry_count); tracing::trace!( target: LOG_TARGET, diff --git a/services/storage/src/api/backend/rocksdb/membership.rs b/services/storage/src/api/backend/rocksdb/membership.rs index 859bad699..72b707054 100644 --- a/services/storage/src/api/backend/rocksdb/membership.rs +++ b/services/storage/src/api/backend/rocksdb/membership.rs @@ -1,95 +1,20 @@ -use std::collections::{BTreeSet, HashMap}; - use async_trait::async_trait; -use lb_core::{ - block::BlockNumber, - codec::{DeserializeOp as _, SerializeOp as _}, - sdp::{Locator, ProviderId, ServiceType, SessionNumber}, -}; +use lb_core::block::BlockNumber; use lb_log_targets::storage; use overwatch::DynError; use tracing::{debug, error}; use crate::{ - api::{backend::rocksdb::utils::key_bytes, membership::StorageMembershipApi}, + api::membership::StorageMembershipApi, backends::{StorageBackend as _, rocksdb::RocksBackend}, }; -pub const MEMBERSHIP_ACTIVE_SESSION_PREFIX: &str = "membership/active/"; -pub const MEMBERSHIP_NEXT_SESSION_PREFIX: &str = "membership/next/"; pub const MEMBERSHIP_LATEST_BLOCK_KEY: &str = "membership/latest_block"; -type MembershipProviders = (SessionNumber, HashMap>); - const LOG_TARGET: &str = storage::rocksdb::MEMBERSHIP; #[async_trait] impl StorageMembershipApi for RocksBackend { - async fn save_active_session( - &mut self, - service_type: ServiceType, - session_id: SessionNumber, - providers: &HashMap>, - ) -> Result<(), DynError> { - let service_bytes = service_type - .to_bytes() - .expect("Serialization of ServiceType should not fail"); - let key = key_bytes(MEMBERSHIP_ACTIVE_SESSION_PREFIX, service_bytes); - - let session_data = (session_id, providers); - let serialized_data = session_data - .to_bytes() - .expect("Serialization of session data should not fail"); - - match self.store(key, serialized_data).await { - Ok(()) => { - debug!( - target: LOG_TARGET, - "Successfully stored active session {} for service {:?}", - session_id, service_type - ); - Ok(()) - } - Err(e) => { - error!(target: LOG_TARGET, "Failed to store active session: {:?}", e); - Err(e.into()) - } - } - } - - async fn load_active_session( - &mut self, - service_type: ServiceType, - ) -> Result, DynError> { - let service_bytes = service_type - .to_bytes() - .expect("Serialization of ServiceType should not fail"); - let key = key_bytes(MEMBERSHIP_ACTIVE_SESSION_PREFIX, service_bytes); - - let data = self.load(&key).await?; - - data.map_or_else( - || { - debug!(target: LOG_TARGET, "No active session found for service {:?}", service_type); - Ok(None) - }, - |bytes| match MembershipProviders::from_bytes(&bytes) { - Ok(session_data) => { - debug!( - target: LOG_TARGET, - "Successfully loaded active session for service {:?}", - service_type - ); - Ok(Some(session_data)) - } - Err(e) => { - error!(target: LOG_TARGET, "Failed to deserialize active session: {:?}", e); - Ok(None) - } - }, - ) - } - async fn save_latest_block(&mut self, block_number: BlockNumber) -> Result<(), DynError> { let block_bytes = block_number.to_le_bytes(); @@ -136,69 +61,4 @@ impl StorageMembershipApi for RocksBackend { } } } - - async fn save_next_session( - &mut self, - service_type: ServiceType, - session_id: SessionNumber, - providers: &HashMap>, - ) -> Result<(), DynError> { - let service_bytes = service_type - .to_bytes() - .expect("Serialization of ServiceType should not fail"); - let key = key_bytes(MEMBERSHIP_NEXT_SESSION_PREFIX, service_bytes); - - let session_data = (session_id, providers); - let serialized_data = session_data - .to_bytes() - .expect("Serialization of session data should not fail"); - - match self.store(key, serialized_data).await { - Ok(()) => { - debug!( - target: LOG_TARGET, - "Successfully stored next session {} for service {:?}", - session_id, service_type - ); - Ok(()) - } - Err(e) => { - error!(target: LOG_TARGET, "Failed to store next session: {:?}", e); - Err(e.into()) - } - } - } - - async fn load_next_session( - &mut self, - service_type: ServiceType, - ) -> Result, DynError> { - let service_bytes = service_type - .to_bytes() - .expect("Serialization of ServiceType should not fail"); - let key = key_bytes(MEMBERSHIP_NEXT_SESSION_PREFIX, service_bytes); - - let data = self.load(&key).await?; - - data.map_or_else( - || { - debug!(target: LOG_TARGET, "No next session found for service {:?}", service_type); - Ok(None) - }, - |bytes| match ::from_bytes(&bytes) { - Ok(session_data) => { - debug!( - target: LOG_TARGET, - "Successfully loaded next session for service {:?}", - service_type - ); - Ok(Some(session_data)) - } - Err(e) => { - error!(target: LOG_TARGET, "Failed to deserialize next session: {:?}", e); - Ok(None) - } - }, - ) - } } diff --git a/services/storage/src/api/membership/mod.rs b/services/storage/src/api/membership/mod.rs index 8e9319770..617be0483 100644 --- a/services/storage/src/api/membership/mod.rs +++ b/services/storage/src/api/membership/mod.rs @@ -1,41 +1,12 @@ -use std::collections::{BTreeSet, HashMap}; - use async_trait::async_trait; -use lb_core::{ - block::BlockNumber, - sdp::{Locator, ProviderId, ServiceType, SessionNumber}, -}; +use lb_core::block::BlockNumber; use overwatch::DynError; pub mod requests; #[async_trait] pub trait StorageMembershipApi { - async fn save_active_session( - &mut self, - service_type: ServiceType, - session_id: SessionNumber, - providers: &HashMap>, - ) -> Result<(), DynError>; - - async fn load_active_session( - &mut self, - service_type: ServiceType, - ) -> Result>)>, DynError>; - async fn save_latest_block(&mut self, block_number: BlockNumber) -> Result<(), DynError>; async fn load_latest_block(&mut self) -> Result, DynError>; - - async fn save_next_session( - &mut self, - service_type: ServiceType, - session_id: SessionNumber, - providers: &HashMap>, - ) -> Result<(), DynError>; - - async fn load_next_session( - &mut self, - service_type: ServiceType, - ) -> Result>)>, DynError>; } diff --git a/services/storage/src/api/membership/requests.rs b/services/storage/src/api/membership/requests.rs index cda71963e..541b2fa0d 100644 --- a/services/storage/src/api/membership/requests.rs +++ b/services/storage/src/api/membership/requests.rs @@ -1,9 +1,4 @@ -use std::collections::{BTreeSet, HashMap}; - -use lb_core::{ - block::BlockNumber, - sdp::{Locator, ProviderId, ServiceType, SessionNumber}, -}; +use lb_core::block::BlockNumber; use tokio::sync::oneshot::Sender; use crate::{ @@ -12,33 +7,13 @@ use crate::{ backends::StorageBackend, }; -pub type SessionSender = Sender>)>>; - pub enum MembershipApiRequest { - SaveActiveSession { - service_type: ServiceType, - session_id: SessionNumber, - providers: HashMap>, - }, - LoadActiveSession { - service_type: ServiceType, - response_tx: SessionSender, - }, SaveLatestBlock { block_number: BlockNumber, }, LoadLatestBlock { response_tx: Sender>, }, - SaveNextSession { - service_type: ServiceType, - session_id: SessionNumber, - providers: HashMap>, - }, - LoadNextSession { - service_type: ServiceType, - response_tx: SessionSender, - }, } impl StorageOperation for MembershipApiRequest @@ -47,64 +22,16 @@ where { async fn execute(self, backend: &mut Backend) -> Result<(), StorageServiceError> { match self { - Self::SaveActiveSession { - service_type, - session_id, - providers, - } => handle_save_active_session(backend, service_type, session_id, providers).await, - Self::LoadActiveSession { - service_type, - response_tx, - } => handle_load_active_session(backend, service_type, response_tx).await, Self::SaveLatestBlock { block_number } => { handle_save_latest_block(backend, block_number).await } Self::LoadLatestBlock { response_tx } => { handle_load_latest_block(backend, response_tx).await } - Self::SaveNextSession { - service_type, - session_id, - providers, - } => handle_save_next_session(backend, service_type, session_id, providers).await, - Self::LoadNextSession { - service_type, - response_tx, - } => handle_load_next_session(backend, service_type, response_tx).await, } } } -async fn handle_save_active_session( - backend: &mut Backend, - service_type: ServiceType, - session_id: SessionNumber, - providers: HashMap>, -) -> Result<(), StorageServiceError> { - backend - .save_active_session(service_type, session_id, &providers) - .await - .map_err(StorageServiceError::BackendError) -} - -async fn handle_load_active_session( - backend: &mut Backend, - service_type: ServiceType, - response_tx: SessionSender, -) -> Result<(), StorageServiceError> { - let result = backend - .load_active_session(service_type) - .await - .map_err(StorageServiceError::BackendError)?; - - if response_tx.send(result).is_err() { - return Err(StorageServiceError::ReplyError { - message: "Failed to send reply for load active session request".to_owned(), - }); - } - Ok(()) -} - async fn handle_save_latest_block( backend: &mut Backend, block_number: BlockNumber, @@ -131,33 +58,3 @@ async fn handle_load_latest_block( - backend: &mut Backend, - service_type: ServiceType, - session_id: SessionNumber, - providers: HashMap>, -) -> Result<(), StorageServiceError> { - backend - .save_next_session(service_type, session_id, &providers) - .await - .map_err(StorageServiceError::BackendError) -} - -async fn handle_load_next_session( - backend: &mut Backend, - service_type: ServiceType, - response_tx: SessionSender, -) -> Result<(), StorageServiceError> { - let result = backend - .load_next_session(service_type) - .await - .map_err(StorageServiceError::BackendError)?; - - if response_tx.send(result).is_err() { - return Err(StorageServiceError::ReplyError { - message: "Failed to send reply for load next session request".to_owned(), - }); - } - Ok(()) -} diff --git a/services/storage/src/backends/mock.rs b/services/storage/src/backends/mock.rs index 6f3c0128f..e2a16130e 100644 --- a/services/storage/src/backends/mock.rs +++ b/services/storage/src/backends/mock.rs @@ -7,7 +7,7 @@ use std::{ use async_trait::async_trait; use bytes::Bytes; use lb_core::{ - block::{BlockNumber, SessionNumber}, + block::BlockNumber, header::HeaderId, sdp::{Locator, ProviderId, ServiceType}, }; @@ -182,22 +182,6 @@ impl StorageBackendApi for MockStorage {} #[async_trait] impl StorageMembershipApi for MockStorage { - async fn save_active_session( - &mut self, - _service_type: ServiceType, - _session_id: SessionNumber, - _providers: &HashMap>, - ) -> Result<(), DynError> { - unimplemented!() - } - - async fn load_active_session( - &mut self, - _service_type: ServiceType, - ) -> Result>)>, DynError> { - unimplemented!() - } - async fn save_latest_block(&mut self, _block_number: BlockNumber) -> Result<(), DynError> { unimplemented!() } @@ -205,20 +189,4 @@ impl StorageMembershipApi for MockStorage { async fn load_latest_block(&mut self) -> Result, DynError> { unimplemented!() } - - async fn save_next_session( - &mut self, - _service_type: ServiceType, - _session_id: SessionNumber, - _providers: &HashMap>, - ) -> Result<(), DynError> { - unimplemented!() - } - - async fn load_next_session( - &mut self, - _service_type: ServiceType, - ) -> Result>)>, DynError> { - unimplemented!() - } } diff --git a/services/storage/src/lib.rs b/services/storage/src/lib.rs index 3762bf5bb..489f6658c 100644 --- a/services/storage/src/lib.rs +++ b/services/storage/src/lib.rs @@ -3,7 +3,6 @@ pub mod backends; mod metrics; use std::{ - collections::{BTreeSet, HashMap}, fmt::{Debug, Display, Formatter}, marker::PhantomData, num::NonZeroUsize, @@ -16,7 +15,6 @@ use bytes::Bytes; use lb_core::{ block::BlockNumber, codec::{DeserializeOp as _, SerializeOp as _}, - sdp::{Locator, ProviderId, ServiceType, SessionNumber}, }; use lb_log_targets::storage; use overwatch::{ @@ -29,10 +27,7 @@ use overwatch::{ use serde::{Serialize, de::DeserializeOwned}; use tokio::sync::oneshot::Sender; -use crate::api::{ - StorageApiRequest, StorageOperation, - membership::requests::{MembershipApiRequest, SessionSender}, -}; +use crate::api::{StorageApiRequest, StorageOperation, membership::requests::MembershipApiRequest}; const LOG_TARGET: &str = storage::ROOT; @@ -149,34 +144,6 @@ impl StorageMsg { ) } - #[must_use] - pub const fn save_active_session_request( - service_type: ServiceType, - session_id: SessionNumber, - providers: HashMap>, - ) -> Self { - Self::Api { - request: StorageApiRequest::Membership(MembershipApiRequest::SaveActiveSession { - service_type, - session_id, - providers, - }), - } - } - - #[must_use] - pub const fn load_active_session_request( - service_type: ServiceType, - response_tx: SessionSender, - ) -> Self { - Self::Api { - request: StorageApiRequest::Membership(MembershipApiRequest::LoadActiveSession { - service_type, - response_tx, - }), - } - } - #[must_use] pub const fn save_latest_block_request(block_number: BlockNumber) -> Self { Self::Api { @@ -194,34 +161,6 @@ impl StorageMsg { }), } } - - #[must_use] - pub const fn save_next_session_request( - service_type: ServiceType, - session_id: SessionNumber, - providers: HashMap>, - ) -> Self { - Self::Api { - request: StorageApiRequest::Membership(MembershipApiRequest::SaveNextSession { - service_type, - session_id, - providers, - }), - } - } - - #[must_use] - pub const fn load_next_session_request( - service_type: ServiceType, - response_tx: SessionSender, - ) -> Self { - Self::Api { - request: StorageApiRequest::Membership(MembershipApiRequest::LoadNextSession { - service_type, - response_tx, - }), - } - } } // Implement `Debug` manually to avoid constraining `Backend` to `Debug` diff --git a/tests/src/common/manual_cluster.rs b/tests/src/common/manual_cluster.rs index c869820d8..25d0bc3b5 100644 --- a/tests/src/common/manual_cluster.rs +++ b/tests/src/common/manual_cluster.rs @@ -12,6 +12,7 @@ use lb_testing_framework::{ USER_CONFIG_FILE, internal::DeploymentPlan, is_truthy_env, record_system_monitor_event, register_system_monitor_output_file, unregister_system_monitor_output_file, }; +use lb_zone_sdk::Slot; use reqwest::Url; use tempfile::TempDir; use testing_framework_core::scenario::{DynError, PeerSelection, StartNodeOptions, StartedNode}; @@ -253,6 +254,33 @@ pub async fn wait_for_nodes_height( } } +pub async fn wait_for_tip_slot( + client: &NodeHttpClient, + slot: Slot, + timeout: Duration, +) -> Result<(), Elapsed> { + tokio::time::timeout(timeout, async { + loop { + if let Ok(info) = client.consensus_info().await + && info.cryptarchia_info.slot >= slot + { + return; + } + + tokio::time::sleep(Duration::from_millis(500)).await; + } + }) + .await +} + +pub async fn wait_for_nodes_tip_slot(nodes: &[&NodeHttpClient], slot: Slot, duration: Duration) { + for node in nodes { + wait_for_tip_slot(node, slot, duration) + .await + .unwrap_or_else(|_| panic!("node should reach tip slot {slot:?}")); + } +} + pub async fn get_wallet_balance(node: &NodeHttpClient, pk: ZkPublicKey) -> u64 { let pk_hex = hex::encode(lb_groth16::fr_to_bytes(&pk.into())); let url = api_url(node, &format!("wallet/{pk_hex}/balance")); diff --git a/tests/src/tests/mantle/sdp/activity.rs b/tests/src/tests/mantle/sdp/activity.rs index 0efdb08c9..b4fafe630 100644 --- a/tests/src/tests/mantle/sdp/activity.rs +++ b/tests/src/tests/mantle/sdp/activity.rs @@ -1,4 +1,5 @@ use std::{ + collections::HashMap, num::NonZero, path::PathBuf, sync::{ @@ -8,13 +9,15 @@ use std::{ time::Duration, }; -use lb_core::sdp::ServiceType; +use lb_chain_service::Epoch; +use lb_core::sdp::{Declaration, NumberOfEpochs, ProviderId, ServiceType}; use lb_node::config::{RunConfig, cryptarchia::deployment::EpochConfig}; use lb_testing_framework::{DeploymentBuilder, NodeHttpClient, TopologyConfig as TfTopologyConfig}; use lb_utils::math::NonNegativeRatio; +use lb_zone_sdk::Slot; use logos_blockchain_tests::{ common::manual_cluster::{ - ManualNodeLayout, start_local_manual_cluster_with_layout, wait_for_nodes_height, + ManualNodeLayout, start_local_manual_cluster_with_layout, wait_for_nodes_tip_slot, }, cucumber::defaults::E2E_ARTIFACTS_DIR, }; @@ -28,12 +31,12 @@ const NODE_COUNT: usize = 2; /// 1. Spawn two validators with blend declarations in the genesis transaction. /// 2. Wait long enough that declarations would be removed if no activity /// message was submitted during `inactivity_period + retention_period` -/// sessions. +/// epochs. /// 3. Verify that both declarations are still present, proving that the nodes /// automatically submitted valid activity messages that the ledger accepted. #[tokio::test] async fn sdp_blend_activity() { - let blocks_per_session = Arc::new(AtomicU64::new(0)); + let slots_per_epoch = Arc::new(AtomicU64::new(0)); let (_base, nodes) = start_local_manual_cluster_with_layout( "sdp-blend-activity", "mantle-sdp", @@ -44,13 +47,13 @@ async fn sdp_blend_activity() { NODE_COUNT, ManualNodeLayout::SelectNodeSeed(0), { - let blocks_per_session = Arc::clone(&blocks_per_session); - move |config| Ok::<_, DynError>(test_config(config, &blocks_per_session)) + let slots_per_epoch = Arc::clone(&slots_per_epoch); + move |config| Ok::<_, DynError>(test_config(config, &slots_per_epoch)) }, Some(PathBuf::from(E2E_ARTIFACTS_DIR)), ) .await; - let blocks_per_session = blocks_per_session.load(Ordering::Relaxed); + let slots_per_epoch = slots_per_epoch.load(Ordering::Relaxed); let node0 = &nodes[0]; let node1 = &nodes[1]; @@ -66,35 +69,20 @@ async fn sdp_blend_activity() { // Wait past the point where declarations would be removed if no activity // proofs were submitted. - // - // session_duration = blocks_per_epoch = 6 blocks - // - // A declaration created at genesis with no activity would be removed - // after (inactivity_period + retention_period) * session_duration - // = (1 + 1) * 6 = 12 blocks. - // - // We wait for more sessions to give a solid margin and ensure that if - // activity proofs are being submitted, they keep the declarations alive. - let survival_sessions = INACTIVITY_PERIOD + RETENTION_PERIOD + 1; // +1 margin - let target_height = survival_sessions * blocks_per_session; - println!( - "Waiting for {target_height} blocks ({survival_sessions} sessions, {blocks_per_session} blocks/session)", - ); - - wait_for_nodes_height( + let initial_active_epoch = declarations.values().next().unwrap().active; + let survival_epochs = + initial_active_epoch + INACTIVITY_PERIOD + RETENTION_PERIOD + Epoch::new(2); // +1 margin + let survival_slots = Slot::new(u64::from(u32::from(survival_epochs)) * slots_per_epoch); + wait_for_nodes_tip_slot( &[&node0.client, &node1.client], - target_height, + survival_slots, Duration::from_secs(500), ) .await; // Declarations must still be present — this proves that activity messages were // submitted/accepted, keeping the declarations alive. - let declarations_after = node0 - .client - .get_sdp_declarations() - .await - .expect("querying SDP declarations should succeed"); + let declarations_after = wait_for_declarations(&node0.client, Duration::from_secs(30)).await; // Check if at least one declaration is still present because blocks may have // been produced by only one nodes by coincidence @@ -102,12 +90,22 @@ async fn sdp_blend_activity() { !declarations_after.is_empty(), "At least one blend declaration should survive past the inactivity window. Activity proofs may not have been submitted/accepted" ); + + // Check that the declarations have the refreshed `active` epoch number. + for (provider_id, declaration) in declarations { + let old_active = declaration.active; + let new_active = declarations_after.get(&provider_id).unwrap().active; + assert!( + new_active > old_active, + "Declaration must have the refreshed `active` epoch number larger than the initial one ({old_active:?}), but got {new_active:?}" + ); + } } -const INACTIVITY_PERIOD: u64 = 1; -const RETENTION_PERIOD: u64 = 1; +const INACTIVITY_PERIOD: NumberOfEpochs = NumberOfEpochs::new(Epoch::new(1)); +const RETENTION_PERIOD: NumberOfEpochs = NumberOfEpochs::new(Epoch::new(1)); -fn test_config(mut config: RunConfig, blocks_per_session: &AtomicU64) -> RunConfig { +fn test_config(mut config: RunConfig, slots_per_epoch: &AtomicU64) -> RunConfig { config.deployment.time.slot_duration = Duration::from_secs(1); config.deployment.cryptarchia.epoch_config = EpochConfig { epoch_stake_distribution_stabilization: 1.try_into().unwrap(), @@ -116,10 +114,10 @@ fn test_config(mut config: RunConfig, blocks_per_session: &AtomicU64) -> RunConf }; config.deployment.cryptarchia.security_param = NonZero::new(2).unwrap(); config.deployment.cryptarchia.slot_activation_coeff = - NonNegativeRatio::new(1, 10.try_into().unwrap()); + NonNegativeRatio::new(1, 2.try_into().unwrap()); - blocks_per_session.store( - config.deployment.cryptarchia.blocks_per_epoch(), + slots_per_epoch.store( + config.deployment.cryptarchia.slots_per_epoch(), Ordering::Relaxed, ); @@ -141,11 +139,14 @@ fn test_config(mut config: RunConfig, blocks_per_session: &AtomicU64) -> RunConf async fn wait_for_declarations( node: &NodeHttpClient, duration: Duration, -) -> Vec { +) -> HashMap { tokio::time::timeout(duration, async { loop { if let Ok(declarations) = node.get_sdp_declarations().await { - return declarations; + return declarations + .into_iter() + .map(|declaration| (declaration.provider_id, declaration)) + .collect(); } sleep(Duration::from_millis(200)).await; } diff --git a/tests/src/tests/mantle/sdp/ops.rs b/tests/src/tests/mantle/sdp/ops.rs index 47817bf1d..7cad1681d 100644 --- a/tests/src/tests/mantle/sdp/ops.rs +++ b/tests/src/tests/mantle/sdp/ops.rs @@ -2,9 +2,14 @@ use std::{ collections::{HashMap, HashSet}, num::NonZero, path::PathBuf, + sync::{ + Arc, Mutex, + atomic::{AtomicU64, Ordering}, + }, time::Duration, }; +use lb_chain_service::Epoch; use lb_core::{ mantle::{ GenesisTx as _, MantleTx, NoteId, OpProof, SignedMantleTx, Transaction as _, Utxo, @@ -13,14 +18,17 @@ use lb_core::{ tx::{GasPrices, MantleTxGasContext}, tx_builder::MantleTxBuilder, }, - sdp::{Declaration, DeclarationMessage, Locator, ServiceType, WithdrawMessage}, + sdp::{Declaration, DeclarationMessage, Locator, NumberOfEpochs, ServiceType, WithdrawMessage}, }; use lb_key_management_system_service::keys::{Ed25519Key, Ed25519Signature, ZkKey}; -use lb_node::config::RunConfig; +use lb_node::config::{ + RunConfig, blend::deployment::MinimumNetworkSize, cryptarchia::deployment::EpochConfig, +}; use lb_testing_framework::{ DeploymentBuilder, NodeHttpClient, TopologyConfig as TfTopologyConfig, configs::wallet::{WalletAccount, WalletConfig}, }; +use lb_utils::math::NonNegativeRatio; use logos_blockchain_tests::{ common::{ chain::wait_for_transactions_inclusion, @@ -36,7 +44,7 @@ use num_bigint::BigUint; use testing_framework_core::scenario::{DynError, StartNodeOptions}; use tokio::time::{sleep, timeout}; -const LOCK_PERIOD: u64 = 3; +const LOCK_PERIOD: NumberOfEpochs = NumberOfEpochs::new(Epoch::new(1)); /// High-level SDP flow covered by this E2E: /// - submit a `Declare` transaction backed by an unused genesis note and wait @@ -67,6 +75,8 @@ async fn sdp_ops_e2e() { spare_note_secret_key, spare_note_id, lock_period, + slots_per_epoch, + slot_duration, ) = start_sdp_manual_cluster("sdp-ops").await; let inclusion_timeout = Duration::from_mins(1); @@ -152,21 +162,16 @@ async fn sdp_ops_e2e() { .await .expect("declaration should appear after submission"); - let created_height = declaration_state.created; - let current_nonce = declaration_state.nonce; - - wait_for_manual_cluster_height( - &node0, - created_height + lock_period + 1, - Duration::from_mins(2), - ) - .await - .expect("consensus height should pass the SDP lock period"); + // Wait until we're past the lock period + let wait_lock_period = (Epoch::new(1) + lock_period).into_inner() // +1 buffer + * u32::try_from(slots_per_epoch).unwrap() + * slot_duration; + sleep(wait_lock_period).await; let withdraw_message = WithdrawMessage { declaration_id, locked_note_id, - nonce: current_nonce + 1, + nonce: declaration_state.nonce + 1, }; let (withdraw_mantle_tx, withdraw_signing_keys) = fund_sdp_transaction( @@ -356,8 +361,12 @@ async fn start_sdp_manual_cluster( WalletAccount, ZkKey, NoteId, + NumberOfEpochs, u64, + Duration, ) { + let slots_per_epoch = Arc::new(AtomicU64::new(0)); + let slot_duration = Arc::new(Mutex::new(Duration::ZERO)); let funding_wallet = WalletAccount::deterministic(0, 2_000_000, false).expect("funding wallet should build"); @@ -384,7 +393,19 @@ async fn start_sdp_manual_cluster( "0", StartNodeOptions::default() .with_persist_dir(node0_persist_dir) - .create_patch(|config| Ok::<_, DynError>(patch_sdp_manual_cluster_config(config))), + .create_patch({ + let slots_per_epoch = Arc::clone(&slots_per_epoch); + let slot_duration = Arc::clone(&slot_duration); + move |config| { + let config = patch_sdp_manual_cluster_config(config); + slots_per_epoch.store( + config.deployment.cryptarchia.slots_per_epoch(), + Ordering::Relaxed, + ); + *slot_duration.lock().unwrap() = config.deployment.time.slot_duration; + Ok::<_, DynError>(config) + } + }), ) .await .expect("starting node-0 should succeed"); @@ -439,6 +460,8 @@ async fn start_sdp_manual_cluster( spare_wallet.secret_key, spare_note_id, LOCK_PERIOD, + slots_per_epoch.load(Ordering::Relaxed), + *slot_duration.lock().unwrap(), ) } @@ -450,15 +473,37 @@ fn patch_sdp_manual_cluster_config(mut config: RunConfig) -> RunConfig { .service .bootstrap .prolonged_bootstrap_period = Duration::ZERO; - config.deployment.cryptarchia.security_param = NonZero::new(5).unwrap(); - config + config.deployment.cryptarchia.security_param = NonZero::new(2).unwrap(); + config.deployment.cryptarchia.slot_activation_coeff = + NonNegativeRatio::new(1, 2.try_into().unwrap()); + config.deployment.cryptarchia.epoch_config = EpochConfig { + epoch_stake_distribution_stabilization: 1.try_into().unwrap(), + epoch_period_nonce_buffer: 1.try_into().unwrap(), + epoch_period_nonce_stabilization: 1.try_into().unwrap(), + }; + config.deployment.cryptarchia.learning_rate = 0.5.try_into().unwrap(); + + let service_params = config .deployment .cryptarchia .sdp_config .service_params .get_mut(&ServiceType::BlendNetwork) - .expect("blend network params should exist") - .lock_period = LOCK_PERIOD; + .expect("blend network params should exist"); + service_params.lock_period = LOCK_PERIOD; + service_params.inactivity_period = 10.into(); + service_params.retention_period = 10.into(); + + config.deployment.blend.common.num_blend_layers = 1.try_into().unwrap(); + config.deployment.blend.common.minimum_network_size = MinimumNetworkSize::try_new(2).unwrap(); + config + .deployment + .blend + .core + .scheduler + .delayer + .maximum_release_delay_in_rounds = 1.try_into().unwrap(); + config } diff --git a/tests/src/topology/mod.rs b/tests/src/topology/mod.rs new file mode 100644 index 000000000..f1a703d3a --- /dev/null +++ b/tests/src/topology/mod.rs @@ -0,0 +1,462 @@ +use std::{collections::HashSet, time::Duration}; + +use configs::{ + GeneralConfig, + consensus::{GeneralConsensusConfig, ProviderInfo, create_genesis_block_with_declarations}, + network::{NetworkParams, create_network_configs}, + tracing::create_tracing_configs, +}; +pub use lb_config as configs; +use lb_config::kms::key_id_for_preload_backend; +use lb_core::{ + block::genesis::GenesisBlock, + mantle::{GenesisTx as _, Note, NoteId}, + sdp::{Locator, NumberOfEpochs, ServiceType}, +}; +use lb_key_management_system_service::keys::ZkKey; +use lb_network_service::backends::libp2p::Libp2pInfo; +use lb_node::config::{KmsConfig, kms::serde::PreloadKmsBackendSettings}; +use lb_testing_framework::get_reserved_available_udp_port; +use rand::{Rng as _, thread_rng}; + +use crate::{ + nodes::validator::{Validator, create_validator_config}, + topology::configs::{ + api::create_api_configs, + blend::{GeneralBlendConfig, create_blend_configs}, + consensus::{SHORT_PROLONGED_BOOTSTRAP_PERIOD, create_consensus_configs}, + deployment::e2e_deployment_settings_with_genesis_block, + sdp::create_sdp_configs, + time::set_time_config, + }, +}; + +pub struct TopologyConfig { + pub n_validators: usize, + pub blend_core_nodes: usize, + pub network_params: NetworkParams, + pub extra_genesis_notes: Vec, + /// Override the SDP `lock_period` for this test topology. + /// If None, uses the default from deployment settings (10). + pub lock_period_override: Option, +} + +impl TopologyConfig { + #[must_use] + pub fn one_validator() -> Self { + Self { + n_validators: 1, + blend_core_nodes: 1, + network_params: NetworkParams::default(), + extra_genesis_notes: Vec::new(), + lock_period_override: None, + } + } + + #[must_use] + pub fn two_validators() -> Self { + Self { + n_validators: 2, + blend_core_nodes: 2, + network_params: NetworkParams::default(), + extra_genesis_notes: Vec::new(), + lock_period_override: None, + } + } + + #[must_use] + pub fn n_validators(n_validators: usize) -> Self { + Self { + n_validators, + blend_core_nodes: n_validators, + network_params: NetworkParams::default(), + extra_genesis_notes: Vec::new(), + lock_period_override: None, + } + } + + #[must_use] + pub fn with_extra_genesis_note(mut self, note_spec: GenesisNoteSpec) -> Self { + self.extra_genesis_notes.push(note_spec); + self + } + + #[must_use] + pub const fn with_lock_period(mut self, lock_period: NumberOfEpochs) -> Self { + self.lock_period_override = Some(lock_period); + self + } + + #[must_use] + pub fn n_validators_with_m_blend_node(n: usize, m: usize) -> Self { + assert!( + m <= n, + "Number of Blend core nodes `m` must be less than or equal to total number of validators `n`." + ); + Self { + n_validators: n, + blend_core_nodes: m, + network_params: NetworkParams::default(), + extra_genesis_notes: Vec::new(), + lock_period_override: None, + } + } +} + +#[derive(Clone)] +pub struct GenesisNoteSpec { + pub note: Note, + pub note_sk: ZkKey, +} + +#[derive(Clone)] +pub struct InjectedGenesisNote { + pub note_id: NoteId, +} + +pub struct Topology { + validators: Vec, + general_configs: Vec, + injected_genesis_notes: Vec, +} + +impl Topology { + pub async fn spawn(config: TopologyConfig, test_context: Option<&str>) -> Self { + let n_participants = config.n_validators; + + // we use the same random bytes for: + // * coin sk + // * coin nonce + // * libp2p node key + let mut ids = vec![[0; 32]; n_participants]; + let mut blend_ports = vec![]; + for id in &mut ids { + thread_rng().fill(id); + blend_ports.push(get_reserved_available_udp_port().unwrap()); + } + + let (consensus_configs, genesis_block) = + create_consensus_configs(&ids, SHORT_PROLONGED_BOOTSTRAP_PERIOD, test_context); + let network_configs = create_network_configs(&ids, &config.network_params); + let blend_configs = create_blend_configs(&ids, &blend_ports); + let api_configs = create_api_configs(&ids); + let tracing_configs = create_tracing_configs(&ids); + let time_config = set_time_config(); + + // Setup genesis TX with Blend service declarations. + let base_transfer_op = genesis_block + .transactions() + .next() + .expect("Genesis block should contain a genesis tx") + .genesis_transfer() + .clone(); + let mut transfer_op = base_transfer_op.clone(); + let base_outputs = transfer_op.outputs.len(); + for note_spec in &config.extra_genesis_notes { + transfer_op.outputs.as_mut().push(note_spec.note); + } + let providers: Vec<_> = blend_configs + .iter() + .enumerate() + .map( + |(i, (blend_conf, private_key, zk_secret_key))| ProviderInfo { + service_type: ServiceType::BlendNetwork, + provider_sk: private_key.clone(), + zk_sk: zk_secret_key.clone(), + locator: Locator::new_unchecked( + blend_conf.core.backend.listening_address.clone(), + ), + note: consensus_configs[i].blend_note.clone(), + }, + ) + .collect(); + + // Update genesis TX to contain Blend providers. + let genesis_block = + create_genesis_block_with_declarations(transfer_op, providers, test_context); + let updated_transfer_op = genesis_block.genesis_tx().genesis_transfer().clone(); + let injected_utxos: Vec<_> = updated_transfer_op + .outputs + .utxos(&updated_transfer_op) + .skip(base_outputs) + .collect::>(); + + let injected_infos = injected_utxos + .iter() + .map(|utxo| InjectedGenesisNote { note_id: utxo.id() }) + .collect::>(); + + // Set Blend keys in KMS of each node config. + let kms_configs = create_kms_configs(&blend_configs, &consensus_configs); + + let sdp_configs = create_sdp_configs(&genesis_block.genesis_tx(), n_participants); + + let mut node_configs = vec![]; + + for i in 0..n_participants { + node_configs.push(GeneralConfig { + consensus_config: consensus_configs[i].clone(), + network_config: network_configs[i].clone(), + blend_config: blend_configs[i].clone(), + api_config: api_configs[i].clone(), + tracing_config: tracing_configs[i].clone(), + time_config: time_config.clone(), + kms_config: kms_configs[i].clone(), + sdp_config: sdp_configs[i].clone(), + }); + } + + let general_configs = node_configs.clone(); + + let validators = + Self::spawn_validators(node_configs, genesis_block, config.lock_period_override).await; + + Self { + validators, + general_configs, + injected_genesis_notes: injected_infos, + } + } + + async fn spawn_validators( + config: Vec, + genesis_block: GenesisBlock, + lock_period_override: Option, + ) -> Vec { + let mut validators = Vec::new(); + for general_config in config { + let mut deployment = e2e_deployment_settings_with_genesis_block(&genesis_block); + if let Some(lock_period) = lock_period_override { + for params in deployment + .cryptarchia + .sdp_config + .service_params + .values_mut() + { + params.lock_period = lock_period; + } + } + let config = create_validator_config(general_config, deployment); + validators.push(Validator::spawn(config).await.unwrap()); + } + validators + } + + #[must_use] + pub fn validators(&self) -> &[Validator] { + &self.validators + } + + #[must_use] + pub fn validators_mut(&mut self) -> &mut [Validator] { + &mut self.validators + } + + #[must_use] + pub fn general_config(&self, index: usize) -> Option<&GeneralConfig> { + self.general_configs.get(index) + } + + #[must_use] + pub fn validator_consensus_config( + &self, + validator_index: usize, + ) -> Option<&GeneralConsensusConfig> { + self.general_config(validator_index) + .map(|config| &config.consensus_config) + } + + #[must_use] + pub fn injected_genesis_notes(&self) -> &[InjectedGenesisNote] { + &self.injected_genesis_notes + } + + pub async fn wait_network_ready(&self) { + let listen_ports = self.node_listen_ports(); + if listen_ports.len() <= 1 { + return; + } + + let initial_peer_ports = self.node_initial_peer_ports(); + let expected_peer_counts = find_expected_peer_counts(&listen_ports, &initial_peer_ports); + let labels = self.node_labels(); + + let check = NetworkReadiness { + topology: self, + expected_peer_counts: &expected_peer_counts, + labels: &labels, + }; + + check.wait().await; + } + + fn node_listen_ports(&self) -> Vec { + self.validators + .iter() + .map(|node| node.config().user.network.backend.swarm.port) + .collect() + } + + fn node_initial_peer_ports(&self) -> Vec> { + self.validators + .iter() + .map(|v| { + v.config() + .user + .network + .backend + .initial_peers + .iter() + .filter_map(multiaddr_port) + .collect() + }) + .collect() + } + + fn node_labels(&self) -> Vec { + self.validators + .iter() + .enumerate() + .map(|(i, _)| format!("validator_{i}")) + .collect() + } +} + +#[async_trait::async_trait] +trait ReadinessCheck<'a> { + type Data: Send; + + async fn collect(&'a self) -> Self::Data; + fn is_ready(&self, data: &Self::Data) -> bool; + fn timeout_message(&self, data: Self::Data) -> String; + + async fn wait(&'a self) { + let timeout = tokio::time::timeout(Duration::from_mins(1), async { + loop { + let data = self.collect().await; + if self.is_ready(&data) { + return; + } + tokio::time::sleep(Duration::from_millis(100)).await; + } + }); + + if timeout.await.is_err() { + let data = self.collect().await; + panic!("{}", self.timeout_message(data)); + } + } +} + +struct NetworkReadiness<'a> { + topology: &'a Topology, + expected_peer_counts: &'a [usize], + labels: &'a [String], +} + +#[async_trait::async_trait] +impl<'a> ReadinessCheck<'a> for NetworkReadiness<'a> { + type Data = Vec; + + async fn collect(&'a self) -> Self::Data { + futures::future::join_all(self.topology.validators.iter().map(Validator::network_info)) + .await + } + + fn is_ready(&self, data: &Self::Data) -> bool { + data.iter() + .zip(self.expected_peer_counts.iter()) + .all(|(info, expected)| info.n_peers >= *expected) + } + + fn timeout_message(&self, data: Self::Data) -> String { + let summary = build_timeout_summary(self.labels, data, self.expected_peer_counts); + format!("timed out waiting for network readiness: {summary}") + } +} + +fn build_timeout_summary( + labels: &[String], + infos: Vec, + expected_counts: &[usize], +) -> String { + infos + .into_iter() + .zip(expected_counts.iter()) + .zip(labels.iter()) + .map(|((info, expected), label)| { + format!("{}: peers={}, expected={}", label, info.n_peers, expected) + }) + .collect::>() + .join(", ") +} + +fn multiaddr_port(addr: &lb_libp2p::Multiaddr) -> Option { + for protocol in addr { + match protocol { + lb_libp2p::Protocol::Udp(port) | lb_libp2p::Protocol::Tcp(port) => { + return Some(port); + } + _ => {} + } + } + None +} + +fn find_expected_peer_counts( + listen_ports: &[u16], + initial_peer_ports: &[HashSet], +) -> Vec { + let mut expected: Vec> = vec![HashSet::new(); initial_peer_ports.len()]; + + for (idx, ports) in initial_peer_ports.iter().enumerate() { + for port in ports { + let Some(peer_idx) = listen_ports.iter().position(|p| p == port) else { + continue; + }; + if peer_idx == idx { + continue; + } + + expected[idx].insert(peer_idx); + expected[peer_idx].insert(idx); + } + } + + expected.into_iter().map(|set| set.len()).collect() +} + +#[must_use] +pub fn create_kms_configs( + blend_configs: &[GeneralBlendConfig], + consensus_configs: &[GeneralConsensusConfig], +) -> Vec { + blend_configs + .iter() + .enumerate() + .map(|(i, (blend_conf, private_key, zk_secret_key))| KmsConfig { + backend: PreloadKmsBackendSettings { + keys: [ + ( + blend_conf.non_ephemeral_signing_key_id.clone(), + private_key.clone().into(), + ), + ( + blend_conf.core.zk.secret_key_kms_id.clone(), + zk_secret_key.clone().into(), + ), + ( + key_id_for_preload_backend(&consensus_configs[i].known_key.clone().into()), + consensus_configs[i].known_key.clone().into(), + ), + // SDP funding secret key - used by wallet for signing SDP transactions + ( + key_id_for_preload_backend(&consensus_configs[i].funding_sk.clone().into()), + consensus_configs[i].funding_sk.clone().into(), + ), + ] + .into(), + }, + }) + .collect() +} diff --git a/tests/testing_framework/src/diagnostics.rs b/tests/testing_framework/src/diagnostics.rs index 9e91691cd..d9ae62490 100644 --- a/tests/testing_framework/src/diagnostics.rs +++ b/tests/testing_framework/src/diagnostics.rs @@ -371,14 +371,14 @@ impl NodeDiagnostic { resolve_peer_label(peer_labels, &info.node_id) ), |core_info| { let current = core_info - .current_session_peers + .current_epoch_peers .iter() .map(|(peer, healthy)| { let suffix = if *healthy { "healthy" } else { "unhealthy" }; format!("{}:{suffix}", resolve_peer_label(peer_labels, peer)) }) .collect::>(); - let old = core_info.old_session_peers.as_ref().map_or_else( + let old = core_info.old_epoch_peers.as_ref().map_or_else( || "none".to_owned(), |peers| { peers @@ -390,11 +390,11 @@ impl NodeDiagnostic { ); format!( - "ok node_id={} mode=core session_peers={} unhealthy={} old_session=[{}] current_session=[{}]", + "ok node_id={} mode=core epoch_peers={} unhealthy={} old_epoch=[{}] current_epoch=[{}]", resolve_peer_label(peer_labels, &info.node_id), - core_info.current_session_peers.len(), + core_info.current_epoch_peers.len(), core_info - .current_session_peers + .current_epoch_peers .iter() .filter(|(_, healthy)| !healthy) .count(), @@ -473,21 +473,21 @@ struct BlendSnapshot { #[derive(Clone)] struct BlendCoreSnapshot { - current_session_peers: Vec<(String, bool)>, - old_session_peers: Option>, + current_epoch_peers: Vec<(String, bool)>, + old_epoch_peers: Option>, } impl From> for BlendSnapshot { fn from(value: BlendNetworkInfo) -> Self { let core_info = value.core_info.map(|core_info| { - let mut current_session_peers = core_info - .current_session_peers + let mut current_epoch_peers = core_info + .current_epoch_peers .into_iter() .map(|(peer, healthy)| (peer.to_string(), healthy)) .collect::>(); - current_session_peers.sort_by(|left, right| left.0.cmp(&right.0)); + current_epoch_peers.sort_by(|left, right| left.0.cmp(&right.0)); - let old_session_peers = core_info.old_session_peers.map(|peers| { + let old_epoch_peers = core_info.old_epoch_peers.map(|peers| { let mut peers = peers .into_iter() .map(|peer| peer.to_string()) @@ -497,8 +497,8 @@ impl From> for Bl }); BlendCoreSnapshot { - current_session_peers, - old_session_peers, + current_epoch_peers, + old_epoch_peers, } }); @@ -538,7 +538,7 @@ struct ClusterSummary { peer_range: String, connection_range: String, pending_total: u64, - blend_session_peer_range: String, + blend_epoch_peer_range: String, blend_unhealthy_total: usize, blend_transitioning_nodes: Vec, mempool_pending_range: String, @@ -630,16 +630,16 @@ impl ClusterSummary { .iter() .map(|snapshot| u64::from(snapshot.n_pending_connections)) .sum(), - blend_session_peer_range: format_range_usize( + blend_epoch_peer_range: format_range_usize( &blend_core_snapshots .iter() - .map(|(_, info)| info.current_session_peers.len()) + .map(|(_, info)| info.current_epoch_peers.len()) .collect::>(), ), blend_unhealthy_total: blend_core_snapshots .iter() .map(|(_, info)| { - info.current_session_peers + info.current_epoch_peers .iter() .filter(|(_, healthy)| !healthy) .count() @@ -647,7 +647,7 @@ impl ClusterSummary { .sum(), blend_transitioning_nodes: blend_core_snapshots .iter() - .filter(|(_, info)| info.old_session_peers.is_some()) + .filter(|(_, info)| info.old_epoch_peers.is_some()) .map(|(label, _)| (*label).clone()) .collect(), mempool_pending_range: format_range_usize( @@ -689,8 +689,8 @@ impl ClusterSummary { self.peer_range, self.connection_range, self.pending_total ), format!( - " blend session_peer_range={} unhealthy_total={} transitioning_nodes=[{}]", - self.blend_session_peer_range, + " blend epoch_peer_range={} unhealthy_total={} transitioning_nodes=[{}]", + self.blend_epoch_peer_range, self.blend_unhealthy_total, self.blend_transitioning_nodes.join(", ") ), diff --git a/tools/config/Cargo.toml b/tools/config/Cargo.toml index 12b21000f..6bbf07e38 100644 --- a/tools/config/Cargo.toml +++ b/tools/config/Cargo.toml @@ -19,6 +19,7 @@ testing = ["lb-node/testing"] [dependencies] hex = { workspace = true } lb-core = { workspace = true } +lb-cryptarchia-engine = { workspace = true } lb-groth16 = { workspace = true } lb-key-management-system-service = { workspace = true } lb-libp2p = { workspace = true } diff --git a/tools/config/src/deployment.rs b/tools/config/src/deployment.rs index 347859d82..7371b0369 100644 --- a/tools/config/src/deployment.rs +++ b/tools/config/src/deployment.rs @@ -3,7 +3,11 @@ use core::{ time::Duration, }; -use lb_core::{block::genesis::GenesisBlock, sdp::ServiceType}; +use lb_core::{ + block::genesis::GenesisBlock, + sdp::{NumberOfEpochs, ServiceType}, +}; +use lb_cryptarchia_engine::Epoch; use lb_libp2p::protocol_name::StreamProtocol; use lb_node::config::{ blend::deployment::{ @@ -33,7 +37,6 @@ const DATA_REPLICATION_FACTOR: u64 = 0; const MINIMUM_MESSAGES_COEFFICIENT: u64 = 1; const BLEND_NORMALIZATION_CONSTANT: f64 = 1.03; -const COVER_SAFETY_BUFFER_INTERVALS: u64 = 100; const COVER_MESSAGE_FREQUENCY_PER_ROUND: f64 = 1.0; const MAXIMUM_RELEASE_DELAY_IN_ROUNDS: u64 = 3; const ACTIVITY_THRESHOLD_SENSITIVITY: u64 = 1; @@ -50,10 +53,10 @@ const EPOCH_STAKE_DISTRIBUTION_STABILIZATION: u8 = 3; const EPOCH_PERIOD_NONCE_BUFFER: u8 = 3; const EPOCH_PERIOD_NONCE_STABILIZATION: u8 = 4; -const SDP_LOCK_PERIOD: u64 = 10; -const SDP_INACTIVITY_PERIOD: u64 = 1; -const SDP_RETENTION_PERIOD: u64 = 1; -const SDP_TIMESTAMP: u64 = 0; +const SDP_LOCK_PERIOD: NumberOfEpochs = NumberOfEpochs::new(Epoch::new(10)); +const SDP_INACTIVITY_PERIOD: NumberOfEpochs = NumberOfEpochs::new(Epoch::new(1)); +const SDP_RETENTION_PERIOD: NumberOfEpochs = NumberOfEpochs::new(Epoch::new(1)); +const SDP_EPOCH: Epoch = Epoch::new(0); const MIN_STAKE_THRESHOLD: u64 = 1; const MIN_STAKE_TIMESTAMP: u64 = 0; const LEARNING_RATE: f64 = 0.1; @@ -89,7 +92,6 @@ pub fn e2e_deployment_settings_with_genesis_block( .expect("Normalization constant cannot be negative."), scheduler: SchedulerSettings { cover: CoverTrafficSettings { - intervals_for_safety_buffer: COVER_SAFETY_BUFFER_INTERVALS, message_frequency_per_round: NonNegativeF64::try_from( COVER_MESSAGE_FREQUENCY_PER_ROUND, ) @@ -133,7 +135,7 @@ pub fn e2e_deployment_settings_with_genesis_block( lock_period: SDP_LOCK_PERIOD, inactivity_period: SDP_INACTIVITY_PERIOD, retention_period: SDP_RETENTION_PERIOD, - timestamp: SDP_TIMESTAMP, + epoch: SDP_EPOCH, }, )] .into(), diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index e641b712d..79b82e376 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -1091,34 +1091,37 @@ mod tests { #[must_use] fn ledger_config() -> lb_ledger::Config { + let epoch_config = EpochConfig { + epoch_stake_distribution_stabilization: NonZero::new(1).unwrap(), + epoch_period_nonce_buffer: NonZero::new(1).unwrap(), + epoch_period_nonce_stabilization: NonZero::new(1).unwrap(), + }; + let consensus_config = lb_cryptarchia_engine::Config::new( + NonZero::new(1).unwrap(), + NonNegativeRatio::new(1, 10.try_into().unwrap()), + 1f64.try_into().expect("1 > 0"), + ); + let epoch_length = epoch_config.epoch_length(consensus_config.base_period_length()); + lb_ledger::Config { - epoch_config: EpochConfig { - epoch_stake_distribution_stabilization: NonZero::new(1).unwrap(), - epoch_period_nonce_buffer: NonZero::new(1).unwrap(), - epoch_period_nonce_stabilization: NonZero::new(1).unwrap(), - }, - consensus_config: lb_cryptarchia_engine::Config::new( - NonZero::new(1).unwrap(), - NonNegativeRatio::new(1, 10.try_into().unwrap()), - 1f64.try_into().expect("1 > 0"), - ), + epoch_config, + consensus_config, sdp_config: lb_ledger::mantle::sdp::Config { service_params: Arc::new( [( ServiceType::BlendNetwork, ServiceParameters { - lock_period: 10, - inactivity_period: 20, - retention_period: 100, - timestamp: 0, - session_duration: 10, + lock_period: 10.into(), + inactivity_period: 20.into(), + retention_period: 100.into(), + epoch: 0.into(), }, )] .into(), ), service_rewards_params: ServiceRewardsParameters { blend: rewards::blend::RewardsParameters { - rounds_per_session: NonZeroU64::new(10).unwrap(), + rounds_per_epoch: epoch_length.try_into().unwrap(), message_frequency_per_round: NonNegativeF64::try_from(1.0).unwrap(), num_blend_layers: NonZeroU64::new(3).unwrap(), minimum_network_size: NonZeroU64::new(1).unwrap(), diff --git a/zk/proofs/poq/benches/prove.rs b/zk/proofs/poq/benches/prove.rs index e6ac45f2c..2a1e41b11 100644 --- a/zk/proofs/poq/benches/prove.rs +++ b/zk/proofs/poq/benches/prove.rs @@ -44,7 +44,6 @@ fn common_data_leader() -> PoQCommonInputsData { fn make_core_node_inputs() -> PoQWitnessInputs { let (lottery_0, lottery_1) = lottery(); let chain_data = PoQChainInputsData { - session: 150, core_root: BigUint::from_str( "10774149910279330054096178616484626574938100628643657398591620611653283350567", ) @@ -164,7 +163,6 @@ fn make_core_node_inputs() -> PoQWitnessInputs { fn make_leader_inputs() -> PoQWitnessInputs { let (lottery_0, lottery_1) = lottery(); let chain_data = PoQChainInputsData { - session: 150, core_root: BigUint::from_str( "11932007478822307154060471648284351639702201082133930350572683284818742022376", ) diff --git a/zk/proofs/poq/src/chain_inputs.rs b/zk/proofs/poq/src/chain_inputs.rs index 348030139..a33d4e61c 100644 --- a/zk/proofs/poq/src/chain_inputs.rs +++ b/zk/proofs/poq/src/chain_inputs.rs @@ -1,12 +1,9 @@ use lb_groth16::{Fr, Groth16Input, Groth16InputDeser}; -use lb_pol::P; -use num_bigint::BigUint; use serde::{Deserialize, Serialize}; use thiserror::Error; #[derive(Copy, Clone)] pub struct PoQChainInputs { - session: Groth16Input, core_root: Groth16Input, pol_ledger_aged: Groth16Input, pol_epoch_nonce: Groth16Input, @@ -16,7 +13,6 @@ pub struct PoQChainInputs { #[derive(Clone, Copy)] pub struct PoQChainInputsData { - pub session: u64, pub core_root: Fr, pub pol_ledger_aged: Fr, pub pol_epoch_nonce: Fr, @@ -26,7 +22,6 @@ pub struct PoQChainInputsData { #[derive(Deserialize, Serialize)] pub struct PoQChainInputsJson { - session: Groth16InputDeser, core_root: Groth16InputDeser, pol_ledger_aged: Groth16InputDeser, pol_epoch_nonce: Groth16InputDeser, @@ -39,7 +34,6 @@ impl TryFrom for PoQChainInputs { fn try_from( PoQChainInputsJson { - session, core_root, pol_ledger_aged, pol_epoch_nonce, @@ -48,7 +42,6 @@ impl TryFrom for PoQChainInputs { }: PoQChainInputsJson, ) -> Result { Ok(Self { - session: session.try_into()?, core_root: core_root.try_into()?, pol_ledger_aged: pol_ledger_aged.try_into()?, pol_epoch_nonce: pol_epoch_nonce.try_into()?, @@ -61,7 +54,6 @@ impl TryFrom for PoQChainInputs { impl From<&PoQChainInputs> for PoQChainInputsJson { fn from( PoQChainInputs { - session, core_root, pol_ledger_aged, pol_epoch_nonce, @@ -70,7 +62,6 @@ impl From<&PoQChainInputs> for PoQChainInputsJson { }: &PoQChainInputs, ) -> Self { Self { - session: session.into(), core_root: core_root.into(), pol_ledger_aged: pol_ledger_aged.into(), pol_epoch_nonce: pol_epoch_nonce.into(), @@ -82,8 +73,6 @@ impl From<&PoQChainInputs> for PoQChainInputsJson { #[derive(Debug, Error)] pub enum PoQInputsFromDataError { - #[error("Session number is greater than P")] - SessionGreaterThanP, #[error("Core quota is greater than 20 bits")] CoreQuotaMoreThan20Bits, #[error("Leader quota is greater than 20 bits")] @@ -95,7 +84,6 @@ impl TryFrom for PoQChainInputs { fn try_from( PoQChainInputsData { - session, core_root, pol_ledger_aged, pol_epoch_nonce, @@ -103,12 +91,7 @@ impl TryFrom for PoQChainInputs { lottery_1, }: PoQChainInputsData, ) -> Result { - let session = BigUint::from(session); - if session > *P { - return Err(PoQInputsFromDataError::SessionGreaterThanP); - } Ok(Self { - session: Groth16Input::new(session.into()), core_root: core_root.into(), pol_ledger_aged: pol_ledger_aged.into(), pol_epoch_nonce: pol_epoch_nonce.into(), diff --git a/zk/proofs/poq/src/inputs.rs b/zk/proofs/poq/src/inputs.rs index 430e347eb..361108b98 100644 --- a/zk/proofs/poq/src/inputs.rs +++ b/zk/proofs/poq/src/inputs.rs @@ -91,11 +91,10 @@ impl From for PoQInputsJson { } #[derive(Deserialize, Serialize)] -pub struct PoQVerifierInputJson([Groth16InputDeser; 11]); +pub struct PoQVerifierInputJson([Groth16InputDeser; 10]); pub struct PoQVerifierInput { pub key_nullifier: Groth16Input, - pub session: Groth16Input, pub core_quota: Groth16Input, pub leader_quota: Groth16Input, pub core_root: Groth16Input, @@ -109,7 +108,6 @@ pub struct PoQVerifierInput { pub struct PoQVerifierInputData { pub key_nullifier: Fr, - pub session: u64, pub core_quota: u64, pub leader_quota: u64, pub core_root: Fr, @@ -127,7 +125,6 @@ impl TryFrom for PoQVerifierInput { fn try_from(value: PoQVerifierInputJson) -> Result { let [ key_nullifier, - session, core_quota, leader_quota, core_root, @@ -140,7 +137,6 @@ impl TryFrom for PoQVerifierInput { ] = value.0; Ok(Self { key_nullifier: key_nullifier.try_into()?, - session: session.try_into()?, core_quota: core_quota.try_into()?, leader_quota: leader_quota.try_into()?, core_root: core_root.try_into()?, @@ -156,10 +152,9 @@ impl TryFrom for PoQVerifierInput { impl PoQVerifierInput { #[must_use] - pub const fn to_inputs(self) -> [Fr; 11] { + pub const fn to_inputs(self) -> [Fr; 10] { [ self.key_nullifier.into_inner(), - self.session.into_inner(), self.core_quota.into_inner(), self.leader_quota.into_inner(), self.core_root.into_inner(), @@ -186,7 +181,6 @@ impl From for PoQVerifierInput { pol_ledger_aged: value.pol_ledger_aged.into(), pol_t0: Groth16Input::new(value.lottery_0), pol_t1: Groth16Input::new(value.lottery_1), - session: Groth16Input::new(value.session.into()), } } } diff --git a/zk/proofs/poq/src/lib.rs b/zk/proofs/poq/src/lib.rs index 3dab13af9..4bdc18a31 100644 --- a/zk/proofs/poq/src/lib.rs +++ b/zk/proofs/poq/src/lib.rs @@ -116,91 +116,91 @@ mod tests { fn test_core_node_full_flow() { let blend_data = PoQBlendInputsData { core_sk: BigUint::from_str( - "6576495977526760241501499963811136028674473098047722736567052812753357178145", + "8905547699320869461831852104817789469875723637735901345741281588946310324235", ) .unwrap() .into(), core_path_and_selectors: [ ( - "13664394408134714148474230123476522725121022841643115538772760906625244311247", + "16869616652852667189559016717243338331068120739649173884502336155264218010727", false, ), ( - "19991080109804084229148819955447485980685271866236940378355513894509403472825", - false, - ), - ( - "1719423980074249650282196742833822060536614400613876573240720330836944657046", + "13091718879067163858034522323644176312242009811093736972498389939059085177974", true, ), ( - "9139059421670197230454683564023894631544291367010097325710917953251896109820", + "5615497624396623086866765843762273938268745340944523291991473843820399587958", true, ), ( - "5364655248740251237070591917425080374871240308613634110599105641060308262480", - false, - ), - ( - "4321492523488456904492279703871677986518013774282254436225897785632983582392", - false, - ), - ( - "2990224148203398705081847530695215902031679797575769158839150593627687374477", + "12252618726807031528274527895794936410875679656522675436955173925070564886479", true, ), ( - "10115668570671746589151792513284313334215992216493690986604010084064526087897", + "15676171816835539786276219242768254181009067816239948700832551985003974594975", false, ), ( - "6861547258030107481535610157091754819359096245516659866713074178195667725661", + "19704272158800036501582000538191584664181486372889137807570676877252162790252", false, ), ( - "7263098007194293142502159691381240540663513841811088748578355471278661748398", + "17220466132075642970726646606687934619880159579542773841528618335448239077575", + false, + ), + ( + "19568444328548325046015987107341253380027495775774622463368229855377989226290", true, ), ( - "19870876389339998317953427375552419155167215853924118334240834433312282250010", - true, - ), - ( - "8986537640578106247026054824010082082887537390181434985471297569735311570527", - true, - ), - ( - "2677828888032047082692090535060454810065355495500936244017540612071241172074", + "21061581629256637244739982573784550742652688800467757972406229545047035347638", false, ), ( - "17555659938930335545491158932916791395314469587225130013830235195468776678581", + "21518036264660447465371694976644098419156368727076924435365409888210990153413", true, ), ( - "9071399479181997918431555676567419518917037685697025275563780306843446304571", - false, - ), - ( - "6281992864280083991852918191693022891803454129450254484599792207873685708287", - false, - ), - ( - "17218725285443376817865240748479799866804154572691475341349156521930190101705", - false, - ), - ( - "14439152042823579940992841514440450319175689090721997229482427278436880180442", + "18601741813400480889163627600644403937577783284065467384061180740490941766846", true, ), ( - "4375092350636859316334567236773062868645005901112681926345752432026107616905", + "3276819021875171213616879397115905648698624783190949445325727117619871062684", + false, + ), + ( + "16346413070923069365311460658994366491438423790174305844846934643885739701955", + false, + ), + ( + "18052187374311288196723181314946958960293262462466262108812751192264820802527", true, ), ( - "2391466682452790260991116539507901841740149925567473331054306831262572734122", + "18798652887540222143344482340074977888006884307412611551550673334473344453748", false, ), + ( + "6340439308577685187208962346420910386234054724707742363615331612477333766009", + true, + ), + ( + "6116045944554536720728524915454748535792176096846806949818056548601205224613", + true, + ), + ( + "7495766221993429009053819635630800227825524659472307268648873524676590491716", + true, + ), + ( + "4135588845309195152336413092515000771731525447810984287718633658115964724154", + true, + ), + ( + "16515196782727402790941217652547735699069943062019512321107139568403168848690", + true, + ), ] .map(|(value, selector)| (BigUint::from_str(value).unwrap().into(), selector)), }; @@ -208,19 +208,18 @@ mod tests { LotteryConstants::new(NonNegativeRatio::new(1, 10.try_into().unwrap())) .compute_lottery_values(5000); let chain_data = PoQChainInputsData { - session: 150, core_root: BigUint::from_str( - "10774149910279330054096178616484626574938100628643657398591620611653283350567", + "12497635102173390657108276580981678137198622257613601634363274282193270703654", ) .unwrap() .into(), pol_ledger_aged: BigUint::from_str( - "5899427723120145663289932094176488517692686385716511263841021495659364229709", + "3285301153701124106247898475175231377413013906491501039690975241845353257824", ) .unwrap() .into(), pol_epoch_nonce: BigUint::from_str( - "14296132653141589082634452040005706132398442975462231668085056116416860738326", + "12441952563111122190593710132868692987264877387856913250622883462242318292882", ) .unwrap() .into(), @@ -235,7 +234,7 @@ mod tests { BigUint::from(654_321u32).into(), ), selector: false, - index: 6, + index: 2, }; let witness_inputs = @@ -256,7 +255,6 @@ mod tests { leader_quota: common_data.leader_quota, pol_epoch_nonce: chain_data.pol_epoch_nonce, pol_ledger_aged: chain_data.pol_ledger_aged, - session: chain_data.session, lottery_0: chain_data.lottery_0, lottery_1: chain_data.lottery_1, }; @@ -270,19 +268,18 @@ mod tests { LotteryConstants::new(NonNegativeRatio::new(1, 10.try_into().unwrap())) .compute_lottery_values(5000); let chain_data = PoQChainInputsData { - session: 150, core_root: BigUint::from_str( - "11932007478822307154060471648284351639702201082133930350572683284818742022376", + "12185619490528295825098195451407217973914857946530829707990590609721812577473", ) .unwrap() .into(), pol_ledger_aged: BigUint::from_str( - "3342945051364076680578394748596576255113072679391591642880837961076314972848", + "367970463510572346469727551309607212474618104803583434125220973330505217187", ) .unwrap() .into(), pol_epoch_nonce: BigUint::from_str( - "11175261890624684044649299918101786672368403302354398651534872142981877782699", + "19183275818966466009388966937999847470836267418943165691463680548408801149509", ) .unwrap() .into(), @@ -297,150 +294,150 @@ mod tests { BigUint::from(654_321u32).into(), ), selector: true, - index: 6, + index: 2, }; let wallet_data = PoQWalletInputsData { - slot: 2_966_761_920, + slot: 3_934_028_363, note_value: 50, transaction_hash: BigUint::from_str( - "2329282056243366650798295627018033051629488564285541185679647538827693341905", + "2264455058971045832756600439690776883891824153136465835848552732282836700508", ) .unwrap() .into(), - output_number: 939, + output_number: 1108, aged_path_and_selectors: [ ( - "11421803042917225627078901218595334185635793494732072039365745057621038812605", + "11104092157301530528415266289001896179719940037074919849624292254162611405786", true, ), ( - "16169325055187512554252523735961738240564294866494072628885024705839052661540", - true, - ), - ( - "13656910552343360495181196437444817159012126550371459801043514651056011592431", + "7132185073809921262555691477157103985074819342411989543016526500095228117354", false, ), ( - "12449448263899051439932276028399740866954101672665020621064512907852896180600", - true, - ), - ( - "9885876318176059648673564567836315509808157847432213345713880847517777412402", + "11907580727147699209958498886101128744892522133797395449242728594286922858086", false, ), ( - "8388384044942534377652016263681525995454565014077139337139691792794605158759", + "13075221440612698710788795973343285593006804290474838812157163926886238383760", + true, + ), + ( + "17200451570433990100996559280535385805985882628682996186008504614174096791798", + true, + ), + ( + "16750120177808191972292661243802361139298740550633002442851428788856687150475", false, ), ( - "19235582700862041818897072745866691959011049676952434709510674286292920807586", - true, - ), - ( - "20395204849666255127128061293127220286095933826612425507561746733356595329205", - true, - ), - ( - "9792237822684654427827473660300770828015365397927307769302890576073867096291", - true, - ), - ( - "14079764112025655291976470097660114251011231109847561011521815867773307300092", - true, - ), - ( - "2067627040071925958630121165903165914396130574920765783200205766592922273940", - true, - ), - ( - "11868870016995490053316691978613163802307585966396648283452535241128386725435", + "12679489866011049826521697310427217461017078670664946750194685078601853495880", false, ), ( - "7279049315994133684852941934863027518847276009575066241366658887613953973411", + "15235439481027011453071738627024275793651222671965241576356428348106464813175", false, ), ( - "1653700858957932143991083596354839511712596833499052382473339976736220545455", + "9961832183928877694020893796261063404160642210645529511233145074735042190217", true, ), ( - "14593791812533011392330970050125225769124415454833286335602775158366056740177", + "4326894103682035305319995256704866698441157935817295295020304417938631996729", true, ), ( - "15914545113232326797695053101280159396305447124529609345850784179045489818165", - true, - ), - ( - "12662726781317585032946137898723100436309385600280503938290557115123483697307", - true, - ), - ( - "12794954106056182456059399831190158694070172097205191410351234102340878136610", + "17555384873204675431996467471744635783468598594303097832615135885710898785356", false, ), ( - "9139468540558771360630099778350062729951097762632752410540325438044212613391", + "2067987409200354598518658724620929803515175794312186642703606119551864949264", + true, + ), + ( + "18414033735029028807991144814771963824730052421471947864345037264843089845734", false, ), ( - "14427444938386674320013309938491006982580864902951055386283865455921637245718", + "11883340736248057870265402327022279289678280641364875114447762692303561445604", false, ), ( - "16217323319160426166397199974441670931472458277592563266744427901413561452567", + "7157973471780523265294561800756652242511668640260136396798977970116460727342", true, ), ( - "5808913222465608048971511479506235998696112918992016407762164600898363761443", - true, - ), - ( - "14139659546950817665773560726774860101672140396656643863078950566542608882454", - true, - ), - ( - "9629332877235332256602865161617849630375965416260459249008709304721048200537", + "10322140357911263063410611191898227332467973200575964702516054623940247053118", false, ), ( - "15194682846338116581211354907699755414549403072108697153631776325415459532015", + "12567824105074446475873393314739330450709906448171155888539789943621309224508", true, ), ( - "16388444833249387234308154394290310584264985017796614640314011031393000328142", + "3778057320817011605792674467466589599030650184482297863404214181285644124757", + true, + ), + ( + "15139857989447407851586537192536170660643402207011818138460800689622488846498", + true, + ), + ( + "4686260554210282279381725438723974181592826312599841000574494090500374964214", + true, + ), + ( + "2017001248552489998639360772220278151302627680149021505074390044394703925937", false, ), ( - "12893209696859870840964135760414645596172125459127736744879723498588409315420", + "15644113980009580949735073673522118459196764019649835645357551831476441064371", true, ), ( - "4252466064505889568654035534564515419775193443498121206766286051578204786863", - true, - ), - ( - "4143206562946824264148426319984303092795894066542641394672778393375938922561", + "5851335105157805239260310112724712629219062160101168478507890653975790109833", false, ), ( - "9596842943859750941026234720461866030080253052472491176442559534941513029841", - true, - ), - ( - "4874668976049850093271116246322279706660345846619050886670675494003997035667", + "4506645166310701016292123296822379297660598150676608439399796403413830281952", false, ), ( - "20880637667070744859491879413494925400228379072955349596713793323424330258245", + "19761070472419507198412744968226383034230108135701041508981821408866293379367", + false, + ), + ( + "16473696535741964167503054455797711177314106892505373545741412745155986927894", + true, + ), + ( + "7024125351521471566160638647544413645870621144999681104961251077959669126366", + false, + ), + ( + "4223261943159590385933901257711007435848118520945782486495445973028111080784", + true, + ), + ( + "2480120922453299704385093407865595899109107879489738668468480942431779023090", + false, + ), + ( + "4872555691447955405629869337126212288862528483324109155414334863586541089650", + false, + ), + ( + "2885249961213344608420596395566111778633573487967873292737118191013543595441", + false, + ), + ( + "20675954944665714887729603321541315818168820606980234992582325333134286444983", false, ), ] .map(|(value, selector)| (BigUint::from_str(value).unwrap().into(), selector)), secret_key: BigUint::from_str( - "17558656186380761716563192806052137702710836103853413805442046164934618201080", + "14029017592631272654768426994822986470491530234220356957377918534745805708829", ) .unwrap() .into(), @@ -464,7 +461,6 @@ mod tests { leader_quota: common_data.leader_quota, pol_epoch_nonce: chain_data.pol_epoch_nonce, pol_ledger_aged: chain_data.pol_ledger_aged, - session: chain_data.session, lottery_0: chain_data.lottery_0, lottery_1: chain_data.lottery_1, };