diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 046200c3..f769c9fb 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -141,6 +141,8 @@ jobs: run: just run_sailfish_demo - name: Test with block-maker run: just test-all + - name: Test dynamic committee change + run: just test-dyn-comm contracts: runs-on: ubuntu-latest diff --git a/Cargo.lock b/Cargo.lock index 310065bb..522d946b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -183,7 +183,7 @@ dependencies = [ "alloy-network 0.13.0", "alloy-node-bindings 0.13.0", "alloy-provider 0.13.0", - "alloy-pubsub", + "alloy-pubsub 0.13.0", "alloy-rpc-client 0.13.0", "alloy-rpc-types 0.13.0", "alloy-serde 0.13.0", @@ -192,7 +192,7 @@ dependencies = [ "alloy-signer-local 0.13.0", "alloy-transport 0.13.0", "alloy-transport-http 0.13.0", - "alloy-transport-ws", + "alloy-transport-ws 0.13.0", ] [[package]] @@ -209,6 +209,7 @@ dependencies = [ "alloy-network 1.0.30", "alloy-node-bindings 1.0.30", "alloy-provider 1.0.30", + "alloy-pubsub 1.0.30", "alloy-rpc-client 1.0.30", "alloy-rpc-types 1.0.30", "alloy-serde 1.0.30", @@ -216,6 +217,7 @@ dependencies = [ "alloy-signer-local 1.0.30", "alloy-transport 1.0.30", "alloy-transport-http 1.0.30", + "alloy-transport-ws 1.0.30", "alloy-trie 0.9.1", ] @@ -343,7 +345,7 @@ dependencies = [ "alloy-network-primitives 0.13.0", "alloy-primitives 0.8.25", "alloy-provider 0.13.0", - "alloy-pubsub", + "alloy-pubsub 0.13.0", "alloy-rpc-types-eth 0.13.0", "alloy-sol-types 0.8.25", "alloy-transport 0.13.0", @@ -365,6 +367,7 @@ dependencies = [ "alloy-network-primitives 1.0.30", "alloy-primitives 1.3.1", "alloy-provider 1.0.30", + "alloy-pubsub 1.0.30", "alloy-rpc-types-eth 1.0.30", "alloy-sol-types 1.3.1", "alloy-transport 1.0.30", @@ -854,7 +857,7 @@ dependencies = [ "alloy-network-primitives 0.13.0", "alloy-node-bindings 0.13.0", "alloy-primitives 0.8.25", - "alloy-pubsub", + "alloy-pubsub 0.13.0", "alloy-rpc-client 0.13.0", "alloy-rpc-types-anvil 0.13.0", "alloy-rpc-types-eth 0.13.0", @@ -862,7 +865,7 @@ dependencies = [ "alloy-sol-types 0.8.25", "alloy-transport 0.13.0", "alloy-transport-http 0.13.0", - "alloy-transport-ws", + "alloy-transport-ws 0.13.0", "async-stream", "async-trait", "auto_impl", @@ -897,6 +900,7 @@ dependencies = [ "alloy-network-primitives 1.0.30", "alloy-node-bindings 1.0.30", "alloy-primitives 1.3.1", + "alloy-pubsub 1.0.30", "alloy-rpc-client 1.0.30", "alloy-rpc-types-anvil 1.0.30", "alloy-rpc-types-eth 1.0.30", @@ -944,6 +948,28 @@ dependencies = [ "tracing", ] +[[package]] +name = "alloy-pubsub" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f97c18795ce1ce8151c5539ce1e4200940389674173f677c7455f79bfb00e5df" +dependencies = [ + "alloy-json-rpc 1.0.30", + "alloy-primitives 1.3.1", + "alloy-transport 1.0.30", + "auto_impl", + "bimap", + "futures", + "parking_lot", + "serde", + "serde_json", + "tokio", + "tokio-stream", + "tower 0.5.2", + "tracing", + "wasmtimer", +] + [[package]] name = "alloy-rlp" version = "0.3.12" @@ -974,10 +1000,10 @@ checksum = "cec6dc89c4c3ef166f9fa436d1831f8142c16cf2e637647c936a6aaaabd8d898" dependencies = [ "alloy-json-rpc 0.13.0", "alloy-primitives 0.8.25", - "alloy-pubsub", + "alloy-pubsub 0.13.0", "alloy-transport 0.13.0", "alloy-transport-http 0.13.0", - "alloy-transport-ws", + "alloy-transport-ws 0.13.0", "async-stream", "futures", "pin-project", @@ -1001,6 +1027,7 @@ checksum = "25289674cd8c58fcca2568b5350423cb0dd7bca8c596c5e2869bfe4c5c57ed14" dependencies = [ "alloy-json-rpc 1.0.30", "alloy-primitives 1.3.1", + "alloy-pubsub 1.0.30", "alloy-transport 1.0.30", "alloy-transport-http 1.0.30", "futures", @@ -1484,7 +1511,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ef7a4301e8967c1998f193755fd9429e0ca81730e2e134e30c288c43dbf96f0" dependencies = [ - "alloy-pubsub", + "alloy-pubsub 0.13.0", "alloy-transport 0.13.0", "futures", "http 1.3.1", @@ -1496,6 +1523,24 @@ dependencies = [ "ws_stream_wasm", ] +[[package]] +name = "alloy-transport-ws" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6d44395e6793566e9c89bd82297cc4b0566655c1e78a1d69362640814784cc6" +dependencies = [ + "alloy-pubsub 1.0.30", + "alloy-transport 1.0.30", + "futures", + "http 1.3.1", + "rustls 0.23.31", + "serde_json", + "tokio", + "tokio-tungstenite 0.26.2", + "tracing", + "ws_stream_wasm", +] + [[package]] name = "alloy-trie" version = "0.7.9" @@ -11900,7 +11945,9 @@ dependencies = [ "clap", "cliquenet", "committable", + "futures", "http 1.3.1", + "itertools 0.14.0", "metrics", "multisig", "prost 0.14.1", @@ -11987,9 +12034,12 @@ dependencies = [ "anyhow", "bincode 2.0.1", "clap", + "futures", "rand 0.9.2", + "reqwest", "serde", "timeboost-config", + "timeboost-crypto", "timeboost-types", "timeboost-utils", "tokio", @@ -13987,6 +14037,7 @@ dependencies = [ "futures", "reqwest", "timeboost", + "timeboost-contract", "timeboost-utils", "tokio", "tracing", diff --git a/Cargo.toml b/Cargo.toml index 7ac6ea0d..f03dc22d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ rust-version = "1.85.0" [workspace.dependencies] aes-gcm = { version = "0.10.3" } -alloy = { version = "1.0", features = ["default", "arbitrary", "k256", "serde", "rlp", "node-bindings", "getrandom", "signer-mnemonic"] } +alloy = { version = "1.0", features = ["default", "arbitrary", "k256", "serde", "rlp", "node-bindings", "getrandom", "signer-mnemonic", "transport-ws"] } alloy-chains = "0.2" # derive feature is not exposed via `alloy`, thus has to explicitly declare here alloy-rlp = { version = "0.3.12", features = ["derive"] } @@ -69,6 +69,7 @@ ethereum_ssz = "0.9.0" futures = { version = "0.3", default-features = false, features = ["alloc"] } generic-array = { version = "0.14.7", features = ["serde", "zeroize"] } http = "1.3.1" +itertools = "0.14.0" jiff = { version = "0.2", default-features = false, features = ["serde", "std"] } minicbor = { version = "2.1.1", features = ["full"] } nohash-hasher = "0.2" diff --git a/contracts b/contracts index d367ab38..9a48cd17 160000 --- a/contracts +++ b/contracts @@ -1 +1 @@ -Subproject commit d367ab389a735182f245abfc217121e3bb2a9a85 +Subproject commit 9a48cd17444e23a0bc5a30c07fabe37fcee80f7d diff --git a/justfile b/justfile index 6fad8258..adfa04e9 100644 --- a/justfile +++ b/justfile @@ -19,6 +19,9 @@ update-submodules: build_release *ARGS: cargo build --release --workspace --all-targets {{ARGS}} +build_release_until: + cargo build --release --workspace --all-targets --features "until" + build_docker: docker build . -f ./docker/timeboost.Dockerfile -t timeboost:latest docker build . -f ./docker/yapper.Dockerfile -t yapper:latest @@ -103,6 +106,7 @@ mkconfig NUM_NODES DATETIME *ARGS: --http-api "127.0.0.1:8004" \ --chain-namespace 10101 \ --parent-rpc-url "http://127.0.0.1:8545" \ + --parent-ws-url "ws://127.0.0.1:8545" \ --parent-chain-id 31337 \ --parent-ibox-contract "0xa0f3a1a4e2b2bcb7b48c8527c28098f207572ec1" \ --key-manager-contract "0x2bbf15bc655c4cc157b769cfcb1ea9924b9e1a35" \ @@ -118,6 +122,7 @@ mkconfig_docker DATETIME *ARGS: --mode "increment-address" \ --chain-namespace 10101 \ --parent-rpc-url "http://127.0.0.1:8545" \ + --parent-ws-url "ws://127.0.0.1:8545" \ --parent-chain-id 31337 \ --parent-ibox-contract "0xa0f3a1a4e2b2bcb7b48c8527c28098f207572ec1" \ --key-manager-contract "0x2bbf15bc655c4cc157b769cfcb1ea9924b9e1a35" \ @@ -133,6 +138,7 @@ mkconfig_nitro DATETIME *ARGS: --nitro-addr "localhost:55000" \ --chain-namespace 412346 \ --parent-rpc-url "http://127.0.0.1:8545" \ + --parent-ws-url "ws://127.0.0.1:8546" \ --parent-chain-id 1337 \ --parent-ibox-contract "0xa0f3a1a4e2b2bcb7b48c8527c28098f207572ec1" \ --key-manager-contract "0x2bbf15bc655c4cc157b769cfcb1ea9924b9e1a35" \ @@ -179,9 +185,53 @@ test-all: build_release build-test-utils --run "3:scripts/deploy-test-contract" \ --spawn "4:target/release/block-maker --port 55000 --committee test-configs/local/committee.toml" \ --spawn "4:target/release/yapper --keyset-file test-configs/local/committee.toml" \ - --spawn "5:target/release/run-committee --configs test-configs/local/ --committee 0 --timeboost target/release/timeboost" \ + --spawn "5:target/release/run-committee --configs test-configs/local/ --committee-id 0 --timeboost target/release/timeboost" \ target/release/block-checker -- \ --config test-configs/local/node_0.toml \ --committee test-configs/local/committee.toml \ --committee-id 0 \ --blocks 1000 + +test-dyn-comm: build_release_until build-test-utils + env RUST_LOG=sailfish=warn,yapper=error,timeboost=info,info target/release/run \ + --verbose \ + --timeout 120 \ + --spawn "1:anvil --port 8545" \ + --run "2:sleep 2" \ + --run "3:scripts/deploy-test-contract" \ + --spawn "4:target/release/run-committee --configs test-configs/c0/ --committee-id 0 --until 2000" \ + --run "5:target/release/mkconfig -n 4 \ + --public-addr 127.0.0.1:9000 \ + --internal-addr 127.0.0.1:9003 \ + --http-api 127.0.0.1:9004 \ + --chain-namespace 10101 \ + --parent-rpc-url http://127.0.0.1:8545 \ + --parent-ws-url ws://127.0.0.1:8545 \ + --parent-chain-id 31337 \ + --parent-ibox-contract 0xa0f3a1a4e2b2bcb7b48c8527c28098f207572ec1 \ + --key-manager-contract 0x2bbf15bc655c4cc157b769cfcb1ea9924b9e1a35 \ + --timestamp +16s \ + --stamp-dir /tmp \ + --output test-configs/c1" \ + --run "6:sleep 6" \ + --run "7:target/release/register \ + -a threshold-enc-key \ + -m 'attend year erase basket blind adapt stove broccoli isolate unveil acquire category' \ + -u http://localhost:8545 \ + -k 0x2bbf15bc655c4cc157b769cfcb1ea9924b9e1a35 \ + -c test-configs/c0/committee.toml" \ + --run "8:target/release/register \ + -a new-committee \ + -m 'attend year erase basket blind adapt stove broccoli isolate unveil acquire category' \ + -u http://localhost:8545 \ + -k 0x2bbf15bc655c4cc157b769cfcb1ea9924b9e1a35 \ + -c test-configs/c1/committee.toml" \ + --spawn "9:target/release/yapper \ + --keyset-file test-configs/c1/committee.toml \ + --parent-url http://localhost:8545 \ + --key-manager-contract 0x2bbf15bc655c4cc157b769cfcb1ea9924b9e1a35" \ + target/release/run-committee -- \ + --configs test-configs/c1/ \ + --committee-id 1 \ + --until 800 \ + --required-decrypt-rounds 3 && rm -rf test-configs/c1 diff --git a/multisig/src/committee.rs b/multisig/src/committee.rs index 313d4905..29e4af74 100644 --- a/multisig/src/committee.rs +++ b/multisig/src/committee.rs @@ -1,4 +1,5 @@ use std::num::{NonZeroUsize, ParseIntError}; +use std::ops::{Add, Sub}; use std::{fmt, str::FromStr}; use std::sync::Arc; @@ -107,6 +108,7 @@ impl Committee { #[derive( Debug, Copy, + Default, Clone, PartialEq, Eq, @@ -163,3 +165,19 @@ impl Committable for CommitteeId { .finalize() } } + +impl Add for CommitteeId { + type Output = Self; + + fn add(self, rhs: u64) -> Self::Output { + Self(self.0 + rhs) + } +} + +impl Sub for CommitteeId { + type Output = Self; + + fn sub(self, rhs: u64) -> Self::Output { + Self(self.0 - rhs) + } +} diff --git a/scripts/deploy-test-contract b/scripts/deploy-test-contract index aa40c732..f52583e7 100755 --- a/scripts/deploy-test-contract +++ b/scripts/deploy-test-contract @@ -22,6 +22,7 @@ km_addr=$(sed -nr 's/^key_manager.*=.*"(.+)"/\1/p' "$DEPLOYMENT_FILE") # Update the contract env RUST_LOG=info cargo run --release --bin register -- \ + -a new-committee \ -m "$MANAGER_MNEMONIC" \ -i "$MANAGER_ACCOUNT_INDEX" \ -u "$URL" \ diff --git a/scripts/run-timeboost-demo b/scripts/run-timeboost-demo index e2d8102e..64394812 100755 --- a/scripts/run-timeboost-demo +++ b/scripts/run-timeboost-demo @@ -116,7 +116,6 @@ i=0 for f in "$config_dir"/node_*.toml; do cmd=(target/release/timeboost --committee-id 0 - --committee "$config_dir/committee.toml" --config "$f" --until $rounds --watchdog-timeout 120) diff --git a/scripts/test-contract-deploy b/scripts/test-contract-deploy index afd5ea74..ce5919ea 100755 --- a/scripts/test-contract-deploy +++ b/scripts/test-contract-deploy @@ -84,7 +84,7 @@ km_addr=$(sed -nr 's/^key_manager.*=.*"(.+)"/\1/p' "$DEPLOYMENT_FILE") # Update the contract committee_config="test-configs/$COMMITTEE_PATH/committee.toml" -RUST_LOG=info cargo run --release --bin register -- -m "$MANAGER_MNEMONIC" -i "$MANAGER_ACCOUNT_INDEX" -u "$URL" -k "$km_addr" -c "$committee_config" +RUST_LOG=info cargo run --release --bin register -- -a new-committee -m "$MANAGER_MNEMONIC" -i "$MANAGER_ACCOUNT_INDEX" -u "$URL" -k "$km_addr" -c "$committee_config" # Finally, clean up the temporary deployment file rm -f "$DEPLOYMENT_FILE" diff --git a/test-configs/README.md b/test-configs/README.md index da9614f6..f74ae2c9 100644 --- a/test-configs/README.md +++ b/test-configs/README.md @@ -26,14 +26,14 @@ To generate configs for all nodes in a new committee: ``` sh # see mkconfig.rs Args or `mkconfig --help` for more options -just mkconfig 5 2025-01-09T02:00:00Z --seed 42 -just mkconfig 13 2025-01-09T02:00:00Z --nitro-addr "localhost:55000" +just mkconfig 5 2025-09-01T02:00:00Z --seed 42 +just mkconfig 13 2025-09-01T02:00:00Z --nitro-addr "localhost:55000" # recipe for docker env is fixed at 5 nodes -just mkconfig_docker 2025-01-09T02:00:00Z --seed 42 +just mkconfig_docker 2025-09-01T02:00:00Z --seed 42 # recipe for nitro CI test, fixed at 2 nodes with nitro chain config -just mkconfig_nitro 2025-01-09T02:00:00Z --seed 42 +just mkconfig_nitro 2025-09-01T02:00:00Z --seed 42 ``` ### On test wallet mnemonic diff --git a/test-configs/c0/node_0.toml b/test-configs/c0/node_0.toml index 171c2089..ac228c86 100644 --- a/test-configs/c0/node_0.toml +++ b/test-configs/c0/node_0.toml @@ -25,6 +25,7 @@ namespace = 10101 [chain.parent] id = 31337 rpc-url = "http://127.0.0.1:8545/" +ws-url = "ws://127.0.0.1:8545/" ibox-contract = "0xa0f3a1a4e2b2bcb7b48c8527c28098f207572ec1" block-tag = "finalized" key-manager-contract = "0x2bbf15bc655c4cc157b769cfcb1ea9924b9e1a35" diff --git a/test-configs/c0/node_1.toml b/test-configs/c0/node_1.toml index 3a69505f..56e35eff 100644 --- a/test-configs/c0/node_1.toml +++ b/test-configs/c0/node_1.toml @@ -25,6 +25,7 @@ namespace = 10101 [chain.parent] id = 31337 rpc-url = "http://127.0.0.1:8545/" +ws-url = "ws://127.0.0.1:8545/" ibox-contract = "0xa0f3a1a4e2b2bcb7b48c8527c28098f207572ec1" block-tag = "finalized" key-manager-contract = "0x2bbf15bc655c4cc157b769cfcb1ea9924b9e1a35" diff --git a/test-configs/c0/node_2.toml b/test-configs/c0/node_2.toml index e5f43aa0..5d3be44b 100644 --- a/test-configs/c0/node_2.toml +++ b/test-configs/c0/node_2.toml @@ -25,6 +25,7 @@ namespace = 10101 [chain.parent] id = 31337 rpc-url = "http://127.0.0.1:8545/" +ws-url = "ws://127.0.0.1:8545/" ibox-contract = "0xa0f3a1a4e2b2bcb7b48c8527c28098f207572ec1" block-tag = "finalized" key-manager-contract = "0x2bbf15bc655c4cc157b769cfcb1ea9924b9e1a35" diff --git a/test-configs/c0/node_3.toml b/test-configs/c0/node_3.toml index 07c9689e..ddd97028 100644 --- a/test-configs/c0/node_3.toml +++ b/test-configs/c0/node_3.toml @@ -25,6 +25,7 @@ namespace = 10101 [chain.parent] id = 31337 rpc-url = "http://127.0.0.1:8545/" +ws-url = "ws://127.0.0.1:8545/" ibox-contract = "0xa0f3a1a4e2b2bcb7b48c8527c28098f207572ec1" block-tag = "finalized" key-manager-contract = "0x2bbf15bc655c4cc157b769cfcb1ea9924b9e1a35" diff --git a/test-configs/c0/node_4.toml b/test-configs/c0/node_4.toml index d75eb234..0362d88d 100644 --- a/test-configs/c0/node_4.toml +++ b/test-configs/c0/node_4.toml @@ -25,6 +25,7 @@ namespace = 10101 [chain.parent] id = 31337 rpc-url = "http://127.0.0.1:8545/" +ws-url = "ws://127.0.0.1:8545/" ibox-contract = "0xa0f3a1a4e2b2bcb7b48c8527c28098f207572ec1" block-tag = "finalized" key-manager-contract = "0x2bbf15bc655c4cc157b769cfcb1ea9924b9e1a35" diff --git a/test-configs/docker/committee.toml b/test-configs/docker/committee.toml index b43a0c83..823b5888 100644 --- a/test-configs/docker/committee.toml +++ b/test-configs/docker/committee.toml @@ -1,4 +1,4 @@ -effective_timestamp = "2025-09-01T20:00:00Z" +effective_timestamp = "2025-09-01T02:00:00Z" [[members]] signing_key = "eiwaGN1NNaQdbnR9FsjKzUeLghQZsTLPjiL4RcQgfLoX" diff --git a/test-configs/docker/node_0.toml b/test-configs/docker/node_0.toml index 217bbe9c..1fdf5219 100644 --- a/test-configs/docker/node_0.toml +++ b/test-configs/docker/node_0.toml @@ -25,6 +25,7 @@ namespace = 10101 [chain.parent] id = 31337 rpc-url = "http://127.0.0.1:8545/" +ws-url = "ws://127.0.0.1:8545/" ibox-contract = "0xa0f3a1a4e2b2bcb7b48c8527c28098f207572ec1" block-tag = "finalized" key-manager-contract = "0x2bbf15bc655c4cc157b769cfcb1ea9924b9e1a35" diff --git a/test-configs/docker/node_1.toml b/test-configs/docker/node_1.toml index fcadfd86..cdc9686c 100644 --- a/test-configs/docker/node_1.toml +++ b/test-configs/docker/node_1.toml @@ -25,6 +25,7 @@ namespace = 10101 [chain.parent] id = 31337 rpc-url = "http://127.0.0.1:8545/" +ws-url = "ws://127.0.0.1:8545/" ibox-contract = "0xa0f3a1a4e2b2bcb7b48c8527c28098f207572ec1" block-tag = "finalized" key-manager-contract = "0x2bbf15bc655c4cc157b769cfcb1ea9924b9e1a35" diff --git a/test-configs/docker/node_2.toml b/test-configs/docker/node_2.toml index d4c66484..08d51922 100644 --- a/test-configs/docker/node_2.toml +++ b/test-configs/docker/node_2.toml @@ -25,6 +25,7 @@ namespace = 10101 [chain.parent] id = 31337 rpc-url = "http://127.0.0.1:8545/" +ws-url = "ws://127.0.0.1:8545/" ibox-contract = "0xa0f3a1a4e2b2bcb7b48c8527c28098f207572ec1" block-tag = "finalized" key-manager-contract = "0x2bbf15bc655c4cc157b769cfcb1ea9924b9e1a35" diff --git a/test-configs/docker/node_3.toml b/test-configs/docker/node_3.toml index 16bceeb5..35baa563 100644 --- a/test-configs/docker/node_3.toml +++ b/test-configs/docker/node_3.toml @@ -25,6 +25,7 @@ namespace = 10101 [chain.parent] id = 31337 rpc-url = "http://127.0.0.1:8545/" +ws-url = "ws://127.0.0.1:8545/" ibox-contract = "0xa0f3a1a4e2b2bcb7b48c8527c28098f207572ec1" block-tag = "finalized" key-manager-contract = "0x2bbf15bc655c4cc157b769cfcb1ea9924b9e1a35" diff --git a/test-configs/docker/node_4.toml b/test-configs/docker/node_4.toml index 056caa13..b5f2c27f 100644 --- a/test-configs/docker/node_4.toml +++ b/test-configs/docker/node_4.toml @@ -25,6 +25,7 @@ namespace = 10101 [chain.parent] id = 31337 rpc-url = "http://127.0.0.1:8545/" +ws-url = "ws://127.0.0.1:8545/" ibox-contract = "0xa0f3a1a4e2b2bcb7b48c8527c28098f207572ec1" block-tag = "finalized" key-manager-contract = "0x2bbf15bc655c4cc157b769cfcb1ea9924b9e1a35" diff --git a/test-configs/local/node_0.toml b/test-configs/local/node_0.toml index 4cb05652..2d5b5431 100644 --- a/test-configs/local/node_0.toml +++ b/test-configs/local/node_0.toml @@ -26,6 +26,7 @@ namespace = 10101 [chain.parent] id = 31337 rpc-url = "http://127.0.0.1:8545/" +ws-url = "ws://127.0.0.1:8545/" ibox-contract = "0xa0f3a1a4e2b2bcb7b48c8527c28098f207572ec1" block-tag = "finalized" key-manager-contract = "0x2bbf15bc655c4cc157b769cfcb1ea9924b9e1a35" diff --git a/test-configs/local/node_1.toml b/test-configs/local/node_1.toml index b8467635..16aabd2f 100644 --- a/test-configs/local/node_1.toml +++ b/test-configs/local/node_1.toml @@ -26,6 +26,7 @@ namespace = 10101 [chain.parent] id = 31337 rpc-url = "http://127.0.0.1:8545/" +ws-url = "ws://127.0.0.1:8545/" ibox-contract = "0xa0f3a1a4e2b2bcb7b48c8527c28098f207572ec1" block-tag = "finalized" key-manager-contract = "0x2bbf15bc655c4cc157b769cfcb1ea9924b9e1a35" diff --git a/test-configs/local/node_2.toml b/test-configs/local/node_2.toml index 0058bc47..ea6632cd 100644 --- a/test-configs/local/node_2.toml +++ b/test-configs/local/node_2.toml @@ -26,6 +26,7 @@ namespace = 10101 [chain.parent] id = 31337 rpc-url = "http://127.0.0.1:8545/" +ws-url = "ws://127.0.0.1:8545/" ibox-contract = "0xa0f3a1a4e2b2bcb7b48c8527c28098f207572ec1" block-tag = "finalized" key-manager-contract = "0x2bbf15bc655c4cc157b769cfcb1ea9924b9e1a35" diff --git a/test-configs/local/node_3.toml b/test-configs/local/node_3.toml index e0644469..fe8afa43 100644 --- a/test-configs/local/node_3.toml +++ b/test-configs/local/node_3.toml @@ -26,6 +26,7 @@ namespace = 10101 [chain.parent] id = 31337 rpc-url = "http://127.0.0.1:8545/" +ws-url = "ws://127.0.0.1:8545/" ibox-contract = "0xa0f3a1a4e2b2bcb7b48c8527c28098f207572ec1" block-tag = "finalized" key-manager-contract = "0x2bbf15bc655c4cc157b769cfcb1ea9924b9e1a35" diff --git a/test-configs/local/node_4.toml b/test-configs/local/node_4.toml index 33fc399e..ab076b27 100644 --- a/test-configs/local/node_4.toml +++ b/test-configs/local/node_4.toml @@ -26,6 +26,7 @@ namespace = 10101 [chain.parent] id = 31337 rpc-url = "http://127.0.0.1:8545/" +ws-url = "ws://127.0.0.1:8545/" ibox-contract = "0xa0f3a1a4e2b2bcb7b48c8527c28098f207572ec1" block-tag = "finalized" key-manager-contract = "0x2bbf15bc655c4cc157b769cfcb1ea9924b9e1a35" diff --git a/test-configs/nitro-ci-committee/node_0.toml b/test-configs/nitro-ci-committee/node_0.toml index 5b748b26..c0581190 100644 --- a/test-configs/nitro-ci-committee/node_0.toml +++ b/test-configs/nitro-ci-committee/node_0.toml @@ -26,6 +26,7 @@ namespace = 412346 [chain.parent] id = 1337 rpc-url = "http://127.0.0.1:8545/" +ws-url = "ws://127.0.0.1:8546/" ibox-contract = "0xa0f3a1a4e2b2bcb7b48c8527c28098f207572ec1" block-tag = "finalized" key-manager-contract = "0x2bbf15bc655c4cc157b769cfcb1ea9924b9e1a35" diff --git a/test-configs/nitro-ci-committee/node_1.toml b/test-configs/nitro-ci-committee/node_1.toml index ac785a95..2b715caf 100644 --- a/test-configs/nitro-ci-committee/node_1.toml +++ b/test-configs/nitro-ci-committee/node_1.toml @@ -26,6 +26,7 @@ namespace = 412346 [chain.parent] id = 1337 rpc-url = "http://127.0.0.1:8545/" +ws-url = "ws://127.0.0.1:8546/" ibox-contract = "0xa0f3a1a4e2b2bcb7b48c8527c28098f207572ec1" block-tag = "finalized" key-manager-contract = "0x2bbf15bc655c4cc157b769cfcb1ea9924b9e1a35" diff --git a/test-utils/src/binaries/run-committee.rs b/test-utils/src/binaries/run-committee.rs index f9accd24..bc20322a 100644 --- a/test-utils/src/binaries/run-committee.rs +++ b/test-utils/src/binaries/run-committee.rs @@ -11,13 +11,19 @@ struct Args { configs: PathBuf, #[clap(long, short)] - committee: u64, + committee_id: u64, - #[clap(long, short)] + #[clap(long, short, default_value = "target/release/timeboost")] timeboost: PathBuf, #[clap(long, short, default_value = "/tmp")] tmp: PathBuf, + + #[clap(long)] + until: Option, + + #[clap(long)] + required_decrypt_rounds: Option, } #[tokio::main] @@ -48,10 +54,17 @@ async fn main() -> Result<()> { } let mut cmd = Command::new(args.timeboost.as_os_str()); cmd.arg("--committee-id") - .arg(args.committee.to_string()) + .arg(args.committee_id.to_string()) .arg("--config") .arg(entry.path()) .arg("--ignore-stamp"); + + if let Some(until) = args.until { + cmd.arg("--until").arg(until.to_string()); + } + if let Some(r) = args.required_decrypt_rounds { + cmd.arg("--required-decrypt-rounds").arg(r.to_string()); + } commands.push(cmd); } diff --git a/test-utils/src/binaries/run.rs b/test-utils/src/binaries/run.rs index 078096dd..035f022b 100644 --- a/test-utils/src/binaries/run.rs +++ b/test-utils/src/binaries/run.rs @@ -1,7 +1,7 @@ use std::future::pending; use std::ops::{Deref, DerefMut}; +use std::process::ExitStatus; use std::time::Duration; -use std::{ffi::OsStr, process::ExitStatus}; use anyhow::{Result, anyhow, bail}; use clap::Parser; @@ -57,8 +57,9 @@ async fn main() -> Result<()> { eprintln!("spawning command: {}", commandline.args) } } - let mut pg = ProcessGroup::spawn(commandline.args.split_whitespace())?; + if commandline.sync { + let mut pg = ProcessGroup::spawn(commandline.args.split_whitespace())?; let status = select! { s = pg.wait() => s?, _ = term.recv() => return Ok(()), @@ -68,8 +69,8 @@ async fn main() -> Result<()> { bail!("{:?} failed with {:?}", commandline.args, status.code()); } } else { + let mut pg = ProcessGroup::spawn(commandline.args.split_whitespace())?; helpers.spawn(async move { - let mut pg = ProcessGroup::spawn(commandline.args.split_whitespace())?; let status = pg.wait().await?; Ok(status) }); @@ -130,15 +131,32 @@ impl ProcessGroup { fn spawn(it: I) -> Result where I: IntoIterator, - S: AsRef, + S: Into + AsRef, { let mut args = it.into_iter(); let exe = args .next() .ok_or_else(|| anyhow!("invalid command-line args"))?; - let mut cmd = Command::new(exe); + let mut cmd = Command::new(exe.as_ref()); + let mut buf: Option> = None; for a in args { - cmd.arg(a); + if let Some(b) = &mut buf { + let mut a = a.into(); + if a.ends_with("'") { + a.pop(); + b.push(a); + cmd.arg(b.join(" ")); + buf = None + } else { + b.push(a); + } + } else if a.as_ref().starts_with("'") { + let mut a = a.into(); + a.remove(0); + buf = Some(vec![a]); + } else { + cmd.arg(a.as_ref()); + } } cmd.process_group(0); let child = cmd.spawn()?; diff --git a/tests/src/tests/timeboost.rs b/tests/src/tests/timeboost.rs index 9c7bf9c9..94617781 100644 --- a/tests/src/tests/timeboost.rs +++ b/tests/src/tests/timeboost.rs @@ -120,6 +120,11 @@ where .parse::() .expect("valid url"), ) + .ws_url( + "wss://theserversroom.com/ethereumws/54cmzzhcj1o/" + .parse::() + .expect("valid url"), + ) .ibox_contract(alloy::primitives::Address::default()) .key_manager_contract(alloy::primitives::Address::default()) .block_tag(BlockNumberOrTag::Finalized) diff --git a/tests/src/tests/timeboost/handover.rs b/tests/src/tests/timeboost/handover.rs index 432b5d1a..d7d4e152 100644 --- a/tests/src/tests/timeboost/handover.rs +++ b/tests/src/tests/timeboost/handover.rs @@ -168,6 +168,11 @@ where .parse::() .expect("valid url"), ) + .ws_url( + "wss://theserversroom.com/ethereumws/54cmzzhcj1o/" + .parse::() + .expect("valid url"), + ) .ibox_contract(alloy::primitives::Address::default()) .key_manager_contract(alloy::primitives::Address::default()) .block_tag(BlockNumberOrTag::Finalized) diff --git a/tests/src/tests/timeboost/timeboost_handover.rs b/tests/src/tests/timeboost/timeboost_handover.rs index 08f49334..ffbe88fb 100644 --- a/tests/src/tests/timeboost/timeboost_handover.rs +++ b/tests/src/tests/timeboost/timeboost_handover.rs @@ -420,6 +420,11 @@ async fn mk_configs( .parse::() .expect("valid url"), ) + .ws_url( + "wss://theserversroom.com/ethereumws/54cmzzhcj1o/" + .parse::() + .expect("valid url"), + ) .ibox_contract(alloy::primitives::Address::default()) .key_manager_contract(alloy::primitives::Address::default()) .block_tag(BlockNumberOrTag::Finalized) diff --git a/timeboost-config/src/binaries/mkconfig.rs b/timeboost-config/src/binaries/mkconfig.rs index c4856291..78f5a3de 100644 --- a/timeboost-config/src/binaries/mkconfig.rs +++ b/timeboost-config/src/binaries/mkconfig.rs @@ -3,12 +3,14 @@ use std::io::Write; use std::net::IpAddr; use std::num::NonZeroU8; use std::path::PathBuf; +use std::str::FromStr; use alloy::eips::BlockNumberOrTag; use anyhow::{Result, bail}; use ark_std::rand::SeedableRng as _; use clap::{Parser, ValueEnum}; use cliquenet::Address; +use jiff::{SignedDuration, Timestamp}; use multisig::x25519; use secp256k1::rand::SeedableRng as _; use timeboost_config::{ChainConfig, ParentChain}; @@ -38,7 +40,7 @@ struct Args { /// The timestamp format corresponds to RFC 3339, section 5.6, for example: /// 1970-01-01T18:00:00Z. #[clap(long)] - timestamp: jiff::Timestamp, + timestamp: TimestampOrOffset, /// The sailfish network address. Decrypter, certifier, and internal address are derived: /// sharing the same IP as the sailfish IP, and a different (but fixed) port number. @@ -68,6 +70,10 @@ struct Args { #[clap(long)] parent_rpc_url: Url, + /// Parent chain websocket url + #[clap(long)] + parent_ws_url: Url, + /// Parent chain id #[clap(long)] parent_chain_id: u64, @@ -104,6 +110,23 @@ struct Args { output: PathBuf, } +#[derive(Debug, Clone)] +struct TimestampOrOffset(Timestamp); + +impl FromStr for TimestampOrOffset { + type Err = String; + + fn from_str(s: &str) -> Result { + if let Ok(ts) = Timestamp::from_str(s) { + Ok(Self(ts)) + } else if let Ok(dur) = s.parse::() { + Ok(Self(Timestamp::now() + dur)) + } else { + Err("Expected RFC3339 timestamp or [+/-]duration".into()) + } + } +} + /// How should addresses be updated? #[derive(Clone, Copy, Debug, Default, ValueEnum)] enum Mode { @@ -185,6 +208,7 @@ impl Args { parent: ParentChain { id: self.parent_chain_id, rpc_url: self.parent_rpc_url.clone(), + ws_url: self.parent_ws_url.clone(), ibox_contract: self.parent_ibox_contract, block_tag: self.parent_block_tag, key_manager_contract: self.key_manager_contract, @@ -211,7 +235,7 @@ impl Args { } let committee_config = CommitteeConfig { - effective_timestamp: self.timestamp, + effective_timestamp: self.timestamp.0, members, }; committee_config_file.write_all(toml::to_string_pretty(&committee_config)?.as_bytes())?; diff --git a/timeboost-config/src/chain.rs b/timeboost-config/src/chain.rs index b2d7ad7d..6ed10849 100644 --- a/timeboost-config/src/chain.rs +++ b/timeboost-config/src/chain.rs @@ -15,6 +15,7 @@ pub struct ChainConfig { pub struct ParentChain { pub id: u64, pub rpc_url: Url, + pub ws_url: Url, pub ibox_contract: Address, pub block_tag: BlockNumberOrTag, pub key_manager_contract: Address, diff --git a/timeboost-config/src/node.rs b/timeboost-config/src/node.rs index a6a532e2..96890f44 100644 --- a/timeboost-config/src/node.rs +++ b/timeboost-config/src/node.rs @@ -119,6 +119,7 @@ namespace = 10101 [chain.parent] id = 31337 rpc-url = "http://127.0.0.1:8545/" +ws-url = "ws://127.0.0.1:8545/" ibox-contract = "0x4dbd4fc535ac27206064b68ffcf827b0a60bab3f" block-tag = "finalized" key-manager-contract = "0x2bbf15bc655c4cc157b769cfcb1ea9924b9e1a35" diff --git a/timeboost-contract/Cargo.toml b/timeboost-contract/Cargo.toml index 0981b914..b34bd0c8 100644 --- a/timeboost-contract/Cargo.toml +++ b/timeboost-contract/Cargo.toml @@ -18,9 +18,12 @@ alloy = { workspace = true } anyhow = { workspace = true } bincode = { workspace = true } clap = { workspace = true } +futures = { workspace = true } rand = { workspace = true } +reqwest = { workspace = true } serde = { workspace = true } timeboost-config = { path = "../timeboost-config" } +timeboost-crypto = { path = "../timeboost-crypto" } timeboost-types = { path = "../timeboost-types" } timeboost-utils = { path = "../timeboost-utils" } tokio = { workspace = true } @@ -29,4 +32,4 @@ tracing = { workspace = true } url = { workspace = true } [build-dependencies] -alloy = { workspace = true } \ No newline at end of file +alloy = { workspace = true } diff --git a/timeboost-contract/src/binaries/register.rs b/timeboost-contract/src/binaries/register.rs index 86a93295..a52649c2 100644 --- a/timeboost-contract/src/binaries/register.rs +++ b/timeboost-contract/src/binaries/register.rs @@ -4,13 +4,17 @@ use alloy::{ primitives::Address, providers::{Provider, WalletProvider}, }; -use anyhow::{Context, Result, bail}; -use clap::Parser; +use anyhow::{Context, Result, anyhow, bail}; +use clap::{Parser, ValueEnum}; +use reqwest::Client; use std::path::PathBuf; +use std::time::Duration; use timeboost_config::CommitteeConfig; use timeboost_contract::{CommitteeMemberSol, KeyManager, provider::build_provider}; +use timeboost_crypto::prelude::ThresholdEncKey; +use timeboost_utils::enc_key::ThresholdEncKeyCellAccumulator; use timeboost_utils::types::logging; -use tracing::info; +use tracing::{info, warn}; use url::Url; #[derive(Clone, Debug, Parser)] @@ -18,7 +22,7 @@ struct Args { #[clap(short, long)] mnemonic: String, - #[clap(short, long)] + #[clap(short, long, default_value_t = 0)] index: u32, #[clap(short, long)] @@ -31,6 +35,19 @@ struct Args { /// Path to the committee.toml config for the next committee #[clap(short, long)] config: PathBuf, + + /// What to register (new committee or threshold enc key?) + #[clap(short, long)] + action: Action, +} + +/// Specific register action +#[derive(Clone, Copy, Debug, ValueEnum)] +enum Action { + /// register the next committee + NewCommittee, + /// register the threshold encryption key (when ready) + ThresholdEncKey, } #[tokio::main] @@ -42,8 +59,6 @@ async fn main() -> Result<()> { .await .context(format!("Failed to read config file: {:?}", &args.config))?; - info!("Start committee registration"); - let provider = build_provider(args.mnemonic.clone(), args.index, args.url.clone())?; let addr = args.key_manager_addr; if provider @@ -59,40 +74,79 @@ async fn main() -> Result<()> { let contract = KeyManager::new(addr, provider); - // prepare input argument from config file - let members = config - .members - .iter() - .map(|m| { - Ok::<_, anyhow::Error>(CommitteeMemberSol { - sigKey: m.signing_key.to_bytes().into(), - dhKey: m.dh_key.as_bytes().into(), - dkgKey: m.dkg_enc_key.to_bytes()?.into(), - networkAddress: m.public_address.to_string(), - }) - }) - .collect::, _>>()?; - - let timestamp: u64 = config - .effective_timestamp - .as_second() - .try_into() - .with_context(|| { - format!( - "failed to convert timestamp {} to u64", - config.effective_timestamp - ) - })?; - - // send tx and invoke the contract - let _tx_receipt = contract - .setNextCommittee(timestamp, members) - .send() - .await? - .get_receipt() - .await?; - - let registered_cid = contract.nextCommitteeId().call().await? - 1; - info!("Registered new committee with id: {registered_cid}"); + match args.action { + Action::NewCommittee => { + info!("Start committee registration"); + // prepare input argument from config file + let members = config + .members + .iter() + .map(|m| { + Ok::<_, anyhow::Error>(CommitteeMemberSol { + sigKey: m.signing_key.to_bytes().into(), + dhKey: m.dh_key.as_bytes().into(), + dkgKey: m.dkg_enc_key.to_bytes()?.into(), + networkAddress: m.public_address.to_string(), + }) + }) + .collect::, _>>()?; + + let timestamp: u64 = config + .effective_timestamp + .as_second() + .try_into() + .with_context(|| { + format!( + "failed to convert timestamp {} to u64", + config.effective_timestamp + ) + })?; + + // send tx and invoke the contract + let _tx_receipt = contract + .setNextCommittee(timestamp, members) + .send() + .await? + .get_receipt() + .await?; + + let registered_cid = contract.nextCommitteeId().call().await? - 1; + info!("Registered new committee with id: {registered_cid}"); + } + Action::ThresholdEncKey => { + info!("Start threshold encryption key registration"); + let client = Client::builder().timeout(Duration::from_secs(1)).build()?; + let urls = config + .members + .iter() + .map(|m| { + let addr = m.http_api.clone(); + Url::parse(&format!("http://{addr}/v1/encryption-key")) + .with_context(|| format!("parsing {addr} into a url")) + }) + .collect::, _>>()?; + + let mut acc = ThresholdEncKeyCellAccumulator::new(client, urls.into_iter()); + let Some(key) = acc.enc_key().await else { + warn!("encryption key not available yet"); + return Err(anyhow!( + "threshold enc key not available on enough nodes, try later" + )); + }; + + let _tx_receipt = contract + .setThresholdEncryptionKey(key.to_owned().to_bytes()?.into()) + .send() + .await? + .get_receipt() + .await?; + assert_eq!( + &ThresholdEncKey::from_bytes(&contract.thresholdEncryptionKey().call().await?.0)?, + key + ); + + info!("Registered threshold encryption key"); + } + } Ok(()) } diff --git a/timeboost-contract/src/deployer.rs b/timeboost-contract/src/deployer.rs index 760536d6..3e7eec2d 100644 --- a/timeboost-contract/src/deployer.rs +++ b/timeboost-contract/src/deployer.rs @@ -48,8 +48,18 @@ where #[cfg(test)] mod tests { - use crate::{CommitteeMemberSol, CommitteeSol, KeyManager}; - use alloy::{providers::WalletProvider, sol_types::SolValue}; + use super::deploy_key_manager_contract; + use crate::{CommitteeMemberSol, CommitteeSol, KeyManager, KeyManager::CommitteeCreated}; + use alloy::{ + eips::BlockNumberOrTag, + node_bindings::Anvil, + primitives::U256, + providers::{Provider, ProviderBuilder, WalletProvider}, + rpc::types::Filter, + sol_types::{SolEvent, SolValue}, + transports::ws::WsConnect, + }; + use futures::StreamExt; use rand::prelude::*; #[tokio::test] @@ -85,12 +95,73 @@ mod tests { .await .unwrap() .abi_encode_sequence(), + // deploy takes first 2 blocks: deploying implementation contract and proxy contract + // setNextCommittee is the 3rd tx, thus in 3rd block CommitteeSol { id: 0, + registeredBlockNumber: U256::from(3), effectiveTimestamp: timestamp, members, } .abi_encode_sequence() ); } + + #[tokio::test] + async fn test_event_stream() { + let anvil = Anvil::new().spawn(); + let wallet = anvil.wallet().unwrap(); + let provider = ProviderBuilder::new() + .wallet(wallet) + .connect_http(anvil.endpoint_url()); + let pubsub_provider = ProviderBuilder::new() + .connect_pubsub_with(WsConnect::new(anvil.ws_endpoint_url())) + .await + .unwrap(); + assert_eq!( + pubsub_provider.get_chain_id().await.unwrap(), + provider.get_chain_id().await.unwrap() + ); + + let manager = provider.default_signer_address(); + let km_addr = deploy_key_manager_contract(&provider, manager) + .await + .unwrap(); + let contract = KeyManager::new(km_addr, &provider); + + // setup event stream + let filter = Filter::new() + .address(km_addr) + .event(KeyManager::CommitteeCreated::SIGNATURE) + .from_block(BlockNumberOrTag::Latest); + let mut events = pubsub_provider + .subscribe_logs(&filter) + .await + .unwrap() + .into_stream(); + + // register some committees on the contract, which emit events + let rng = &mut rand::rng(); + let c0_timestamp = rng.random::(); + for i in 0..5 { + let members = (0..5) + .map(|_| CommitteeMemberSol::random()) + .collect::>(); + let timestamp = c0_timestamp + 1000 * i; + + let _tx_receipt = contract + .setNextCommittee(timestamp, members.clone()) + .send() + .await + .unwrap() + .get_receipt() + .await + .unwrap(); + + // Read the corresponding event + let log = events.next().await.unwrap(); + let typed_log = log.log_decode_validate::().unwrap(); + assert_eq!(typed_log.data().id, i); + } + } } diff --git a/timeboost-contract/src/provider.rs b/timeboost-contract/src/provider.rs index efb4ac4c..974f865c 100644 --- a/timeboost-contract/src/provider.rs +++ b/timeboost-contract/src/provider.rs @@ -1,13 +1,21 @@ //! Helper functions to build Ethereum [providers](https://docs.rs/alloy/latest/alloy/providers/trait.Provider.html) //! Partial Credit: +use std::{ops::Deref, pin::Pin}; + use alloy::{ + eips::BlockNumberOrTag, network::EthereumWallet, - providers::ProviderBuilder, + primitives::Address, + providers::{Provider, ProviderBuilder}, + rpc::types::{Filter, Log}, signers::local::{LocalSignerError, MnemonicBuilder, PrivateKeySigner, coins_bip39::English}, - transports::http::reqwest::Url, + sol_types::SolEvent, + transports::{http::reqwest::Url, ws::WsConnect}, }; -use timeboost_types::HttpProviderWithWallet; +use futures::{Stream, StreamExt}; +use timeboost_types::{HttpProvider, HttpProviderWithWallet}; +use tracing::error; /// Build a local signer from wallet mnemonic and account index pub fn build_signer( @@ -31,3 +39,62 @@ pub fn build_provider( let wallet = EthereumWallet::from(signer); Ok(ProviderBuilder::new().wallet(wallet).connect_http(url)) } + +/// A PubSub service (with backend handle), disconnect on drop. +#[derive(Clone)] +pub struct PubSubProvider(HttpProvider); + +impl Deref for PubSubProvider { + type Target = HttpProvider; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl PubSubProvider { + pub async fn new(ws_url: Url) -> anyhow::Result { + let ws = WsConnect::new(ws_url); + let provider = ProviderBuilder::new() + .connect_pubsub_with(ws) + .await + .map_err(|err| { + error!(?err, "event pubsub failed to start"); + err + })?; + Ok(Self(provider)) + } + + /// create an event stream of event type `E`, subscribing since `from_block` on `contract` + pub async fn event_stream( + &self, + contract: Address, + from_block: BlockNumberOrTag, + ) -> anyhow::Result>>>> { + let filter = Filter::new() + .address(contract) + .event(E::SIGNATURE) + .from_block(from_block); + + let events = self + .subscribe_logs(&filter) + .await + .map_err(|err| { + error!(?err, "pubsub subscription failed"); + err + })? + .into_stream(); + + let validated = events.filter_map(|log| async move { + match log.log_decode_validate::() { + Ok(event) => Some(event), + Err(err) => { + error!(%err, "failed to parse `CommitteeCreated` event log"); + None + } + } + }); + + Ok(Box::pin(validated)) + } +} diff --git a/timeboost-crypto/src/sg_encryption.rs b/timeboost-crypto/src/sg_encryption.rs index 3e3b5902..f2c81ee3 100644 --- a/timeboost-crypto/src/sg_encryption.rs +++ b/timeboost-crypto/src/sg_encryption.rs @@ -5,6 +5,7 @@ use anyhow::anyhow; use ark_ec::{AffineRepr, CurveGroup, hashing::HashToCurve}; use ark_ff::{PrimeField, UniformRand}; use ark_poly::{DenseUVPolynomial, Polynomial, polynomial::univariate::DensePolynomial}; +use ark_serialize::SerializationError; use ark_std::rand::Rng; use ark_std::rand::rngs::OsRng; use derive_more::From; @@ -342,6 +343,19 @@ pub struct PublicKey { key: C, } +impl PublicKey { + pub fn to_bytes(&self) -> Result, SerializationError> { + let mut v = Vec::new(); + self.key.serialize_compressed(&mut v)?; + Ok(v) + } + + pub fn from_bytes(value: &[u8]) -> Result { + let key = C::deserialize_compressed(value)?; + Ok(Self { key }) + } +} + #[serde_as] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Zeroize, ZeroizeOnDrop, From)] pub struct KeyShare { diff --git a/yapper/src/enc_key.rs b/timeboost-utils/src/enc_key.rs similarity index 91% rename from yapper/src/enc_key.rs rename to timeboost-utils/src/enc_key.rs index 994563a9..af164164 100644 --- a/yapper/src/enc_key.rs +++ b/timeboost-utils/src/enc_key.rs @@ -1,6 +1,6 @@ use reqwest::{Client, Url}; use std::collections::HashMap; -use timeboost::crypto::prelude::ThresholdEncKey; +use timeboost_crypto::prelude::ThresholdEncKey; use tracing::warn; @@ -28,7 +28,7 @@ async fn fetch_encryption_key(client: &Client, enckey_url: &Url) -> Option>, @@ -40,7 +40,7 @@ pub(crate) struct ThresholdEncKeyCellAccumulator { impl ThresholdEncKeyCellAccumulator { /// give a list of TimeboostApi's endpoint to query `/enckey` status - pub(crate) fn new(client: Client, urls: impl Iterator) -> Self { + pub fn new(client: Client, urls: impl Iterator) -> Self { let results: HashMap> = urls.map(|url| (url, None)).collect(); let threshold = results.len().div_ceil(3); Self { @@ -53,7 +53,7 @@ impl ThresholdEncKeyCellAccumulator { /// try to get the threshold encryption key, only available after a threshold of nodes /// finish their DKG processes. - pub(crate) async fn enc_key(&mut self) -> Option<&ThresholdEncKey> { + pub async fn enc_key(&mut self) -> Option<&ThresholdEncKey> { // if result is already available, directly return if self.output.is_some() { self.output.as_ref() diff --git a/timeboost-utils/src/lib.rs b/timeboost-utils/src/lib.rs index 92b37afa..a7ad98a5 100644 --- a/timeboost-utils/src/lib.rs +++ b/timeboost-utils/src/lib.rs @@ -1,3 +1,4 @@ +pub mod enc_key; pub mod load_generation; pub mod types; pub mod until; diff --git a/timeboost/Cargo.toml b/timeboost/Cargo.toml index 08b11e79..53067324 100644 --- a/timeboost/Cargo.toml +++ b/timeboost/Cargo.toml @@ -26,7 +26,9 @@ bon = { workspace = true } clap = { workspace = true } cliquenet = { path = "../cliquenet" } committable = { workspace = true } +futures = { workspace = true } http = { workspace = true } +itertools = { workspace = true } metrics = { path = "../metrics", features = ["prometheus"]} multisig = { path = "../multisig" } prost = { workspace = true } diff --git a/timeboost/src/binaries/sailfish.rs b/timeboost/src/binaries/sailfish.rs index 281cd2b4..1e5d4d6a 100644 --- a/timeboost/src/binaries/sailfish.rs +++ b/timeboost/src/binaries/sailfish.rs @@ -1,20 +1,18 @@ use std::{iter::repeat_with, path::PathBuf, sync::Arc, time::Duration}; use ::metrics::prometheus::PrometheusMetrics; -use alloy::providers::{Provider, ProviderBuilder}; use anyhow::{Context, Result}; use cliquenet::{Network, NetworkMetrics, Overlay}; use committable::{Commitment, Committable, RawCommitmentBuilder}; -use multisig::{Committee, CommitteeId, Keypair, x25519}; +use multisig::{CommitteeId, Keypair, x25519}; use sailfish::{ Coordinator, consensus::{Consensus, ConsensusMetrics}, rbc::{Rbc, RbcConfig, RbcMetrics}, - types::{Action, HasTime, Timestamp, UNKNOWN_COMMITTEE_ID}, + types::{Action, HasTime, Timestamp}, }; use serde::{Deserialize, Serialize}; -use timeboost::config::NodeConfig; -use timeboost_contract::{CommitteeMemberSol, KeyManager}; +use timeboost::{committee::CommitteeInfo, config::NodeConfig}; use timeboost_utils::types::logging; use tokio::{select, signal, time::sleep}; use tracing::{error, info}; @@ -91,40 +89,20 @@ async fn main() -> Result<()> { let dh_keypair = x25519::Keypair::from(config.keys.dh.secret.clone()); // syncing with contract to get peers keys and network addresses - let provider = ProviderBuilder::new().connect_http(config.chain.parent.rpc_url); - assert_eq!( - provider.get_chain_id().await?, - config.chain.parent.id, - "Parent chain RPC has mismatched chain_id" - ); - - let contract = KeyManager::new(config.chain.parent.key_manager_contract, &provider); - let members: Vec = contract - .getCommitteeById(cli.committee_id.into()) - .call() - .await? - .members; + let comm_info = CommitteeInfo::fetch( + config.chain.parent.rpc_url, + config.chain.parent.key_manager_contract, + cli.committee_id, + ) + .await?; info!(label = %config.keys.signing.public, committee_id = %cli.committee_id, "committee info synced"); - let peer_hosts_and_keys = members - .iter() - .map(|peer| { - let sig_key = multisig::PublicKey::try_from(peer.sigKey.as_ref()) - .expect("Failed to parse sigKey"); - let dh_key = - x25519::PublicKey::try_from(peer.dhKey.as_ref()).expect("Failed to parse dhKey"); - let sailfish_address = cliquenet::Address::try_from(peer.networkAddress.as_ref()) - .expect("Failed to parse networkAddress"); - (sig_key, dh_key, sailfish_address) - }) - .collect::>(); - let prom = Arc::new(PrometheusMetrics::default()); let sf_metrics = ConsensusMetrics::new(prom.as_ref()); let net_metrics = NetworkMetrics::new( "sailfish", prom.as_ref(), - peer_hosts_and_keys.iter().map(|(k, ..)| *k), + comm_info.signing_keys().iter().cloned(), ); let rbc_metrics = RbcMetrics::new(prom.as_ref()); let network = Network::create( @@ -132,19 +110,12 @@ async fn main() -> Result<()> { config.net.public.address.clone(), signing_keypair.public_key(), dh_keypair.clone(), - peer_hosts_and_keys.clone(), + comm_info.address_info(), net_metrics, ) .await?; - let committee = Committee::new( - UNKNOWN_COMMITTEE_ID, - peer_hosts_and_keys - .iter() - .map(|b| b.0) - .enumerate() - .map(|(i, key)| (i as u8, key)), - ); + let committee = comm_info.committee(); // If the stamp file exists we need to recover from a previous run. let recover = if cli.ignore_stamp { diff --git a/timeboost/src/binaries/timeboost.rs b/timeboost/src/binaries/timeboost.rs index a06b864c..d35a08fa 100644 --- a/timeboost/src/binaries/timeboost.rs +++ b/timeboost/src/binaries/timeboost.rs @@ -1,15 +1,12 @@ use std::path::PathBuf; -use alloy::providers::{Provider, ProviderBuilder}; use anyhow::{Context, Result, bail}; -use cliquenet::AddressableCommittee; use multisig::CommitteeId; -use multisig::{Committee, Keypair, x25519}; +use multisig::{Keypair, x25519}; +use timeboost::committee::CommitteeInfo; use timeboost::{Timeboost, TimeboostConfig}; use timeboost_builder::robusta; -use timeboost_contract::{CommitteeMemberSol, KeyManager}; -use timeboost_crypto::prelude::DkgEncKey; -use timeboost_types::{KeyStore, ThresholdKeyCell}; +use timeboost_types::ThresholdKeyCell; use tokio::select; use tokio::signal; use tokio::task::spawn; @@ -40,11 +37,6 @@ struct Cli { #[clap(long, default_value_t = true, action = clap::ArgAction::Set)] https_only: bool, - /// Path to committee config toml. - #[cfg(feature = "until")] - #[clap(long)] - committee: PathBuf, - /// The until value to use for the committee config. #[cfg(feature = "until")] #[clap(long, default_value_t = 1000)] @@ -78,99 +70,18 @@ async fn main() -> Result<()> { let dh_keypair = x25519::Keypair::from(node_config.keys.dh.secret.clone()); // syncing with contract to get peers keys and network addresses - let provider = ProviderBuilder::new().connect_http(node_config.chain.parent.rpc_url.clone()); - let chain_id = provider.get_chain_id().await?; - - assert_eq!( - chain_id, node_config.chain.parent.id, - "parent chain rpc has mismatched chain_id" - ); - - let contract = KeyManager::new(node_config.chain.parent.key_manager_contract, &provider); - - let members: Vec = contract - .getCommitteeById(cli.committee_id.into()) - .call() - .await? - .members; - + let comm_info = CommitteeInfo::fetch( + node_config.chain.parent.rpc_url.clone(), + node_config.chain.parent.key_manager_contract, + cli.committee_id, + ) + .await?; info!(label = %sign_keypair.public_key(), committee_id = %cli.committee_id, "committee info synced"); - let peer_hosts_and_keys = members - .iter() - .map(|peer| { - let sig_key = multisig::PublicKey::try_from(peer.sigKey.as_ref()) - .expect("Failed to parse sigKey"); - let dh_key = - x25519::PublicKey::try_from(peer.dhKey.as_ref()).expect("Failed to parse dhKey"); - let dkg_enc_key = DkgEncKey::from_bytes(peer.dkgKey.as_ref()) - .expect("Blackbox from_bytes should work"); - let sailfish_address = cliquenet::Address::try_from(peer.networkAddress.as_ref()) - .expect("Failed to parse networkAddress"); - (sig_key, dh_key, dkg_enc_key, sailfish_address) - }) - .collect::>(); - - let mut sailfish_peer_hosts_and_keys = Vec::new(); - let mut decrypt_peer_hosts_and_keys = Vec::new(); - let mut certifier_peer_hosts_and_keys = Vec::new(); - let mut dkg_enc_keys = Vec::new(); - - for (signing_key, dh_key, dkg_enc_key, sailfish_addr) in peer_hosts_and_keys.iter().cloned() { - sailfish_peer_hosts_and_keys.push((signing_key, dh_key, sailfish_addr.clone())); - decrypt_peer_hosts_and_keys.push(( - signing_key, - dh_key, - sailfish_addr.clone().with_offset(DECRYPTER_PORT_OFFSET), - )); - certifier_peer_hosts_and_keys.push(( - signing_key, - dh_key, - sailfish_addr.clone().with_offset(CERTIFIER_PORT_OFFSET), - )); - dkg_enc_keys.push(dkg_enc_key.clone()); - } - - let sailfish_committee = { - let c = Committee::new( - cli.committee_id, - sailfish_peer_hosts_and_keys - .iter() - .enumerate() - .map(|(i, (k, ..))| (i as u8, *k)), - ); - AddressableCommittee::new(c, sailfish_peer_hosts_and_keys.iter().cloned()) - }; - - let decrypt_committee = { - let c = Committee::new( - cli.committee_id, - decrypt_peer_hosts_and_keys - .iter() - .enumerate() - .map(|(i, (k, ..))| (i as u8, *k)), - ); - AddressableCommittee::new(c, decrypt_peer_hosts_and_keys.iter().cloned()) - }; - - let certifier_committee = { - let c = Committee::new( - cli.committee_id, - certifier_peer_hosts_and_keys - .iter() - .enumerate() - .map(|(i, (k, ..))| (i as u8, *k)), - ); - AddressableCommittee::new(c, certifier_peer_hosts_and_keys.iter().cloned()) - }; - - let key_store = KeyStore::new( - sailfish_committee.committee().clone(), - dkg_enc_keys - .into_iter() - .enumerate() - .map(|(i, k)| (i as u8, k)), - ); + let sailfish_committee = comm_info.sailfish_committee(); + let decrypt_committee = comm_info.decrypt_committee(); + let certifier_committee = comm_info.certifier_committee(); + let key_store = comm_info.dkg_key_store(); let is_recover = !cli.ignore_stamp && node_config.stamp.is_file(); @@ -183,8 +94,21 @@ async fn main() -> Result<()> { let pubkey = sign_keypair.public_key(); + // optionally fetch previous committee info + let cid = cli.committee_id; + let prev_comm = if cid > CommitteeId::default() { + let c = &node_config.chain.parent; + let prev_comm = + CommitteeInfo::fetch(c.rpc_url.clone(), c.key_manager_contract, cid - 1).await?; + Some(prev_comm) + } else { + None + }; + let config = TimeboostConfig::builder() .sailfish_committee(sailfish_committee) + .registered_blk(*comm_info.registered_block()) + .maybe_prev_committee(prev_comm) .decrypt_committee(decrypt_committee) .certifier_committee(certifier_committee) .sign_keypair(sign_keypair) @@ -246,9 +170,10 @@ async fn main() -> Result<()> { use tokio::time::sleep; use url::Url; - let committee = CommitteeConfig::read(&cli.committee) + let committee_conf = cli.config.with_file_name("committee.toml"); + let committee = CommitteeConfig::read(&committee_conf.to_str().unwrap()) .await - .with_context(|| format!("failed to read committee config {:?}", cli.committee))?; + .with_context(|| format!("failed to read committee config {:?}", committee_conf))?; let handle = { let Some(member) = committee diff --git a/timeboost/src/committee.rs b/timeboost/src/committee.rs new file mode 100644 index 00000000..ee5189c9 --- /dev/null +++ b/timeboost/src/committee.rs @@ -0,0 +1,203 @@ +//! Syncing committee info from the KeyManager contract + +use std::pin::Pin; + +use alloy::{ + eips::BlockNumberOrTag, + primitives::Address, + providers::{Provider, ProviderBuilder}, +}; +use anyhow::{Context as AnyhowContext, Result}; +use cliquenet::AddressableCommittee; +use futures::{Stream, StreamExt}; +use itertools::{Itertools, izip}; +use multisig::{Committee, CommitteeId, x25519}; +use timeboost_config::{CERTIFIER_PORT_OFFSET, DECRYPTER_PORT_OFFSET, ParentChain}; +use timeboost_contract::KeyManager::{self, CommitteeCreated}; +use timeboost_contract::provider::PubSubProvider; +use timeboost_crypto::prelude::DkgEncKey; +use timeboost_types::{BlockNumber, KeyStore, Timestamp}; +use tracing::error; +use url::Url; + +/// Type alias for the committee stream +pub type NewCommitteeStream = Pin>>; + +/// The committee info stored on the KeyManager contract, a subset of [`CommitteeConfig`] +/// Keys and hosts are ordered in the same as they were registered (with KeyId from 0..n) +#[derive(Debug, Clone)] +pub struct CommitteeInfo { + id: CommitteeId, + timestamp: Timestamp, + registered_blk: BlockNumber, + signing_keys: Vec, + dh_keys: Vec, + dkg_keys: Vec, + public_addresses: Vec, +} + +impl CommitteeInfo { + /// Fetch the committee info for `committee_id` from `key_manager_addr` on chain + pub async fn fetch( + rpc: Url, + key_manager_addr: Address, + committee_id: CommitteeId, + ) -> Result { + let provider = ProviderBuilder::new().connect_http(rpc); + Self::fetch_with(provider, key_manager_addr, committee_id).await + } + + pub(crate) async fn fetch_with( + provider: impl Provider, + key_manager_addr: Address, + committee_id: CommitteeId, + ) -> Result { + let contract = KeyManager::new(key_manager_addr, &provider); + let c = contract + .getCommitteeById(committee_id.into()) + .call() + .await?; + + let (signing_keys, dh_keys, dkg_keys, public_addresses) = c + .members + .iter() + .map(|m| { + let sig_key = multisig::PublicKey::try_from(m.sigKey.as_ref()) + .with_context(|| "Failed to parse sigKey bytes")?; + let dh_key = x25519::PublicKey::try_from(m.dhKey.as_ref()) + .with_context(|| "Failed to parse dhKey bytes")?; + let dkg_key = DkgEncKey::from_bytes(m.dkgKey.as_ref()) + .with_context(|| "Failed to parse dkgKey bytes")?; + let addr = cliquenet::Address::try_from(m.networkAddress.as_ref()) + .with_context(|| "Failed to parse networkAddress string")?; + Ok((sig_key, dh_key, dkg_key, addr)) + }) + .collect::>>()? + .into_iter() + .multiunzip(); + + Ok(Self { + id: committee_id, + timestamp: c.effectiveTimestamp.into(), + registered_blk: c.registeredBlockNumber.to::().into(), + signing_keys, + dh_keys, + dkg_keys, + public_addresses, + }) + } + + pub fn id(&self) -> CommitteeId { + self.id + } + + pub fn effective_timestamp(&self) -> Timestamp { + self.timestamp + } + + pub fn registered_block(&self) -> BlockNumber { + self.registered_blk + } + + pub fn signing_keys(&self) -> &[multisig::PublicKey] { + &self.signing_keys + } + + pub fn committee(&self) -> Committee { + Committee::new( + self.id, + self.signing_keys + .iter() + .enumerate() + .map(|(i, k)| (i as u8, *k)), + ) + } + + pub fn address_info( + &self, + ) -> impl Iterator { + izip!( + self.signing_keys.iter().cloned(), + self.dh_keys.iter().cloned(), + self.public_addresses.iter().cloned() + ) + } + + pub fn sailfish_committee(&self) -> AddressableCommittee { + AddressableCommittee::new(self.committee(), self.address_info()) + } + + pub fn decrypt_committee(&self) -> AddressableCommittee { + AddressableCommittee::new( + self.committee(), + izip!( + self.signing_keys.iter().cloned(), + self.dh_keys.iter().cloned(), + self.public_addresses + .iter() + .map(|a| a.clone().with_offset(DECRYPTER_PORT_OFFSET)), + ), + ) + } + + pub fn certifier_committee(&self) -> AddressableCommittee { + AddressableCommittee::new( + self.committee(), + izip!( + self.signing_keys.iter().cloned(), + self.dh_keys.iter().cloned(), + self.public_addresses + .iter() + .map(|a| a.clone().with_offset(CERTIFIER_PORT_OFFSET)), + ), + ) + } + + pub fn dkg_key_store(&self) -> KeyStore { + KeyStore::new( + self.committee(), + self.dkg_keys + .iter() + .enumerate() + .map(|(i, k)| (i as u8, k.clone())), + ) + } + + /// subscribe an event stream + pub async fn new_committee_stream( + provider: &PubSubProvider, + from_block: BlockNumber, + config: &ParentChain, + ) -> Result { + let events = provider + .event_stream::( + config.key_manager_contract, + BlockNumberOrTag::Number(*from_block), + ) + .await + .map_err(|e| { + error!("Failed to create CommitteeCreated stream: {:?}", e); + e + })?; + + let provider = provider.clone(); + let key_manager_contract = config.key_manager_contract; + let s = events.filter_map(move |log| { + let provider = provider.clone(); + async move { + let committee_id: CommitteeId = log.data().id.into(); + match CommitteeInfo::fetch_with(&*provider, key_manager_contract, committee_id) + .await + { + Ok(comm_info) => Some(comm_info), + Err(err) => { + error!(%committee_id, %err, "fail to fetch new `CommitteeInfo`"); + None + } + } + } + }); + + Ok(Box::pin(s)) + } +} diff --git a/timeboost/src/conf.rs b/timeboost/src/conf.rs index 92c2dfb5..56eba434 100644 --- a/timeboost/src/conf.rs +++ b/timeboost/src/conf.rs @@ -1,3 +1,4 @@ +use alloy::primitives::BlockNumber; use bon::Builder; use cliquenet::{Address, AddressableCommittee}; use multisig::{Keypair, x25519}; @@ -7,11 +8,19 @@ use timeboost_crypto::prelude::DkgDecKey; use timeboost_sequencer::SequencerConfig; use timeboost_types::{KeyStore, ThresholdKeyCell}; +use crate::committee::CommitteeInfo; + #[derive(Debug, Clone, Builder)] pub struct TimeboostConfig { /// The sailfish peers that this node will connect to. pub(crate) sailfish_committee: AddressableCommittee, + /// The block in which the current committee is registered + pub(crate) registered_blk: BlockNumber, + + /// Previous committee info stored on chain + pub(crate) prev_committee: Option, + /// The decrypt peers that this node will connect to. pub(crate) decrypt_committee: AddressableCommittee, @@ -74,6 +83,14 @@ impl TimeboostConfig { .sailfish_committee(self.sailfish_committee.clone()) .decrypt_committee((self.decrypt_committee.clone(), self.key_store.clone())) .recover(self.recover) + .maybe_previous_sailfish_committee( + self.prev_committee.as_ref().map(|c| c.sailfish_committee()), + ) + .maybe_previous_decrypt_committee( + self.prev_committee + .as_ref() + .map(|c| (c.decrypt_committee(), c.dkg_key_store())), + ) .leash_len(self.leash_len) .threshold_dec_key(self.threshold_dec_key.clone()) .chain_config(self.chain_config.clone()) diff --git a/timeboost/src/lib.rs b/timeboost/src/lib.rs index ea8fbdb8..d116872f 100644 --- a/timeboost/src/lib.rs +++ b/timeboost/src/lib.rs @@ -4,12 +4,15 @@ use std::iter::once; use std::sync::Arc; use ::metrics::prometheus::PrometheusMetrics; -use anyhow::Result; +use anyhow::{Result, anyhow}; +use committee::NewCommitteeStream; +use futures::StreamExt; use metrics::TimeboostMetrics; use multisig::PublicKey; use timeboost_builder::{Certifier, CertifierDown, SenderTaskDown, Submitter}; +use timeboost_contract::provider::PubSubProvider; use timeboost_sequencer::{Output, Sequencer}; -use timeboost_types::BundleVariant; +use timeboost_types::{BundleVariant, ConsensusTime}; use tokio::select; use tokio::sync::mpsc::{self, Receiver, Sender}; use tracing::{info, warn}; @@ -24,9 +27,11 @@ pub use timeboost_types as types; use crate::api::ApiServer; use crate::api::internal::GrpcServer; +use crate::committee::CommitteeInfo; use crate::forwarder::nitro_forwarder::NitroForwarder; pub mod api; +pub mod committee; pub mod forwarder; pub mod metrics; @@ -41,12 +46,16 @@ pub struct Timeboost { prometheus: Arc, nitro_forwarder: Option, submitter: Submitter, + // pubsub service (+backend) handle, disconnect on drop + _pubsub_provider: PubSubProvider, + events: NewCommitteeStream, } impl Timeboost { pub async fn new(cfg: TimeboostConfig) -> Result { let pro = Arc::new(PrometheusMetrics::default()); let met = Arc::new(TimeboostMetrics::new(&*pro)); + let seq = Sequencer::new(cfg.sequencer_config(), &*pro).await?; let blk = Certifier::new(cfg.certifier_config(), &*pro).await?; let sub = Submitter::new(cfg.submitter_config(), &*pro); @@ -58,6 +67,14 @@ impl Timeboost { None }; + let provider = PubSubProvider::new(cfg.chain_config.parent.ws_url.clone()).await?; + let events = CommitteeInfo::new_committee_stream( + &provider, + cfg.registered_blk.into(), + &cfg.chain_config.parent, + ) + .await?; + let (tx, rx) = mpsc::channel(100); Ok(Self { @@ -71,6 +88,8 @@ impl Timeboost { _metrics: met, nitro_forwarder, submitter: sub, + _pubsub_provider: provider, + events, }) } @@ -130,6 +149,29 @@ impl Timeboost { let e: CertifierDown = e; return Err(e.into()) } + }, + res = self.events.next() => match res { + Some(comm_info) => { + let cur = self.config.key_store.committee().id(); + let new_id = comm_info.id(); + + // contract ensures consecutive CommitteeId assignment + if new_id == cur + 1 { + info!(node = %self.label, committee_id = %new_id, current = %cur, "setting next committee"); + self.sequencer.set_next_committee( + ConsensusTime(comm_info.effective_timestamp()), + comm_info.sailfish_committee(), + comm_info.dkg_key_store() + ).await?; + } else { + warn!(node = %self.label, committee_id = %new_id, current = %cur, "ignored new CommitteeCreated event"); + continue; + } + }, + None => { + warn!(node = %self.label, "event subscription stream ended"); + return Err(anyhow!("contract event pubsub service prematurely shutdown")); + } } } } diff --git a/yapper/Cargo.toml b/yapper/Cargo.toml index 486da357..1b11fe8b 100644 --- a/yapper/Cargo.toml +++ b/yapper/Cargo.toml @@ -14,6 +14,7 @@ cliquenet = { path = "../cliquenet" } futures = { workspace = true } reqwest = { workspace = true } timeboost = { path = "../timeboost" } +timeboost-contract = { path = "../timeboost-contract" } timeboost-utils = { path = "../timeboost-utils" } tokio = { workspace = true } tracing = { workspace = true } diff --git a/yapper/src/config.rs b/yapper/src/config.rs index 6213a930..e7fee873 100644 --- a/yapper/src/config.rs +++ b/yapper/src/config.rs @@ -1,6 +1,7 @@ use bon::Builder; use cliquenet::Address; use reqwest::Url; +use timeboost::crypto::prelude::ThresholdEncKey; #[derive(Debug, Clone, Builder)] pub(crate) struct YapperConfig { @@ -12,4 +13,6 @@ pub(crate) struct YapperConfig { pub(crate) nitro_url: Option, /// Chain id for l2 chain pub(crate) chain_id: u64, + /// Use a given threshold encryption key, instead of fetching from nodes + pub(crate) threshold_enc_key: Option, } diff --git a/yapper/src/main.rs b/yapper/src/main.rs index 08f93243..44d4560f 100644 --- a/yapper/src/main.rs +++ b/yapper/src/main.rs @@ -6,11 +6,13 @@ use std::path::PathBuf; +use alloy::{primitives::Address, providers::ProviderBuilder}; use anyhow::{Context, Result}; use clap::Parser; use reqwest::Url; -use timeboost::config::CommitteeConfig; +use timeboost::{config::CommitteeConfig, crypto::prelude::ThresholdEncKey}; +use timeboost_contract::KeyManager; use timeboost_utils::types::logging::init_logging; use timeboost_utils::wait_for_live_peer; use tokio::signal::{ @@ -23,7 +25,6 @@ use crate::config::YapperConfig; use crate::yapper::Yapper; mod config; -mod enc_key; mod yapper; #[derive(Parser, Debug)] @@ -46,6 +47,14 @@ struct Cli { /// Nitro node url. #[clap(long)] nitro_url: Option, + + /// Parent chain where KeyManager is deployed + #[clap(long)] + parent_url: Option, + + /// KeyManager contract address on the parent chain + #[clap(long)] + key_manager_contract: Option
, } #[tokio::main] @@ -67,10 +76,25 @@ async fn main() -> Result<()> { addresses.push(node.http_api); } + let threshold_enc_key = if let Some(rpc) = cli.parent_url { + let km_addr = cli + .key_manager_contract + .expect("provide both parent chain and key manager contract"); + let provider = ProviderBuilder::new().connect_http(rpc); + let contract = KeyManager::new(km_addr, provider); + + Some(ThresholdEncKey::from_bytes( + &contract.thresholdEncryptionKey().call().await?.0, + )?) + } else { + None + }; + let config = YapperConfig::builder() .addresses(addresses) .tps(cli.tps) .maybe_nitro_url(cli.nitro_url) + .maybe_threshold_enc_key(threshold_enc_key) .chain_id(cli.chain_id) .build(); let yapper = Yapper::new(config).await?; diff --git a/yapper/src/yapper.rs b/yapper/src/yapper.rs index c31fbf4c..918da629 100644 --- a/yapper/src/yapper.rs +++ b/yapper/src/yapper.rs @@ -10,12 +10,13 @@ use alloy::{ use anyhow::{Context, Result}; use futures::future::join_all; use reqwest::{Client, Url}; -use timeboost::types::BundleVariant; +use timeboost::{crypto::prelude::ThresholdEncKey, types::BundleVariant}; +use timeboost_utils::enc_key::ThresholdEncKeyCellAccumulator; use timeboost_utils::load_generation::{TxInfo, make_bundle, make_dev_acct_bundle, tps_to_millis}; use tokio::time::interval; use tracing::warn; -use crate::{config::YapperConfig, enc_key::ThresholdEncKeyCellAccumulator}; +use crate::config::YapperConfig; /// This is the address of the prefunded dev account for nitro chain /// https://docs.arbitrum.io/run-arbitrum-node/run-local-full-chain-simulation#default-endpoints-and-addresses @@ -41,6 +42,7 @@ pub(crate) struct Yapper { interval: Duration, chain_id: u64, provider: Option, + enc_key: Option, } impl Yapper { @@ -67,12 +69,14 @@ impl Yapper { } else { None }; + Ok(Self { urls, interval: Duration::from_millis(tps_to_millis(cfg.tps)), client, provider, chain_id: cfg.chain_id, + enc_key: cfg.threshold_enc_key, }) } @@ -92,12 +96,15 @@ impl Yapper { warn!("failed to prepare txn"); continue; }; - let enc_key = match acc.enc_key().await { + let enc_key = match self.enc_key.as_ref() { Some(key) => key, - None => { - warn!("encryption key not available yet"); - continue; - } + None => match acc.enc_key().await { + Some(key) => key, + None => { + warn!("encryption key not available yet"); + continue; + } + }, }; let Ok(b) = make_dev_acct_bundle(enc_key, txn) else { warn!("failed to generate dev account bundle"); @@ -105,12 +112,15 @@ impl Yapper { }; b } else { - let enc_key = match acc.enc_key().await { + let enc_key = match self.enc_key.as_ref() { Some(key) => key, - None => { - warn!("encryption key not available yet"); - continue; - } + None => match acc.enc_key().await { + Some(key) => key, + None => { + warn!("encryption key not available yet"); + continue; + } + }, }; let Ok(b) = make_bundle(enc_key) else { warn!("failed to generate bundle");