From 8bf55cfb3a1d096c0330ba62d03ed1e5f7298cf8 Mon Sep 17 00:00:00 2001 From: Brendon Fish Date: Tue, 19 Mar 2024 17:33:16 -0400 Subject: [PATCH] Bring `hotshot-types` and `hs-builder-api` into `Hotshot` (#2812) * WIP * Still chasing a bug * One down, one to go * ... and done. * Cleanup and finishing touches * Fixing the post-merge code, and one missed issue in the pre-merge code. * Some clarifying name changes. * Removing bincode dependency where applicable * cleanup of some (not quite all) remaining bincode invocations * dependency versioning tags and cleanup * bump version * Bumping anticipated version * Now with new VBS * branch => tag on dependencies * Update lock? * Cargo.lock update * Apparently `just * lint` runs with --features="hotshot-testing" enabled * integrate new storage type * fix build, tie up stragglers, modifying tasks next * merge latest stable tabs * merge latest tags and fix method signatures * Update *-disco * Dependencies updated to tags * Add builder-api and types to repo * fix doc * hs-builder-api -> hotshot-builder-api * fmt --------- Co-authored-by: Nathan F Yospe Co-authored-by: Jarred Parr --- Cargo.lock | 303 ++------ Cargo.toml | 28 +- crates/builder-api/Cargo.toml | 19 + crates/builder-api/README.md | 4 + crates/builder-api/api/builder.toml | 65 ++ crates/builder-api/api/submit.toml | 33 + crates/builder-api/src/api.rs | 59 ++ crates/builder-api/src/block_info.rs | 38 + crates/builder-api/src/builder.rs | 197 +++++ crates/builder-api/src/data_source.rs | 43 ++ crates/builder-api/src/lib.rs | 5 + crates/builder-api/src/query_data.rs | 15 + crates/example-types/Cargo.toml | 8 +- crates/examples/Cargo.toml | 22 +- crates/hotshot-stake-table/Cargo.toml | 2 +- crates/hotshot/Cargo.toml | 18 +- crates/libp2p-networking/Cargo.toml | 4 +- crates/orchestrator/Cargo.toml | 2 +- crates/task-impls/Cargo.toml | 8 +- crates/task-impls/src/builder.rs | 2 +- crates/testing-macros/Cargo.toml | 2 +- crates/testing/Cargo.toml | 8 +- crates/testing/src/block_builder.rs | 14 +- crates/types/Cargo.toml | 65 ++ crates/types/src/consensus.rs | 394 ++++++++++ crates/types/src/constants.rs | 52 ++ crates/types/src/data.rs | 483 +++++++++++++ crates/types/src/error.rs | 112 +++ crates/types/src/event.rs | 160 ++++ crates/types/src/lib.rs | 190 +++++ crates/types/src/light_client.rs | 223 ++++++ crates/types/src/message.rs | 316 ++++++++ crates/types/src/qc.rs | 310 ++++++++ crates/types/src/signature_key.rs | 127 ++++ crates/types/src/simple_certificate.rs | 179 +++++ crates/types/src/simple_vote.rs | 258 +++++++ crates/types/src/stake_table.rs | 31 + crates/types/src/traits.rs | 15 + crates/types/src/traits/block_contents.rs | 149 ++++ crates/types/src/traits/consensus_api.rs | 42 ++ crates/types/src/traits/election.rs | 91 +++ crates/types/src/traits/metrics.rs | 295 ++++++++ crates/types/src/traits/network.rs | 681 ++++++++++++++++++ .../types/src/traits/node_implementation.rs | 238 ++++++ crates/types/src/traits/qc.rs | 95 +++ crates/types/src/traits/signature_key.rs | 140 ++++ crates/types/src/traits/stake_table.rs | 235 ++++++ crates/types/src/traits/states.rs | 89 +++ crates/types/src/traits/storage.rs | 21 + crates/types/src/utils.rs | 189 +++++ crates/types/src/vid.rs | 277 +++++++ crates/types/src/vote.rs | 183 +++++ crates/web_server/Cargo.toml | 2 +- 53 files changed, 6251 insertions(+), 290 deletions(-) create mode 100644 crates/builder-api/Cargo.toml create mode 100644 crates/builder-api/README.md create mode 100644 crates/builder-api/api/builder.toml create mode 100644 crates/builder-api/api/submit.toml create mode 100644 crates/builder-api/src/api.rs create mode 100644 crates/builder-api/src/block_info.rs create mode 100644 crates/builder-api/src/builder.rs create mode 100644 crates/builder-api/src/data_source.rs create mode 100644 crates/builder-api/src/lib.rs create mode 100644 crates/builder-api/src/query_data.rs create mode 100644 crates/types/Cargo.toml create mode 100644 crates/types/src/consensus.rs create mode 100644 crates/types/src/constants.rs create mode 100644 crates/types/src/data.rs create mode 100644 crates/types/src/error.rs create mode 100644 crates/types/src/event.rs create mode 100644 crates/types/src/lib.rs create mode 100644 crates/types/src/light_client.rs create mode 100644 crates/types/src/message.rs create mode 100644 crates/types/src/qc.rs create mode 100644 crates/types/src/signature_key.rs create mode 100644 crates/types/src/simple_certificate.rs create mode 100644 crates/types/src/simple_vote.rs create mode 100644 crates/types/src/stake_table.rs create mode 100644 crates/types/src/traits.rs create mode 100644 crates/types/src/traits/block_contents.rs create mode 100644 crates/types/src/traits/consensus_api.rs create mode 100644 crates/types/src/traits/election.rs create mode 100644 crates/types/src/traits/metrics.rs create mode 100644 crates/types/src/traits/network.rs create mode 100644 crates/types/src/traits/node_implementation.rs create mode 100644 crates/types/src/traits/qc.rs create mode 100644 crates/types/src/traits/signature_key.rs create mode 100644 crates/types/src/traits/stake_table.rs create mode 100644 crates/types/src/traits/states.rs create mode 100644 crates/types/src/traits/storage.rs create mode 100644 crates/types/src/utils.rs create mode 100644 crates/types/src/vid.rs create mode 100644 crates/types/src/vote.rs diff --git a/Cargo.lock b/Cargo.lock index c12e65c26b..0c5a9e1e83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -510,7 +510,7 @@ dependencies = [ "proc-macro2", "quote", "syn 1.0.109", - "synstructure 0.12.6", + "synstructure", ] [[package]] @@ -576,26 +576,6 @@ dependencies = [ "pin-project-lite 0.2.13", ] -[[package]] -name = "async-compatibility-layer" -version = "1.0.0" -source = "git+https://github.com/EspressoSystems/async-compatibility-layer.git?tag=1.4.1#568122b0ef39648daee91d16546985d41c3b0b15" -dependencies = [ - "async-channel 1.9.0", - "async-lock 2.8.0", - "async-std", - "async-trait", - "color-eyre", - "console-subscriber 0.1.10", - "flume 0.11.0", - "futures", - "tokio", - "tokio-stream", - "tracing", - "tracing-error", - "tracing-subscriber", -] - [[package]] name = "async-compatibility-layer" version = "1.0.0" @@ -606,7 +586,7 @@ dependencies = [ "async-std", "async-trait", "color-eyre", - "console-subscriber 0.2.0", + "console-subscriber", "flume 0.11.0", "futures", "tokio", @@ -1096,7 +1076,7 @@ version = "0.69.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "cexpr", "clang-sys", "itertools 0.12.1", @@ -1121,9 +1101,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.2" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" dependencies = [ "serde", ] @@ -1211,9 +1191,9 @@ dependencies = [ [[package]] name = "bs58" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5353f36341f7451062466f0b755b96ac3a9547e4d7f6b70d603fc721a7d7896" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" dependencies = [ "tinyvec", ] @@ -1622,18 +1602,6 @@ dependencies = [ "yaml-rust", ] -[[package]] -name = "console-api" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2895653b4d9f1538a83970077cb01dfc77a4810524e51a110944688e916b18e" -dependencies = [ - "prost 0.11.9", - "prost-types 0.11.9", - "tonic 0.9.2", - "tracing-core", -] - [[package]] name = "console-api" version = "0.6.0" @@ -1641,34 +1609,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd326812b3fd01da5bb1af7d340d0d555fd3d4b641e7f1dfcf5962a902952787" dependencies = [ "futures-core", - "prost 0.12.3", - "prost-types 0.12.3", - "tonic 0.10.2", - "tracing-core", -] - -[[package]] -name = "console-subscriber" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4cf42660ac07fcebed809cfe561dd8730bcd35b075215e6479c516bcd0d11cb" -dependencies = [ - "console-api 0.5.0", - "crossbeam-channel", - "crossbeam-utils", - "futures", - "hdrhistogram", - "humantime", - "prost-types 0.11.9", - "serde", - "serde_json", - "thread_local", - "tokio", - "tokio-stream", - "tonic 0.9.2", - "tracing", + "prost", + "prost-types", + "tonic", "tracing-core", - "tracing-subscriber", ] [[package]] @@ -1677,19 +1621,19 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7481d4c57092cd1c19dd541b92bdce883de840df30aa5d03fd48a3935c01842e" dependencies = [ - "console-api 0.6.0", + "console-api", "crossbeam-channel", "crossbeam-utils", "futures-task", "hdrhistogram", "humantime", - "prost-types 0.12.3", + "prost-types", "serde", "serde_json", "thread_local", "tokio", "tokio-stream", - "tonic 0.10.2", + "tonic", "tracing", "tracing-core", "tracing-subscriber", @@ -2042,16 +1986,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89e0ae2c2a42be29595d05c50e3ce6096c0698a97e021c3289790f0750cc8e2" dependencies = [ - "custom_debug_derive 0.5.1", -] - -[[package]] -name = "custom_debug" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14e715bf0e503e909c7076c052e39dd215202e8edeb32f1c194fd630c314d256" -dependencies = [ - "custom_debug_derive 0.6.1", + "custom_debug_derive", ] [[package]] @@ -2062,21 +1997,7 @@ checksum = "08a9f3941234c9f62ceaa2782974827749de9b0a8a6487275a278da068e1baf7" dependencies = [ "proc-macro2", "syn 1.0.109", - "synstructure 0.12.6", -] - -[[package]] -name = "custom_debug_derive" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f731440b39c73910e253cb465ec1fac97732b3c7af215639881ec0c2a38f4f69" -dependencies = [ - "darling 0.20.8", - "itertools 0.12.1", - "proc-macro2", - "quote", - "syn 2.0.53", - "synstructure 0.13.1", + "synstructure", ] [[package]] @@ -2156,7 +2077,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown 0.14.3", + "hashbrown 0.14.4", "lock_api", "once_cell", "parking_lot_core", @@ -3059,9 +2980,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e57fa0ae458eb99874f54c09f4f9174f8b45fb87e854536a4e608696247f0c23" dependencies = [ "ahash 0.8.11", "allocator-api2", @@ -3073,7 +2994,7 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ - "hashbrown 0.14.3", + "hashbrown 0.14.4", ] [[package]] @@ -3277,7 +3198,7 @@ version = "0.5.26" dependencies = [ "anyhow", "async-broadcast", - "async-compatibility-layer 1.0.0 (git+https://github.com/EspressoSystems/async-compatibility-layer.git?tag=1.4.2)", + "async-compatibility-layer", "async-lock 2.8.0", "async-std", "async-trait", @@ -3289,7 +3210,7 @@ dependencies = [ "cdn-marshal", "clap", "commit", - "custom_debug 0.5.1", + "custom_debug", "dashmap", "derive_more", "either", @@ -3317,13 +3238,30 @@ dependencies = [ "versioned-binary-serialization", ] +[[package]] +name = "hotshot-builder-api" +version = "0.1.6" +dependencies = [ + "async-trait", + "clap", + "derive_more", + "futures", + "hotshot-types", + "serde", + "snafu 0.8.2", + "tagged-base64", + "tide-disco", + "toml", + "versioned-binary-serialization", +] + [[package]] name = "hotshot-example-types" version = "0.5.26" dependencies = [ "anyhow", "async-broadcast", - "async-compatibility-layer 1.0.0 (git+https://github.com/EspressoSystems/async-compatibility-layer.git?tag=1.4.2)", + "async-compatibility-layer", "async-lock 2.8.0", "async-std", "async-trait", @@ -3352,7 +3290,7 @@ name = "hotshot-examples" version = "0.5.26" dependencies = [ "async-broadcast", - "async-compatibility-layer 1.0.0 (git+https://github.com/EspressoSystems/async-compatibility-layer.git?tag=1.4.2)", + "async-compatibility-layer", "async-lock 2.8.0", "async-std", "async-trait", @@ -3364,7 +3302,7 @@ dependencies = [ "chrono", "clap", "commit", - "custom_debug 0.5.1", + "custom_debug", "dashmap", "derive_more", "either", @@ -3408,7 +3346,7 @@ dependencies = [ name = "hotshot-orchestrator" version = "0.5.26" dependencies = [ - "async-compatibility-layer 1.0.0 (git+https://github.com/EspressoSystems/async-compatibility-layer.git?tag=1.4.2)", + "async-compatibility-layer", "async-lock 2.8.0", "async-std", "blake3", @@ -3453,7 +3391,7 @@ name = "hotshot-task" version = "0.5.26" dependencies = [ "async-broadcast", - "async-compatibility-layer 1.0.0 (git+https://github.com/EspressoSystems/async-compatibility-layer.git?tag=1.4.2)", + "async-compatibility-layer", "async-std", "futures", "tokio", @@ -3465,7 +3403,7 @@ name = "hotshot-task-impls" version = "0.5.26" dependencies = [ "async-broadcast", - "async-compatibility-layer 1.0.0 (git+https://github.com/EspressoSystems/async-compatibility-layer.git?tag=1.4.2)", + "async-compatibility-layer", "async-lock 2.8.0", "async-std", "async-trait", @@ -3475,9 +3413,9 @@ dependencies = [ "commit", "either", "futures", + "hotshot-builder-api", "hotshot-task", "hotshot-types", - "hs-builder-api", "jf-primitives", "rand 0.8.5", "serde", @@ -3496,7 +3434,7 @@ name = "hotshot-testing" version = "0.5.26" dependencies = [ "async-broadcast", - "async-compatibility-layer 1.0.0 (git+https://github.com/EspressoSystems/async-compatibility-layer.git?tag=1.4.2)", + "async-compatibility-layer", "async-lock 2.8.0", "async-std", "async-trait", @@ -3506,13 +3444,13 @@ dependencies = [ "ethereum-types", "futures", "hotshot", + "hotshot-builder-api", "hotshot-example-types", "hotshot-macros", "hotshot-orchestrator", "hotshot-task", "hotshot-task-impls", "hotshot-types", - "hs-builder-api", "jf-primitives", "portpicker", "rand 0.8.5", @@ -3531,7 +3469,7 @@ name = "hotshot-testing-macros" version = "0.5.26" dependencies = [ "ark-bls12-381", - "async-compatibility-layer 1.0.0 (git+https://github.com/EspressoSystems/async-compatibility-layer.git?tag=1.4.2)", + "async-compatibility-layer", "async-lock 2.8.0", "async-std", "async-trait", @@ -3558,7 +3496,6 @@ dependencies = [ [[package]] name = "hotshot-types" version = "0.1.11" -source = "git+https://github.com/EspressoSystems/hotshot-types?tag=0.1.11#65a06f75612f529c11e97c4320f1f6d0f49ba81e" dependencies = [ "anyhow", "ark-bls12-381", @@ -3568,7 +3505,7 @@ dependencies = [ "ark-ff", "ark-serialize", "ark-std", - "async-compatibility-layer 1.0.0 (git+https://github.com/EspressoSystems/async-compatibility-layer.git?tag=1.4.1)", + "async-compatibility-layer", "async-lock 2.8.0", "async-std", "async-trait", @@ -3576,7 +3513,7 @@ dependencies = [ "bitvec", "blake3", "commit", - "custom_debug 0.6.1", + "custom_debug", "derivative", "digest 0.10.7", "displaydoc", @@ -3593,6 +3530,7 @@ dependencies = [ "rand 0.8.5", "rand_chacha 0.3.1", "serde", + "serde_json", "sha2 0.10.8", "snafu 0.8.2", "tagged-base64", @@ -3607,7 +3545,7 @@ dependencies = [ name = "hotshot-web-server" version = "0.5.26" dependencies = [ - "async-compatibility-layer 1.0.0 (git+https://github.com/EspressoSystems/async-compatibility-layer.git?tag=1.4.2)", + "async-compatibility-layer", "async-lock 2.8.0", "async-std", "clap", @@ -3621,24 +3559,6 @@ dependencies = [ "versioned-binary-serialization", ] -[[package]] -name = "hs-builder-api" -version = "0.1.6" -source = "git+https://github.com/EspressoSystems/hs-builder-api?tag=0.1.6#ecc3f6e8eaa6f0744084a49ec085cea891a7b2fa" -dependencies = [ - "async-trait", - "clap", - "derive_more", - "futures", - "hotshot-types", - "serde", - "snafu 0.8.2", - "tagged-base64", - "tide-disco", - "toml", - "versioned-binary-serialization", -] - [[package]] name = "http" version = "0.2.12" @@ -3974,7 +3894,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown 0.14.4", "serde", ] @@ -4120,7 +4040,7 @@ dependencies = [ "downcast-rs", "dyn-clone 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", "espresso-systems-common 0.4.0", - "hashbrown 0.14.3", + "hashbrown 0.14.4", "itertools 0.12.1", "jf-primitives", "jf-relation", @@ -4162,7 +4082,7 @@ dependencies = [ "displaydoc", "espresso-systems-common 0.4.0", "generic-array", - "hashbrown 0.14.3", + "hashbrown 0.14.4", "icicle-bn254", "icicle-core", "icicle-cuda-runtime", @@ -4200,7 +4120,7 @@ dependencies = [ "displaydoc", "downcast-rs", "dyn-clone 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", - "hashbrown 0.14.3", + "hashbrown 0.14.4", "itertools 0.12.1", "jf-utils", "num-bigint", @@ -4630,12 +4550,12 @@ name = "libp2p-networking" version = "0.5.26" dependencies = [ "anyhow", - "async-compatibility-layer 1.0.0 (git+https://github.com/EspressoSystems/async-compatibility-layer.git?tag=1.4.2)", + "async-compatibility-layer", "async-lock 2.8.0", "async-std", "async-trait", "blake3", - "custom_debug 0.5.1", + "custom_debug", "dashmap", "derive_builder 0.20.0", "either", @@ -4969,7 +4889,7 @@ version = "0.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "libc", "redox_syscall", ] @@ -5112,7 +5032,7 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" dependencies = [ - "hashbrown 0.14.3", + "hashbrown 0.14.4", ] [[package]] @@ -5632,7 +5552,7 @@ version = "0.10.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "cfg-if", "foreign-types", "libc", @@ -6142,16 +6062,6 @@ dependencies = [ "syn 2.0.53", ] -[[package]] -name = "prost" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" -dependencies = [ - "bytes 1.5.0", - "prost-derive 0.11.9", -] - [[package]] name = "prost" version = "0.12.3" @@ -6159,20 +6069,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" dependencies = [ "bytes 1.5.0", - "prost-derive 0.12.3", -] - -[[package]] -name = "prost-derive" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" -dependencies = [ - "anyhow", - "itertools 0.10.5", - "proc-macro2", - "quote", - "syn 1.0.109", + "prost-derive", ] [[package]] @@ -6188,22 +6085,13 @@ dependencies = [ "syn 2.0.53", ] -[[package]] -name = "prost-types" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13" -dependencies = [ - "prost 0.11.9", -] - [[package]] name = "prost-types" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e" dependencies = [ - "prost 0.12.3", + "prost", ] [[package]] @@ -6637,7 +6525,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ "base64 0.21.7", - "bitflags 2.4.2", + "bitflags 2.5.0", "serde", "serde_derive", ] @@ -6769,7 +6657,7 @@ version = "0.38.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "errno", "libc", "linux-raw-sys 0.4.13", @@ -7535,7 +7423,7 @@ checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" dependencies = [ "atoi", "base64 0.21.7", - "bitflags 2.4.2", + "bitflags 2.5.0", "byteorder", "bytes 1.5.0", "crc", @@ -7578,7 +7466,7 @@ checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" dependencies = [ "atoi", "base64 0.21.7", - "bitflags 2.4.2", + "bitflags 2.5.0", "byteorder", "crc", "dotenvy", @@ -7903,17 +7791,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "synstructure" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.53", -] - [[package]] name = "system-configuration" version = "0.5.1" @@ -8339,34 +8216,6 @@ dependencies = [ "winnow 0.6.5", ] -[[package]] -name = "tonic" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3082666a3a6433f7f511c7192923fa1fe07c69332d3c6a2e6bb040b569199d5a" -dependencies = [ - "async-trait", - "axum", - "base64 0.21.7", - "bytes 1.5.0", - "futures-core", - "futures-util", - "h2", - "http 0.2.12", - "http-body", - "hyper", - "hyper-timeout", - "percent-encoding", - "pin-project", - "prost 0.11.9", - "tokio", - "tokio-stream", - "tower", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "tonic" version = "0.10.2" @@ -8385,7 +8234,7 @@ dependencies = [ "hyper-timeout", "percent-encoding", "pin-project", - "prost 0.12.3", + "prost", "tokio", "tokio-stream", "tower", @@ -8731,9 +8580,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" [[package]] name = "valuable" @@ -8743,9 +8592,9 @@ checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "value-bag" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec26a25bd6fca441cdd0f769fd7f891bae119f996de31f86a5eddccef54c1d" +checksum = "74797339c3b98616c009c7c3eb53a0ce41e85c8ec66bd3db96ed132d20cfdee8" dependencies = [ "value-bag-serde1", "value-bag-sval2", @@ -8753,9 +8602,9 @@ dependencies = [ [[package]] name = "value-bag-serde1" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ead5b693d906686203f19a49e88c477fb8c15798b68cf72f60b4b5521b4ad891" +checksum = "cc35703541cbccb5278ef7b589d79439fc808ff0b5867195a3230f9a47421d39" dependencies = [ "erased-serde", "serde", @@ -8764,9 +8613,9 @@ dependencies = [ [[package]] name = "value-bag-sval2" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9d0f4a816370c3a0d7d82d603b62198af17675b12fe5e91de6b47ceb505882" +checksum = "285b43c29d0b4c0e65aad24561baee67a1b69dc9be9375d4a85138cbf556f7f8" dependencies = [ "sval", "sval_buffer", diff --git a/Cargo.toml b/Cargo.toml index 6c9fcb4243..12aaab4e37 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,16 +12,18 @@ repository = "https://github.com/EspressoSystems/HotShot" # when implementing traits externally [workspace] members = [ - "crates/hotshot", - "crates/hotshot-stake-table", - "crates/libp2p-networking", - "crates/macros", - "crates/testing-macros", - "crates/task", - "crates/task-impls", - "crates/testing", - "crates/examples", - "crates/example-types", + "crates/hotshot", + "crates/hotshot-stake-table", + "crates/libp2p-networking", + "crates/macros", + "crates/testing-macros", + "crates/task", + "crates/task-impls", + "crates/testing", + "crates/examples", + "crates/example-types", + "crates/types", + "crates/builder-api", ] resolver = "2" @@ -59,9 +61,9 @@ futures = "0.3.30" # TODO generic-array should not be a direct dependency # https://github.com/EspressoSystems/HotShot/issues/1850 generic-array = { version = "0.14.7", features = ["serde"] } -hotshot-types = { git = "https://github.com/EspressoSystems/hotshot-types", tag = "0.1.11" } -hs-builder-api = { git = "https://github.com/EspressoSystems/hs-builder-api", tag = "0.1.6" } -jf-primitives = { git = "https://github.com/EspressoSystems/jellyfish", tag = "0.4.2" } +jf-primitives = { git = "https://github.com/EspressoSystems/jellyfish", tag = "0.4.2", features = [ + "test-srs", +] } jf-plonk = { git = "https://github.com/EspressoSystems/jellyfish", tag = "0.4.2" } jf-relation = { git = "https://github.com/EspressoSystems/jellyfish", tag = "0.4.2" } jf-utils = { git = "https://github.com/espressosystems/jellyfish", tag = "0.4.2" } diff --git a/crates/builder-api/Cargo.toml b/crates/builder-api/Cargo.toml new file mode 100644 index 0000000000..a351f1099f --- /dev/null +++ b/crates/builder-api/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "hotshot-builder-api" +version = "0.1.6" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +async-trait = { workspace = true } +clap = { version = "4.4", features = ["derive", "env"] } +derive_more = "0.99" +futures = "0.3" +hotshot-types = { path = "../types" } +serde = { workspace = true } +snafu = { workspace = true } +tagged-base64 = { workspace = true } +tide-disco = { workspace = true } +toml = { workspace = true } +versioned-binary-serialization = { workspace = true } diff --git a/crates/builder-api/README.md b/crates/builder-api/README.md new file mode 100644 index 0000000000..8f4788f16e --- /dev/null +++ b/crates/builder-api/README.md @@ -0,0 +1,4 @@ +# hotshot-builder-api +Minimal dependencies shared API definitions for HotShot Builder protocol + +# HotShot Consensus Module diff --git a/crates/builder-api/api/builder.toml b/crates/builder-api/api/builder.toml new file mode 100644 index 0000000000..e64b94da48 --- /dev/null +++ b/crates/builder-api/api/builder.toml @@ -0,0 +1,65 @@ +# Copyright (c) 2024 Espresso Systems (espressosys.com) +# This file is part of the HotShot Builder Protocol. +# +# MIT License +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +[meta] +NAME = "hs-builder-get" +DESCRIPTION = "" +FORMAT_VERSION = "0.1.0" + +[route.available_blocks] +PATH = ["availableblocks/:parent_hash"] +":parent_hash" = "TaggedBase64" +DOC = """ +Get descriptions for all block candidates based on a specific parent block. + +Returns +``` +[ + "block_metadata": { + "block_hash": TaggedBase64, + "block_size": integer, + "offered_fee": integer, + }, +] +``` +""" + +[route.claim_block] +PATH = ["claimblock/:block_hash/:signature"] +":block_hash" = "TaggedBase64" +":signature" = "TaggedBase64" +DOC = """ +Get the specified block candidate. + +Returns application-specific encoded transactions type +""" + +[route.claim_header_input] +PATH = ["claimheaderinput/:block_hash/:signature"] +":block_hash" = "TaggedBase64" +":signature" = "TaggedBase64" +DOC = """ +Get the specified block candidate. + +Returns application-specific block header type +""" diff --git a/crates/builder-api/api/submit.toml b/crates/builder-api/api/submit.toml new file mode 100644 index 0000000000..a9d1db4b46 --- /dev/null +++ b/crates/builder-api/api/submit.toml @@ -0,0 +1,33 @@ +# Copyright (c) 2024 Espresso Systems (espressosys.com) +# This file is part of the HotShot Builder Protocol. +# +# MIT License +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +[meta] +NAME = "hs-builder-submit" +DESCRIPTION = "" +FORMAT_VERSION = "0.1.0" + +[route.submit_txn] +PATH = ["/submit"] +METHOD = "POST" +DOC = "Submit a transaction to builder's private mempool." diff --git a/crates/builder-api/src/api.rs b/crates/builder-api/src/api.rs new file mode 100644 index 0000000000..62f2edf08f --- /dev/null +++ b/crates/builder-api/src/api.rs @@ -0,0 +1,59 @@ +// Copyright (c) 2022 Espresso Systems (espressosys.com) +// This file is part of the HotShot Query Service library. +// +// This program is free software: you can redistribute it and/or modify it under the terms of the GNU +// General Public License as published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without +// even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// You should have received a copy of the GNU General Public License along with this program. If not, +// see . + +use std::fs; +use std::path::Path; +use tide_disco::api::{Api, ApiError}; +use toml::{map::Entry, Value}; +use versioned_binary_serialization::version::StaticVersionType; + +pub(crate) fn load_api( + path: Option>, + default: &str, + extensions: impl IntoIterator, +) -> Result, ApiError> { + let mut toml = match path { + Some(path) => load_toml(path.as_ref())?, + None => toml::from_str(default).map_err(|err| ApiError::CannotReadToml { + reason: err.to_string(), + })?, + }; + for extension in extensions { + merge_toml(&mut toml, extension); + } + Api::new(toml) +} + +fn merge_toml(into: &mut Value, from: Value) { + if let (Value::Table(into), Value::Table(from)) = (into, from) { + for (key, value) in from { + match into.entry(key) { + Entry::Occupied(mut entry) => merge_toml(entry.get_mut(), value), + Entry::Vacant(entry) => { + entry.insert(value); + } + } + } + } +} + +fn load_toml(path: &Path) -> Result { + let bytes = fs::read(path).map_err(|err| ApiError::CannotReadToml { + reason: err.to_string(), + })?; + let string = std::str::from_utf8(&bytes).map_err(|err| ApiError::CannotReadToml { + reason: err.to_string(), + })?; + toml::from_str(string).map_err(|err| ApiError::CannotReadToml { + reason: err.to_string(), + }) +} diff --git a/crates/builder-api/src/block_info.rs b/crates/builder-api/src/block_info.rs new file mode 100644 index 0000000000..0afb546e4b --- /dev/null +++ b/crates/builder-api/src/block_info.rs @@ -0,0 +1,38 @@ +use std::{hash::Hash, marker::PhantomData}; + +use hotshot_types::{ + traits::{node_implementation::NodeType, signature_key::SignatureKey, BlockPayload}, + utils::BuilderCommitment, + vid::VidCommitment, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)] +#[serde(bound = "")] +pub struct AvailableBlockInfo { + pub block_hash: BuilderCommitment, + pub block_size: u64, + pub offered_fee: u64, + pub signature: <::SignatureKey as SignatureKey>::PureAssembledSignatureType, + pub sender: ::SignatureKey, + pub _phantom: PhantomData, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)] +#[serde(bound = "")] +pub struct AvailableBlockData { + pub block_payload: ::BlockPayload, + pub metadata: <::BlockPayload as BlockPayload>::Metadata, + pub signature: <::SignatureKey as SignatureKey>::PureAssembledSignatureType, + pub sender: ::SignatureKey, + pub _phantom: PhantomData, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)] +#[serde(bound = "")] +pub struct AvailableBlockHeaderInput { + pub vid_commitment: VidCommitment, + pub signature: <::SignatureKey as SignatureKey>::PureAssembledSignatureType, + pub sender: ::SignatureKey, + pub _phantom: PhantomData, +} diff --git a/crates/builder-api/src/builder.rs b/crates/builder-api/src/builder.rs new file mode 100644 index 0000000000..23026675b9 --- /dev/null +++ b/crates/builder-api/src/builder.rs @@ -0,0 +1,197 @@ +use std::{fmt::Display, path::PathBuf}; + +use clap::Args; +use derive_more::From; +use futures::FutureExt; +use hotshot_types::{ + traits::{node_implementation::NodeType, signature_key::SignatureKey}, + utils::BuilderCommitment, +}; +use serde::{Deserialize, Serialize}; +use snafu::{ResultExt, Snafu}; +use tagged_base64::TaggedBase64; +use tide_disco::{ + api::ApiError, + method::{ReadState, WriteState}, + Api, RequestError, StatusCode, +}; +use versioned_binary_serialization::version::StaticVersionType; + +use crate::{ + api::load_api, + data_source::{AcceptsTxnSubmits, BuilderDataSource}, +}; + +#[derive(Args, Default)] +pub struct Options { + #[arg(long = "builder-api-path", env = "HOTSHOT_BUILDER_API_PATH")] + pub api_path: Option, + + /// Additional API specification files to merge with `builder-api-path`. + /// + /// These optional files may contain route definitions for application-specific routes that have + /// been added as extensions to the basic builder API. + #[arg( + long = "builder-extension", + env = "HOTSHOT_BUILDER_EXTENSIONS", + value_delimiter = ',' + )] + pub extensions: Vec, +} + +#[derive(Clone, Debug, Snafu, Deserialize, Serialize)] +#[snafu(visibility(pub))] +pub enum BuildError { + /// The requested resource does not exist or is not known to this builder service. + NotFound, + /// The requested resource exists but is not currently available. + Missing, + /// There was an error while trying to fetch the requested resource. + #[snafu(display("Failed to fetch requested resource: {message}"))] + Error { message: String }, +} + +#[derive(Clone, Debug, From, Snafu, Deserialize, Serialize)] +#[snafu(visibility(pub))] +pub enum Error { + Request { + source: RequestError, + }, + #[snafu(display("error building block from {resource}: {source}"))] + #[from(ignore)] + BlockAvailable { + source: BuildError, + resource: String, + }, + #[snafu(display("error claiming block {resource}: {source}"))] + #[from(ignore)] + BlockClaim { + source: BuildError, + resource: String, + }, + #[snafu(display("error unpacking transaction: {source}"))] + #[from(ignore)] + TxnUnpack { + source: RequestError, + }, + #[snafu(display("error submitting transaction: {source}"))] + #[from(ignore)] + TxnSubmit { + source: BuildError, + }, + Custom { + message: String, + status: StatusCode, + }, +} + +impl tide_disco::error::Error for Error { + fn catch_all(status: StatusCode, msg: String) -> Self { + Error::Custom { + message: msg, + status, + } + } + + fn status(&self) -> StatusCode { + match self { + Error::Request { .. } => StatusCode::BadRequest, + Error::BlockAvailable { source, .. } | Error::BlockClaim { source, .. } => match source + { + BuildError::NotFound => StatusCode::NotFound, + BuildError::Missing => StatusCode::NotFound, + BuildError::Error { .. } => StatusCode::InternalServerError, + }, + Error::TxnUnpack { .. } => StatusCode::BadRequest, + Error::TxnSubmit { .. } => StatusCode::InternalServerError, + Error::Custom { .. } => StatusCode::InternalServerError, + } + } +} + +pub fn define_api( + options: &Options, +) -> Result, ApiError> +where + State: 'static + Send + Sync + ReadState, + ::State: Send + Sync + BuilderDataSource, + Types: NodeType, + <::SignatureKey as SignatureKey>::PureAssembledSignatureType: + for<'a> TryFrom<&'a TaggedBase64> + Into + Display, + for<'a> <<::SignatureKey as SignatureKey>::PureAssembledSignatureType as TryFrom< + &'a TaggedBase64, + >>::Error: Display, +{ + let mut api = load_api::( + options.api_path.as_ref(), + include_str!("../api/builder.toml"), + options.extensions.clone(), + )?; + api.with_version("0.0.1".parse().unwrap()) + .get("available_blocks", |req, state| { + async move { + let hash = req.blob_param("parent_hash")?; + state + .get_available_blocks(&hash) + .await + .context(BlockAvailableSnafu { + resource: hash.to_string(), + }) + } + .boxed() + })? + .get("claim_block", |req, state| { + async move { + let hash: BuilderCommitment = req.blob_param("block_hash")?; + let signature = req.blob_param("signature")?; + state + .claim_block(&hash, &signature) + .await + .context(BlockClaimSnafu { + resource: hash.to_string(), + }) + } + .boxed() + })? + .get("claim_header_input", |req, state| { + async move { + let hash: BuilderCommitment = req.blob_param("block_hash")?; + let signature = req.blob_param("signature")?; + state + .claim_block_header_input(&hash, &signature) + .await + .context(BlockClaimSnafu { + resource: hash.to_string(), + }) + } + .boxed() + })?; + Ok(api) +} + +pub fn submit_api( + options: &Options, +) -> Result, ApiError> +where + State: 'static + Send + Sync + WriteState, + ::State: Send + Sync + AcceptsTxnSubmits, + Types: NodeType, +{ + let mut api = load_api::( + options.api_path.as_ref(), + include_str!("../api/submit.toml"), + options.extensions.clone(), + )?; + api.with_version("0.0.1".parse().unwrap()) + .post("submit_txn", |req, state| { + async move { + let tx = req + .body_auto::<::Transaction, Ver>(Ver::instance()) + .context(TxnUnpackSnafu)?; + state.submit_txn(tx).await.context(TxnSubmitSnafu)?; + Ok(()) + } + .boxed() + })?; + Ok(api) +} diff --git a/crates/builder-api/src/data_source.rs b/crates/builder-api/src/data_source.rs new file mode 100644 index 0000000000..69a75feb85 --- /dev/null +++ b/crates/builder-api/src/data_source.rs @@ -0,0 +1,43 @@ +use async_trait::async_trait; +use hotshot_types::{ + traits::{node_implementation::NodeType, signature_key::SignatureKey}, + utils::BuilderCommitment, + vid::VidCommitment, +}; +use tagged_base64::TaggedBase64; + +use crate::{ + block_info::{AvailableBlockData, AvailableBlockHeaderInput, AvailableBlockInfo}, + builder::BuildError, +}; + +#[async_trait] +pub trait BuilderDataSource +where + I: NodeType, + <::SignatureKey as SignatureKey>::PureAssembledSignatureType: + for<'a> TryFrom<&'a TaggedBase64> + Into, +{ + async fn get_available_blocks( + &self, + for_parent: &VidCommitment, + ) -> Result>, BuildError>; + async fn claim_block( + &self, + block_hash: &BuilderCommitment, + signature: &<::SignatureKey as SignatureKey>::PureAssembledSignatureType, + ) -> Result, BuildError>; + async fn claim_block_header_input( + &self, + block_hash: &BuilderCommitment, + signature: &<::SignatureKey as SignatureKey>::PureAssembledSignatureType, + ) -> Result, BuildError>; +} + +#[async_trait] +pub trait AcceptsTxnSubmits +where + I: NodeType, +{ + async fn submit_txn(&mut self, txn: ::Transaction) -> Result<(), BuildError>; +} diff --git a/crates/builder-api/src/lib.rs b/crates/builder-api/src/lib.rs new file mode 100644 index 0000000000..c89608d85c --- /dev/null +++ b/crates/builder-api/src/lib.rs @@ -0,0 +1,5 @@ +mod api; +pub mod block_info; +pub mod builder; +pub mod data_source; +pub mod query_data; diff --git a/crates/builder-api/src/query_data.rs b/crates/builder-api/src/query_data.rs new file mode 100644 index 0000000000..44b2d1c24f --- /dev/null +++ b/crates/builder-api/src/query_data.rs @@ -0,0 +1,15 @@ +// Copyright (c) 2024 Espresso Systems (espressosys.com) +// This file is part of the HotShot HotShot Builder Protocol. +// +// TODO: License + +use hotshot_types::traits::node_implementation::NodeType; +use serde::{Deserialize, Serialize}; + +use crate::block_info::AvailableBlockInfo; + +#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq, Hash)] +#[serde(bound = "")] +pub struct AvailableBlocksQueryData { + pub blocks: Vec>, +} diff --git a/crates/example-types/Cargo.toml b/crates/example-types/Cargo.toml index f86e7c67b7..19034f2c71 100644 --- a/crates/example-types/Cargo.toml +++ b/crates/example-types/Cargo.toml @@ -9,21 +9,19 @@ authors = { workspace = true } default = [] # NOTE this is used to activate the slow tests we don't wish to run in CI slow-tests = [] -gpu-vid = [ - "hotshot-task-impls/gpu-vid", -] +gpu-vid = ["hotshot-task-impls/gpu-vid"] [dependencies] async-broadcast = { workspace = true } async-compatibility-layer = { workspace = true } async-trait = { workspace = true } -anyhow = { workspace = true } +anyhow = { workspace = true } sha3 = "^0.10" commit = { workspace = true } either = { workspace = true } futures = { workspace = true } hotshot = { path = "../hotshot" } -hotshot-types = { workspace = true } +hotshot-types = { path = "../types" } hotshot-orchestrator = { version = "0.5.26", path = "../orchestrator", default-features = false } hotshot-task-impls = { path = "../task-impls", version = "0.5.26", default-features = false } rand = { workspace = true } diff --git a/crates/examples/Cargo.toml b/crates/examples/Cargo.toml index de083e5c3b..32ae3315ae 100644 --- a/crates/examples/Cargo.toml +++ b/crates/examples/Cargo.toml @@ -9,9 +9,7 @@ rust-version = "1.65.0" [features] default = ["docs", "doc-images"] -gpu-vid = [ - "hotshot-task-impls/gpu-vid", -] +gpu-vid = ["hotshot-task-impls/gpu-vid"] # Features required for binaries bin-orchestrator = ["clap"] @@ -105,7 +103,7 @@ embed-doc-image = "0.1.4" futures = { workspace = true } hotshot-web-server = { version = "0.5.26", path = "../web_server", default-features = false } hotshot-orchestrator = { version = "0.5.26", path = "../orchestrator", default-features = false } -hotshot-types = { workspace = true } +hotshot-types = { path = "../types" } hotshot-task-impls = { path = "../task-impls", version = "0.5.26", default-features = false } libp2p-identity = { workspace = true } libp2p-networking = { workspace = true } @@ -129,10 +127,10 @@ tracing = { workspace = true } tokio = { workspace = true } cdn-client = { workspace = true, features = ["insecure", "runtime-tokio"] } cdn-broker = { workspace = true, features = [ - "insecure", - "runtime-tokio", - "strong_consistency", - "local_discovery", + "insecure", + "runtime-tokio", + "strong_consistency", + "local_discovery", ] } cdn-marshal = { workspace = true, features = ["insecure", "runtime-tokio"] } @@ -140,10 +138,10 @@ cdn-marshal = { workspace = true, features = ["insecure", "runtime-tokio"] } async-std = { workspace = true } cdn-client = { workspace = true, features = ["insecure", "runtime-async-std"] } cdn-broker = { workspace = true, features = [ - "insecure", - "runtime-async-std", - "strong_consistency", - "local_discovery", + "insecure", + "runtime-async-std", + "strong_consistency", + "local_discovery", ] } cdn-marshal = { workspace = true, features = ["insecure", "runtime-async-std"] } diff --git a/crates/hotshot-stake-table/Cargo.toml b/crates/hotshot-stake-table/Cargo.toml index dad2699625..601d6c1f63 100644 --- a/crates/hotshot-stake-table/Cargo.toml +++ b/crates/hotshot-stake-table/Cargo.toml @@ -14,7 +14,7 @@ ark-serialize = { workspace = true } ark-std = { workspace = true } digest = { workspace = true } ethereum-types = { workspace = true } -hotshot-types = { workspace = true } +hotshot-types = { path = "../types" } jf-primitives = { workspace = true } jf-utils = { workspace = true } serde = { workspace = true, features = ["rc"] } diff --git a/crates/hotshot/Cargo.toml b/crates/hotshot/Cargo.toml index d75f7228b6..39ebb32512 100644 --- a/crates/hotshot/Cargo.toml +++ b/crates/hotshot/Cargo.toml @@ -40,7 +40,7 @@ futures = { workspace = true } hotshot-orchestrator = { version = "0.5.26", path = "../orchestrator", default-features = false } hotshot-task = { path = "../task" } hotshot-task-impls = { path = "../task-impls", version = "0.5.26", default-features = false } -hotshot-types = { workspace = true } +hotshot-types = { path = "../types" } hotshot-web-server = { version = "0.5.26", path = "../web_server", default-features = false } libp2p-identity = { workspace = true } libp2p-networking = { workspace = true } @@ -58,10 +58,10 @@ versioned-binary-serialization = { workspace = true } tokio = { workspace = true } cdn-client = { workspace = true, features = ["insecure", "runtime-tokio"] } cdn-broker = { workspace = true, features = [ - "insecure", - "runtime-tokio", - "strong_consistency", - "local_discovery", + "insecure", + "runtime-tokio", + "strong_consistency", + "local_discovery", ] } cdn-marshal = { workspace = true, features = ["insecure", "runtime-tokio"] } @@ -69,10 +69,10 @@ cdn-marshal = { workspace = true, features = ["insecure", "runtime-tokio"] } async-std = { workspace = true } cdn-client = { workspace = true, features = ["insecure", "runtime-async-std"] } cdn-broker = { workspace = true, features = [ - "insecure", - "runtime-async-std", - "strong_consistency", - "local_discovery", + "insecure", + "runtime-async-std", + "strong_consistency", + "local_discovery", ] } cdn-marshal = { workspace = true, features = ["insecure", "runtime-async-std"] } diff --git a/crates/libp2p-networking/Cargo.toml b/crates/libp2p-networking/Cargo.toml index 1083ff638b..b5ba3e00f1 100644 --- a/crates/libp2p-networking/Cargo.toml +++ b/crates/libp2p-networking/Cargo.toml @@ -21,7 +21,7 @@ custom_debug = { workspace = true } derive_builder = "0.20.0" either = { workspace = true } futures = { workspace = true } -hotshot-types = { workspace = true } +hotshot-types = { path = "../types" } libp2p-swarm-derive = { workspace = true } libp2p-identity = { workspace = true } rand = { workspace = true } @@ -30,7 +30,7 @@ serde_bytes = { workspace = true } serde_json = { workspace = true } snafu = { workspace = true } tide = { version = "0.16", optional = true, default-features = false, features = [ - "h1-server", + "h1-server", ] } tracing = { workspace = true } versioned-binary-serialization = { workspace = true } diff --git a/crates/orchestrator/Cargo.toml b/crates/orchestrator/Cargo.toml index a564a8d19f..001d0ea41d 100644 --- a/crates/orchestrator/Cargo.toml +++ b/crates/orchestrator/Cargo.toml @@ -10,7 +10,7 @@ clap = { version = "4.0", features = ["derive", "env"], optional = false } futures = { workspace = true } libp2p = { workspace = true } blake3 = { workspace = true } -hotshot-types = { workspace = true } +hotshot-types = { path = "../types" } tide-disco = { workspace = true } surf-disco = { workspace = true } tracing = { workspace = true } diff --git a/crates/task-impls/Cargo.toml b/crates/task-impls/Cargo.toml index 387b245e5e..7886a22388 100644 --- a/crates/task-impls/Cargo.toml +++ b/crates/task-impls/Cargo.toml @@ -17,8 +17,8 @@ commit = { workspace = true } either = { workspace = true } futures = { workspace = true } hotshot-task = { path = "../task" } -hotshot-types = { workspace = true } -hs-builder-api = { workspace = true } +hotshot-types = { path = "../types" } +hotshot-builder-api = { path = "../builder-api" } jf-primitives = { workspace = true } rand = { workspace = true } serde = { workspace = true } @@ -31,9 +31,7 @@ tracing = { workspace = true } versioned-binary-serialization = { workspace = true } [features] -gpu-vid = [ - "hotshot-types/gpu-vid" -] +gpu-vid = ["hotshot-types/gpu-vid"] [target.'cfg(all(async_executor_impl = "tokio"))'.dependencies] tokio = { workspace = true } diff --git a/crates/task-impls/src/builder.rs b/crates/task-impls/src/builder.rs index 9c950f162a..92ae382df6 100644 --- a/crates/task-impls/src/builder.rs +++ b/crates/task-impls/src/builder.rs @@ -1,12 +1,12 @@ use async_compatibility_layer::art::async_sleep; use std::time::{Duration, Instant}; +use hotshot_builder_api::builder::{BuildError, Error as BuilderApiError}; use hotshot_types::{ traits::{node_implementation::NodeType, signature_key::SignatureKey}, utils::BuilderCommitment, vid::VidCommitment, }; -use hs_builder_api::builder::{BuildError, Error as BuilderApiError}; use serde::{Deserialize, Serialize}; use snafu::Snafu; use surf_disco::{client::HealthStatus, Client, Url}; diff --git a/crates/testing-macros/Cargo.toml b/crates/testing-macros/Cargo.toml index 68410cc2b1..022176586d 100644 --- a/crates/testing-macros/Cargo.toml +++ b/crates/testing-macros/Cargo.toml @@ -15,7 +15,7 @@ commit = { workspace = true } either = { workspace = true } futures = { workspace = true } hotshot = { path = "../hotshot", default-features = false } -hotshot-types = { workspace = true } +hotshot-types = { path = "../types" } hotshot-testing = { path = "../testing", default-features = false } hotshot-example-types = { path = "../example-types" } jf-primitives = { workspace = true } diff --git a/crates/testing/Cargo.toml b/crates/testing/Cargo.toml index 314c320237..54af33edf7 100644 --- a/crates/testing/Cargo.toml +++ b/crates/testing/Cargo.toml @@ -9,9 +9,7 @@ authors = { workspace = true } default = [] # NOTE this is used to activate the slow tests we don't wish to run in CI slow-tests = [] -gpu-vid = [ - "hotshot-types/gpu-vid" -] +gpu-vid = ["hotshot-types/gpu-vid"] [dependencies] async-broadcast = { workspace = true } @@ -29,8 +27,8 @@ hotshot-macros = { path = "../macros" } hotshot-orchestrator = { version = "0.5.26", path = "../orchestrator", default-features = false } hotshot-task = { path = "../task" } hotshot-task-impls = { path = "../task-impls", version = "0.5.26", default-features = false } -hotshot-types = { workspace = true } -hs-builder-api = { workspace = true } +hotshot-types = { path = "../types" } +hotshot-builder-api = { path = "../builder-api" } jf-primitives = { workspace = true } portpicker = { workspace = true } rand = { workspace = true } diff --git a/crates/testing/src/block_builder.rs b/crates/testing/src/block_builder.rs index 5b7bc20968..1074ea0500 100644 --- a/crates/testing/src/block_builder.rs +++ b/crates/testing/src/block_builder.rs @@ -3,6 +3,11 @@ use async_trait::async_trait; use futures::future::BoxFuture; use hotshot::traits::BlockPayload; use hotshot::types::SignatureKey; +use hotshot_builder_api::{ + block_info::{AvailableBlockData, AvailableBlockHeaderInput, AvailableBlockInfo}, + builder::{BuildError, Options}, + data_source::BuilderDataSource, +}; use hotshot_example_types::{block_types::TestBlockPayload, node_types::TestTypes}; use hotshot_types::{ constants::{Version01, STATIC_VER_0_1}, @@ -10,11 +15,6 @@ use hotshot_types::{ utils::BuilderCommitment, vid::VidCommitment, }; -use hs_builder_api::{ - block_info::{AvailableBlockData, AvailableBlockHeaderInput, AvailableBlockInfo}, - builder::{BuildError, Options}, - data_source::BuilderDataSource, -}; use tide_disco::{method::ReadState, App, Url}; /// The only block [`TestableBuilderSource`] provides @@ -111,13 +111,13 @@ impl BuilderDataSource for TestableBuilderSource { /// If constructing and launching the builder fails for any reason pub fn run_builder(url: Url) { let builder_api = - hs_builder_api::builder::define_api::( + hotshot_builder_api::builder::define_api::( &Options::default(), ) .expect("Failed to construct the builder API"); let (pub_key, priv_key) = ::SignatureKey::generated_from_seed_indexed([1; 32], 0); - let mut app: App = + let mut app: App = App::with_state(TestableBuilderSource { priv_key, pub_key }); app.register_module("/", builder_api) .expect("Failed to register the builder API"); diff --git a/crates/types/Cargo.toml b/crates/types/Cargo.toml new file mode 100644 index 0000000000..720932342b --- /dev/null +++ b/crates/types/Cargo.toml @@ -0,0 +1,65 @@ +[package] +authors = ["Espresso Systems "] +description = "Types and traits for the HotShot consesus module" +edition = "2021" +name = "hotshot-types" +version = "0.1.11" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = { workspace = true } +ark-bls12-381 = { workspace = true } +ark-bn254 = { workspace = true } +ark-ec = { workspace = true } +ark-ed-on-bn254 = { workspace = true } +ark-ff = { workspace = true } +ark-serialize = { workspace = true } +ark-std = { workspace = true } +async-compatibility-layer = { workspace = true } +async-lock = { workspace = true } +async-trait = { workspace = true } +bincode = { workspace = true } +bitvec = { workspace = true } +blake3 = { workspace = true } +commit = { workspace = true } +custom_debug = { workspace = true } +digest = { workspace = true } +either = { workspace = true } +espresso-systems-common = { workspace = true } +ethereum-types = { workspace = true } +futures = { workspace = true } + +generic-array = { workspace = true } + +# TODO generic-array should not be a direct dependency +# https://github.com/EspressoSystems/HotShot/issues/1850 +lazy_static = { workspace = true } +rand = { workspace = true } +sha2 = { workspace = true } +snafu = { workspace = true } +time = { workspace = true } +tracing = { workspace = true } +typenum = { workspace = true } +derivative = "2.2.0" +jf-primitives = { workspace = true } +jf-plonk = { workspace = true } +jf-utils = { workspace = true } +rand_chacha = { workspace = true } +serde = { workspace = true } +tagged-base64 = { workspace = true } +versioned-binary-serialization = { workspace = true } +displaydoc = { version = "0.2.3", default-features = false } +dyn-clone = { git = "https://github.com/dtolnay/dyn-clone", tag = "1.0.17" } + +[dev-dependencies] +serde_json = { workspace = true } + +[features] +gpu-vid = ["jf-primitives/gpu-vid"] + +[target.'cfg(all(async_executor_impl = "async-std"))'.dependencies] +async-std = { workspace = true } + +[target.'cfg(all(async_executor_impl = "tokio"))'.dependencies] +tokio = { workspace = true } diff --git a/crates/types/src/consensus.rs b/crates/types/src/consensus.rs new file mode 100644 index 0000000000..aa14eaacdb --- /dev/null +++ b/crates/types/src/consensus.rs @@ -0,0 +1,394 @@ +//! Provides the core consensus types + +pub use crate::utils::{View, ViewInner}; +use displaydoc::Display; + +use crate::{ + data::{Leaf, VidDisperse}, + error::HotShotError, + message::Proposal, + simple_certificate::{DACertificate, QuorumCertificate, UpgradeCertificate}, + traits::{ + metrics::{Counter, Gauge, Histogram, Label, Metrics, NoMetrics}, + node_implementation::NodeType, + ValidatedState, + }, + utils::{StateAndDelta, Terminator}, +}; +use commit::Commitment; + +use std::{ + collections::{BTreeMap, HashMap}, + sync::{Arc, Mutex}, +}; +use tracing::error; + +/// A type alias for `HashMap, T>` +type CommitmentMap = HashMap, T>; + +/// A reference to the consensus algorithm +/// +/// This will contain the state of all rounds. +#[derive(custom_debug::Debug)] +pub struct Consensus { + /// Immutable instance-level state. + pub instance_state: TYPES::InstanceState, + + /// The validated states that are currently loaded in memory. + pub validated_state_map: BTreeMap>, + + /// All the VID shares we've received for current and future views. + /// In the future we will need a different struct similar to VidDisperse except + /// it stores only one share. + /// TODO + pub vid_shares: BTreeMap>>, + + /// All the DA certs we've received for current and future views. + /// view -> DA cert + pub saved_da_certs: HashMap>, + + /// All the upgrade certs we've received for current and future views. + /// view -> upgrade cert + pub saved_upgrade_certs: HashMap>, + + /// View number that is currently on. + pub cur_view: TYPES::Time, + + /// last view had a successful decide event + pub last_decided_view: TYPES::Time, + + /// Map of leaf hash -> leaf + /// - contains undecided leaves + /// - includes the MOST RECENT decided leaf + pub saved_leaves: CommitmentMap>, + + /// Saved payloads. + /// + /// Encoded transactions for every view if we got a payload for that view. + pub saved_payloads: BTreeMap>, + + /// The `locked_qc` view number + pub locked_view: TYPES::Time, + + /// the highqc per spec + pub high_qc: QuorumCertificate, + + /// A reference to the metrics trait + pub metrics: Arc, +} + +/// Contains several `ConsensusMetrics` that we're interested in from the consensus interfaces +#[derive(Clone, Debug)] +pub struct ConsensusMetricsValue { + /// The number of last synced block height + pub last_synced_block_height: Box, + /// The number of last decided view + pub last_decided_view: Box, + /// Number of timestamp for the last decided time + pub last_decided_time: Box, + /// The current view + pub current_view: Box, + /// Number of views that are in-flight since the last decided view + pub number_of_views_since_last_decide: Box, + /// Number of views that are in-flight since the last anchor view + pub number_of_views_per_decide_event: Box, + /// Number of invalid QCs we've seen since the last commit. + pub invalid_qc: Box, + /// Number of outstanding transactions + pub outstanding_transactions: Box, + /// Memory size in bytes of the serialized transactions still outstanding + pub outstanding_transactions_memory_size: Box, + /// Number of views that timed out + pub number_of_timeouts: Box, +} + +/// The wrapper with a string name for the networking metrics +#[derive(Clone, Debug)] +pub struct ConsensusMetrics { + /// a prefix which tracks the name of the metric + prefix: String, + /// a map of values + values: Arc>, +} + +/// the set of counters and gauges for the networking metrics +#[derive(Clone, Debug, Default, Display)] +pub struct InnerConsensusMetrics { + /// All the counters of the networking metrics + pub counters: HashMap, + /// All the gauges of the networking metrics + pub gauges: HashMap, + /// All the histograms of the networking metrics + pub histograms: HashMap>, + /// All the labels of the networking metrics + pub labels: HashMap, +} + +impl ConsensusMetrics { + #[must_use] + /// For the creation and naming of gauge, counter, histogram and label. + pub fn sub(&self, name: String) -> Self { + let prefix = if self.prefix.is_empty() { + name + } else { + format!("{}-{name}", self.prefix) + }; + Self { + prefix, + values: Arc::clone(&self.values), + } + } +} + +impl Metrics for ConsensusMetrics { + fn create_counter(&self, label: String, _unit_label: Option) -> Box { + Box::new(self.sub(label)) + } + + fn create_gauge(&self, label: String, _unit_label: Option) -> Box { + Box::new(self.sub(label)) + } + + fn create_histogram(&self, label: String, _unit_label: Option) -> Box { + Box::new(self.sub(label)) + } + + fn create_label(&self, label: String) -> Box { + Box::new(self.sub(label)) + } + + fn subgroup(&self, subgroup_name: String) -> Box { + Box::new(self.sub(subgroup_name)) + } +} + +impl Counter for ConsensusMetrics { + fn add(&self, amount: usize) { + *self + .values + .lock() + .unwrap() + .counters + .entry(self.prefix.clone()) + .or_default() += amount; + } +} + +impl Gauge for ConsensusMetrics { + fn set(&self, amount: usize) { + *self + .values + .lock() + .unwrap() + .gauges + .entry(self.prefix.clone()) + .or_default() = amount; + } + fn update(&self, delta: i64) { + let mut values = self.values.lock().unwrap(); + let value = values.gauges.entry(self.prefix.clone()).or_default(); + let signed_value = i64::try_from(*value).unwrap_or(i64::MAX); + *value = usize::try_from(signed_value + delta).unwrap_or(0); + } +} + +impl Histogram for ConsensusMetrics { + fn add_point(&self, point: f64) { + self.values + .lock() + .unwrap() + .histograms + .entry(self.prefix.clone()) + .or_default() + .push(point); + } +} + +impl Label for ConsensusMetrics { + fn set(&self, value: String) { + *self + .values + .lock() + .unwrap() + .labels + .entry(self.prefix.clone()) + .or_default() = value; + } +} + +impl ConsensusMetricsValue { + /// Create a new instance of this [`ConsensusMetricsValue`] struct, setting all the counters and gauges + #[must_use] + pub fn new(metrics: &dyn Metrics) -> Self { + Self { + last_synced_block_height: metrics + .create_gauge(String::from("last_synced_block_height"), None), + last_decided_view: metrics.create_gauge(String::from("last_decided_view"), None), + last_decided_time: metrics.create_gauge(String::from("last_decided_time"), None), + current_view: metrics.create_gauge(String::from("current_view"), None), + number_of_views_since_last_decide: metrics + .create_gauge(String::from("number_of_views_since_last_decide"), None), + number_of_views_per_decide_event: metrics + .create_histogram(String::from("number_of_views_per_decide_event"), None), + invalid_qc: metrics.create_gauge(String::from("invalid_qc"), None), + outstanding_transactions: metrics + .create_gauge(String::from("outstanding_transactions"), None), + outstanding_transactions_memory_size: metrics + .create_gauge(String::from("outstanding_transactions_memory_size"), None), + number_of_timeouts: metrics.create_counter(String::from("number_of_timeouts"), None), + } + } +} + +impl Default for ConsensusMetricsValue { + fn default() -> Self { + Self::new(&*NoMetrics::boxed()) + } +} + +impl Consensus { + /// Update the current view. + pub fn update_view(&mut self, view_number: TYPES::Time) { + self.cur_view = view_number; + } + + /// gather information from the parent chain of leafs + /// # Errors + /// If the leaf or its ancestors are not found in storage + pub fn visit_leaf_ancestors( + &self, + start_from: TYPES::Time, + terminator: Terminator, + ok_when_finished: bool, + mut f: F, + ) -> Result<(), HotShotError> + where + F: FnMut( + &Leaf, + Arc<::ValidatedState>, + Option::ValidatedState as ValidatedState>::Delta>>, + ) -> bool, + { + let mut next_leaf = if let Some(view) = self.validated_state_map.get(&start_from) { + view.get_leaf_commitment() + .ok_or_else(|| HotShotError::InvalidState { + context: format!( + "Visited failed view {start_from:?} leaf. Expected successfuil leaf" + ), + })? + } else { + return Err(HotShotError::InvalidState { + context: format!("View {start_from:?} leaf does not exist in state map "), + }); + }; + + while let Some(leaf) = self.saved_leaves.get(&next_leaf) { + let view = leaf.get_view_number(); + if let (Some(state), delta) = self.get_state_and_delta(view) { + if let Terminator::Exclusive(stop_before) = terminator { + if stop_before == view { + if ok_when_finished { + return Ok(()); + } + break; + } + } + next_leaf = leaf.get_parent_commitment(); + if !f(leaf, state, delta) { + return Ok(()); + } + if let Terminator::Inclusive(stop_after) = terminator { + if stop_after == view { + if ok_when_finished { + return Ok(()); + } + break; + } + } + } else { + return Err(HotShotError::InvalidState { + context: format!("View {view:?} state does not exist in state map "), + }); + } + } + Err(HotShotError::LeafNotFound {}) + } + + /// Garbage collects based on state change right now, this removes from both the + /// `saved_payloads` and `validated_state_map` fields of `Consensus`. + /// # Panics + /// On inconsistent stored entries + pub fn collect_garbage(&mut self, old_anchor_view: TYPES::Time, new_anchor_view: TYPES::Time) { + // state check + let anchor_entry = self + .validated_state_map + .iter() + .next() + .expect("INCONSISTENT STATE: anchor leaf not in state map!"); + if *anchor_entry.0 != old_anchor_view { + error!( + "Something about GC has failed. Older leaf exists than the previous anchor leaf." + ); + } + // perform gc + self.saved_da_certs + .retain(|view_number, _| *view_number >= old_anchor_view); + self.saved_upgrade_certs + .retain(|view_number, _| *view_number >= old_anchor_view); + self.validated_state_map + .range(old_anchor_view..new_anchor_view) + .filter_map(|(_view_number, view)| view.get_leaf_commitment()) + .for_each(|leaf| { + self.saved_leaves.remove(&leaf); + }); + self.validated_state_map = self.validated_state_map.split_off(&new_anchor_view); + self.saved_payloads = self.saved_payloads.split_off(&new_anchor_view); + self.vid_shares = self.vid_shares.split_off(&new_anchor_view); + } + + /// Gets the last decided leaf. + /// + /// # Panics + /// if the last decided view's leaf does not exist in the state map or saved leaves, which + /// should never happen. + #[must_use] + pub fn get_decided_leaf(&self) -> Leaf { + let decided_view_num = self.last_decided_view; + let view = self.validated_state_map.get(&decided_view_num).unwrap(); + let leaf = view + .get_leaf_commitment() + .expect("Decided leaf not found! Consensus internally inconsistent"); + self.saved_leaves.get(&leaf).unwrap().clone() + } + + /// Gets the validated state with the given view number, if in the state map. + #[must_use] + pub fn get_state(&self, view_number: TYPES::Time) -> Option<&Arc> { + match self.validated_state_map.get(&view_number) { + Some(view) => view.get_state(), + None => None, + } + } + + /// Gets the validated state and state delta with the given view number, if in the state map. + #[must_use] + pub fn get_state_and_delta(&self, view_number: TYPES::Time) -> StateAndDelta { + match self.validated_state_map.get(&view_number) { + Some(view) => view.get_state_and_delta(), + None => (None, None), + } + } + + /// Gets the last decided validated state. + /// + /// # Panics + /// If the last decided view's state does not exist in the state map, which should never + /// happen. + #[must_use] + pub fn get_decided_state(&self) -> Arc { + let decided_view_num = self.last_decided_view; + self.get_state_and_delta(decided_view_num) + .0 + .expect("Decided state not found! Consensus internally inconsistent") + } +} diff --git a/crates/types/src/constants.rs b/crates/types/src/constants.rs new file mode 100644 index 0000000000..645cc39d4f --- /dev/null +++ b/crates/types/src/constants.rs @@ -0,0 +1,52 @@ +//! configurable constants for hotshot + +use versioned_binary_serialization::version::{StaticVersion, Version}; + +/// the number of views to gather information for ahead of time +pub const LOOK_AHEAD: u64 = 5; + +/// the default kademlia record republication interval (in seconds) +pub const KAD_DEFAULT_REPUB_INTERVAL_SEC: u64 = 28800; + +/// the number of messages to cache in the combined network +pub const COMBINED_NETWORK_CACHE_SIZE: usize = 1000; + +/// the number of messages to attempt to send over the primary network before switching to prefer the secondary network +pub const COMBINED_NETWORK_MIN_PRIMARY_FAILURES: u64 = 5; + +/// the number of messages to send over the secondary network before re-attempting the (presumed down) primary network +pub const COMBINED_NETWORK_PRIMARY_CHECK_INTERVAL: u64 = 5; + +/// CONSTANT for protocol major version +pub const VERSION_MAJ: u16 = 0; + +/// CONSTANT for protocol major version +pub const VERSION_MIN: u16 = 1; + +/// Constant for protocol version 0.1. +pub const VERSION_0_1: Version = Version { + major: VERSION_MAJ, + minor: VERSION_MIN, +}; + +/// Type for protocol static version 0.1. +pub type Version01 = StaticVersion; + +/// Constant for protocol static version 0.1. +pub const STATIC_VER_0_1: Version01 = StaticVersion {}; + +/// Default Channel Size for consensus event sharing +pub const EVENT_CHANNEL_SIZE: usize = 100_000; + +/// Constants for `WebServerNetwork` and `WebServer` +/// The Web CDN is not, strictly speaking, bound to the network; it can have its own versioning. +/// Web Server CDN Version (major) +pub const WEB_SERVER_MAJOR_VERSION: u16 = 0; +/// Web Server CDN Version (minor) +pub const WEB_SERVER_MINOR_VERSION: u16 = 1; + +/// Type for Web Server CDN Version +pub type WebServerVersion = StaticVersion; + +/// Constant for Web Server CDN Version +pub const WEB_SERVER_VERSION: WebServerVersion = StaticVersion {}; diff --git a/crates/types/src/data.rs b/crates/types/src/data.rs new file mode 100644 index 0000000000..0552ec3f4d --- /dev/null +++ b/crates/types/src/data.rs @@ -0,0 +1,483 @@ +//! Provides types useful for representing `HotShot`'s data structures +//! +//! This module provides types for representing consensus internal state, such as leaves, +//! `HotShot`'s version of a block, and proposals, messages upon which to reach the consensus. + +use crate::{ + simple_certificate::{ + QuorumCertificate, TimeoutCertificate, UpgradeCertificate, ViewSyncFinalizeCertificate2, + }, + simple_vote::UpgradeProposalData, + traits::{ + block_contents::{ + vid_commitment, BlockHeader, TestableBlock, GENESIS_VID_NUM_STORAGE_NODES, + }, + election::Membership, + node_implementation::{ConsensusTime, NodeType}, + signature_key::SignatureKey, + states::TestableState, + BlockPayload, + }, + utils::bincode_opts, + vid::{VidCommitment, VidCommon, VidSchemeType, VidShare}, + vote::{Certificate, HasViewNumber}, +}; +use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; +use bincode::Options; +use commit::{Commitment, Committable, RawCommitmentBuilder}; +use derivative::Derivative; +use jf_primitives::vid::VidDisperse as JfVidDisperse; +use rand::Rng; +use serde::{Deserialize, Serialize}; +use snafu::Snafu; +use std::{ + collections::BTreeMap, + fmt::{Debug, Display}, + hash::Hash, + sync::Arc, +}; + +/// Type-safe wrapper around `u64` so we know the thing we're talking about is a view number. +#[derive( + Copy, + Clone, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + Serialize, + Deserialize, + CanonicalSerialize, + CanonicalDeserialize, +)] +pub struct ViewNumber(u64); + +impl ConsensusTime for ViewNumber { + /// Create a genesis view number (0) + fn genesis() -> Self { + Self(0) + } + /// Create a new `ViewNumber` with the given value. + fn new(n: u64) -> Self { + Self(n) + } + /// Returen the u64 format + fn get_u64(&self) -> u64 { + self.0 + } +} + +impl Committable for ViewNumber { + fn commit(&self) -> Commitment { + let builder = RawCommitmentBuilder::new("View Number Commitment"); + builder.u64(self.0).finalize() + } +} + +impl std::ops::Add for ViewNumber { + type Output = ViewNumber; + + fn add(self, rhs: u64) -> Self::Output { + Self(self.0 + rhs) + } +} + +impl std::ops::AddAssign for ViewNumber { + fn add_assign(&mut self, rhs: u64) { + self.0 += rhs; + } +} + +impl std::ops::Deref for ViewNumber { + type Target = u64; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::Sub for ViewNumber { + type Output = ViewNumber; + fn sub(self, rhs: u64) -> Self::Output { + Self(self.0 - rhs) + } +} + +/// A proposal to start providing data availability for a block. +#[derive(custom_debug::Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)] +pub struct DAProposal { + /// Encoded transactions in the block to be applied. + pub encoded_transactions: Vec, + /// Metadata of the block to be applied. + pub metadata: ::Metadata, + /// View this proposal applies to + pub view_number: TYPES::Time, +} + +/// A proposal to upgrade the network +#[derive(custom_debug::Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)] +#[serde(bound = "TYPES: NodeType")] +pub struct UpgradeProposal +where + TYPES: NodeType, +{ + /// The information about which version we are upgrading to. + pub upgrade_proposal: UpgradeProposalData, + /// View this proposal applies to + pub view_number: TYPES::Time, +} + +/// VID dispersal data +/// +/// Like [`DAProposal`]. +/// +/// TODO move to vid.rs? +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)] +pub struct VidDisperse { + /// The view number for which this VID data is intended + pub view_number: TYPES::Time, + /// Block payload commitment + pub payload_commitment: VidCommitment, + /// A storage node's key and its corresponding VID share + pub shares: BTreeMap, + /// VID common data sent to all storage nodes + pub common: VidCommon, +} + +impl VidDisperse { + /// Create VID dispersal from a specified membership + /// Uses the specified function to calculate share dispersal + /// Allows for more complex stake table functionality + pub fn from_membership( + view_number: TYPES::Time, + mut vid_disperse: JfVidDisperse, + membership: &Arc, + ) -> Self { + let shares = membership + .get_staked_committee(view_number) + .iter() + .map(|node| (node.clone(), vid_disperse.shares.remove(0))) + .collect(); + + Self { + view_number, + shares, + common: vid_disperse.common, + payload_commitment: vid_disperse.commit, + } + } +} + +/// Proposal to append a block. +#[derive(custom_debug::Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)] +#[serde(bound(deserialize = ""))] +pub struct QuorumProposal { + /// The block header to append + pub block_header: TYPES::BlockHeader, + + /// CurView from leader when proposing leaf + pub view_number: TYPES::Time, + + /// Per spec, justification + pub justify_qc: QuorumCertificate, + + /// Possible timeout certificate. Only present if the justify_qc is not for the preceding view + pub timeout_certificate: Option>, + + /// Possible upgrade certificate, which the leader may optionally attach. + pub upgrade_certificate: Option>, + + /// Possible view sync certificate. Only present if the justify_qc and timeout_cert are not + /// present. + pub view_sync_certificate: Option>, + + /// the propser id + pub proposer_id: TYPES::SignatureKey, +} + +impl HasViewNumber for DAProposal { + fn get_view_number(&self) -> TYPES::Time { + self.view_number + } +} + +impl HasViewNumber for VidDisperse { + fn get_view_number(&self) -> TYPES::Time { + self.view_number + } +} + +impl HasViewNumber for QuorumProposal { + fn get_view_number(&self) -> TYPES::Time { + self.view_number + } +} + +impl HasViewNumber for UpgradeProposal { + fn get_view_number(&self) -> TYPES::Time { + self.view_number + } +} + +/// The error type for block and its transactions. +#[derive(Snafu, Debug, Serialize, Deserialize)] +pub enum BlockError { + /// Invalid block header. + InvalidBlockHeader, + /// Invalid transaction length. + InvalidTransactionLength, + /// Inconsistent payload commitment. + InconsistentPayloadCommitment, +} + +/// Additional functions required to use a [`Leaf`] with hotshot-testing. +pub trait TestableLeaf { + /// Type of nodes participating in the network. + type NodeType: NodeType; + + /// Create a transaction that can be added to the block contained in this leaf. + fn create_random_transaction( + &self, + rng: &mut dyn rand::RngCore, + padding: u64, + ) -> <::BlockPayload as BlockPayload>::Transaction; +} + +/// This is the consensus-internal analogous concept to a block, and it contains the block proper, +/// as well as the hash of its parent `Leaf`. +/// NOTE: `State` is constrained to implementing `BlockContents`, is `TypeMap::BlockPayload` +#[derive(Serialize, Deserialize, Clone, Debug, Derivative, Eq)] +#[serde(bound(deserialize = ""))] +pub struct Leaf { + /// CurView from leader when proposing leaf + pub view_number: TYPES::Time, + + /// Per spec, justification + pub justify_qc: QuorumCertificate, + + /// The hash of the parent `Leaf` + /// So we can ask if it extends + pub parent_commitment: Commitment, + + /// Block header. + pub block_header: TYPES::BlockHeader, + + /// Optional block payload. + /// + /// It may be empty for nodes not in the DA committee. + pub block_payload: Option, + + /// the proposer id of the leaf + pub proposer_id: TYPES::SignatureKey, +} + +impl PartialEq for Leaf { + fn eq(&self, other: &Self) -> bool { + self.view_number == other.view_number + && self.justify_qc == other.justify_qc + && self.parent_commitment == other.parent_commitment + && self.block_header == other.block_header + } +} + +impl Hash for Leaf { + fn hash(&self, state: &mut H) { + self.view_number.hash(state); + self.justify_qc.hash(state); + self.parent_commitment.hash(state); + self.block_header.hash(state); + } +} + +impl Display for Leaf { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "view: {:?}, height: {:?}, justify: {}", + self.view_number, + self.get_height(), + self.justify_qc + ) + } +} + +impl Leaf { + /// Create a new leaf from its components. + /// + /// # Panics + /// + /// Panics if the genesis payload (`TYPES::BlockPayload::genesis()`) is malformed (unable to be + /// interpreted as bytes). + #[must_use] + pub fn genesis(instance_state: &TYPES::InstanceState) -> Self { + let (payload, metadata) = TYPES::BlockPayload::genesis(); + let payload_bytes = payload + .encode() + .expect("unable to encode genesis payload") + .collect(); + let payload_commitment = vid_commitment(&payload_bytes, GENESIS_VID_NUM_STORAGE_NODES); + let block_header = + TYPES::BlockHeader::genesis(instance_state, payload_commitment, metadata); + Self { + view_number: TYPES::Time::genesis(), + justify_qc: QuorumCertificate::::genesis(), + parent_commitment: fake_commitment(), + block_header: block_header.clone(), + block_payload: Some(payload), + proposer_id: <::SignatureKey as SignatureKey>::genesis_proposer_pk(), + } + } + + /// Time when this leaf was created. + pub fn get_view_number(&self) -> TYPES::Time { + self.view_number + } + /// Height of this leaf in the chain. + /// + /// Equivalently, this is the number of leaves before this one in the chain. + pub fn get_height(&self) -> u64 { + self.block_header.block_number() + } + /// The QC linking this leaf to its parent in the chain. + pub fn get_justify_qc(&self) -> QuorumCertificate { + self.justify_qc.clone() + } + /// Commitment to this leaf's parent. + pub fn get_parent_commitment(&self) -> Commitment { + self.parent_commitment + } + /// The block header contained in this leaf. + pub fn get_block_header(&self) -> &::BlockHeader { + &self.block_header + } + /// Fill this leaf with the block payload. + /// + /// # Errors + /// + /// Fails if the payload commitment doesn't match `self.block_header.payload_commitment()` + /// or if the transactions are of invalid length + pub fn fill_block_payload( + &mut self, + block_payload: TYPES::BlockPayload, + num_storage_nodes: usize, + ) -> Result<(), BlockError> { + let encoded_txns = match block_payload.encode() { + // TODO (Keyao) [VALIDATED_STATE] - Avoid collect/copy on the encoded transaction bytes. + // + Ok(encoded) => encoded.into_iter().collect(), + Err(_) => return Err(BlockError::InvalidTransactionLength), + }; + let commitment = vid_commitment(&encoded_txns, num_storage_nodes); + if commitment != self.block_header.payload_commitment() { + return Err(BlockError::InconsistentPayloadCommitment); + } + self.block_payload = Some(block_payload); + Ok(()) + } + + /// Fill this leaf with the block payload, without checking + /// header and payload consistency + pub fn fill_block_payload_unchecked(&mut self, block_payload: TYPES::BlockPayload) { + self.block_payload = Some(block_payload); + } + + /// Optional block payload. + pub fn get_block_payload(&self) -> Option { + self.block_payload.clone() + } + + /// A commitment to the block payload contained in this leaf. + pub fn get_payload_commitment(&self) -> VidCommitment { + self.get_block_header().payload_commitment() + } + + /// Identity of the network participant who proposed this leaf. + pub fn get_proposer_id(&self) -> TYPES::SignatureKey { + self.proposer_id.clone() + } +} + +impl TestableLeaf for Leaf +where + TYPES::ValidatedState: TestableState, + TYPES::BlockPayload: TestableBlock, +{ + type NodeType = TYPES; + + fn create_random_transaction( + &self, + rng: &mut dyn rand::RngCore, + padding: u64, + ) -> <::BlockPayload as BlockPayload>::Transaction { + TYPES::ValidatedState::create_random_transaction(None, rng, padding) + } +} +/// Fake the thing a genesis block points to. Needed to avoid infinite recursion +#[must_use] +pub fn fake_commitment() -> Commitment { + RawCommitmentBuilder::new("Dummy commitment for arbitrary genesis").finalize() +} + +/// create a random commitment +#[must_use] +pub fn random_commitment(rng: &mut dyn rand::RngCore) -> Commitment { + let random_array: Vec = (0u8..100u8).map(|_| rng.gen_range(0..255)).collect(); + RawCommitmentBuilder::new("Random Commitment") + .constant_str("Random Field") + .var_size_bytes(&random_array) + .finalize() +} + +/// Serialization for the QC assembled signature +/// # Panics +/// if serialization fails +pub fn serialize_signature2( + signatures: &::QCType, +) -> Vec { + let mut signatures_bytes = vec![]; + signatures_bytes.extend("Yes".as_bytes()); + + let (sig, proof) = TYPES::SignatureKey::get_sig_proof(signatures); + let proof_bytes = bincode_opts() + .serialize(&proof.as_bitslice()) + .expect("This serialization shouldn't be able to fail"); + signatures_bytes.extend("bitvec proof".as_bytes()); + signatures_bytes.extend(proof_bytes.as_slice()); + let sig_bytes = bincode_opts() + .serialize(&sig) + .expect("This serialization shouldn't be able to fail"); + signatures_bytes.extend("aggregated signature".as_bytes()); + signatures_bytes.extend(sig_bytes.as_slice()); + signatures_bytes +} + +impl Committable for Leaf { + fn commit(&self) -> commit::Commitment { + let signatures_bytes = if self.justify_qc.is_genesis { + let mut bytes = vec![]; + bytes.extend("genesis".as_bytes()); + bytes + } else { + serialize_signature2::(self.justify_qc.signatures.as_ref().unwrap()) + }; + + // Skip the transaction commitments, so that the repliacs can reconstruct the leaf. + RawCommitmentBuilder::new("leaf commitment") + .u64_field("view number", *self.view_number) + .u64_field("block number", self.get_height()) + .field("parent Leaf commitment", self.parent_commitment) + .constant_str("block payload commitment") + .fixed_size_bytes(self.get_payload_commitment().as_ref().as_ref()) + .constant_str("justify_qc view number") + .u64(*self.justify_qc.view_number) + .field( + "justify_qc leaf commitment", + self.justify_qc.get_data().leaf_commit, + ) + .constant_str("justify_qc signatures") + .var_size_bytes(&signatures_bytes) + .finalize() + } +} diff --git a/crates/types/src/error.rs b/crates/types/src/error.rs new file mode 100644 index 0000000000..127f81506b --- /dev/null +++ b/crates/types/src/error.rs @@ -0,0 +1,112 @@ +//! Error type for `HotShot` +//! +//! This module provides [`HotShotError`], which is an enum representing possible faults that can +//! occur while interacting with this crate. + +//use crate::traits::network::TimeoutErr; +use crate::traits::{block_contents::BlockPayload, node_implementation::NodeType}; +#[cfg(async_executor_impl = "async-std")] +use async_std::future::TimeoutError; +use serde::{Deserialize, Serialize}; +use snafu::Snafu; +use std::num::NonZeroU64; +#[cfg(async_executor_impl = "tokio")] +use tokio::time::error::Elapsed as TimeoutError; +#[cfg(not(any(async_executor_impl = "async-std", async_executor_impl = "tokio")))] +compile_error! {"Either config option \"async-std\" or \"tokio\" must be enabled for this crate."} + +/// Error type for `HotShot` +#[derive(Debug, Snafu)] +#[snafu(visibility(pub))] +#[non_exhaustive] +pub enum HotShotError { + /// Failed to Message the leader in the given stage + #[snafu(display("Failed to message leader with error: {source}"))] + FailedToMessageLeader { + /// The underlying network fault + source: crate::traits::network::NetworkError, + }, + /// Failed to broadcast a message on the network + #[snafu(display("Failed to broadcast a message"))] + FailedToBroadcast { + /// The underlying network fault + source: crate::traits::network::NetworkError, + }, + /// Failure in the block. + #[snafu(display("Failed to build or verify a block: {source}"))] + BlockError { + /// The underlying block error. + source: ::Error, + }, + /// Failure in networking layer + #[snafu(display("Failure in networking layer: {source}"))] + NetworkFault { + /// Underlying network fault + source: crate::traits::network::NetworkError, + }, + /// Item was not present in storage + LeafNotFound {/* TODO we should create a way to to_string */}, + /// Error accesing storage + /// Invalid state machine state + #[snafu(display("Invalid state machine state: {}", context))] + InvalidState { + /// Context + context: String, + }, + /// HotShot timed out waiting for msgs + TimeoutError { + /// source of error + source: TimeoutError, + }, + /// HotShot timed out during round + ViewTimeoutError { + /// view number + view_number: TYPES::Time, + /// The state that the round was in when it timed out + state: RoundTimedoutState, + }, + /// Not enough valid signatures for a quorum + #[snafu(display("Insufficient number of valid signatures: the threshold is {}, but only {} signatures were valid", threshold, num_valid_signatures))] + InsufficientValidSignatures { + /// Number of valid signatures + num_valid_signatures: usize, + /// Threshold of signatures needed for a quorum + threshold: NonZeroU64, + }, + /// Miscelaneous error + /// TODO fix this with + /// #181 + Misc { + /// source of error + context: String, + }, + /// Internal value used to drive the state machine + Continue, +} +/// Contains information about what the state of the hotshot-consensus was when a round timed out +#[derive(Debug, Clone, Serialize, Deserialize)] +#[non_exhaustive] +pub enum RoundTimedoutState { + /// Leader is in a Prepare phase and is waiting for a HighQC + LeaderWaitingForHighQC, + /// Leader is in a Prepare phase and timed out before the round min time is reached + LeaderMinRoundTimeNotReached, + /// Leader is waiting for prepare votes + LeaderWaitingForPrepareVotes, + /// Leader is waiting for precommit votes + LeaderWaitingForPreCommitVotes, + /// Leader is waiting for commit votes + LeaderWaitingForCommitVotes, + + /// Replica is waiting for a prepare message + ReplicaWaitingForPrepare, + /// Replica is waiting for a pre-commit message + ReplicaWaitingForPreCommit, + /// Replica is waiting for a commit message + ReplicaWaitingForCommit, + /// Replica is waiting for a decide message + ReplicaWaitingForDecide, + + /// HotShot-testing tried to collect round events, but it timed out + TestCollectRoundEventsTimedOut, +} diff --git a/crates/types/src/event.rs b/crates/types/src/event.rs new file mode 100644 index 0000000000..8a2e59eedf --- /dev/null +++ b/crates/types/src/event.rs @@ -0,0 +1,160 @@ +//! Events that a `HotShot` instance can emit + +use serde::{Deserialize, Serialize}; + +use crate::{ + data::{DAProposal, Leaf, QuorumProposal, UpgradeProposal, VidDisperse}, + error::HotShotError, + message::Proposal, + simple_certificate::QuorumCertificate, + traits::{node_implementation::NodeType, ValidatedState}, +}; + +use std::sync::Arc; +/// A status event emitted by a `HotShot` instance +/// +/// This includes some metadata, such as the stage and view number that the event was generated in, +/// as well as an inner [`EventType`] describing the event proper. +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(bound(deserialize = "TYPES: NodeType"))] +pub struct Event { + /// The view number that this event originates from + pub view_number: TYPES::Time, + /// The underlying event + pub event: EventType, +} + +/// Decided leaf with the corresponding state and VID info. +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(bound(deserialize = "TYPES: NodeType"))] +pub struct LeafInfo { + /// Decided leaf. + pub leaf: Leaf, + /// Validated state. + pub state: Arc<::ValidatedState>, + /// Optional application-specific state delta. + pub delta: Option::ValidatedState as ValidatedState>::Delta>>, + /// Optional VID disperse data. + pub vid: Option>, +} + +impl LeafInfo { + /// Constructor. + pub fn new( + leaf: Leaf, + state: Arc<::ValidatedState>, + delta: Option::ValidatedState as ValidatedState>::Delta>>, + vid: Option>, + ) -> Self { + Self { + leaf, + state, + delta, + vid, + } + } +} + +/// The chain of decided leaves with its corresponding state and VID info. +pub type LeafChain = Vec>; + +pub mod error_adaptor { + use super::*; + use serde::{de::Deserializer, ser::Serializer}; + pub fn serialize( + elem: &Arc>, + serializer: S, + ) -> Result { + serializer.serialize_str(&format!("{}", elem)) + } + + pub fn deserialize<'de, D: Deserializer<'de>, TYPES: NodeType>( + deserializer: D, + ) -> Result>, D::Error> { + let str = String::deserialize(deserializer)?; + Ok(Arc::new(HotShotError::Misc { context: str })) + } +} +/// The type and contents of a status event emitted by a `HotShot` instance +/// +/// This enum does not include metadata shared among all variants, such as the stage and view +/// number, and is thus always returned wrapped in an [`Event`]. +#[non_exhaustive] +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(bound(deserialize = "TYPES: NodeType"))] +pub enum EventType { + /// A view encountered an error and was interrupted + Error { + /// The underlying error + #[serde(with = "error_adaptor")] + error: Arc>, + }, + /// A new decision event was issued + Decide { + /// The chain of Leafs that were committed by this decision + /// + /// This list is sorted in reverse view number order, with the newest (highest view number) + /// block first in the list. + /// + /// This list may be incomplete if the node is currently performing catchup. + /// Vid Info for a decided view may be missing if this node never saw it's share. + leaf_chain: Arc>, + /// The QC signing the most recent leaf in `leaf_chain`. + /// + /// Note that the QC for each additional leaf in the chain can be obtained from the leaf + /// before it using + qc: Arc>, + /// Optional information of the number of transactions in the block, for logging purposes. + block_size: Option, + }, + /// A replica task was canceled by a timeout interrupt + ReplicaViewTimeout { + /// The view that timed out + view_number: TYPES::Time, + }, + /// A next leader task was canceled by a timeout interrupt + NextLeaderViewTimeout { + /// The view that timed out + view_number: TYPES::Time, + }, + /// The view has finished. If values were decided on, a `Decide` event will also be emitted. + ViewFinished { + /// The view number that has just finished + view_number: TYPES::Time, + }, + /// The view timed out + ViewTimeout { + /// The view that timed out + view_number: TYPES::Time, + }, + /// New transactions were received from the network + /// or submitted to the network by us + Transactions { + /// The list of transactions + transactions: Vec, + }, + /// DA proposal was received from the network + /// or submitted to the network by us + DAProposal { + /// Contents of the proposal + proposal: Proposal>, + /// Public key of the leader submitting the proposal + sender: TYPES::SignatureKey, + }, + /// Quorum proposal was received from the network + /// or submitted to the network by us + QuorumProposal { + /// Contents of the proposal + proposal: Proposal>, + /// Public key of the leader submitting the proposal + sender: TYPES::SignatureKey, + }, + /// Upgrade proposal was received from the network + /// or submitted to the network by us + UpgradeProposal { + /// Contents of the proposal + proposal: Proposal>, + /// Public key of the leader submitting the proposal + sender: TYPES::SignatureKey, + }, +} diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs new file mode 100644 index 0000000000..4f882f165c --- /dev/null +++ b/crates/types/src/lib.rs @@ -0,0 +1,190 @@ +//! Types and Traits for the `HotShot` consensus module +use crate::utils::bincode_opts; +use bincode::Options; +use displaydoc::Display; +use light_client::StateVerKey; +use std::fmt::Debug; +use std::{future::Future, num::NonZeroUsize, pin::Pin, time::Duration}; +use tracing::error; +use traits::{election::ElectionConfig, signature_key::SignatureKey}; +pub mod consensus; +pub mod constants; +pub mod data; +pub mod error; +pub mod event; +pub mod light_client; +pub mod message; +pub mod qc; +pub mod signature_key; +pub mod simple_certificate; +pub mod simple_vote; +pub mod stake_table; +pub mod traits; +pub mod utils; +pub mod vid; +pub mod vote; + +/// Pinned future that is Send and Sync +pub type BoxSyncFuture<'a, T> = Pin + Send + Sync + 'a>>; + +/// yoinked from futures crate +pub fn assert_future(future: F) -> F +where + F: Future, +{ + future +} +/// yoinked from futures crate, adds sync bound that we need +pub fn boxed_sync<'a, F>(fut: F) -> BoxSyncFuture<'a, F::Output> +where + F: Future + Sized + Send + Sync + 'a, +{ + assert_future::(Box::pin(fut)) +} +/// the type of consensus to run. Either: +/// wait for a signal to start a view, +/// or constantly run +/// you almost always want continuous +/// incremental is just for testing +#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)] +pub enum ExecutionType { + /// constantly increment view as soon as view finishes + Continuous, + /// wait for a signal + Incremental, +} + +#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Display)] +#[serde(bound(deserialize = ""))] +/// config for validator, including public key, private key, stake value +pub struct ValidatorConfig { + /// The validator's public key and stake value + pub public_key: KEY, + /// The validator's private key, should be in the mempool, not public + pub private_key: KEY::PrivateKey, + /// The validator's stake + pub stake_value: u64, + /// the validator's key pairs for state signing/verification + pub state_key_pair: light_client::StateKeyPair, +} + +impl ValidatorConfig { + /// generate validator config from input seed, index and stake value + #[must_use] + pub fn generated_from_seed_indexed(seed: [u8; 32], index: u64, stake_value: u64) -> Self { + let (public_key, private_key) = KEY::generated_from_seed_indexed(seed, index); + let state_key_pairs = light_client::StateKeyPair::generate_from_seed_indexed(seed, index); + Self { + public_key, + private_key, + stake_value, + state_key_pair: state_key_pairs, + } + } + + /// get the public config of the validator + pub fn get_public_config(&self) -> PeerConfig { + PeerConfig { + stake_table_entry: self.public_key.get_stake_table_entry(self.stake_value), + state_ver_key: self.state_key_pair.0.ver_key(), + } + } +} + +impl Default for ValidatorConfig { + fn default() -> Self { + Self::generated_from_seed_indexed([0u8; 32], 0, 1) + } +} + +#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Display)] +#[serde(bound(deserialize = ""))] +/// structure of peers' config, including public key, stake value, and state key. +pub struct PeerConfig { + /// The peer's public key and stake value + pub stake_table_entry: KEY::StakeTableEntry, + /// the peer's state public key + pub state_ver_key: StateVerKey, +} + +impl PeerConfig { + /// Serialize a peer's config to bytes + pub fn to_bytes(config: &Self) -> Vec { + let x = bincode_opts().serialize(config); + match x { + Ok(x) => x, + Err(e) => { + error!(?e, "Failed to serialize public key"); + vec![] + } + } + } + + /// Deserialize a peer's config from bytes + /// # Errors + /// Will return `None` if deserialization fails + pub fn from_bytes(bytes: &[u8]) -> Option { + let x: Result, _> = bincode_opts().deserialize(bytes); + match x { + Ok(pub_key) => Some(pub_key), + Err(e) => { + error!(?e, "Failed to deserialize public key"); + None + } + } + } +} + +impl Default for PeerConfig { + fn default() -> Self { + let default_validator_config = ValidatorConfig::::default(); + default_validator_config.get_public_config() + } +} + +/// Holds configuration for a `HotShot` +#[derive(Clone, custom_debug::Debug, serde::Serialize, serde::Deserialize)] +#[serde(bound(deserialize = ""))] +pub struct HotShotConfig { + /// Whether to run one view or continuous views + pub execution_type: ExecutionType, + /// Total number of nodes in the network + // Earlier it was total_nodes + pub num_nodes_with_stake: NonZeroUsize, + /// Number of nodes without stake + pub num_nodes_without_stake: usize, + /// Minimum transactions per block + pub min_transactions: usize, + /// Maximum transactions per block + pub max_transactions: NonZeroUsize, + /// List of known node's public keys and stake value for certificate aggregation, serving as public parameter + pub known_nodes_with_stake: Vec>, + /// List of known non-staking nodes' public keys + pub known_nodes_without_stake: Vec, + /// My own validator config, including my public key, private key, stake value, serving as private parameter + pub my_own_validator_config: ValidatorConfig, + /// List of DA committee (staking)nodes for static DA committe + pub da_staked_committee_size: usize, + /// List of DA committee nodes (non-staking)nodes for static DA committe + pub da_non_staked_committee_size: usize, + /// Base duration for next-view timeout, in milliseconds + pub next_view_timeout: u64, + /// Duration of view sync round timeouts + pub view_sync_timeout: Duration, + /// The exponential backoff ration for the next-view timeout + pub timeout_ratio: (u64, u64), + /// The delay a leader inserts before starting pre-commit, in milliseconds + pub round_start_delay: u64, + /// Delay after init before starting consensus, in milliseconds + pub start_delay: u64, + /// Number of network bootstrap nodes + pub num_bootstrap: usize, + /// The minimum amount of time a leader has to wait to start a round + pub propose_min_round_time: Duration, + /// The maximum amount of time a leader can wait to start a round + pub propose_max_round_time: Duration, + /// time to wait until we request data associated with a proposal + pub data_request_delay: Duration, + /// the election configuration + pub election_config: Option, +} diff --git a/crates/types/src/light_client.rs b/crates/types/src/light_client.rs new file mode 100644 index 0000000000..ce98eedd3e --- /dev/null +++ b/crates/types/src/light_client.rs @@ -0,0 +1,223 @@ +//! Types and structs associated with light client state + +use ark_ed_on_bn254::EdwardsConfig as Config; +use ark_ff::PrimeField; +use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; +use ethereum_types::U256; +use jf_primitives::signatures::schnorr; +use rand::SeedableRng; +use rand_chacha::ChaCha20Rng; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use tagged_base64::tagged; + +/// Base field in the prover circuit +pub type CircuitField = ark_ed_on_bn254::Fq; +/// Concrete type for light client state +pub type LightClientState = GenericLightClientState; +/// Signature scheme +pub type StateSignatureScheme = + jf_primitives::signatures::schnorr::SchnorrSignatureScheme; +/// Signatures +pub type StateSignature = schnorr::Signature; +/// Verification key for verifying state signatures +pub type StateVerKey = schnorr::VerKey; +/// Signing key for signing a light client state +pub type StateSignKey = schnorr::SignKey; +/// Concrete for circuit's public input +pub type PublicInput = GenericPublicInput; +/// Key pairs for signing/verifying a light client state +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +pub struct StateKeyPair(pub schnorr::KeyPair); + +/// Request body to send to the state relay server +#[derive(Clone, Debug, CanonicalSerialize, CanonicalDeserialize, Serialize, Deserialize)] +pub struct StateSignatureRequestBody { + /// The public key associated with this request + pub key: StateVerKey, + /// The associated light client state + pub state: LightClientState, + /// The associated signature of the light client state + pub signature: StateSignature, +} + +/// The state signatures bundle is a light client state and its signatures collected +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct StateSignaturesBundle { + /// The state for this signatures bundle + pub state: LightClientState, + /// The collected signatures + pub signatures: HashMap, + /// Total stakes associated with the signer + pub accumulated_weight: U256, +} + +/// A light client state +#[tagged("LIGHT_CLIENT_STATE")] +#[derive( + Clone, + Debug, + CanonicalSerialize, + CanonicalDeserialize, + Default, + Eq, + PartialEq, + PartialOrd, + Ord, + Hash, +)] +pub struct GenericLightClientState { + /// Current view number + pub view_number: usize, + /// Current block height + pub block_height: usize, + /// Root of the block commitment tree + pub block_comm_root: F, + /// Commitment for fee ledger + pub fee_ledger_comm: F, + /// Commitment for the stake table + pub stake_table_comm: (F, F, F), +} + +impl From> for [F; 7] { + fn from(state: GenericLightClientState) -> Self { + [ + F::from(state.view_number as u64), + F::from(state.block_height as u64), + state.block_comm_root, + state.fee_ledger_comm, + state.stake_table_comm.0, + state.stake_table_comm.1, + state.stake_table_comm.2, + ] + } +} +impl From<&GenericLightClientState> for [F; 7] { + fn from(state: &GenericLightClientState) -> Self { + [ + F::from(state.view_number as u64), + F::from(state.block_height as u64), + state.block_comm_root, + state.fee_ledger_comm, + state.stake_table_comm.0, + state.stake_table_comm.1, + state.stake_table_comm.2, + ] + } +} + +impl std::ops::Deref for StateKeyPair { + type Target = schnorr::KeyPair; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl StateKeyPair { + /// Generate key pairs from private signing keys + #[must_use] + pub fn from_sign_key(sk: StateSignKey) -> Self { + Self(schnorr::KeyPair::::from(sk)) + } + + /// Generate key pairs from `thread_rng()` + #[must_use] + pub fn generate() -> StateKeyPair { + schnorr::KeyPair::generate(&mut rand::thread_rng()).into() + } + + /// Generate key pairs from seed + #[must_use] + pub fn generate_from_seed(seed: [u8; 32]) -> StateKeyPair { + schnorr::KeyPair::generate(&mut ChaCha20Rng::from_seed(seed)).into() + } + + /// Generate key pairs from an index and a seed + #[must_use] + pub fn generate_from_seed_indexed(seed: [u8; 32], index: u64) -> StateKeyPair { + let mut hasher = blake3::Hasher::new(); + hasher.update(&seed); + hasher.update(&index.to_le_bytes()); + let new_seed = *hasher.finalize().as_bytes(); + Self::generate_from_seed(new_seed) + } +} + +impl From> for StateKeyPair { + fn from(value: schnorr::KeyPair) -> Self { + StateKeyPair(value) + } +} + +/// Public input to the light client state prover service +#[derive(Clone, Debug)] +pub struct GenericPublicInput(Vec); + +impl AsRef<[F]> for GenericPublicInput { + fn as_ref(&self) -> &[F] { + &self.0 + } +} + +impl From> for GenericPublicInput { + fn from(v: Vec) -> Self { + Self(v) + } +} + +impl GenericPublicInput { + /// Return the threshold + #[must_use] + pub fn threshold(&self) -> F { + self.0[0] + } + + /// Return the view number of the light client state + #[must_use] + pub fn view_number(&self) -> F { + self.0[1] + } + + /// Return the block height of the light client state + #[must_use] + pub fn block_height(&self) -> F { + self.0[2] + } + + /// Return the block commitment root of the light client state + #[must_use] + pub fn block_comm_root(&self) -> F { + self.0[3] + } + + /// Return the fee ledger commitment of the light client state + #[must_use] + pub fn fee_ledger_comm(&self) -> F { + self.0[4] + } + + /// Return the stake table commitment of the light client state + #[must_use] + pub fn stake_table_comm(&self) -> (F, F, F) { + (self.0[5], self.0[6], self.0[7]) + } + + /// Return the qc key commitment of the light client state + #[must_use] + pub fn qc_key_comm(&self) -> F { + self.0[5] + } + + /// Return the state key commitment of the light client state + #[must_use] + pub fn state_key_comm(&self) -> F { + self.0[6] + } + + /// Return the stake amount commitment of the light client state + #[must_use] + pub fn stake_amount_comm(&self) -> F { + self.0[7] + } +} diff --git a/crates/types/src/message.rs b/crates/types/src/message.rs new file mode 100644 index 0000000000..99f29d4907 --- /dev/null +++ b/crates/types/src/message.rs @@ -0,0 +1,316 @@ +//! Network message types +//! +//! This module contains types used to represent the various types of messages that +//! `HotShot` nodes can send among themselves. + +use crate::data::{QuorumProposal, UpgradeProposal}; +use crate::simple_certificate::{ + DACertificate, ViewSyncCommitCertificate2, ViewSyncFinalizeCertificate2, + ViewSyncPreCommitCertificate2, +}; +use crate::simple_vote::{ + DAVote, TimeoutVote, UpgradeVote, ViewSyncCommitVote, ViewSyncFinalizeVote, + ViewSyncPreCommitVote, +}; +use crate::traits::network::ResponseMessage; +use crate::traits::signature_key::SignatureKey; +use crate::vote::HasViewNumber; +use crate::{ + data::{DAProposal, VidDisperse}, + simple_vote::QuorumVote, + traits::{ + network::{DataRequest, NetworkMsg, ViewMessage}, + node_implementation::{ConsensusTime, NodeType}, + }, +}; +use derivative::Derivative; +use either::Either::{self, Left, Right}; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use std::{fmt::Debug, marker::PhantomData}; + +/// Incoming message +#[derive(Serialize, Deserialize, Clone, Debug, Derivative, PartialEq, Eq, Hash)] +#[serde(bound(deserialize = "", serialize = ""))] +pub struct Message { + /// The sender of this message + pub sender: TYPES::SignatureKey, + + /// The message kind + pub kind: MessageKind, +} + +impl NetworkMsg for Message {} + +impl ViewMessage for Message { + /// get the view number out of a message + fn get_view_number(&self) -> TYPES::Time { + self.kind.get_view_number() + } + fn purpose(&self) -> MessagePurpose { + self.kind.purpose() + } +} + +/// A wrapper type for implementing `PassType` on a vector of `Message`. +#[derive(Clone, Debug)] +pub struct Messages(pub Vec>); + +/// A message type agnostic description of a message's purpose +#[derive(PartialEq, Copy, Clone)] +pub enum MessagePurpose { + /// Message with a [quorum/DA] proposal. + Proposal, + /// Message with most recent [quorum/DA] proposal the server has + LatestProposal, + /// Message with most recent view sync certificate the server has + LatestViewSyncCertificate, + /// Message with a quorum vote. + Vote, + /// Message with a view sync vote. + ViewSyncVote, + /// Message with a view sync certificate. + ViewSyncCertificate, + /// Message with a DAC. + DAC, + /// Message for internal use + Internal, + /// Data message + Data, + /// VID disperse, like [`Proposal`]. + VidDisperse, + /// Message with an upgrade proposal. + Upgrade, +} + +// TODO (da) make it more customized to the consensus layer, maybe separating the specific message +// data from the kind enum. +/// Enum representation of any message type +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Hash, Eq)] +#[serde(bound(deserialize = "", serialize = ""))] +pub enum MessageKind { + /// Messages related to the consensus protocol + Consensus(SequencingMessage), + /// Messages relating to sharing data between nodes + Data(DataMessage), +} + +impl MessageKind { + // Can't implement `From` directly due to potential conflict with + // `From`. + /// Construct a [`MessageKind`] from [`SequencingMessage`]. + pub fn from_consensus_message(m: SequencingMessage) -> Self { + Self::Consensus(m) + } +} + +impl From> for MessageKind { + fn from(m: DataMessage) -> Self { + Self::Data(m) + } +} + +impl ViewMessage for MessageKind { + fn get_view_number(&self) -> TYPES::Time { + match &self { + MessageKind::Consensus(message) => message.view_number(), + MessageKind::Data(DataMessage::SubmitTransaction(_, v)) => *v, + MessageKind::Data(DataMessage::RequestData(msg)) => msg.view, + MessageKind::Data(DataMessage::DataResponse(msg)) => match msg { + ResponseMessage::Found(m) => m.view_number(), + ResponseMessage::NotFound | ResponseMessage::Denied => TYPES::Time::new(1), + }, + } + } + + fn purpose(&self) -> MessagePurpose { + match &self { + MessageKind::Consensus(message) => message.purpose(), + MessageKind::Data(_) => MessagePurpose::Data, + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)] +#[serde(bound(deserialize = "", serialize = ""))] +/// Messages related to both validating and sequencing consensus. +pub enum GeneralConsensusMessage { + /// Message with a quorum proposal. + Proposal(Proposal>), + + /// Message with a quorum vote. + Vote(QuorumVote), + + /// Message with a view sync pre-commit vote + ViewSyncPreCommitVote(ViewSyncPreCommitVote), + + /// Message with a view sync commit vote + ViewSyncCommitVote(ViewSyncCommitVote), + + /// Message with a view sync finalize vote + ViewSyncFinalizeVote(ViewSyncFinalizeVote), + + /// Message with a view sync pre-commit certificate + ViewSyncPreCommitCertificate(ViewSyncPreCommitCertificate2), + + /// Message with a view sync commit certificate + ViewSyncCommitCertificate(ViewSyncCommitCertificate2), + + /// Message with a view sync finalize certificate + ViewSyncFinalizeCertificate(ViewSyncFinalizeCertificate2), + + /// Message with a Timeout vote + TimeoutVote(TimeoutVote), + + /// Message with an upgrade proposal + UpgradeProposal(Proposal>), + + /// Message with an upgrade vote + UpgradeVote(UpgradeVote), +} + +#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Hash, Eq)] +#[serde(bound(deserialize = "", serialize = ""))] +/// Messages related to the sequencing consensus protocol for the DA committee. +pub enum CommitteeConsensusMessage { + /// Proposal for data availability committee + DAProposal(Proposal>), + + /// vote for data availability committee + DAVote(DAVote), + + /// Certificate data is available + DACertificate(DACertificate), + + /// Initiate VID dispersal. + /// + /// Like [`DAProposal`]. Use `Msg` suffix to distinguish from [`VidDisperse`]. + /// TODO this variant should not be a [`CommitteeConsensusMessage`] because + VidDisperseMsg(Proposal>), +} + +/// Messages for sequencing consensus. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)] +#[serde(bound(deserialize = "", serialize = ""))] +pub struct SequencingMessage( + pub Either, CommitteeConsensusMessage>, +); + +impl SequencingMessage { + // TODO: Disable panic after the `ViewSync` case is implemented. + /// Get the view number this message relates to + #[allow(clippy::panic)] + fn view_number(&self) -> TYPES::Time { + match &self.0 { + Left(general_message) => { + match general_message { + GeneralConsensusMessage::Proposal(p) => { + // view of leader in the leaf when proposal + // this should match replica upon receipt + p.data.get_view_number() + } + GeneralConsensusMessage::Vote(vote_message) => vote_message.get_view_number(), + GeneralConsensusMessage::TimeoutVote(message) => message.get_view_number(), + GeneralConsensusMessage::ViewSyncPreCommitVote(message) => { + message.get_view_number() + } + GeneralConsensusMessage::ViewSyncCommitVote(message) => { + message.get_view_number() + } + GeneralConsensusMessage::ViewSyncFinalizeVote(message) => { + message.get_view_number() + } + GeneralConsensusMessage::ViewSyncPreCommitCertificate(message) => { + message.get_view_number() + } + GeneralConsensusMessage::ViewSyncCommitCertificate(message) => { + message.get_view_number() + } + GeneralConsensusMessage::ViewSyncFinalizeCertificate(message) => { + message.get_view_number() + } + GeneralConsensusMessage::UpgradeProposal(message) => { + message.data.get_view_number() + } + GeneralConsensusMessage::UpgradeVote(message) => message.get_view_number(), + } + } + Right(committee_message) => { + match committee_message { + CommitteeConsensusMessage::DAProposal(p) => { + // view of leader in the leaf when proposal + // this should match replica upon receipt + p.data.get_view_number() + } + CommitteeConsensusMessage::DAVote(vote_message) => { + vote_message.get_view_number() + } + CommitteeConsensusMessage::DACertificate(cert) => cert.view_number, + CommitteeConsensusMessage::VidDisperseMsg(disperse) => { + disperse.data.get_view_number() + } + } + } + } + } + + // TODO: Disable panic after the `ViewSync` case is implemented. + /// Get the message purpos + #[allow(clippy::panic)] + fn purpose(&self) -> MessagePurpose { + match &self.0 { + Left(general_message) => match general_message { + GeneralConsensusMessage::Proposal(_) => MessagePurpose::Proposal, + GeneralConsensusMessage::Vote(_) | GeneralConsensusMessage::TimeoutVote(_) => { + MessagePurpose::Vote + } + GeneralConsensusMessage::ViewSyncPreCommitVote(_) + | GeneralConsensusMessage::ViewSyncCommitVote(_) + | GeneralConsensusMessage::ViewSyncFinalizeVote(_) => MessagePurpose::ViewSyncVote, + + GeneralConsensusMessage::ViewSyncPreCommitCertificate(_) + | GeneralConsensusMessage::ViewSyncCommitCertificate(_) + | GeneralConsensusMessage::ViewSyncFinalizeCertificate(_) => { + MessagePurpose::ViewSyncCertificate + } + + GeneralConsensusMessage::UpgradeProposal(_) + | GeneralConsensusMessage::UpgradeVote(_) => MessagePurpose::Upgrade, + }, + Right(committee_message) => match committee_message { + CommitteeConsensusMessage::DAProposal(_) => MessagePurpose::Proposal, + CommitteeConsensusMessage::DAVote(_) => MessagePurpose::Vote, + CommitteeConsensusMessage::DACertificate(_) => MessagePurpose::DAC, + CommitteeConsensusMessage::VidDisperseMsg(_) => MessagePurpose::VidDisperse, + }, + } + } +} + +#[derive(Serialize, Deserialize, Derivative, Clone, Debug, PartialEq, Eq, Hash)] +#[serde(bound(deserialize = ""))] +/// Messages related to sending data between nodes +pub enum DataMessage { + /// Contains a transaction to be submitted + /// TODO rethink this when we start to send these messages + /// we only need the view number for broadcast + SubmitTransaction(TYPES::Transaction, TYPES::Time), + /// A request for data + RequestData(DataRequest), + /// A response to a data request + DataResponse(ResponseMessage), +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)] +#[serde(bound(deserialize = ""))] +/// Prepare qc from the leader +pub struct Proposal + DeserializeOwned> { + // NOTE: optimization could include view number to help look up parent leaf + // could even do 16 bit numbers if we want + /// The data being proposed. + pub data: PROPOSAL, + /// The proposal must be signed by the view leader + pub signature: ::PureAssembledSignatureType, + /// Phantom for TYPES + pub _pd: PhantomData, +} diff --git a/crates/types/src/qc.rs b/crates/types/src/qc.rs new file mode 100644 index 0000000000..cef6cdd2c0 --- /dev/null +++ b/crates/types/src/qc.rs @@ -0,0 +1,310 @@ +//! Implementation for `BitVectorQC` that uses BLS signature + Bit vector. +//! See more details in hotshot paper. + +use crate::{ + stake_table::StakeTableEntry, + traits::{qc::QuorumCertificateScheme, signature_key::SignatureKey}, +}; +use ark_std::{ + fmt::Debug, + format, + marker::PhantomData, + rand::{CryptoRng, RngCore}, + vec, + vec::Vec, +}; +use bitvec::prelude::*; +use ethereum_types::U256; +use generic_array::GenericArray; +use jf_primitives::{ + errors::{PrimitivesError, PrimitivesError::ParameterError}, + signatures::AggregateableSignatureSchemes, +}; +use serde::{Deserialize, Serialize}; +use typenum::U32; + +/// An implementation of QC using BLS signature and a bit-vector. +#[derive(Serialize, Deserialize)] +pub struct BitVectorQC Deserialize<'a>>( + PhantomData, +); + +/// Public parameters of [`BitVectorQC`] +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, Hash)] +#[serde(bound(deserialize = ""))] +pub struct QCParams Deserialize<'a>> { + /// the stake table (snapshot) this QC is verified against + pub stake_entries: Vec>, + /// threshold for the accumulated "weight" of votes to form a QC + pub threshold: U256, + /// public parameter for the aggregated signature scheme + pub agg_sig_pp: P, +} + +impl QuorumCertificateScheme for BitVectorQC +where + A: AggregateableSignatureSchemes + Serialize + for<'a> Deserialize<'a>, + A::VerificationKey: SignatureKey, +{ + type QCProverParams = QCParams; + + // TODO: later with SNARKs we'll use a smaller verifier parameter + type QCVerifierParams = QCParams; + + type QC = (A::Signature, BitVec); + type MessageLength = U32; + type QuorumSize = U256; + + /// Sign a message with the signing key + fn sign>( + pp: &A::PublicParameter, + sk: &A::SigningKey, + msg: M, + prng: &mut R, + ) -> Result { + A::sign(pp, sk, msg, prng) + } + + fn assemble( + qc_pp: &Self::QCProverParams, + signers: &BitSlice, + sigs: &[A::Signature], + ) -> Result { + if signers.len() != qc_pp.stake_entries.len() { + return Err(ParameterError(format!( + "bit vector len {} != the number of stake entries {}", + signers.len(), + qc_pp.stake_entries.len(), + ))); + } + let total_weight: U256 = + qc_pp + .stake_entries + .iter() + .zip(signers.iter()) + .fold(U256::zero(), |acc, (entry, b)| { + if *b { + acc + entry.stake_amount + } else { + acc + } + }); + if total_weight < qc_pp.threshold { + return Err(ParameterError(format!( + "total_weight {} less than threshold {}", + total_weight, qc_pp.threshold, + ))); + } + let mut ver_keys = vec![]; + for (entry, b) in qc_pp.stake_entries.iter().zip(signers.iter()) { + if *b { + ver_keys.push(entry.stake_key.clone()); + } + } + if ver_keys.len() != sigs.len() { + return Err(ParameterError(format!( + "the number of ver_keys {} != the number of partial signatures {}", + ver_keys.len(), + sigs.len(), + ))); + } + let sig = A::aggregate(&qc_pp.agg_sig_pp, &ver_keys[..], sigs)?; + + Ok((sig, signers.into())) + } + + fn check( + qc_vp: &Self::QCVerifierParams, + message: &GenericArray, + qc: &Self::QC, + ) -> Result { + let (sig, signers) = qc; + if signers.len() != qc_vp.stake_entries.len() { + return Err(ParameterError(format!( + "signers bit vector len {} != the number of stake entries {}", + signers.len(), + qc_vp.stake_entries.len(), + ))); + } + let total_weight: U256 = + qc_vp + .stake_entries + .iter() + .zip(signers.iter()) + .fold(U256::zero(), |acc, (entry, b)| { + if *b { + acc + entry.stake_amount + } else { + acc + } + }); + if total_weight < qc_vp.threshold { + return Err(ParameterError(format!( + "total_weight {} less than threshold {}", + total_weight, qc_vp.threshold, + ))); + } + let mut ver_keys = vec![]; + for (entry, b) in qc_vp.stake_entries.iter().zip(signers.iter()) { + if *b { + ver_keys.push(entry.stake_key.clone()); + } + } + A::multi_sig_verify(&qc_vp.agg_sig_pp, &ver_keys[..], message, sig)?; + + Ok(total_weight) + } + + fn trace( + qc_vp: &Self::QCVerifierParams, + message: &GenericArray<::MessageUnit, Self::MessageLength>, + qc: &Self::QC, + ) -> Result::VerificationKey>, PrimitivesError> { + let (_sig, signers) = qc; + if signers.len() != qc_vp.stake_entries.len() { + return Err(ParameterError(format!( + "signers bit vector len {} != the number of stake entries {}", + signers.len(), + qc_vp.stake_entries.len(), + ))); + } + + Self::check(qc_vp, message, qc)?; + + let signer_pks: Vec<_> = qc_vp + .stake_entries + .iter() + .zip(signers.iter()) + .filter(|(_, b)| **b) + .map(|(pk, _)| pk.stake_key.clone()) + .collect(); + Ok(signer_pks) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use jf_primitives::signatures::{ + bls_over_bn254::{BLSOverBN254CurveSignatureScheme, KeyPair}, + SignatureScheme, + }; + use versioned_binary_serialization::{version::StaticVersion, BinarySerializer, Serializer}; + type Version = StaticVersion<0, 1>; + + macro_rules! test_quorum_certificate { + ($aggsig:tt) => { + let mut rng = jf_utils::test_rng(); + let agg_sig_pp = $aggsig::param_gen(Some(&mut rng)).unwrap(); + let key_pair1 = KeyPair::generate(&mut rng); + let key_pair2 = KeyPair::generate(&mut rng); + let key_pair3 = KeyPair::generate(&mut rng); + let entry1 = StakeTableEntry { + stake_key: key_pair1.ver_key(), + stake_amount: U256::from(3u8), + }; + let entry2 = StakeTableEntry { + stake_key: key_pair2.ver_key(), + stake_amount: U256::from(5u8), + }; + let entry3 = StakeTableEntry { + stake_key: key_pair3.ver_key(), + stake_amount: U256::from(7u8), + }; + let qc_pp = QCParams { + stake_entries: vec![entry1, entry2, entry3], + threshold: U256::from(10u8), + agg_sig_pp, + }; + let msg = [72u8; 32]; + let sig1 = + BitVectorQC::<$aggsig>::sign(&agg_sig_pp, key_pair1.sign_key_ref(), &msg, &mut rng) + .unwrap(); + let sig2 = + BitVectorQC::<$aggsig>::sign(&agg_sig_pp, key_pair2.sign_key_ref(), &msg, &mut rng) + .unwrap(); + let sig3 = + BitVectorQC::<$aggsig>::sign(&agg_sig_pp, key_pair3.sign_key_ref(), &msg, &mut rng) + .unwrap(); + + // happy path + let signers = bitvec![0, 1, 1]; + let qc = BitVectorQC::<$aggsig>::assemble( + &qc_pp, + signers.as_bitslice(), + &[sig2.clone(), sig3.clone()], + ) + .unwrap(); + assert!(BitVectorQC::<$aggsig>::check(&qc_pp, &msg.into(), &qc).is_ok()); + assert_eq!( + BitVectorQC::<$aggsig>::trace(&qc_pp, &msg.into(), &qc).unwrap(), + vec![key_pair2.ver_key(), key_pair3.ver_key()], + ); + + // Check the QC and the QCParams can be serialized / deserialized + assert_eq!( + qc, + Serializer::::deserialize(&Serializer::::serialize(&qc).unwrap()) + .unwrap() + ); + + assert_eq!( + qc_pp, + Serializer::::deserialize( + &Serializer::::serialize(&qc_pp).unwrap() + ) + .unwrap() + ); + + // bad paths + // number of signatures unmatch + assert!(BitVectorQC::<$aggsig>::assemble( + &qc_pp, + signers.as_bitslice(), + &[sig2.clone()] + ) + .is_err()); + // total weight under threshold + let active_bad = bitvec![1, 1, 0]; + assert!(BitVectorQC::<$aggsig>::assemble( + &qc_pp, + active_bad.as_bitslice(), + &[sig1.clone(), sig2.clone()] + ) + .is_err()); + // wrong bool vector length + let active_bad_2 = bitvec![0, 1, 1, 0]; + assert!(BitVectorQC::<$aggsig>::assemble( + &qc_pp, + active_bad_2.as_bitslice(), + &[sig2, sig3], + ) + .is_err()); + + assert!(BitVectorQC::<$aggsig>::check( + &qc_pp, + &msg.into(), + &(qc.0.clone(), active_bad) + ) + .is_err()); + assert!(BitVectorQC::<$aggsig>::check( + &qc_pp, + &msg.into(), + &(qc.0.clone(), active_bad_2) + ) + .is_err()); + let bad_msg = [70u8; 32]; + assert!(BitVectorQC::<$aggsig>::check(&qc_pp, &bad_msg.into(), &qc).is_err()); + + let bad_sig = &sig1; + assert!( + BitVectorQC::<$aggsig>::check(&qc_pp, &msg.into(), &(bad_sig.clone(), qc.1)) + .is_err() + ); + }; + } + #[test] + fn test_quorum_certificate() { + test_quorum_certificate!(BLSOverBN254CurveSignatureScheme); + } +} diff --git a/crates/types/src/signature_key.rs b/crates/types/src/signature_key.rs new file mode 100644 index 0000000000..55f0a9c8b5 --- /dev/null +++ b/crates/types/src/signature_key.rs @@ -0,0 +1,127 @@ +//! Types and structs for the hotshot signature keys + +use crate::{ + qc::{BitVectorQC, QCParams}, + stake_table::StakeTableEntry, + traits::{qc::QuorumCertificateScheme, signature_key::SignatureKey}, +}; +use bitvec::{slice::BitSlice, vec::BitVec}; +use ethereum_types::U256; +use generic_array::GenericArray; +use jf_primitives::{ + errors::PrimitivesError, + signatures::{ + bls_over_bn254::{BLSOverBN254CurveSignatureScheme, KeyPair, SignKey, VerKey}, + SignatureScheme, + }, +}; +use rand::SeedableRng; +use rand_chacha::ChaCha20Rng; +use tracing::instrument; + +/// BLS private key used to sign a message +pub type BLSPrivKey = SignKey; +/// BLS public key used to verify a signature +pub type BLSPubKey = VerKey; +/// Public parameters for BLS signature scheme +pub type BLSPublicParam = (); + +impl SignatureKey for BLSPubKey { + type PrivateKey = BLSPrivKey; + type StakeTableEntry = StakeTableEntry; + type QCParams = + QCParams::PublicParameter>; + type PureAssembledSignatureType = + ::Signature; + type QCType = (Self::PureAssembledSignatureType, BitVec); + type SignError = PrimitivesError; + + #[instrument(skip(self))] + fn validate(&self, signature: &Self::PureAssembledSignatureType, data: &[u8]) -> bool { + // This is the validation for QC partial signature before append(). + BLSOverBN254CurveSignatureScheme::verify(&(), self, data, signature).is_ok() + } + + fn sign( + sk: &Self::PrivateKey, + data: &[u8], + ) -> Result { + BitVectorQC::::sign( + &(), + sk, + data, + &mut rand::thread_rng(), + ) + } + + fn from_private(private_key: &Self::PrivateKey) -> Self { + BLSPubKey::from(private_key) + } + + fn to_bytes(&self) -> Vec { + let mut buf = vec![]; + ark_serialize::CanonicalSerialize::serialize_compressed(self, &mut buf) + .expect("Serialization should not fail."); + buf + } + + fn from_bytes(bytes: &[u8]) -> Result { + Ok(ark_serialize::CanonicalDeserialize::deserialize_compressed( + bytes, + )?) + } + + fn generated_from_seed_indexed(seed: [u8; 32], index: u64) -> (Self, Self::PrivateKey) { + let mut hasher = blake3::Hasher::new(); + hasher.update(&seed); + hasher.update(&index.to_le_bytes()); + let new_seed = *hasher.finalize().as_bytes(); + let kp = KeyPair::generate(&mut ChaCha20Rng::from_seed(new_seed)); + (kp.ver_key(), kp.sign_key_ref().clone()) + } + + fn get_stake_table_entry(&self, stake: u64) -> Self::StakeTableEntry { + StakeTableEntry { + stake_key: *self, + stake_amount: U256::from(stake), + } + } + + fn get_public_key(entry: &Self::StakeTableEntry) -> Self { + entry.stake_key + } + + fn get_public_parameter( + stake_entries: Vec, + threshold: U256, + ) -> Self::QCParams { + QCParams { + stake_entries, + threshold, + agg_sig_pp: (), + } + } + + fn check(real_qc_pp: &Self::QCParams, data: &[u8], qc: &Self::QCType) -> bool { + let msg = GenericArray::from_slice(data); + BitVectorQC::::check(real_qc_pp, msg, qc).is_ok() + } + + fn get_sig_proof(signature: &Self::QCType) -> (Self::PureAssembledSignatureType, BitVec) { + signature.clone() + } + + fn assemble( + real_qc_pp: &Self::QCParams, + signers: &BitSlice, + sigs: &[Self::PureAssembledSignatureType], + ) -> Self::QCType { + BitVectorQC::::assemble(real_qc_pp, signers, sigs) + .expect("this assembling shouldn't fail") + } + + fn genesis_proposer_pk() -> Self { + let kp = KeyPair::generate(&mut ChaCha20Rng::from_seed([0u8; 32])); + kp.ver_key() + } +} diff --git a/crates/types/src/simple_certificate.rs b/crates/types/src/simple_certificate.rs new file mode 100644 index 0000000000..1592583e90 --- /dev/null +++ b/crates/types/src/simple_certificate.rs @@ -0,0 +1,179 @@ +//! Implementations of the simple certificate type. Used for Quorum, DA, and Timeout Certificates + +use std::{ + fmt::{self, Debug, Display, Formatter}, + hash::Hash, + marker::PhantomData, +}; + +use commit::{Commitment, CommitmentBoundsArkless, Committable}; +use ethereum_types::U256; + +use crate::{ + data::Leaf, + simple_vote::{ + DAData, QuorumData, TimeoutData, UpgradeProposalData, ViewSyncCommitData, + ViewSyncFinalizeData, ViewSyncPreCommitData, Voteable, + }, + traits::{ + election::Membership, node_implementation::ConsensusTime, node_implementation::NodeType, + signature_key::SignatureKey, + }, + vote::{Certificate, HasViewNumber}, +}; + +use serde::{Deserialize, Serialize}; + +/// Trait which allows use to inject different threshold calculations into a Certificate type +pub trait Threshold { + /// Calculate a threshold based on the membership + fn threshold>(membership: &MEMBERSHIP) -> u64; +} + +/// Defines a threshold which is 2f + 1 (Amount needed for Quorum) +#[derive(Serialize, Deserialize, Eq, Hash, PartialEq, Debug, Clone)] +pub struct SuccessThreshold {} + +impl Threshold for SuccessThreshold { + fn threshold>(membership: &MEMBERSHIP) -> u64 { + membership.success_threshold().into() + } +} + +/// Defines a threshold which is f + 1 (i.e at least one of the stake is honest) +#[derive(Serialize, Deserialize, Eq, Hash, PartialEq, Debug, Clone)] +pub struct OneHonestThreshold {} + +impl Threshold for OneHonestThreshold { + fn threshold>(membership: &MEMBERSHIP) -> u64 { + membership.failure_threshold().into() + } +} + +/// Defines a threshold which is 0.9n + 1 (i.e. over 90% of the nodes with stake) +#[derive(Serialize, Deserialize, Eq, Hash, PartialEq, Debug, Clone)] +pub struct UpgradeThreshold {} + +impl Threshold for UpgradeThreshold { + fn threshold>(membership: &MEMBERSHIP) -> u64 { + membership.upgrade_threshold().into() + } +} + +/// A certificate which can be created by aggregating many simple votes on the commitment. +#[derive(Serialize, Deserialize, Eq, Hash, PartialEq, Debug, Clone)] +pub struct SimpleCertificate> { + /// The data this certificate is for. I.e the thing that was voted on to create this Certificate + pub data: VOTEABLE, + /// commitment of all the votes this cert should be signed over + pub vote_commitment: Commitment, + /// Which view this QC relates to + pub view_number: TYPES::Time, + /// assembled signature for certificate aggregation + pub signatures: Option<::QCType>, + /// If this QC is for the genesis block + pub is_genesis: bool, + /// phantom data for `THRESHOLD` and `TYPES` + pub _pd: PhantomData<(TYPES, THRESHOLD)>, +} + +impl> Certificate + for SimpleCertificate +{ + type Voteable = VOTEABLE; + type Threshold = THRESHOLD; + + fn create_signed_certificate( + vote_commitment: Commitment, + data: Self::Voteable, + sig: ::QCType, + view: TYPES::Time, + ) -> Self { + SimpleCertificate { + data, + vote_commitment, + view_number: view, + signatures: Some(sig), + is_genesis: false, + _pd: PhantomData, + } + } + fn is_valid_cert>(&self, membership: &MEMBERSHIP) -> bool { + if self.is_genesis && self.view_number == TYPES::Time::genesis() { + return true; + } + let real_qc_pp = ::get_public_parameter( + membership.get_committee_qc_stake_table(), + U256::from(Self::threshold(membership)), + ); + ::check( + &real_qc_pp, + self.vote_commitment.as_ref(), + self.signatures.as_ref().unwrap(), + ) + } + fn threshold>(membership: &MEMBERSHIP) -> u64 { + THRESHOLD::threshold(membership) + } + fn get_data(&self) -> &Self::Voteable { + &self.data + } + fn get_data_commitment(&self) -> Commitment { + self.vote_commitment + } +} + +impl> + HasViewNumber for SimpleCertificate +{ + fn get_view_number(&self) -> TYPES::Time { + self.view_number + } +} +impl Display for QuorumCertificate { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!( + f, + "view: {:?}, is_genesis: {:?}", + self.view_number, self.is_genesis + ) + } +} + +impl QuorumCertificate { + #[must_use] + /// Creat the Genisis certificate + pub fn genesis() -> Self { + let data = QuorumData { + leaf_commit: Commitment::>::default_commitment_no_preimage(), + }; + let commit = data.commit(); + Self { + data, + vote_commitment: commit, + view_number: ::genesis(), + signatures: None, + is_genesis: true, + _pd: PhantomData, + } + } +} + +/// Type alias for a `QuorumCertificate`, which is a `SimpleCertificate` of `QuorumVotes` +pub type QuorumCertificate = SimpleCertificate, SuccessThreshold>; +/// Type alias for a DA certificate over `DAData` +pub type DACertificate = SimpleCertificate; +/// Type alias for a Timeout certificate over a view number +pub type TimeoutCertificate = SimpleCertificate, SuccessThreshold>; +/// Type alias for a `ViewSyncPreCommit` certificate over a view number +pub type ViewSyncPreCommitCertificate2 = + SimpleCertificate, OneHonestThreshold>; +/// Type alias for a `ViewSyncCommit` certificate over a view number +pub type ViewSyncCommitCertificate2 = + SimpleCertificate, SuccessThreshold>; +/// Type alias for a `ViewSyncFinalize` certificate over a view number +pub type ViewSyncFinalizeCertificate2 = + SimpleCertificate, SuccessThreshold>; +/// Type alias for a `UpgradeCertificate`, which is a `SimpleCertificate` of `UpgradeProposalData` +pub type UpgradeCertificate = + SimpleCertificate, UpgradeThreshold>; diff --git a/crates/types/src/simple_vote.rs b/crates/types/src/simple_vote.rs new file mode 100644 index 0000000000..3573ad4628 --- /dev/null +++ b/crates/types/src/simple_vote.rs @@ -0,0 +1,258 @@ +//! Implementations of the simple vote types. + +use std::{fmt::Debug, hash::Hash}; + +use commit::{Commitment, Committable}; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; + +use crate::{ + data::Leaf, + traits::{node_implementation::NodeType, signature_key::SignatureKey}, + vid::VidCommitment, + vote::{HasViewNumber, Vote}, +}; +use versioned_binary_serialization::version::Version; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Hash, Eq)] +/// Data used for a yes vote. +#[serde(bound(deserialize = ""))] +pub struct QuorumData { + /// Commitment to the leaf + pub leaf_commit: Commitment>, +} +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Hash, Eq)] +/// Data used for a DA vote. +pub struct DAData { + /// Commitment to a block payload + pub payload_commit: VidCommitment, +} +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Hash, Eq)] +/// Data used for a timeout vote. +pub struct TimeoutData { + /// View the timeout is for + pub view: TYPES::Time, +} +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Hash, Eq)] +/// Data used for a VID vote. +pub struct VIDData { + /// Commitment to the block payload the VID vote is on. + pub payload_commit: VidCommitment, +} +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Hash, Eq)] +/// Data used for a Pre Commit vote. +pub struct ViewSyncPreCommitData { + /// The relay this vote is intended for + pub relay: u64, + /// The view number we are trying to sync on + pub round: TYPES::Time, +} +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Hash, Eq)] +/// Data used for a Commit vote. +pub struct ViewSyncCommitData { + /// The relay this vote is intended for + pub relay: u64, + /// The view number we are trying to sync on + pub round: TYPES::Time, +} +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Hash, Eq)] +/// Data used for a Finalize vote. +pub struct ViewSyncFinalizeData { + /// The relay this vote is intended for + pub relay: u64, + /// The view number we are trying to sync on + pub round: TYPES::Time, +} +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Hash, Eq)] +/// Data used for a Upgrade vote. +pub struct UpgradeProposalData { + /// The old version that we are upgrading from. + pub old_version: Version, + /// The new version that we are upgrading to. + pub new_version: Version, + /// A unique identifier for the specific protocol being voted on. + pub new_version_hash: Vec, + /// The last block for which the old version will be in effect. + pub old_version_last_block: TYPES::Time, + /// The first block for which the new version will be in effect. + pub new_version_first_block: TYPES::Time, +} + +/// Marker trait for data or commitments that can be voted on. +/// Only structs in this file can implement voteable. This is enforced with the `Sealed` trait +/// Sealing this trait prevents creating new vote types outside this file. +pub trait Voteable: + sealed::Sealed + Committable + Clone + Serialize + Debug + PartialEq + Hash + Eq +{ +} + +/// Sealed is used to make sure no other files can implement the Voteable trait. +/// All simple voteable types should be implemented here. This prevents us from +/// creating/using improper types when using the vote types. +mod sealed { + use commit::Committable; + + /// Only structs in this file can impl `Sealed` + pub trait Sealed {} + + // TODO: Does the implement for things outside this file that are commitable? + impl Sealed for C {} +} + +/// A simple yes vote over some votable type. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Hash, Eq)] +pub struct SimpleVote { + /// The signature share associated with this vote + pub signature: ( + TYPES::SignatureKey, + ::PureAssembledSignatureType, + ), + /// The leaf commitment being voted on. + pub data: DATA, + /// The view this vote was cast for + pub view_number: TYPES::Time, +} + +impl HasViewNumber for SimpleVote { + fn get_view_number(&self) -> ::Time { + self.view_number + } +} + +impl Vote for SimpleVote { + type Commitment = DATA; + + fn get_signing_key(&self) -> ::SignatureKey { + self.signature.0.clone() + } + + fn get_signature(&self) -> ::PureAssembledSignatureType { + self.signature.1.clone() + } + + fn get_data(&self) -> &DATA { + &self.data + } + + fn get_data_commitment(&self) -> Commitment { + self.data.commit() + } +} + +impl SimpleVote { + /// Creates and signs a simple vote + /// # Errors + /// If we are unable to sign the data + pub fn create_signed_vote( + data: DATA, + view: TYPES::Time, + pub_key: &TYPES::SignatureKey, + private_key: &::PrivateKey, + ) -> Result::SignError> { + match TYPES::SignatureKey::sign(private_key, data.commit().as_ref()) { + Ok(signature) => Ok(Self { + signature: (pub_key.clone(), signature), + data, + view_number: view, + }), + Err(e) => Err(e), + } + } +} + +impl Committable for QuorumData { + fn commit(&self) -> Commitment { + commit::RawCommitmentBuilder::new("Yes Vote") + .var_size_bytes(self.leaf_commit.as_ref()) + .finalize() + } +} + +impl Committable for TimeoutData { + fn commit(&self) -> Commitment { + commit::RawCommitmentBuilder::new("Timeout Vote") + .u64(*self.view) + .finalize() + } +} + +impl Committable for DAData { + fn commit(&self) -> Commitment { + commit::RawCommitmentBuilder::new("DA Vote") + .var_size_bytes(self.payload_commit.as_ref()) + .finalize() + } +} + +impl Committable for VIDData { + fn commit(&self) -> Commitment { + commit::RawCommitmentBuilder::new("VID Vote") + .var_size_bytes(self.payload_commit.as_ref()) + .finalize() + } +} + +impl Committable for UpgradeProposalData { + fn commit(&self) -> Commitment { + let builder = commit::RawCommitmentBuilder::new("Upgrade Vote"); + builder + .u64(*self.new_version_first_block) + .u64(*self.old_version_last_block) + .var_size_bytes(self.new_version_hash.as_slice()) + .u16(self.new_version.minor) + .u16(self.new_version.major) + .u16(self.old_version.minor) + .u16(self.old_version.major) + .finalize() + } +} + +/// This implements commit for all the types which contain a view and relay public key. +fn view_and_relay_commit( + view: TYPES::Time, + relay: u64, + tag: &str, +) -> Commitment { + let builder = commit::RawCommitmentBuilder::new(tag); + builder.u64(*view).u64(relay).finalize() +} + +impl Committable for ViewSyncPreCommitData { + fn commit(&self) -> Commitment { + view_and_relay_commit::(self.round, self.relay, "View Sync Precommit") + } +} + +impl Committable for ViewSyncFinalizeData { + fn commit(&self) -> Commitment { + view_and_relay_commit::(self.round, self.relay, "View Sync Finalize") + } +} +impl Committable for ViewSyncCommitData { + fn commit(&self) -> Commitment { + view_and_relay_commit::(self.round, self.relay, "View Sync Commit") + } +} + +// impl votable for all the data types in this file sealed marker should ensure nothing is accidently +// implemented for structs that aren't "voteable" +impl Voteable + for V +{ +} + +// Type aliases for simple use of all the main votes. We should never see `SimpleVote` outside this file +/// Quorum vote Alias +pub type QuorumVote = SimpleVote>; +/// DA vote type alias +pub type DAVote = SimpleVote; +/// Timeout Vote type alias +pub type TimeoutVote = SimpleVote>; +/// View Sync Commit Vote type alias +pub type ViewSyncCommitVote = SimpleVote>; +/// View Sync Pre Commit Vote type alias +pub type ViewSyncPreCommitVote = SimpleVote>; +/// View Sync Finalize Vote type alias +pub type ViewSyncFinalizeVote = SimpleVote>; +/// Upgrade proposal vote +pub type UpgradeVote = SimpleVote>; diff --git a/crates/types/src/stake_table.rs b/crates/types/src/stake_table.rs new file mode 100644 index 0000000000..7c6525e0eb --- /dev/null +++ b/crates/types/src/stake_table.rs @@ -0,0 +1,31 @@ +//! Types and structs related to the stake table + +use crate::traits::signature_key::{SignatureKey, StakeTableEntryType}; +use ethereum_types::U256; +use serde::{Deserialize, Serialize}; + +/// Stake table entry +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, Hash, Eq)] +#[serde(bound(deserialize = ""))] +pub struct StakeTableEntry { + /// The public key + pub stake_key: K, + /// The associated stake amount + pub stake_amount: U256, +} + +impl StakeTableEntryType for StakeTableEntry { + /// Get the stake amount + fn get_stake(&self) -> U256 { + self.stake_amount + } +} + +impl StakeTableEntry { + /// Get the public key + pub fn get_key(&self) -> &K { + &self.stake_key + } +} + +// TODO(Chengyu): add stake table snapshot here diff --git a/crates/types/src/traits.rs b/crates/types/src/traits.rs new file mode 100644 index 0000000000..a698d2c158 --- /dev/null +++ b/crates/types/src/traits.rs @@ -0,0 +1,15 @@ +//! Common traits for the `HotShot` protocol +pub mod block_contents; +pub mod consensus_api; +pub mod election; +pub mod metrics; +pub mod network; +pub mod node_implementation; +pub mod qc; +pub mod signature_key; +pub mod stake_table; +pub mod states; +pub mod storage; + +pub use block_contents::BlockPayload; +pub use states::ValidatedState; diff --git a/crates/types/src/traits/block_contents.rs b/crates/types/src/traits/block_contents.rs new file mode 100644 index 0000000000..24080dbb18 --- /dev/null +++ b/crates/types/src/traits/block_contents.rs @@ -0,0 +1,149 @@ +//! Abstraction over the contents of a block +//! +//! This module provides the [`Transaction`], [`BlockPayload`], and [`BlockHeader`] traits, which +//! describe the behaviors that a block is expected to have. + +use crate::{ + data::Leaf, + traits::{node_implementation::NodeType, ValidatedState}, + utils::BuilderCommitment, + vid::{vid_scheme, VidCommitment, VidSchemeType}, +}; +use commit::{Commitment, Committable}; +use jf_primitives::vid::VidScheme; +use serde::{de::DeserializeOwned, Serialize}; + +use std::{ + error::Error, + fmt::{Debug, Display}, + future::Future, + hash::Hash, +}; + +/// Abstraction over any type of transaction. Used by [`BlockPayload`]. +pub trait Transaction: + Clone + Serialize + DeserializeOwned + Debug + PartialEq + Eq + Sync + Send + Committable + Hash +{ +} + +/// Abstraction over the full contents of a block +/// +/// This trait encapsulates the behaviors that the transactions of a block must have in order to be +/// used by consensus +/// * Must have a predefined error type ([`BlockPayload::Error`]) +/// * Must have a transaction type that can be compared for equality, serialized and serialized, +/// sent between threads, and can have a hash produced of it +/// * Must be hashable +pub trait BlockPayload: + Serialize + Clone + Debug + Display + Hash + PartialEq + Eq + Send + Sync + DeserializeOwned +{ + /// The error type for this type of block + type Error: Error + Debug + Send + Sync + Serialize + DeserializeOwned; + + /// The type of the transitions we are applying + type Transaction: Transaction; + + /// Data created during block building which feeds into the block header + type Metadata: Clone + Debug + DeserializeOwned + Eq + Hash + Send + Sync + Serialize; + + /// Encoded payload. + type Encode<'a>: 'a + Iterator + Send + where + Self: 'a; + + /// Build a payload and associated metadata with the transactions. + /// + /// # Errors + /// If the transaction length conversion fails. + fn from_transactions( + transactions: impl IntoIterator, + ) -> Result<(Self, Self::Metadata), Self::Error>; + + /// Build a payload with the encoded transaction bytes, metadata, + /// and the associated number of VID storage nodes + /// + /// `I` may be, but not necessarily is, the `Encode` type directly from `fn encode`. + fn from_bytes(encoded_transactions: I, metadata: &Self::Metadata) -> Self + where + I: Iterator; + + /// Build the genesis payload and metadata. + fn genesis() -> (Self, Self::Metadata); + + /// Encode the payload + /// + /// # Errors + /// If the transaction length conversion fails. + fn encode(&self) -> Result, Self::Error>; + + /// List of transaction commitments. + fn transaction_commitments( + &self, + metadata: &Self::Metadata, + ) -> Vec>; + + /// Generate commitment that builders use to sign block options. + fn builder_commitment(&self, metadata: &Self::Metadata) -> BuilderCommitment; + + /// Get the transactions in the payload. + fn get_transactions(&self, metadata: &Self::Metadata) -> &Vec; +} + +/// extra functions required on block to be usable by hotshot-testing +pub trait TestableBlock: BlockPayload + Debug { + /// generate a genesis block + fn genesis() -> Self; + + /// the number of transactions in this block + fn txn_count(&self) -> u64; +} + +/// Compute the VID payload commitment. +/// TODO(Gus) delete this function? +/// # Panics +/// If the VID computation fails. +#[must_use] +pub fn vid_commitment( + encoded_transactions: &Vec, + num_storage_nodes: usize, +) -> ::Commit { + #[allow(clippy::panic)] + vid_scheme(num_storage_nodes).commit_only(encoded_transactions).unwrap_or_else(|err| panic!("VidScheme::commit_only failure:\n\t(num_storage_nodes,payload_byte_len)=({num_storage_nodes},{}\n\t{err}", encoded_transactions.len())) +} + +/// The number of storage nodes to use when computing the genesis VID commitment. +/// +/// The number of storage nodes for the genesis VID commitment is arbitrary, since we don't actually +/// do dispersal for the genesis block. For simplicity and performance, we use 1. +pub const GENESIS_VID_NUM_STORAGE_NODES: usize = 1; + +/// Header of a block, which commits to a [`BlockPayload`]. +pub trait BlockHeader: + Serialize + Clone + Debug + Hash + PartialEq + Eq + Send + Sync + DeserializeOwned + Committable +{ + /// Build a header with the parent validate state, instance-level state, parent leaf, payload + /// commitment, and metadata. + fn new( + parent_state: &TYPES::ValidatedState, + instance_state: &>::Instance, + parent_leaf: &Leaf, + payload_commitment: VidCommitment, + metadata: ::Metadata, + ) -> impl Future + Send; + + /// Build the genesis header, payload, and metadata. + fn genesis( + instance_state: &>::Instance, + payload_commitment: VidCommitment, + metadata: ::Metadata, + ) -> Self; + + /// Get the block number. + fn block_number(&self) -> u64; + + /// Get the payload commitment. + fn payload_commitment(&self) -> VidCommitment; + + /// Get the metadata. + fn metadata(&self) -> &::Metadata; +} diff --git a/crates/types/src/traits/consensus_api.rs b/crates/types/src/traits/consensus_api.rs new file mode 100644 index 0000000000..b596695f56 --- /dev/null +++ b/crates/types/src/traits/consensus_api.rs @@ -0,0 +1,42 @@ +//! Contains the [`ConsensusApi`] trait. + +use crate::{ + event::Event, + traits::{ + node_implementation::{NodeImplementation, NodeType}, + signature_key::SignatureKey, + }, +}; +use async_trait::async_trait; + +use std::{num::NonZeroUsize, time::Duration}; + +/// The API that tasks use to talk to the system +/// TODO we plan to drop this +#[async_trait] +pub trait ConsensusApi>: Send + Sync { + /// Total number of nodes in the network. Also known as `n`. + fn total_nodes(&self) -> NonZeroUsize; + + /// The minimum amount of time a leader has to wait before sending a propose + fn propose_min_round_time(&self) -> Duration; + + /// The maximum amount of time a leader can wait before sending a propose. + /// If this time is reached, the leader has to send a propose without transactions. + fn propose_max_round_time(&self) -> Duration; + + /// Retuns the maximum transactions allowed in a block + fn max_transactions(&self) -> NonZeroUsize; + + /// Returns the minimum transactions that must be in a block + fn min_transactions(&self) -> usize; + + /// Get a reference to the public key. + fn public_key(&self) -> &TYPES::SignatureKey; + + /// Get a reference to the private key. + fn private_key(&self) -> &::PrivateKey; + + /// Notify the system of an event within `hotshot-consensus`. + async fn send_event(&self, event: Event); +} diff --git a/crates/types/src/traits/election.rs b/crates/types/src/traits/election.rs new file mode 100644 index 0000000000..0f3eec9b53 --- /dev/null +++ b/crates/types/src/traits/election.rs @@ -0,0 +1,91 @@ +//! The election trait, used to decide which node is the leader and determine if a vote is valid. + +// Needed to avoid the non-binding `let` warning. +#![allow(clippy::let_underscore_untyped)] + +use super::node_implementation::NodeType; + +use crate::{traits::signature_key::SignatureKey, PeerConfig}; + +use snafu::Snafu; +use std::{collections::BTreeSet, fmt::Debug, hash::Hash, num::NonZeroU64}; + +/// Error for election problems +#[derive(Snafu, Debug)] +pub enum ElectionError { + /// stub error to be filled in + StubError, + /// Math error doing something + /// NOTE: it would be better to make Election polymorphic over + /// the election error and then have specific math errors + MathError, +} + +/// election config +pub trait ElectionConfig: + Default + + Clone + + serde::Serialize + + for<'de> serde::Deserialize<'de> + + Sync + + Send + + core::fmt::Debug +{ +} + +/// A protocol for determining membership in and participating in a committee. +pub trait Membership: + Clone + Debug + Eq + PartialEq + Send + Sync + Hash + 'static +{ + /// generate a default election configuration + fn default_election_config( + num_nodes_with_stake: u64, + num_nodes_without_stake: u64, + ) -> TYPES::ElectionConfigType; + + /// create an election + /// TODO may want to move this to a testableelection trait + fn create_election( + entries: Vec>, + config: TYPES::ElectionConfigType, + ) -> Self; + + /// Clone the public key and corresponding stake table for current elected committee + fn get_committee_qc_stake_table( + &self, + ) -> Vec<::StakeTableEntry>; + + /// The leader of the committee for view `view_number`. + fn get_leader(&self, view_number: TYPES::Time) -> TYPES::SignatureKey; + + /// The staked members of the committee for view `view_number`. + fn get_staked_committee(&self, view_number: TYPES::Time) -> BTreeSet; + + /// The non-staked members of the committee for view `view_number`. + fn get_non_staked_committee(&self, view_number: TYPES::Time) -> BTreeSet; + + /// Get whole (staked + non-staked) committee for view `view_number`. + fn get_whole_committee(&self, view_number: TYPES::Time) -> BTreeSet; + + /// Check if a key has stake + fn has_stake(&self, pub_key: &TYPES::SignatureKey) -> bool; + + /// Get the stake table entry for a public key, returns `None` if the + /// key is not in the table + fn get_stake( + &self, + pub_key: &TYPES::SignatureKey, + ) -> Option<::StakeTableEntry>; + + /// Returns the number of total nodes in the committee + fn total_nodes(&self) -> usize; + + /// Returns the threshold for a specific `Membership` implementation + fn success_threshold(&self) -> NonZeroU64; + + /// Returns the threshold for a specific `Membership` implementation + fn failure_threshold(&self) -> NonZeroU64; + + /// Returns the threshold required to upgrade the network protocol + fn upgrade_threshold(&self) -> NonZeroU64; +} diff --git a/crates/types/src/traits/metrics.rs b/crates/types/src/traits/metrics.rs new file mode 100644 index 0000000000..fc69b5c077 --- /dev/null +++ b/crates/types/src/traits/metrics.rs @@ -0,0 +1,295 @@ +//! The [`Metrics`] trait is used to collect information from multiple components in the entire system. +//! +//! This trait can be used to spawn the following traits: +//! - [`Counter`]: an ever-increasing value (example usage: total bytes send/received) +//! - [`Gauge`]: a value that store the latest value, and can go up and down (example usage: amount of users logged in) +//! - [`Histogram`]: stores multiple float values based for a graph (example usage: CPU %) +//! - [`Label`]: Stores the last string (example usage: current version, network online/offline) + +use dyn_clone::DynClone; +use std::fmt::Debug; + +/// The metrics type. +pub trait Metrics: Send + Sync + DynClone + Debug { + /// Create a [`Counter`] with an optional `unit_label`. + /// + /// The `unit_label` can be used to indicate what the unit of the value is, e.g. "kb" or "seconds" + fn create_counter(&self, label: String, unit_label: Option) -> Box; + /// Create a [`Gauge`] with an optional `unit_label`. + /// + /// The `unit_label` can be used to indicate what the unit of the value is, e.g. "kb" or "seconds" + fn create_gauge(&self, label: String, unit_label: Option) -> Box; + /// Create a [`Histogram`] with an optional `unit_label`. + /// + /// The `unit_label` can be used to indicate what the unit of the value is, e.g. "kb" or "seconds" + fn create_histogram(&self, label: String, unit_label: Option) -> Box; + /// Create a [`Label`]. + fn create_label(&self, label: String) -> Box; + + /// Create a subgroup with a specified prefix. + fn subgroup(&self, subgroup_name: String) -> Box; +} + +/// Use this if you're not planning to use any metrics. All methods are implemented as a no-op +#[derive(Clone, Copy, Debug, Default)] +pub struct NoMetrics; + +impl NoMetrics { + /// Create a new `Box` with this [`NoMetrics`] + #[must_use] + pub fn boxed() -> Box { + Box::::default() + } +} + +impl Metrics for NoMetrics { + fn create_counter(&self, _: String, _: Option) -> Box { + Box::new(NoMetrics) + } + + fn create_gauge(&self, _: String, _: Option) -> Box { + Box::new(NoMetrics) + } + + fn create_histogram(&self, _: String, _: Option) -> Box { + Box::new(NoMetrics) + } + + fn create_label(&self, _: String) -> Box { + Box::new(NoMetrics) + } + + fn subgroup(&self, _: String) -> Box { + Box::new(NoMetrics) + } +} + +impl Counter for NoMetrics { + fn add(&self, _: usize) {} +} +impl Gauge for NoMetrics { + fn set(&self, _: usize) {} + fn update(&self, _: i64) {} +} +impl Histogram for NoMetrics { + fn add_point(&self, _: f64) {} +} +impl Label for NoMetrics { + fn set(&self, _: String) {} +} + +/// An ever-incrementing counter +pub trait Counter: Send + Sync + Debug + DynClone { + /// Add a value to the counter + fn add(&self, amount: usize); +} +/// A gauge that stores the latest value. +pub trait Gauge: Send + Sync + Debug + DynClone { + /// Set the gauge value + fn set(&self, amount: usize); + + /// Update the guage value + fn update(&self, delts: i64); +} + +/// A histogram which will record a series of points. +pub trait Histogram: Send + Sync + Debug + DynClone { + /// Add a point to this histogram. + fn add_point(&self, point: f64); +} + +/// A label that stores the last string value. +pub trait Label: Send + Sync + DynClone { + /// Set the label value + fn set(&self, value: String); +} +dyn_clone::clone_trait_object!(Metrics); +dyn_clone::clone_trait_object!(Gauge); +dyn_clone::clone_trait_object!(Counter); +dyn_clone::clone_trait_object!(Histogram); +dyn_clone::clone_trait_object!(Label); + +#[cfg(test)] +mod test { + use super::*; + use std::{ + collections::HashMap, + sync::{Arc, Mutex}, + }; + + #[derive(Debug, Clone)] + struct TestMetrics { + prefix: String, + values: Arc>, + } + + impl TestMetrics { + fn sub(&self, name: String) -> Self { + let prefix = if self.prefix.is_empty() { + name + } else { + format!("{}-{name}", self.prefix) + }; + Self { + prefix, + values: Arc::clone(&self.values), + } + } + } + + impl Metrics for TestMetrics { + fn create_counter( + &self, + label: String, + _unit_label: Option, + ) -> Box { + Box::new(self.sub(label)) + } + + fn create_gauge( + &self, + label: String, + _unit_label: Option, + ) -> Box { + Box::new(self.sub(label)) + } + + fn create_histogram( + &self, + label: String, + _unit_label: Option, + ) -> Box { + Box::new(self.sub(label)) + } + + fn create_label(&self, label: String) -> Box { + Box::new(self.sub(label)) + } + + fn subgroup(&self, subgroup_name: String) -> Box { + Box::new(self.sub(subgroup_name)) + } + } + + impl Counter for TestMetrics { + fn add(&self, amount: usize) { + *self + .values + .lock() + .unwrap() + .counters + .entry(self.prefix.clone()) + .or_default() += amount; + } + } + + impl Gauge for TestMetrics { + fn set(&self, amount: usize) { + *self + .values + .lock() + .unwrap() + .gauges + .entry(self.prefix.clone()) + .or_default() = amount; + } + fn update(&self, delta: i64) { + let mut values = self.values.lock().unwrap(); + let value = values.gauges.entry(self.prefix.clone()).or_default(); + let signed_value = i64::try_from(*value).unwrap_or(i64::MAX); + *value = usize::try_from(signed_value + delta).unwrap_or(0); + } + } + + impl Histogram for TestMetrics { + fn add_point(&self, point: f64) { + self.values + .lock() + .unwrap() + .histograms + .entry(self.prefix.clone()) + .or_default() + .push(point); + } + } + + impl Label for TestMetrics { + fn set(&self, value: String) { + *self + .values + .lock() + .unwrap() + .labels + .entry(self.prefix.clone()) + .or_default() = value; + } + } + + #[derive(Default, Debug)] + struct Inner { + counters: HashMap, + gauges: HashMap, + histograms: HashMap>, + labels: HashMap, + } + + #[test] + fn test() { + let values = Arc::default(); + // This is all scoped so all the arcs should go out of scope + { + let metrics: Box = Box::new(TestMetrics { + prefix: String::new(), + values: Arc::clone(&values), + }); + + let gauge = metrics.create_gauge("foo".to_string(), None); + let counter = metrics.create_counter("bar".to_string(), None); + let histogram = metrics.create_histogram("baz".to_string(), None); + + gauge.set(5); + gauge.update(-2); + + for i in 0..5 { + counter.add(i); + } + + for i in 0..10 { + histogram.add_point(f64::from(i)); + } + + let sub = metrics.subgroup("child".to_string()); + + let sub_gauge = sub.create_gauge("foo".to_string(), None); + let sub_counter = sub.create_counter("bar".to_string(), None); + let sub_histogram = sub.create_histogram("baz".to_string(), None); + + sub_gauge.set(10); + + for i in 0..5 { + sub_counter.add(i * 2); + } + + for i in 0..10 { + sub_histogram.add_point(f64::from(i) * 2.0); + } + } + + // The above variables are scoped so they should be dropped at this point + // One of the rare times we can use `Arc::try_unwrap`! + let values = Arc::try_unwrap(values).unwrap().into_inner().unwrap(); + assert_eq!(values.gauges["foo"], 3); + assert_eq!(values.counters["bar"], 10); // 0..5 + assert_eq!( + values.histograms["baz"], + vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] + ); + + assert_eq!(values.gauges["child-foo"], 10); + assert_eq!(values.counters["child-bar"], 20); // 0..5 *2 + assert_eq!( + values.histograms["child-baz"], + vec![0.0, 2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0, 18.0] + ); + } +} diff --git a/crates/types/src/traits/network.rs b/crates/types/src/traits/network.rs new file mode 100644 index 0000000000..07c947c8cb --- /dev/null +++ b/crates/types/src/traits/network.rs @@ -0,0 +1,681 @@ +//! Network access compatibility +//! +//! Contains types and traits used by `HotShot` to abstract over network access + +use async_compatibility_layer::art::async_sleep; +#[cfg(async_executor_impl = "async-std")] +use async_std::future::TimeoutError; +use derivative::Derivative; +use dyn_clone::DynClone; +use futures::channel::{mpsc, oneshot}; +#[cfg(async_executor_impl = "tokio")] +use tokio::time::error::Elapsed as TimeoutError; +#[cfg(not(any(async_executor_impl = "async-std", async_executor_impl = "tokio")))] +compile_error! {"Either config option \"async-std\" or \"tokio\" must be enabled for this crate."} +use super::{node_implementation::NodeType, signature_key::SignatureKey}; +use crate::{ + data::ViewNumber, + message::{MessagePurpose, SequencingMessage}, + BoxSyncFuture, +}; +use async_compatibility_layer::channel::UnboundedSendError; +use async_trait::async_trait; +use rand::{ + distributions::{Bernoulli, Uniform}, + prelude::Distribution, +}; +use serde::{Deserialize, Serialize}; +use snafu::Snafu; +use std::{collections::BTreeSet, fmt::Debug, hash::Hash, sync::Arc, time::Duration}; +use versioned_binary_serialization::version::StaticVersionType; + +/// for any errors we decide to add to memory network +#[derive(Debug, Snafu, Serialize, Deserialize)] +#[snafu(visibility(pub))] +pub enum MemoryNetworkError { + /// stub + Stub, +} + +/// Centralized server specific errors +#[derive(Debug, Snafu, Serialize, Deserialize)] +#[snafu(visibility(pub))] +pub enum CentralizedServerNetworkError { + /// The centralized server could not find a specific message. + NoMessagesInQueue, +} + +/// Centralized server specific errors +#[derive(Debug, Snafu, Serialize, Deserialize)] +#[snafu(visibility(pub))] +pub enum PushCdnNetworkError { + /// Failed to receive a message from the server + FailedToReceive, + /// Failed to send a message to the server + FailedToSend, +} + +/// Web server specific errors +#[derive(Debug, Snafu, Serialize, Deserialize)] +#[snafu(visibility(pub))] +pub enum WebServerNetworkError { + /// The injected consensus data is incorrect + IncorrectConsensusData, + /// The client returned an error + ClientError, + /// Endpoint parsed incorrectly + EndpointError, + /// Client disconnected + ClientDisconnected, +} + +/// the type of transmission +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub enum TransmitType { + /// directly transmit + Direct, + /// broadcast the message to all + Broadcast, + /// broadcast to DA committee + DACommitteeBroadcast, +} + +/// Error type for networking +#[derive(Debug, Snafu)] +#[snafu(visibility(pub))] +pub enum NetworkError { + /// Libp2p specific errors + Libp2p { + /// source of error + source: Box, + }, + /// collection of libp2p secific errors + Libp2pMulti { + /// sources of errors + sources: Vec>, + }, + /// memory network specific errors + MemoryNetwork { + /// source of error + source: MemoryNetworkError, + }, + /// Push CDN network-specific errors + PushCdnNetwork { + /// source of error + source: PushCdnNetworkError, + }, + /// Centralized server specific errors + CentralizedServer { + /// source of error + source: CentralizedServerNetworkError, + }, + + /// Web server specific errors + WebServer { + /// source of error + source: WebServerNetworkError, + }, + /// unimplemented functionality + UnimplementedFeature, + /// Could not deliver a message to a specified recipient + CouldNotDeliver, + /// Attempted to deliver a message to an unknown node + NoSuchNode, + /// Failed to serialize a network message + FailedToSerialize { + /// Originating bincode error + source: anyhow::Error, + }, + /// Failed to deserealize a network message + FailedToDeserialize { + /// originating bincode error + source: anyhow::Error, + }, + /// A timeout occurred + Timeout { + /// Source of error + source: TimeoutError, + }, + /// Error sending output to consumer of NetworkingImplementation + /// TODO this should have more information + ChannelSend, + /// The underlying connection has been shut down + ShutDown, + /// unable to cancel a request, the request has already been cancelled + UnableToCancel, + /// The requested data was not found + NotFound, +} +#[derive(Clone, Debug)] +// Storing view number as a u64 to avoid the need TYPES generic +/// Events to poll or cancel consensus processes. +pub enum ConsensusIntentEvent { + /// Poll for votes for a particular view + PollForVotes(u64), + /// Poll for a proposal for a particular view + PollForProposal(u64), + /// Poll for VID disperse data for a particular view + PollForVIDDisperse(u64), + /// Poll for the most recent [quorum/da] proposal the webserver has + PollForLatestProposal, + /// Poll for the most recent view sync proposal the webserver has + PollForLatestViewSyncCertificate, + /// Poll for a DAC for a particular view + PollForDAC(u64), + /// Poll for view sync votes starting at a particular view + PollForViewSyncVotes(u64), + /// Poll for view sync proposals (certificates) for a particular view + PollForViewSyncCertificate(u64), + /// Poll for new transactions + PollForTransactions(u64), + /// Poll for future leader + PollFutureLeader(u64, K), + /// Cancel polling for votes + CancelPollForVotes(u64), + /// Cancel polling for view sync votes. + CancelPollForViewSyncVotes(u64), + /// Cancel polling for proposals. + CancelPollForProposal(u64), + /// Cancel polling for the latest proposal. + CancelPollForLatestProposal(u64), + /// Cancel polling for the latest view sync certificate + CancelPollForLatestViewSyncCertificate(u64), + /// Cancal polling for DAC. + CancelPollForDAC(u64), + /// Cancel polling for view sync certificate. + CancelPollForViewSyncCertificate(u64), + /// Cancel polling for VID disperse data + CancelPollForVIDDisperse(u64), + /// Cancel polling for transactions + CancelPollForTransactions(u64), +} + +impl ConsensusIntentEvent { + /// Get the view number of the event. + #[must_use] + pub fn view_number(&self) -> u64 { + match &self { + ConsensusIntentEvent::PollForVotes(view_number) + | ConsensusIntentEvent::PollForProposal(view_number) + | ConsensusIntentEvent::PollForDAC(view_number) + | ConsensusIntentEvent::PollForViewSyncVotes(view_number) + | ConsensusIntentEvent::CancelPollForViewSyncVotes(view_number) + | ConsensusIntentEvent::CancelPollForVotes(view_number) + | ConsensusIntentEvent::CancelPollForProposal(view_number) + | ConsensusIntentEvent::CancelPollForLatestProposal(view_number) + | ConsensusIntentEvent::CancelPollForLatestViewSyncCertificate(view_number) + | ConsensusIntentEvent::PollForVIDDisperse(view_number) + | ConsensusIntentEvent::CancelPollForVIDDisperse(view_number) + | ConsensusIntentEvent::CancelPollForDAC(view_number) + | ConsensusIntentEvent::CancelPollForViewSyncCertificate(view_number) + | ConsensusIntentEvent::PollForViewSyncCertificate(view_number) + | ConsensusIntentEvent::PollForTransactions(view_number) + | ConsensusIntentEvent::CancelPollForTransactions(view_number) + | ConsensusIntentEvent::PollFutureLeader(view_number, _) => *view_number, + ConsensusIntentEvent::PollForLatestProposal + | ConsensusIntentEvent::PollForLatestViewSyncCertificate => 1, + } + } +} + +/// common traits we would like our network messages to implement +pub trait NetworkMsg: + Serialize + for<'a> Deserialize<'a> + Clone + Sync + Send + Debug + 'static +{ +} + +/// Trait that bundles what we need from a request ID +pub trait Id: Eq + PartialEq + Hash {} +impl NetworkMsg for Vec {} + +/// a message +pub trait ViewMessage { + /// get the view out of the message + fn get_view_number(&self) -> TYPES::Time; + // TODO move out of this trait. + /// get the purpose of the message + fn purpose(&self) -> MessagePurpose; +} + +/// Wraps a oneshot channel for responding to requests +pub struct ResponseChannel(pub oneshot::Sender); + +/// A request for some data that the consensus layer is asking for. +#[derive(Serialize, Deserialize, Derivative, Clone, Debug, PartialEq, Eq, Hash)] +#[serde(bound(deserialize = ""))] +pub struct DataRequest { + /// Request + pub request: RequestKind, + /// View this message is for + pub view: TYPES::Time, + /// signature of the Sha256 hash of the data so outsiders can't use know + /// public keys with stake. + pub signature: ::PureAssembledSignatureType, +} + +/// Underlying data request +#[derive(Serialize, Deserialize, Derivative, Clone, Debug, PartialEq, Eq, Hash)] +pub enum RequestKind { + /// Request VID data by our key and the VID commitment + VID(TYPES::Time, TYPES::SignatureKey), + /// Request a DA proposal for a certain view + DAProposal(TYPES::Time), +} + +/// A resopnse for a request. `SequencingMessage` is the same as other network messages +/// The kind of message `M` is is determined by what we requested +#[derive(Serialize, Deserialize, Derivative, Clone, Debug, PartialEq, Eq, Hash)] +#[serde(bound(deserialize = ""))] +pub enum ResponseMessage { + /// Peer returned us some data + Found(SequencingMessage), + /// Peer failed to get us data + NotFound, + /// The Request was denied + Denied, +} + +/// represents a networking implmentration +/// exposes low level API for interacting with a network +/// intended to be implemented for libp2p, the centralized server, +/// and memory network +#[async_trait] +pub trait ConnectedNetwork: + Clone + Send + Sync + 'static +{ + /// Pauses the underlying network + fn pause(&self); + + /// Resumes the underlying network + fn resume(&self); + + /// Blocks until the network is successfully initialized + async fn wait_for_ready(&self); + + /// checks if the network is ready + /// nonblocking + async fn is_ready(&self) -> bool; + + /// Blocks until the network is shut down + /// then returns true + fn shut_down<'a, 'b>(&'a self) -> BoxSyncFuture<'b, ()> + where + 'a: 'b, + Self: 'b; + + /// broadcast message to some subset of nodes + /// blocking + async fn broadcast_message( + &self, + message: M, + recipients: BTreeSet, + bind_version: VER, + ) -> Result<(), NetworkError>; + + /// broadcast a message only to a DA committee + /// blocking + async fn da_broadcast_message( + &self, + message: M, + recipients: BTreeSet, + bind_version: VER, + ) -> Result<(), NetworkError>; + + /// Sends a direct message to a specific node + /// blocking + async fn direct_message( + &self, + message: M, + recipient: K, + bind_version: VER, + ) -> Result<(), NetworkError>; + + /// Receive one or many messages from the underlying network. + /// + /// # Errors + /// If there is a network-related failure. + async fn recv_msgs(&self) -> Result, NetworkError>; + + /// Ask request the network for some data. Returns the request ID for that data, + /// The ID returned can be used for cancelling the request + async fn request_data( + &self, + _request: M, + _recipient: K, + _bind_version: VER, + ) -> Result, NetworkError> { + Err(NetworkError::UnimplementedFeature) + } + + /// Spawn a request task in the given network layer. If it supports + /// Request and responses it will return the receiving end of a channel. + /// Requests the network receives will be sent over this channel along + /// with a return channel to send the response back to. + /// + /// Returns `None`` if network does not support handling requests + async fn spawn_request_receiver_task( + &self, + _bind_version: VER, + ) -> Option)>> { + None + } + + /// queues lookup of a node + async fn queue_node_lookup( + &self, + _view_number: ViewNumber, + _pk: K, + ) -> Result<(), UnboundedSendError>> { + Ok(()) + } + + /// Injects consensus data such as view number into the networking implementation + /// blocking + /// Ideally we would pass in the `Time` type, but that requires making the entire trait generic over NodeType + async fn inject_consensus_info(&self, _event: ConsensusIntentEvent) {} + + /// handles view update + fn update_view(&self, _view: u64) {} +} + +/// Describes additional functionality needed by the test network implementation +pub trait TestableNetworkingImplementation +where + Self: Sized, +{ + /// generates a network given an expected node count + #[allow(clippy::type_complexity)] + fn generator( + expected_node_count: usize, + num_bootstrap: usize, + network_id: usize, + da_committee_size: usize, + is_da: bool, + reliability_config: Option>, + secondary_network_delay: Duration, + ) -> Box (Arc, Arc) + 'static>; + + /// Get the number of messages in-flight. + /// + /// Some implementations will not be able to tell how many messages there are in-flight. These implementations should return `None`. + fn in_flight_message_count(&self) -> Option; +} + +/// Changes that can occur in the network +#[derive(Debug)] +pub enum NetworkChange { + /// A node is connected + NodeConnected(P), + + /// A node is disconnected + NodeDisconnected(P), +} + +/// interface describing how reliable the network is +#[async_trait] +pub trait NetworkReliability: Debug + Sync + std::marker::Send + DynClone + 'static { + /// Sample from bernoulli distribution to decide whether + /// or not to keep a packet + /// # Panics + /// + /// Panics if `self.keep_numerator > self.keep_denominator` + /// + fn sample_keep(&self) -> bool { + true + } + + /// sample from uniform distribution to decide whether + /// or not to keep a packet + fn sample_delay(&self) -> Duration { + std::time::Duration::ZERO + } + + /// scramble the packet + fn scramble(&self, msg: Vec) -> Vec { + msg + } + + /// number of times to repeat the packet + fn sample_repeat(&self) -> usize { + 1 + } + + /// given a message and a way to send the message, + /// decide whether or not to send the message + /// how long to delay the message + /// whether or not to send duplicates + /// and whether or not to include noise with the message + /// then send the message + /// note: usually self is stored in a rwlock + /// so instead of doing the sending part, we just fiddle with the message + /// then return a future that does the sending and delaying + fn chaos_send_msg( + &self, + msg: Vec, + send_fn: Arc) -> BoxSyncFuture<'static, ()>>, + ) -> BoxSyncFuture<'static, ()> { + let sample_keep = self.sample_keep(); + let delay = self.sample_delay(); + let repeats = self.sample_repeat(); + let mut msgs = Vec::new(); + for _idx in 0..repeats { + let scrambled = self.scramble(msg.clone()); + msgs.push(scrambled); + } + let closure = async move { + if sample_keep { + async_sleep(delay).await; + for msg in msgs { + send_fn(msg).await; + } + } + }; + Box::pin(closure) + } +} + +// hack to get clone +dyn_clone::clone_trait_object!(NetworkReliability); + +/// ideal network +#[derive(Clone, Copy, Debug, Default)] +pub struct PerfectNetwork {} + +impl NetworkReliability for PerfectNetwork {} + +/// A synchronous network. Packets may be delayed, but are guaranteed +/// to arrive within `timeout` ns +#[derive(Clone, Copy, Debug, Default)] +pub struct SynchronousNetwork { + /// Max value in milliseconds that a packet may be delayed + pub delay_high_ms: u64, + /// Lowest value in milliseconds that a packet may be delayed + pub delay_low_ms: u64, +} + +impl NetworkReliability for SynchronousNetwork { + /// never drop a packet + fn sample_keep(&self) -> bool { + true + } + fn sample_delay(&self) -> Duration { + Duration::from_millis( + Uniform::new_inclusive(self.delay_low_ms, self.delay_high_ms) + .sample(&mut rand::thread_rng()), + ) + } +} + +/// An asynchronous network. Packets may be dropped entirely +/// or delayed for arbitrarily long periods +/// probability that packet is kept = `keep_numerator` / `keep_denominator` +/// packet delay is obtained by sampling from a uniform distribution +/// between `delay_low_ms` and `delay_high_ms`, inclusive +#[derive(Debug, Clone, Copy)] +pub struct AsynchronousNetwork { + /// numerator for probability of keeping packets + pub keep_numerator: u32, + /// denominator for probability of keeping packets + pub keep_denominator: u32, + /// lowest value in milliseconds that a packet may be delayed + pub delay_low_ms: u64, + /// highest value in milliseconds that a packet may be delayed + pub delay_high_ms: u64, +} + +impl NetworkReliability for AsynchronousNetwork { + fn sample_keep(&self) -> bool { + Bernoulli::from_ratio(self.keep_numerator, self.keep_denominator) + .unwrap() + .sample(&mut rand::thread_rng()) + } + fn sample_delay(&self) -> Duration { + Duration::from_millis( + Uniform::new_inclusive(self.delay_low_ms, self.delay_high_ms) + .sample(&mut rand::thread_rng()), + ) + } +} + +/// An partially synchronous network. Behaves asynchronously +/// until some arbitrary time bound, GST, +/// then synchronously after GST +#[allow(clippy::similar_names)] +#[derive(Debug, Clone, Copy)] +pub struct PartiallySynchronousNetwork { + /// asynchronous portion of network + pub asynchronous: AsynchronousNetwork, + /// synchronous portion of network + pub synchronous: SynchronousNetwork, + /// time when GST occurs + pub gst: std::time::Duration, + /// when the network was started + pub start: std::time::Instant, +} + +impl NetworkReliability for PartiallySynchronousNetwork { + /// never drop a packet + fn sample_keep(&self) -> bool { + true + } + fn sample_delay(&self) -> Duration { + // act asyncronous before gst + if self.start.elapsed() < self.gst { + if self.asynchronous.sample_keep() { + self.asynchronous.sample_delay() + } else { + // assume packet was "dropped" and will arrive after gst + self.synchronous.sample_delay() + self.gst + } + } else { + // act syncronous after gst + self.synchronous.sample_delay() + } + } +} + +impl Default for AsynchronousNetwork { + // disable all chance of failure + fn default() -> Self { + AsynchronousNetwork { + keep_numerator: 1, + keep_denominator: 1, + delay_low_ms: 0, + delay_high_ms: 0, + } + } +} + +impl Default for PartiallySynchronousNetwork { + fn default() -> Self { + PartiallySynchronousNetwork { + synchronous: SynchronousNetwork::default(), + asynchronous: AsynchronousNetwork::default(), + gst: std::time::Duration::new(0, 0), + start: std::time::Instant::now(), + } + } +} + +impl SynchronousNetwork { + /// create new `SynchronousNetwork` + #[must_use] + pub fn new(timeout: u64, delay_low_ms: u64) -> Self { + SynchronousNetwork { + delay_high_ms: timeout, + delay_low_ms, + } + } +} + +impl AsynchronousNetwork { + /// create new `AsynchronousNetwork` + #[must_use] + pub fn new( + keep_numerator: u32, + keep_denominator: u32, + delay_low_ms: u64, + delay_high_ms: u64, + ) -> Self { + AsynchronousNetwork { + keep_numerator, + keep_denominator, + delay_low_ms, + delay_high_ms, + } + } +} + +impl PartiallySynchronousNetwork { + /// create new `PartiallySynchronousNetwork` + #[allow(clippy::similar_names)] + #[must_use] + pub fn new( + asynchronous: AsynchronousNetwork, + synchronous: SynchronousNetwork, + gst: std::time::Duration, + ) -> Self { + PartiallySynchronousNetwork { + asynchronous, + synchronous, + gst, + start: std::time::Instant::now(), + } + } +} + +/// A chaotic network using all the networking calls +#[derive(Debug, Clone)] +pub struct ChaosNetwork { + /// numerator for probability of keeping packets + pub keep_numerator: u32, + /// denominator for probability of keeping packets + pub keep_denominator: u32, + /// lowest value in milliseconds that a packet may be delayed + pub delay_low_ms: u64, + /// highest value in milliseconds that a packet may be delayed + pub delay_high_ms: u64, + /// lowest value of repeats for a message + pub repeat_low: usize, + /// highest value of repeats for a message + pub repeat_high: usize, +} + +impl NetworkReliability for ChaosNetwork { + fn sample_keep(&self) -> bool { + Bernoulli::from_ratio(self.keep_numerator, self.keep_denominator) + .unwrap() + .sample(&mut rand::thread_rng()) + } + + fn sample_delay(&self) -> Duration { + Duration::from_millis( + Uniform::new_inclusive(self.delay_low_ms, self.delay_high_ms) + .sample(&mut rand::thread_rng()), + ) + } + + fn sample_repeat(&self) -> usize { + Uniform::new_inclusive(self.repeat_low, self.repeat_high).sample(&mut rand::thread_rng()) + } +} diff --git a/crates/types/src/traits/node_implementation.rs b/crates/types/src/traits/node_implementation.rs new file mode 100644 index 0000000000..cb2c98c403 --- /dev/null +++ b/crates/types/src/traits/node_implementation.rs @@ -0,0 +1,238 @@ +//! Composite trait for node behavior +//! +//! This module defines the [`NodeImplementation`] trait, which is a composite trait used for +//! describing the overall behavior of a node, as a composition of implementations of the node trait. + +use super::{ + block_contents::{BlockHeader, TestableBlock, Transaction}, + election::ElectionConfig, + network::{ConnectedNetwork, NetworkReliability, TestableNetworkingImplementation}, + states::TestableState, + storage::Storage, + ValidatedState, +}; +use crate::{ + data::{Leaf, TestableLeaf}, + message::Message, + traits::{ + election::Membership, signature_key::SignatureKey, states::InstanceState, BlockPayload, + }, +}; +use async_trait::async_trait; +use commit::Committable; +use serde::{Deserialize, Serialize}; +use std::{ + fmt::Debug, + hash::Hash, + ops, + ops::{Deref, Sub}, + sync::Arc, + time::Duration, +}; + +/// Node implementation aggregate trait +/// +/// This trait exists to collect multiple behavior implementations into one type, to allow +/// `HotShot` to avoid annoying numbers of type arguments and type patching. +/// +/// It is recommended you implement this trait on a zero sized type, as `HotShot`does not actually +/// store or keep a reference to any value implementing this trait. + +pub trait NodeImplementation: + Send + Sync + Clone + Eq + Hash + 'static + Serialize + for<'de> Deserialize<'de> +{ + /// Network for all nodes + type QuorumNetwork: ConnectedNetwork, TYPES::SignatureKey>; + + /// Network for those in the DA committee + type CommitteeNetwork: ConnectedNetwork, TYPES::SignatureKey>; + + /// Storage for DA layer interactions + type Storage: Storage; +} + +/// extra functions required on a node implementation to be usable by hotshot-testing +#[allow(clippy::type_complexity)] +#[async_trait] +pub trait TestableNodeImplementation: NodeImplementation { + /// Election config for the DA committee + type CommitteeElectionConfig; + + /// Generates a committee-specific election + fn committee_election_config_generator( + ) -> Box Self::CommitteeElectionConfig + 'static>; + + /// Creates random transaction if possible + /// otherwise panics + /// `padding` is the bytes of padding to add to the transaction + fn state_create_random_transaction( + state: Option<&TYPES::ValidatedState>, + rng: &mut dyn rand::RngCore, + padding: u64, + ) -> ::Transaction; + + /// Creates random transaction if possible + /// otherwise panics + /// `padding` is the bytes of padding to add to the transaction + fn leaf_create_random_transaction( + leaf: &Leaf, + rng: &mut dyn rand::RngCore, + padding: u64, + ) -> ::Transaction; + + /// generate a genesis block + fn block_genesis() -> TYPES::BlockPayload; + + /// the number of transactions in a block + fn txn_count(block: &TYPES::BlockPayload) -> u64; + + /// Generate the communication channels for testing + fn gen_networks( + expected_node_count: usize, + num_bootstrap: usize, + da_committee_size: usize, + reliability_config: Option>, + secondary_network_delay: Duration, + ) -> Box (Arc, Arc)>; +} + +#[async_trait] +impl> TestableNodeImplementation for I +where + TYPES::ValidatedState: TestableState, + TYPES::BlockPayload: TestableBlock, + I::QuorumNetwork: TestableNetworkingImplementation, + I::CommitteeNetwork: TestableNetworkingImplementation, +{ + type CommitteeElectionConfig = TYPES::ElectionConfigType; + + fn committee_election_config_generator( + ) -> Box Self::CommitteeElectionConfig + 'static> { + Box::new(|num_nodes_with_stake, num_nodes_without_stake| { + ::Membership::default_election_config( + num_nodes_with_stake, + num_nodes_without_stake, + ) + }) + } + + fn state_create_random_transaction( + state: Option<&TYPES::ValidatedState>, + rng: &mut dyn rand::RngCore, + padding: u64, + ) -> ::Transaction { + >::create_random_transaction( + state, rng, padding, + ) + } + + fn leaf_create_random_transaction( + leaf: &Leaf, + rng: &mut dyn rand::RngCore, + padding: u64, + ) -> ::Transaction { + Leaf::create_random_transaction(leaf, rng, padding) + } + + fn block_genesis() -> TYPES::BlockPayload { + ::genesis() + } + + fn txn_count(block: &TYPES::BlockPayload) -> u64 { + ::txn_count(block) + } + + fn gen_networks( + expected_node_count: usize, + num_bootstrap: usize, + da_committee_size: usize, + reliability_config: Option>, + secondary_network_delay: Duration, + ) -> Box (Arc, Arc)> { + >::generator( + expected_node_count, + num_bootstrap, + 0, + da_committee_size, + false, + reliability_config.clone(), + secondary_network_delay, + ) + } +} + +/// Trait for time compatibility needed for reward collection +pub trait ConsensusTime: + PartialOrd + + Ord + + Send + + Sync + + Debug + + Clone + + Copy + + Hash + + Deref + + serde::Serialize + + for<'de> serde::Deserialize<'de> + + ops::AddAssign + + ops::Add + + Sub + + 'static + + Committable +{ + /// Create a new instance of this time unit at time number 0 + #[must_use] + fn genesis() -> Self { + Self::new(0) + } + /// Create a new instance of this time unit + fn new(val: u64) -> Self; + /// Get the u64 format of time + fn get_u64(&self) -> u64; +} + +/// Trait with all the type definitions that are used in the current hotshot setup. +pub trait NodeType: + Clone + + Copy + + Debug + + Hash + + PartialEq + + Eq + + PartialOrd + + Ord + + Default + + serde::Serialize + + for<'de> Deserialize<'de> + + Send + + Sync + + 'static +{ + /// The time type that this hotshot setup is using. + /// + /// This should be the same `Time` that `ValidatedState::Time` is using. + type Time: ConsensusTime; + /// The block header type that this hotshot setup is using. + type BlockHeader: BlockHeader; + /// The block type that this hotshot setup is using. + /// + /// This should be the same block that `ValidatedState::BlockPayload` is using. + type BlockPayload: BlockPayload; + /// The signature key that this hotshot setup is using. + type SignatureKey: SignatureKey; + /// The transaction type that this hotshot setup is using. + /// + /// This should be equal to `BlockPayload::Transaction` + type Transaction: Transaction; + /// The election config type that this hotshot setup is using. + type ElectionConfigType: ElectionConfig; + + /// The instance-level state type that this hotshot setup is using. + type InstanceState: InstanceState; + + /// The validated state type that this hotshot setup is using. + type ValidatedState: ValidatedState; + + /// Membership used for this implementation + type Membership: Membership; +} diff --git a/crates/types/src/traits/qc.rs b/crates/types/src/traits/qc.rs new file mode 100644 index 0000000000..7dd11010f9 --- /dev/null +++ b/crates/types/src/traits/qc.rs @@ -0,0 +1,95 @@ +//! The quorum certificate (QC) trait is a certificate of a sufficient quorum of distinct +//! parties voted for a message or statement. + +use ark_std::{ + rand::{CryptoRng, RngCore}, + vec::Vec, +}; +use bitvec::prelude::*; +use generic_array::{ArrayLength, GenericArray}; +use jf_primitives::{errors::PrimitivesError, signatures::AggregateableSignatureSchemes}; +use serde::{Deserialize, Serialize}; + +/// Trait for validating a QC built from different signatures on the same message +pub trait QuorumCertificateScheme< + A: AggregateableSignatureSchemes + Serialize + for<'a> Deserialize<'a>, +> +{ + /// Public parameters for generating the QC + /// E.g: snark proving/verifying keys, list of (or pointer to) public keys stored in the smart contract. + type QCProverParams: Serialize + for<'a> Deserialize<'a>; + + /// Public parameters for validating the QC + /// E.g: verifying keys, stake table commitment + type QCVerifierParams: Serialize + for<'a> Deserialize<'a>; + + /// Allows to fix the size of the message at compilation time. + type MessageLength: ArrayLength; + + /// Type of the actual quorum certificate object + type QC; + + /// Type of the quorum size (e.g. number of votes or accumulated weight of signatures) + type QuorumSize; + + /// Produces a partial signature on a message with a single user signing key + /// NOTE: the original message (vote) should be prefixed with the hash of the stake table. + /// * `agg_sig_pp` - public parameters for aggregate signature + /// * `message` - message to be signed + /// * `sk` - user signing key + /// * `returns` - a "simple" signature + /// + /// # Errors + /// + /// Should return error if the underlying signature scheme fail to sign. + fn sign>( + pp: &A::PublicParameter, + sk: &A::SigningKey, + msg: M, + prng: &mut R, + ) -> Result { + A::sign(pp, sk, msg, prng) + } + + /// Computes an aggregated signature from a set of partial signatures and the verification keys involved + /// * `qc_pp` - public parameters for generating the QC + /// * `signers` - a bool vector indicating the list of verification keys corresponding to the set of partial signatures + /// * `sigs` - partial signatures on the same message + /// + /// # Errors + /// + /// Will return error if some of the partial signatures provided are invalid or the number of + /// partial signatures / verifications keys are different. + fn assemble( + qc_pp: &Self::QCProverParams, + signers: &BitSlice, + sigs: &[A::Signature], + ) -> Result; + + /// Checks an aggregated signature over some message provided as input + /// * `qc_vp` - public parameters for validating the QC + /// * `message` - message to check the aggregated signature against + /// * `qc` - quroum certificate + /// * `returns` - the quorum size if the qc is valid, an error otherwise. + /// + /// # Errors + /// + /// Return error if the QC is invalid, either because accumulated weight didn't exceed threshold, + /// or some partial signatures are invalid. + fn check( + qc_vp: &Self::QCVerifierParams, + message: &GenericArray, + qc: &Self::QC, + ) -> Result; + + /// Trace the list of signers given a qc. + /// + /// # Errors + /// + /// Return error if the inputs mismatch (e.g. wrong verifier parameter or original message). + fn trace( + qc_vp: &Self::QCVerifierParams, + message: &GenericArray, + qc: &Self::QC, + ) -> Result, PrimitivesError>; +} diff --git a/crates/types/src/traits/signature_key.rs b/crates/types/src/traits/signature_key.rs new file mode 100644 index 0000000000..3b36e0ed31 --- /dev/null +++ b/crates/types/src/traits/signature_key.rs @@ -0,0 +1,140 @@ +//! Minimal compatibility over public key signatures +use bitvec::prelude::*; +use ethereum_types::U256; +use jf_primitives::errors::PrimitivesError; +use serde::{Deserialize, Serialize}; +use std::{ + fmt::{Debug, Display}, + hash::Hash, +}; +use tagged_base64::TaggedBase64; + +/// Type representing stake table entries in a `StakeTable` +pub trait StakeTableEntryType { + /// Get the stake value + fn get_stake(&self) -> U256; +} + +/// Trait for abstracting public key signatures +/// Self is the public key type +pub trait SignatureKey: + Send + + Sync + + Clone + + Sized + + Debug + + Hash + + Serialize + + for<'a> Deserialize<'a> + + PartialEq + + Eq + + PartialOrd + + Ord + + Display + + for<'a> TryFrom<&'a TaggedBase64> + + Into +{ + /// The private key type for this signature algorithm + type PrivateKey: Send + + Sync + + Sized + + Clone + + Debug + + Eq + + Serialize + + for<'a> Deserialize<'a> + + Hash; + /// The type of the entry that contain both public key and stake value + type StakeTableEntry: StakeTableEntryType + + Send + + Sync + + Sized + + Clone + + Debug + + Hash + + Eq + + Serialize + + for<'a> Deserialize<'a>; + /// The type of the quorum certificate parameters used for assembled signature + type QCParams: Send + Sync + Sized + Clone + Debug + Hash; + /// The type of the assembled signature, without `BitVec` + type PureAssembledSignatureType: Send + + Sync + + Sized + + Clone + + Debug + + Hash + + PartialEq + + Eq + + Serialize + + for<'a> Deserialize<'a>; + /// The type of the assembled qc: assembled signature + `BitVec` + type QCType: Send + + Sync + + Sized + + Clone + + Debug + + Hash + + PartialEq + + Eq + + Serialize + + for<'a> Deserialize<'a>; + + /// Type of error that can occur when signing data + type SignError: std::error::Error + Send + Sync; + + // Signature type represented as a vec/slice of bytes to let the implementer handle the nuances + // of serialization, to avoid Cryptographic pitfalls + /// Validate a signature + fn validate(&self, signature: &Self::PureAssembledSignatureType, data: &[u8]) -> bool; + + /// Produce a signature + /// # Errors + /// If unable to sign the data with the key + fn sign( + private_key: &Self::PrivateKey, + data: &[u8], + ) -> Result; + + /// Produce a public key from a private key + fn from_private(private_key: &Self::PrivateKey) -> Self; + /// Serialize a public key to bytes + fn to_bytes(&self) -> Vec; + /// Deserialize a public key from bytes + /// # Errors + /// + /// Will return `Err` if deserialization fails + fn from_bytes(bytes: &[u8]) -> Result; + + /// Generate a new key pair + fn generated_from_seed_indexed(seed: [u8; 32], index: u64) -> (Self, Self::PrivateKey); + + /// get the stake table entry from the public key and stake value + fn get_stake_table_entry(&self, stake: u64) -> Self::StakeTableEntry; + + /// only get the public key from the stake table entry + fn get_public_key(entry: &Self::StakeTableEntry) -> Self; + + /// get the public parameter for the assembled signature checking + fn get_public_parameter( + stake_entries: Vec, + threshold: U256, + ) -> Self::QCParams; + + /// check the quorum certificate for the assembled signature + fn check(real_qc_pp: &Self::QCParams, data: &[u8], qc: &Self::QCType) -> bool; + + /// get the assembled signature and the `BitVec` separately from the assembled signature + fn get_sig_proof(signature: &Self::QCType) -> (Self::PureAssembledSignatureType, BitVec); + + /// assemble the signature from the partial signature and the indication of signers in `BitVec` + fn assemble( + real_qc_pp: &Self::QCParams, + signers: &BitSlice, + sigs: &[Self::PureAssembledSignatureType], + ) -> Self::QCType; + + /// generates the genesis public key. Meant to be dummy/filler + #[must_use] + fn genesis_proposer_pk() -> Self; +} diff --git a/crates/types/src/traits/stake_table.rs b/crates/types/src/traits/stake_table.rs new file mode 100644 index 0000000000..598a662650 --- /dev/null +++ b/crates/types/src/traits/stake_table.rs @@ -0,0 +1,235 @@ +//! Trait for stake table data structures + +use ark_std::{rand::SeedableRng, string::ToString, vec::Vec}; +use digest::crypto_common::rand_core::CryptoRngCore; +use displaydoc::Display; +use jf_plonk::errors::PlonkError; +use jf_primitives::errors::PrimitivesError; + +/// Snapshots of the stake table +pub enum SnapshotVersion { + /// the latest "Head" where all new changes are applied to + Head, + /// marks the snapshot at the beginning of the current epoch + EpochStart, + /// marks the beginning of the last epoch + LastEpochStart, + /// at arbitrary block height + BlockNum(u64), +} + +/// Common interfaces required for a stake table used in `HotShot` System. +/// APIs that doesn't take `version: SnapshotVersion` as an input by default works on the head/latest version. +pub trait StakeTableScheme { + /// type for stake key + type Key: Clone; + /// type for the staked amount + type Amount: Clone + Copy; + /// type for the commitment to the current stake table + type Commitment; + /// type for the proof associated with the lookup result (if any) + type LookupProof; + /// type for the iterator over (key, value) entries + type IntoIter: Iterator; + /// Auxiliary information associated with the key + type Aux: Clone; + + /// Register a new key into the stake table. + /// + /// # Errors + /// + /// Return err if key is already registered. + fn register( + &mut self, + new_key: Self::Key, + amount: Self::Amount, + aux: Self::Aux, + ) -> Result<(), StakeTableError>; + + /// Batch register a list of new keys. A default implementation is provided + /// w/o batch optimization. + /// + /// # Errors + /// + /// Return err if any of `new_keys` fails to register. + fn batch_register( + &mut self, + new_keys: I, + amounts: J, + auxs: K, + ) -> Result<(), StakeTableError> + where + I: IntoIterator, + J: IntoIterator, + K: IntoIterator, + { + let _ = new_keys + .into_iter() + .zip(amounts) + .zip(auxs) + .try_for_each(|((key, amount), aux)| Self::register(self, key, amount, aux)); + Ok(()) + } + + /// Deregister an existing key from the stake table. + /// Returns error if some keys are not found. + /// + /// # Errors + /// Return err if `existing_key` wasn't registered. + fn deregister(&mut self, existing_key: &Self::Key) -> Result<(), StakeTableError>; + + /// Batch deregister a list of keys. A default implementation is provided + /// w/o batch optimization. + /// + /// # Errors + /// Return err if any of `existing_keys` fail to deregister. + fn batch_deregister<'a, I>(&mut self, existing_keys: I) -> Result<(), StakeTableError> + where + I: IntoIterator::Key>, + ::Key: 'a, + { + let _ = existing_keys + .into_iter() + .try_for_each(|key| Self::deregister(self, key)); + Ok(()) + } + + /// Returns the commitment to the `version` of stake table. + /// + /// # Errors + /// Return err if the `version` is not supported. + fn commitment(&self, version: SnapshotVersion) -> Result; + + /// Returns the accumulated stakes of all registered keys of the `version` + /// of stake table. + /// + /// # Errors + /// Return err if the `version` is not supported. + fn total_stake(&self, version: SnapshotVersion) -> Result; + + /// Returns the number of keys in the `version` of the table. + /// + /// # Errors + /// Return err if the `version` is not supported. + fn len(&self, version: SnapshotVersion) -> Result; + + /// Returns true if `key` is currently registered, else returns false. + fn contains_key(&self, key: &Self::Key) -> bool; + + /// Returns the stakes withhelded by a public key. + /// + /// # Errors + /// Return err if the `version` is not supported or `key` doesn't exist. + fn lookup( + &self, + version: SnapshotVersion, + key: &Self::Key, + ) -> Result; + + /// Returns the stakes withhelded by a public key along with a membership proof. + /// + /// # Errors + /// Return err if the `version` is not supported or `key` doesn't exist. + fn lookup_with_proof( + &self, + version: SnapshotVersion, + key: &Self::Key, + ) -> Result<(Self::Amount, Self::LookupProof), StakeTableError>; + + /// Return the associated stake amount and auxiliary information of a public key, + /// along with a membership proof. + /// + /// # Errors + /// Return err if the `version` is not supported or `key` doesn't exist. + #[allow(clippy::type_complexity)] + fn lookup_with_aux_and_proof( + &self, + version: SnapshotVersion, + key: &Self::Key, + ) -> Result<(Self::Amount, Self::Aux, Self::LookupProof), StakeTableError>; + + /// Update the stake of the `key` with `(negative ? -1 : 1) * delta`. + /// Return the updated stake or error. + /// + /// # Errors + /// Return err if the `key` doesn't exist of if the update overflow/underflow. + fn update( + &mut self, + key: &Self::Key, + delta: Self::Amount, + negative: bool, + ) -> Result; + + /// Batch update the stake balance of `keys`. Read documentation about + /// [`Self::update()`]. By default, we call `Self::update()` on each + /// (key, amount, negative) tuple. + /// + /// # Errors + /// Return err if any one of the `update` failed. + fn batch_update( + &mut self, + keys: &[Self::Key], + amounts: &[Self::Amount], + negative_flags: Vec, + ) -> Result, StakeTableError> { + let updated_amounts = keys + .iter() + .zip(amounts.iter()) + .zip(negative_flags.iter()) + .map(|((key, &amount), negative)| Self::update(self, key, amount, *negative)) + .collect::, _>>()?; + + Ok(updated_amounts) + } + + /// Randomly sample a (key, stake amount) pair proportional to the stake distributions, + /// given a fixed seed for `rng`, this sampling should be deterministic. + fn sample( + &self, + rng: &mut (impl SeedableRng + CryptoRngCore), + ) -> Option<(&Self::Key, &Self::Amount)>; + + /// Returns an iterator over all (key, value) entries of the `version` of the table + /// + /// # Errors + /// Return err if the `version` is not supported. + fn try_iter(&self, version: SnapshotVersion) -> Result; +} + +/// Error type for [`StakeTableScheme`] +#[derive(Debug, Display)] +pub enum StakeTableError { + /// Internal error caused by Rescue + RescueError, + /// Key mismatched + MismatchedKey, + /// Key not found + KeyNotFound, + /// Key already exists + ExistingKey, + /// Malformed Merkle proof + MalformedProof, + /// Verification Error + VerificationError, + /// Insufficient fund: the number of stake cannot be negative + InsufficientFund, + /// The number of stake exceed U256 + StakeOverflow, + /// The historical snapshot requested is not supported. + SnapshotUnsupported, +} + +impl ark_std::error::Error for StakeTableError {} + +impl From for PrimitivesError { + fn from(value: StakeTableError) -> Self { + // FIXME: (alex) should we define a PrimitivesError::General()? + Self::ParameterError(value.to_string()) + } +} + +impl From for PlonkError { + fn from(value: StakeTableError) -> Self { + Self::PrimitiveError(PrimitivesError::ParameterError(value.to_string())) + } +} diff --git a/crates/types/src/traits/states.rs b/crates/types/src/traits/states.rs new file mode 100644 index 0000000000..2fb851ba0d --- /dev/null +++ b/crates/types/src/traits/states.rs @@ -0,0 +1,89 @@ +//! Abstractions over the immutable instance-level state and hte global state that blocks modify. +//! +//! This module provides the [`InstanceState`] and [`ValidatedState`] traits, which serve as +//! compatibilities over the current network state, which is modified by the transactions contained +//! within blocks. + +use super::block_contents::TestableBlock; +use crate::{ + data::Leaf, + traits::{ + node_implementation::{ConsensusTime, NodeType}, + BlockPayload, + }, +}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use std::{error::Error, fmt::Debug, future::Future, hash::Hash}; + +/// Instance-level state, which allows us to fetch missing validated state. +pub trait InstanceState: Debug + Send + Sync {} + +/// Application-specific state delta, which will be used to store a list of merkle tree entries. +pub trait StateDelta: Debug + Send + Sync + Serialize + for<'a> Deserialize<'a> {} + +/// Abstraction over the state that blocks modify +/// +/// This trait represents the behaviors that the 'global' ledger state must have: +/// * A defined error type ([`Error`](ValidatedState::Error)) +/// * The type of block that modifies this type of state ([`BlockPayload`](`ValidatedStates:: +/// BlockPayload`)) +/// * The ability to validate that a block header is actually a valid extension of this state and +/// produce a new state, with the modifications from the block applied +/// ([`validate_and_apply_header`](`ValidatedState::validate_and_apply_header)) +pub trait ValidatedState: + Serialize + DeserializeOwned + Debug + Default + Hash + PartialEq + Eq + Send + Sync +{ + /// The error type for this particular type of ledger state + type Error: Error + Debug + Send + Sync; + /// The type of the instance-level state this state is assocaited with + type Instance: InstanceState; + /// The type of the state delta this state is assocaited with. + type Delta: StateDelta; + /// Time compatibility needed for reward collection + type Time: ConsensusTime; + + /// Check if the proposed block header is valid and apply it to the state if so. + /// + /// Returns the new state and state delta. + /// + /// # Arguments + /// * `instance` - Immutable instance-level state. + /// + /// # Errors + /// + /// If the block header is invalid or appending it would lead to an invalid state. + fn validate_and_apply_header( + &self, + instance: &Self::Instance, + parent_leaf: &Leaf, + proposed_header: &TYPES::BlockHeader, + ) -> impl Future> + Send; + + /// Construct the state with the given block header. + /// + /// This can also be used to rebuild the state for catchup. + fn from_header(block_header: &TYPES::BlockHeader) -> Self; + + /// Construct a genesis validated state. + #[must_use] + fn genesis(instance: &Self::Instance) -> (Self, Self::Delta); + + /// Gets called to notify the persistence backend that this state has been committed + fn on_commit(&self); +} + +/// extra functions required on state to be usable by hotshot-testing +pub trait TestableState: ValidatedState +where + TYPES: NodeType, + TYPES::BlockPayload: TestableBlock, +{ + /// Creates random transaction if possible + /// otherwise panics + /// `padding` is the bytes of padding to add to the transaction + fn create_random_transaction( + state: Option<&Self>, + rng: &mut dyn rand::RngCore, + padding: u64, + ) -> ::Transaction; +} diff --git a/crates/types/src/traits/storage.rs b/crates/types/src/traits/storage.rs new file mode 100644 index 0000000000..2dab00ee88 --- /dev/null +++ b/crates/types/src/traits/storage.rs @@ -0,0 +1,21 @@ +//! Abstract storage type for storing DA proposals and VID shares +//! +//! This modules provides the [`Storage`] trait. +//! + +use anyhow::Result; +use async_trait::async_trait; + +use crate::{ + data::{DAProposal, VidDisperse}, + message::Proposal, +}; + +use super::node_implementation::NodeType; + +/// Abstraction for storing a variety of consensus payload datum. +#[async_trait] +pub trait Storage: Send + Sync + Clone { + async fn append_vid(&self, proposal: &Proposal>) -> Result<()>; + async fn append_da(&self, proposal: &Proposal>) -> Result<()>; +} diff --git a/crates/types/src/utils.rs b/crates/types/src/utils.rs new file mode 100644 index 0000000000..9cce5bb31b --- /dev/null +++ b/crates/types/src/utils.rs @@ -0,0 +1,189 @@ +//! Utility functions, type aliases, helper structs and enum definitions. + +use crate::{ + data::Leaf, + traits::{node_implementation::NodeType, ValidatedState}, + vid::VidCommitment, +}; +use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; +use bincode::{ + config::{ + FixintEncoding, LittleEndian, RejectTrailing, WithOtherEndian, WithOtherIntEncoding, + WithOtherLimit, WithOtherTrailing, + }, + DefaultOptions, Options, +}; +use commit::Commitment; +use digest::OutputSizeUser; +use sha2::Digest; +use std::{ops::Deref, sync::Arc}; +use tagged_base64::tagged; +use typenum::Unsigned; + +/// A view's state +#[derive(Debug)] +pub enum ViewInner { + /// A pending view with an available block but not leaf proposal yet. + /// + /// Storing this state allows us to garbage collect blocks for views where a proposal is never + /// made. This saves memory when a leader fails and subverts a DoS attack where malicious + /// leaders repeatedly request availability for blocks that they never propose. + DA { + /// Payload commitment to the available block. + payload_commitment: VidCommitment, + }, + /// Undecided view + Leaf { + /// Proposed leaf + leaf: LeafCommitment, + /// Validated state. + state: Arc, + /// Optional state delta. + delta: Option>::Delta>>, + }, + /// Leaf has failed + Failed, +} + +/// The hash of a leaf. +type LeafCommitment = Commitment>; + +/// Optional validated state and state delta. +pub type StateAndDelta = ( + Option::ValidatedState>>, + Option::ValidatedState as ValidatedState>::Delta>>, +); + +impl ViewInner { + /// Return the underlying undecide leaf commitment and validated state if they exist. + #[must_use] + pub fn get_leaf_and_state( + &self, + ) -> Option<(LeafCommitment, &Arc)> { + if let Self::Leaf { leaf, state, .. } = self { + Some((*leaf, state)) + } else { + None + } + } + + /// return the underlying leaf hash if it exists + #[must_use] + pub fn get_leaf_commitment(&self) -> Option> { + if let Self::Leaf { leaf, .. } = self { + Some(*leaf) + } else { + None + } + } + + /// return the underlying validated state if it exists + #[must_use] + pub fn get_state(&self) -> Option<&Arc> { + if let Self::Leaf { state, .. } = self { + Some(state) + } else { + None + } + } + + /// Return the underlying validated state and state delta if they exist. + #[must_use] + pub fn get_state_and_delta(&self) -> StateAndDelta { + if let Self::Leaf { state, delta, .. } = self { + (Some(state.clone()), delta.clone()) + } else { + (None, None) + } + } + + /// return the underlying block paylod commitment if it exists + #[must_use] + pub fn get_payload_commitment(&self) -> Option { + if let Self::DA { payload_commitment } = self { + Some(*payload_commitment) + } else { + None + } + } +} + +impl Deref for View { + type Target = ViewInner; + + fn deref(&self) -> &Self::Target { + &self.view_inner + } +} + +/// This exists so we can perform state transitions mutably +#[derive(Debug)] +pub struct View { + /// The view data. Wrapped in a struct so we can mutate + pub view_inner: ViewInner, +} + +/// A struct containing information about a finished round. +#[derive(Debug, Clone)] +pub struct RoundFinishedEvent { + /// The round that finished + pub view_number: TYPES::Time, +} + +/// Whether or not to stop inclusively or exclusively when walking +#[derive(Copy, Clone, Debug)] +pub enum Terminator { + /// Stop right before this view number + Exclusive(T), + /// Stop including this view number + Inclusive(T), +} + +/// Type alias for byte array of SHA256 digest length +type Sha256Digest = [u8; ::OutputSize::USIZE]; + +#[tagged("BUILDER_COMMITMENT")] +#[derive(Clone, Debug, Hash, PartialEq, Eq, CanonicalSerialize, CanonicalDeserialize)] +/// Commitment that builders use to sign block options. +/// A thin wrapper around a Sha256 digest. +pub struct BuilderCommitment(Sha256Digest); + +impl BuilderCommitment { + /// Create new commitment for `data` + pub fn from_bytes(data: impl AsRef<[u8]>) -> Self { + Self(sha2::Sha256::digest(data.as_ref()).into()) + } + + /// Create a new commitment from a raw Sha256 digest + pub fn from_raw_digest(digest: impl Into) -> Self { + Self(digest.into()) + } +} + +impl AsRef for BuilderCommitment { + fn as_ref(&self) -> &Sha256Digest { + &self.0 + } +} + +/// For the wire format, we use bincode with the following options: +/// - No upper size limit +/// - Litte endian encoding +/// - Varint encoding +/// - Reject trailing bytes +#[allow(clippy::type_complexity)] +#[must_use] +#[allow(clippy::type_complexity)] +pub fn bincode_opts() -> WithOtherTrailing< + WithOtherIntEncoding< + WithOtherEndian, LittleEndian>, + FixintEncoding, + >, + RejectTrailing, +> { + bincode::DefaultOptions::new() + .with_no_limit() + .with_little_endian() + .with_fixint_encoding() + .reject_trailing_bytes() +} diff --git a/crates/types/src/vid.rs b/crates/types/src/vid.rs new file mode 100644 index 0000000000..e6d2c9a5f2 --- /dev/null +++ b/crates/types/src/vid.rs @@ -0,0 +1,277 @@ +//! This module provides: +//! - an opaque constructor [`vid_scheme`] that returns a new instance of a +//! VID scheme. +//! - type aliases [`VidCommitment`], [`VidCommon`], [`VidShare`] +//! for [`VidScheme`] assoc types. +//! +//! Purpose: the specific choice of VID scheme is an implementation detail. +//! This crate and all downstream crates should talk to the VID scheme only +//! via the traits exposed here. + +use ark_bn254::Bn254; +use jf_primitives::{ + pcs::{ + checked_fft_size, + prelude::{UnivariateKzgPCS, UnivariateUniversalParams}, + PolynomialCommitmentScheme, + }, + vid::{ + advz::{ + self, + payload_prover::{LargeRangeProof, SmallRangeProof}, + }, + payload_prover::{PayloadProver, Statement}, + precomputable::Precomputable, + VidDisperse, VidResult, VidScheme, + }, +}; +use lazy_static::lazy_static; +use serde::{Deserialize, Serialize}; +use sha2::Sha256; +use std::{fmt::Debug, ops::Range}; + +/// VID scheme constructor. +/// +/// Returns an opaque type that impls jellyfish traits: +/// [`VidScheme`], [`PayloadProver`], [`Precomputable`]. +/// +/// # Rust forbids naming impl Trait in return types +/// +/// Due to Rust limitations the return type of [`vid_scheme`] is a newtype +/// wrapper [`VidSchemeType`] that impls the above traits. +/// +/// We prefer that the return type of [`vid_scheme`] be `impl Trait` for the +/// above traits. But the ability to name an impl Trait return type is +/// currently missing from Rust: +/// - [Naming impl trait in return types - Impl trait initiative](https://rust-lang.github.io/impl-trait-initiative/explainer/rpit_names.html) +/// - [RFC: Type alias impl trait (TAIT)](https://github.com/rust-lang/rfcs/blob/master/text/2515-type_alias_impl_trait.md) +/// +/// # Panics +/// When the construction fails for the underlying VID scheme. +#[must_use] +pub fn vid_scheme(num_storage_nodes: usize) -> VidSchemeType { + // chunk_size is currently num_storage_nodes rounded down to a power of two + // TODO chunk_size should be a function of the desired erasure code rate + // https://github.com/EspressoSystems/HotShot/issues/2152 + let chunk_size = 1 << num_storage_nodes.ilog2(); + + // TODO intelligent choice of multiplicity + let multiplicity = 1; + + // TODO panic, return `Result`, or make `new` infallible upstream (eg. by panicking)? + #[allow(clippy::panic)] + VidSchemeType(Advz::new(chunk_size, num_storage_nodes, multiplicity, &*KZG_SRS).unwrap_or_else(|err| panic!("advz construction failure:\n\t(num_storage nodes,chunk_size,multiplicity)=({num_storage_nodes},{chunk_size},{multiplicity})\n\terror: : {err}"))) +} + +/// VID commitment type +pub type VidCommitment = ::Commit; +/// VID common type +pub type VidCommon = ::Common; +/// VID share type +pub type VidShare = ::Share; + +#[cfg(not(feature = "gpu-vid"))] +/// Internal Jellyfish VID scheme +type Advz = advz::Advz; +#[cfg(feature = "gpu-vid")] +/// Internal Jellyfish VID scheme +type Advz = advz::AdvzGPU<'static, E, H>; + +/// Newtype wrapper for a VID scheme type that impls +/// [`VidScheme`], [`PayloadProver`], [`Precomputable`]. +pub struct VidSchemeType(Advz); + +/// Newtype wrapper for a large payload range proof. +/// +/// Useful for namespace proofs. +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct LargeRangeProofType( + // # Type complexity + // + // Jellyfish's `LargeRangeProof` type has a prime field generic parameter `F`. + // This `F` is determined by the type parameter `E` for `Advz`. + // Jellyfish needs a more ergonomic way for downstream users to refer to this type. + // + // There is a `KzgEval` type alias in jellyfish that helps a little, but it's currently private: + // + // If it were public then we could instead use + // `LargeRangeProof>` + // but that's still pretty crufty. + LargeRangeProof< as PolynomialCommitmentScheme>::Evaluation>, +); + +/// Newtype wrapper for a small payload range proof. +/// +/// Useful for transaction proofs. +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct SmallRangeProofType( + // # Type complexity + // + // Similar to the comments in `LargeRangeProofType`. + SmallRangeProof< as PolynomialCommitmentScheme>::Proof>, +); + +lazy_static! { + /// SRS comment + /// + /// TODO use a proper SRS + /// https://github.com/EspressoSystems/HotShot/issues/1686 + static ref KZG_SRS: UnivariateUniversalParams = { + let mut rng = jf_utils::test_rng(); + UnivariateKzgPCS::::gen_srs_for_testing( + &mut rng, + // TODO what's the maximum possible SRS size? + checked_fft_size(200).unwrap(), + ) + .unwrap() + }; +} + +/// Private type alias for the EC pairing type parameter for [`Advz`]. +type E = Bn254; +/// Private type alias for the hash type parameter for [`Advz`]. +type H = Sha256; + +// THE REST OF THIS FILE IS BOILERPLATE +// +// All this boilerplate can be deleted when we finally get +// type alias impl trait (TAIT): +// [rfcs/text/2515-type_alias_impl_trait.md at master ยท rust-lang/rfcs](https://github.com/rust-lang/rfcs/blob/master/text/2515-type_alias_impl_trait.md) +impl VidScheme for VidSchemeType { + type Commit = ::Commit; + type Share = ::Share; + type Common = ::Common; + + fn commit_only(&mut self, payload: B) -> VidResult + where + B: AsRef<[u8]>, + { + self.0.commit_only(payload) + } + + fn disperse(&mut self, payload: B) -> VidResult> + where + B: AsRef<[u8]>, + { + self.0.disperse(payload).map(vid_disperse_conversion) + } + + fn verify_share( + &self, + share: &Self::Share, + common: &Self::Common, + commit: &Self::Commit, + ) -> VidResult> { + self.0.verify_share(share, common, commit) + } + + fn recover_payload(&self, shares: &[Self::Share], common: &Self::Common) -> VidResult> { + self.0.recover_payload(shares, common) + } + + fn is_consistent(commit: &Self::Commit, common: &Self::Common) -> VidResult<()> { + ::is_consistent(commit, common) + } + + fn get_payload_byte_len(common: &Self::Common) -> usize { + ::get_payload_byte_len(common) + } + + fn get_num_storage_nodes(common: &Self::Common) -> usize { + ::get_num_storage_nodes(common) + } + + fn get_multiplicity(common: &Self::Common) -> usize { + ::get_multiplicity(common) + } +} + +impl PayloadProver for VidSchemeType { + fn payload_proof(&self, payload: B, range: Range) -> VidResult + where + B: AsRef<[u8]>, + { + self.0 + .payload_proof(payload, range) + .map(LargeRangeProofType) + } + + fn payload_verify( + &self, + stmt: Statement<'_, Self>, + proof: &LargeRangeProofType, + ) -> VidResult> { + self.0.payload_verify(stmt_conversion(stmt), &proof.0) + } +} + +impl PayloadProver for VidSchemeType { + fn payload_proof(&self, payload: B, range: Range) -> VidResult + where + B: AsRef<[u8]>, + { + self.0 + .payload_proof(payload, range) + .map(SmallRangeProofType) + } + + fn payload_verify( + &self, + stmt: Statement<'_, Self>, + proof: &SmallRangeProofType, + ) -> VidResult> { + self.0.payload_verify(stmt_conversion(stmt), &proof.0) + } +} + +impl Precomputable for VidSchemeType { + type PrecomputeData = ::PrecomputeData; + + fn commit_only_precompute( + &self, + payload: B, + ) -> VidResult<(Self::Commit, Self::PrecomputeData)> + where + B: AsRef<[u8]>, + { + self.0.commit_only_precompute(payload) + } + + fn disperse_precompute( + &self, + payload: B, + data: &Self::PrecomputeData, + ) -> VidResult> + where + B: AsRef<[u8]>, + { + self.0 + .disperse_precompute(payload, data) + .map(vid_disperse_conversion) + } +} + +/// Convert a [`VidDisperse`] to a [`VidDisperse`]. +/// +/// Foreign type rules prevent us from doing: +/// - `impl From> for VidDisperse` +/// - `impl VidDisperse {...}` +/// and similarly for `Statement`. +/// Thus, we accomplish type conversion via functions. +fn vid_disperse_conversion(vid_disperse: VidDisperse) -> VidDisperse { + VidDisperse { + shares: vid_disperse.shares, + common: vid_disperse.common, + commit: vid_disperse.commit, + } +} + +/// Convert a [`Statement<'_, VidSchemeType>`] to a [`Statement<'_, Advz>`]. +fn stmt_conversion(stmt: Statement<'_, VidSchemeType>) -> Statement<'_, Advz> { + Statement { + payload_subslice: stmt.payload_subslice, + range: stmt.range, + commit: stmt.commit, + common: stmt.common, + } +} diff --git a/crates/types/src/vote.rs b/crates/types/src/vote.rs new file mode 100644 index 0000000000..ba49f4732d --- /dev/null +++ b/crates/types/src/vote.rs @@ -0,0 +1,183 @@ +//! Vote, Accumulator, and Certificate Types + +use std::{ + collections::{BTreeMap, HashMap}, + marker::PhantomData, +}; + +use bitvec::{bitvec, vec::BitVec}; +use commit::Commitment; +use either::Either; +use ethereum_types::U256; +use tracing::error; + +use crate::{ + simple_certificate::Threshold, + simple_vote::Voteable, + traits::{ + election::Membership, + node_implementation::NodeType, + signature_key::{SignatureKey, StakeTableEntryType}, + }, +}; + +/// A simple vote that has a signer and commitment to the data voted on. +pub trait Vote: HasViewNumber { + /// Type of data commitment this vote uses. + type Commitment: Voteable; + + /// Get the signature of the vote sender + fn get_signature(&self) -> ::PureAssembledSignatureType; + /// Gets the data which was voted on by this vote + fn get_data(&self) -> &Self::Commitment; + /// Gets the Data commitment of the vote + fn get_data_commitment(&self) -> Commitment; + + /// Gets the public signature key of the votes creator/sender + fn get_signing_key(&self) -> TYPES::SignatureKey; +} + +/// Any type that is associated with a view +pub trait HasViewNumber { + /// Returns the view number the type refers to. + fn get_view_number(&self) -> TYPES::Time; +} + +/** +The certificate formed from the collection of signatures a committee. +The committee is defined by the `Membership` associated type. +The votes all must be over the `Commitment` associated type. +*/ +pub trait Certificate: HasViewNumber { + /// The data commitment this certificate certifies. + type Voteable: Voteable; + + /// Threshold Functions + type Threshold: Threshold; + + /// Build a certificate from the data commitment and the quorum of signers + fn create_signed_certificate( + vote_commitment: Commitment, + data: Self::Voteable, + sig: ::QCType, + view: TYPES::Time, + ) -> Self; + + /// Checks if the cert is valid + fn is_valid_cert>(&self, membership: &MEMBERSHIP) -> bool; + /// Returns the amount of stake needed to create this certificate + // TODO: Make this a static ratio of the total stake of `Membership` + fn threshold>(membership: &MEMBERSHIP) -> u64; + /// Get the commitment which was voted on + fn get_data(&self) -> &Self::Voteable; + /// Get the vote commitment which the votes commit to + fn get_data_commitment(&self) -> Commitment; +} +/// Mapping of vote commitment to sigatures and bitvec +type SignersMap = HashMap< + COMMITMENT, + ( + BitVec, + Vec<::PureAssembledSignatureType>, + ), +>; +/// Accumulates votes until a certificate is formed. This implementation works for all simple vote and certificate pairs +pub struct VoteAccumulator< + TYPES: NodeType, + VOTE: Vote, + CERT: Certificate, +> { + /// Map of all signatures accumlated so far + pub vote_outcomes: VoteMap2< + Commitment, + TYPES::SignatureKey, + ::PureAssembledSignatureType, + >, + /// A bitvec to indicate which node is active and send out a valid signature for certificate aggregation, this automatically do uniqueness check + /// And a list of valid signatures for certificate aggregation + pub signers: SignersMap, TYPES::SignatureKey>, + /// Phantom data to specify the types this accumulator is for + pub phantom: PhantomData<(TYPES, VOTE, CERT)>, +} + +impl, CERT: Certificate> + VoteAccumulator +{ + /// Add a vote to the total accumulated votes. Returns the accumulator or the certificate if we + /// have accumulated enough votes to exceed the threshold for creating a certificate. + /// + /// # Panics + /// Panics if the vote comes from a node not in the stake table + pub fn accumulate(&mut self, vote: &VOTE, membership: &TYPES::Membership) -> Either<(), CERT> { + let key = vote.get_signing_key(); + + let vote_commitment = vote.get_data_commitment(); + if !key.validate(&vote.get_signature(), vote_commitment.as_ref()) { + error!("Invalid vote! Vote Data {:?}", vote.get_data()); + return Either::Left(()); + } + + let Some(stake_table_entry) = membership.get_stake(&key) else { + return Either::Left(()); + }; + let stake_table = membership.get_committee_qc_stake_table(); + let vote_node_id = stake_table + .iter() + .position(|x| *x == stake_table_entry.clone()) + .unwrap(); + + let original_signature: ::PureAssembledSignatureType = + vote.get_signature(); + + let (total_stake_casted, total_vote_map) = self + .vote_outcomes + .entry(vote_commitment) + .or_insert_with(|| (U256::from(0), BTreeMap::new())); + + // Check for duplicate vote + if total_vote_map.contains_key(&key) { + return Either::Left(()); + } + let (signers, sig_list) = self + .signers + .entry(vote_commitment) + .or_insert((bitvec![0; membership.total_nodes()], Vec::new())); + if signers.get(vote_node_id).as_deref() == Some(&true) { + error!("Node id is already in signers list"); + return Either::Left(()); + } + signers.set(vote_node_id, true); + sig_list.push(original_signature); + + // TODO: Get the stake from the stake table entry. + *total_stake_casted += stake_table_entry.get_stake(); + total_vote_map.insert(key, (vote.get_signature(), vote.get_data_commitment())); + + if *total_stake_casted >= CERT::threshold(membership).into() { + // Assemble QC + let real_qc_pp: <::SignatureKey as SignatureKey>::QCParams = + ::get_public_parameter( + stake_table, + U256::from(CERT::threshold(membership)), + ); + + let real_qc_sig = ::assemble( + &real_qc_pp, + signers.as_bitslice(), + &sig_list[..], + ); + + let cert = CERT::create_signed_certificate( + vote.get_data_commitment(), + vote.get_data().clone(), + real_qc_sig, + vote.get_view_number(), + ); + return Either::Right(cert); + } + Either::Left(()) + } +} + +/// Mapping of commitments to vote tokens by key. +type VoteMap2 = HashMap)>; diff --git a/crates/web_server/Cargo.toml b/crates/web_server/Cargo.toml index 3d9b342618..3a641c3a91 100644 --- a/crates/web_server/Cargo.toml +++ b/crates/web_server/Cargo.toml @@ -10,7 +10,7 @@ async-compatibility-layer = { workspace = true } async-lock = { workspace = true } clap = { version = "4.0", features = ["derive", "env"], optional = false } futures = { workspace = true } -hotshot-types = { workspace = true } +hotshot-types = { path = "../types" } tide-disco = { workspace = true } tracing = { workspace = true } rand = { workspace = true }