diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 19d659bf..50126080 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -26,11 +26,7 @@ jobs: ~/.cargo/git/db/ target/ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - - name: Install Just - run: | - wget https://github.com/casey/just/releases/download/1.14.0/just-1.14.0-x86_64-unknown-linux-musl.tar.gz - tar -vxf just-1.14.0-x86_64-unknown-linux-musl.tar.gz just - sudo cp just /usr/bin/just + - uses: taiki-e/install-action@just - name: Install rustfmt for nightly run: rustup component add --toolchain nightly rustfmt - name: Install Foundry @@ -59,11 +55,7 @@ jobs: run: | sudo apt-get update sudo apt-get install -y protobuf-compiler - - name: Install Just - run: | - wget https://github.com/casey/just/releases/download/1.14.0/just-1.14.0-x86_64-unknown-linux-musl.tar.gz - tar -vxf just-1.14.0-x86_64-unknown-linux-musl.tar.gz just - sudo cp just /usr/bin/just + - uses: taiki-e/install-action@just - name: Install rustfmt for nightly run: rustup component add --toolchain nightly rustfmt - name: Install Foundry @@ -82,11 +74,7 @@ jobs: run: | sudo apt-get update sudo apt-get install -y protobuf-compiler - - name: Install Just - run: | - wget https://github.com/casey/just/releases/download/1.14.0/just-1.14.0-x86_64-unknown-linux-musl.tar.gz - tar -vxf just-1.14.0-x86_64-unknown-linux-musl.tar.gz just - sudo cp just /usr/bin/just + - uses: taiki-e/install-action@just - name: Install rustfmt for nightly run: rustup component add --toolchain nightly rustfmt - name: Install Foundry @@ -104,11 +92,7 @@ jobs: submodules: recursive fetch-depth: 0 - uses: docker/setup-buildx-action@v3 - - name: Install Just - run: | - wget https://github.com/casey/just/releases/download/1.14.0/just-1.14.0-x86_64-unknown-linux-musl.tar.gz - tar -vxf just-1.14.0-x86_64-unknown-linux-musl.tar.gz just - sudo cp just /usr/bin/just + - uses: taiki-e/install-action@just - name: Build Docker run: just build_docker @@ -124,11 +108,7 @@ jobs: run: | sudo apt-get update sudo apt-get install -y protobuf-compiler - - name: Install Just - run: | - wget https://github.com/casey/just/releases/download/1.14.0/just-1.14.0-x86_64-unknown-linux-musl.tar.gz - tar -vxf just-1.14.0-x86_64-unknown-linux-musl.tar.gz just - sudo cp just /usr/bin/just + - uses: taiki-e/install-action@just - name: Install rustfmt for nightly run: rustup component add --toolchain nightly rustfmt - name: Install Foundry @@ -144,6 +124,28 @@ jobs: - name: Test dynamic committee change run: just test-dyn-comm + netsim: + runs-on: ubuntu-latest + timeout-minutes: 25 + steps: + - uses: actions/checkout@v5 + with: + submodules: recursive + fetch-depth: 0 + - name: Show net devices + run: ip -o addr show scope global + - name: Install protobuf compiler + run: | + sudo apt-get update + sudo apt-get install -y protobuf-compiler + - uses: taiki-e/install-action@just + - name: Install rustfmt for nightly + run: rustup component add --toolchain nightly rustfmt + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + - name: Run network simulation + run: just forward-ipv4 true create-net netsim + contracts: runs-on: ubuntu-latest steps: @@ -151,11 +153,7 @@ jobs: with: submodules: recursive fetch-depth: 0 - - name: Install Just - run: | - wget https://github.com/casey/just/releases/download/1.14.0/just-1.14.0-x86_64-unknown-linux-musl.tar.gz - tar -vxf just-1.14.0-x86_64-unknown-linux-musl.tar.gz just - sudo cp just /usr/bin/just + - uses: taiki-e/install-action@just - name: Install rustfmt for nightly run: rustup component add --toolchain nightly rustfmt - name: Install Foundry @@ -175,11 +173,7 @@ jobs: run: | sudo apt-get update sudo apt-get install -y protobuf-compiler - - name: Install Just - run: | - wget https://github.com/casey/just/releases/download/1.14.0/just-1.14.0-x86_64-unknown-linux-musl.tar.gz - tar -vxf just-1.14.0-x86_64-unknown-linux-musl.tar.gz just - sudo cp just /usr/bin/just + - uses: taiki-e/install-action@just - name: Install rustfmt for nightly run: rustup component add --toolchain nightly rustfmt - name: Install Foundry @@ -203,4 +197,3 @@ jobs: - name: Docker Logs Sequencer 2 if: failure() run: docker logs test-node-sequencer_b-1 - diff --git a/.github/workflows/push-docker.yml b/.github/workflows/push-docker.yml index c7befdeb..b6b162ae 100644 --- a/.github/workflows/push-docker.yml +++ b/.github/workflows/push-docker.yml @@ -47,4 +47,4 @@ jobs: push: true platforms: linux/amd64 file: ./docker/timeboost.Dockerfile - tags: ghcr.io/espressosystems/timeboost:${{ env.TAG }} \ No newline at end of file + tags: ghcr.io/espressosystems/timeboost:${{ env.TAG }} diff --git a/Cargo.lock b/Cargo.lock index f0ce6f41..a15ad2f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6005,6 +6005,9 @@ name = "ipnet" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +dependencies = [ + "serde", +] [[package]] name = "iri-string" @@ -10893,16 +10896,20 @@ dependencies = [ "clap", "either", "futures", + "ipnet", + "jiff", "multisig", "prost 0.14.1", "quick_cache", "robusta", "rustix 1.1.2", "sailfish", + "serde", "timeboost", "timeboost-utils", "tokio", "tokio-util", + "toml 0.9.7", "tonic 0.14.2", "tracing", ] diff --git a/Cargo.toml b/Cargo.toml index f03dc22d..f0ecc02f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,6 @@ 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", "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 +68,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" +ipnet = { version = "2.11.0", features = ["serde"] } itertools = "0.14.0" jiff = { version = "0.2", default-features = false, features = ["serde", "std"] } minicbor = { version = "2.1.1", features = ["full"] } @@ -115,6 +115,20 @@ espresso-types = { git = "https://github.com/EspressoSystems/espresso-network.gi hotshot-query-service = { git = "https://github.com/EspressoSystems/espresso-network.git" } hotshot-types = { git = "https://github.com/EspressoSystems/espresso-network.git" } +[workspace.dependencies.alloy] +version = "1.0" +features = [ + "arbitrary", + "default", + "getrandom", + "k256", + "node-bindings", + "rlp", + "serde", + "signer-mnemonic", + "transport-ws" +] + [profile.test] codegen-units = 16 incremental = false diff --git a/justfile b/justfile index 3cdca3e6..63b5097e 100644 --- a/justfile +++ b/justfile @@ -1,8 +1,7 @@ -set export - export RUSTDOCFLAGS := '-D warnings' -LOG_LEVELS := "RUST_LOG=timeboost=debug,sailfish=debug,cliquenet=debug,tests=debug" +log_levels := "RUST_LOG=timeboost=debug,sailfish=debug,cliquenet=debug,tests=debug" +run_as_root := if env("CI", "") == "true" { "sudo" } else { "run0" } #################### ###BUILD COMMANDS### @@ -34,7 +33,7 @@ build-port-alloc: [private] build-test-utils: - cargo build --release -p test-utils + cargo build --release -p test-utils --all-features #################### ###CHECK COMMANDS### @@ -159,10 +158,10 @@ test-contracts: build-contracts forge test test_ci *ARGS: build-port-alloc - env {{LOG_LEVELS}} NO_COLOR=1 target/release/run \ + env {{log_levels}} NO_COLOR=1 target/release/run \ --spawn target/release/port-alloc \ cargo nextest run -- --workspace {{ARGS}} - env {{LOG_LEVELS}} NO_COLOR=1 cargo test --doc {{ARGS}} + env {{log_levels}} NO_COLOR=1 cargo test --doc {{ARGS}} test-individually: build-port-alloc @for pkg in $(cargo metadata --no-deps --format-version 1 | jq -r '.packages[].name'); do \ @@ -176,30 +175,28 @@ test-contract-deploy *ARGS: scripts/test-contract-deploy {{ARGS}} test-all: build_release build-test-utils - env RUST_LOG=timeboost_builder::submit=debug,block_checker=info,warn target/release/run \ + env RUST_LOG=timeboost_builder::submit=debug,block_checker=info,warn \ + target/release/run \ --verbose \ --timeout 120 \ --spawn "1:anvil --port 8545" \ --run "2:sleep 3" \ - --run "3:scripts/deploy-contract-local" \ - --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-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 + --run "3:scripts/deploy-contract-local test-configs/local/committee.toml http://localhost:8545" \ + --spawn "4:target/release/block-maker --bind 127.0.0.1:55000 -c test-configs/local/committee.toml" \ + --spawn "4:target/release/yapper -c test-configs/local/committee.toml" \ + --spawn "5:target/release/run-committee -c test-configs/local/" \ + target/release/block-checker -- -c test-configs/local -b 1000 test-dyn-comm: build_release_until build-test-utils - env RUST_LOG=sailfish=warn,yapper=error,timeboost=info,info target/release/run \ + env RUST_LOG=sailfish=warn,timeboost=info,info target/release/run \ --verbose \ --timeout 120 \ --spawn "1:anvil --port 8545" \ --run "2:sleep 2" \ - --run "3:scripts/deploy-contract-local" \ - --spawn "4:target/release/run-committee --configs test-configs/c0/ --committee-id 0 --until 2000" \ + --run "3:scripts/deploy-contract-local test-configs/local/committee.toml http://localhost:8545" \ + --spawn "4:target/release/run-committee -c test-configs/c0/ --until 2000" \ --run "5:target/release/mkconfig -n 4 \ + --committee-id 1 \ --public-addr 127.0.0.1:9000 \ --internal-addr 127.0.0.1:9003 \ --http-api 127.0.0.1:9004 \ @@ -226,11 +223,51 @@ test-dyn-comm: build_release_until build-test-utils -k 0x2bbf15bc655c4cc157b769cfcb1ea9924b9e1a35 \ -c test-configs/c1/committee.toml" \ --spawn "9:target/release/yapper \ - --keyset-file test-configs/c1/committee.toml \ + --config 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 \ + -c test-configs/c1/ \ --until 800 \ --required-decrypt-rounds 3 && rm -rf test-configs/c1 + +[linux] +forward-ipv4 val: build-test-utils + {{run_as_root}} target/release/net-setup system --forward-ipv4 {{val}} + +[linux] +create-net: build-test-utils + {{run_as_root}} target/release/net-setup create -c test-configs/linux/net.toml + +[linux] +delete-net: build-test-utils + {{run_as_root}} target/release/net-setup delete -c test-configs/linux/net.toml + +[linux] +netsim: build_release build-test-utils + #!/usr/bin/env bash + set -eo pipefail + function run_as_root { + if [ "$CI" == "true" ]; then + sudo --preserve-env=PATH,HOME,RUST_LOG "$@" + else + run0 --setenv=PATH --setenv=HOME --setenv=RUST_LOG "$@" + fi + } + export RUST_LOG=timeboost_builder::submit=debug,block_checker=info,warn + run_as_root target/release/run \ + --verbose \ + --timeout 120 \ + --clear-env \ + --env PATH \ + --env HOME \ + --env RUST_LOG \ + --uid $(id -u) \ + --gid $(id -g) \ + --spawn "1:anvil --host 11.0.1.0 --port 8545" \ + --run "2:sleep 3" \ + --run "3:scripts/deploy-contract-local test-configs/linux/committee.toml http://11.0.1.0:8545" \ + --spawn "4:target/release/block-maker --bind 11.0.1.0:55000 -c test-configs/linux/committee.toml" \ + --spawn "4:target/release/yapper -c test-configs/linux/committee.toml" \ + --spawn-as-root "5:target/release/run-committee -u $(id -u) -g $(id -g) -c test-configs/linux/" \ + target/release/block-checker -- -c test-configs/linux -b 200 diff --git a/scripts/deploy-contract-local b/scripts/deploy-contract-local index f52583e7..85e92fd0 100755 --- a/scripts/deploy-contract-local +++ b/scripts/deploy-contract-local @@ -2,16 +2,18 @@ set -euo pipefail +COMMITTEE="$1" +URL="$2" + MANAGER_MNEMONIC="attend year erase basket blind adapt stove broccoli isolate unveil acquire category" MANAGER_ACCOUNT_INDEX=0 MANAGER_ADDRESS="0x36561082951eed7ffD59cFD82D70570C57072d02" FAUCET_PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" -URL="http://localhost:8545" DEPLOYMENT_FILE=$(mktemp -t timeboost-deployment-XXXXX) # Fund manager account && create deployment file -cast send --value 1ether --private-key "$FAUCET_PRIVATE_KEY" "$MANAGER_ADDRESS" -env RUST_LOG=info cargo run --release --bin deploy -- \ +cast send --value 1ether -r "$URL" --private-key "$FAUCET_PRIVATE_KEY" "$MANAGER_ADDRESS" +env RUST_LOG=info target/release/deploy \ -m "$MANAGER_MNEMONIC" \ -i "$MANAGER_ACCOUNT_INDEX" \ -u "$URL" \ @@ -21,13 +23,13 @@ env RUST_LOG=info cargo run --release --bin deploy -- \ km_addr=$(sed -nr 's/^key_manager.*=.*"(.+)"/\1/p' "$DEPLOYMENT_FILE") # Update the contract -env RUST_LOG=info cargo run --release --bin register -- \ +env RUST_LOG=info target/release/register \ -a new-committee \ -m "$MANAGER_MNEMONIC" \ -i "$MANAGER_ACCOUNT_INDEX" \ -u "$URL" \ -k "$km_addr" \ - -c "test-configs/c0/committee.toml" + -c "$COMMITTEE" # Clean up the temporary deployment file rm -f "$DEPLOYMENT_FILE" diff --git a/scripts/run-timeboost-demo b/scripts/run-timeboost-demo index 64394812..d55544f5 100755 --- a/scripts/run-timeboost-demo +++ b/scripts/run-timeboost-demo @@ -142,7 +142,7 @@ done if $yapper; then cmd=(target/release/yapper --tps $tps - --keyset-file "$config_dir/committee.toml") + --config "$config_dir/committee.toml") if [ $nitro_url ]; then cmd+=(--nitro-url "$nitro_url") fi diff --git a/test-configs/c0/committee.toml b/test-configs/c0/committee.toml index 73a09462..56b14418 100644 --- a/test-configs/c0/committee.toml +++ b/test-configs/c0/committee.toml @@ -1,3 +1,4 @@ +id = 0 effective_timestamp = "2025-09-01T02:00:00Z" [[members]] diff --git a/test-configs/docker/committee.toml b/test-configs/docker/committee.toml index b4f58efd..2eebbd28 100644 --- a/test-configs/docker/committee.toml +++ b/test-configs/docker/committee.toml @@ -1,3 +1,4 @@ +id = 0 effective_timestamp = "2025-09-01T02:00:00Z" [[members]] diff --git a/test-configs/linux/committee.toml b/test-configs/linux/committee.toml new file mode 100644 index 00000000..636dd228 --- /dev/null +++ b/test-configs/linux/committee.toml @@ -0,0 +1,42 @@ +id = 0 +effective_timestamp = "2025-09-01T02:00:00Z" + +[[members]] +signing_key = "eiwaGN1NNaQdbnR9FsjKzUeLghQZsTLPjiL4RcQgfLoX" +dh_key = "AZrLbV37HAGhBWh49JHzup6Wfpu2AAGWGJJnxCDJibiY" +dkg_enc_key = "7PdmfTS45d2hTXB8NcrTmvDwUVBimpYBbrBaGnu3i5Ne65krVfUpbe7bYRHS3AEg7H" +public_address = "11.0.0.1:8000" +internal_api = "11.0.0.1:8003" +http_api = "11.0.0.1:8004" + +[[members]] +signing_key = "vGKKAxVNfkSCdn8qh36nXdSZqyhPq644sQBoeZtcEUCR" +dh_key = "FHTJAk6oyt3jefEp1ZrPEn2MkqRt2LibEFd57AnEUZdb" +dkg_enc_key = "7p1BtEz7WnFMt6Hr28X3Rngqza6i8hRoswhzZRFd6GzgkspLKHBfDocHP8DwzXiNiZ" +public_address = "11.0.0.10:8010" +internal_api = "11.0.0.10:8013" +http_api = "11.0.0.10:8014" + +[[members]] +signing_key = "264jMLf85hfufg4ck97Hw2jiL6i1PHNoGUqxUqfhtssaE" +dh_key = "63eYNKoW2PsWZFhHHj3eZwHTdPE7gEjEDM7gGeDf9Uaj" +dkg_enc_key = "62bnAAbU58zZUcGqy9JKGZRZHkm3g7JZB2DtJGQyChXQBPGvXSS6fF21yoxiVuD1eb" +public_address = "11.0.0.20:8020" +internal_api = "11.0.0.20:8023" +http_api = "11.0.0.20:8024" + +[[members]] +signing_key = "v6UBdLT5BvMhLW7iKv7M2xYeaW2SCAsnZ5PiSg6AaKfA" +dh_key = "Do2GmAexW5MUdD8nToDiBWGbDgk1AwXoxtLTyirDtKQh" +dkg_enc_key = "7aZBFZUEbXxFH9SiGJeUyjzas4mYJ1R13mTPsPeawVU7JFuocfvX9XsRT8qgr17RCe" +public_address = "11.0.0.30:8030" +internal_api = "11.0.0.30:8033" +http_api = "11.0.0.30:8034" + +[[members]] +signing_key = "tV66KknkDH47hRSNzwJtt7Q7EZtxVxQsNnUGoAJdDn6J" +dh_key = "HXaesvEGFiDgrVTix1fKzSLTarFexTZSJD6ymSrF7vPL" +dkg_enc_key = "6R69TzDg3jo1MTex9Uter9XQud458YPpSvXirkYdS295PV81CRvgz5jVWQbkCpZnYV" +public_address = "11.0.0.40:8040" +internal_api = "11.0.0.40:8043" +http_api = "11.0.0.40:8044" diff --git a/test-configs/linux/net.toml b/test-configs/linux/net.toml new file mode 100644 index 00000000..6eed44d1 --- /dev/null +++ b/test-configs/linux/net.toml @@ -0,0 +1,42 @@ +[nat] +device = "eth0" +cidr = "11.0.0.0/8" + +[bridge] +name = "bridge" +cidr = "11.0.1.0/16" + +[[device]] +node = "node_0" +name = "dev1" +cidr = "11.0.0.1/16" +delay = "50ms" +jitter = "50ms" + +[[device]] +node = "node_1" +name = "dev2" +cidr = "11.0.0.10/16" +delay = "50ms" +jitter = "50ms" + +[[device]] +node = "node_2" +name = "dev3" +cidr = "11.0.0.20/16" +delay = "50ms" +jitter = "50ms" + +[[device]] +node = "node_3" +name = "dev4" +cidr = "11.0.0.30/16" +delay = "50ms" +jitter = "50ms" + +[[device]] +node = "node_4" +name = "dev5" +cidr = "11.0.0.40/16" +delay = "50ms" +jitter = "50ms" diff --git a/test-configs/linux/node_0.toml b/test-configs/linux/node_0.toml new file mode 100644 index 00000000..76a9dbe6 --- /dev/null +++ b/test-configs/linux/node_0.toml @@ -0,0 +1,37 @@ +stamp = "/tmp/timeboost.0.stamp" + +[net.public] +address = "11.0.0.1:8000" +http-api = "11.0.0.1:8004" + +[net.internal] +address = "11.0.0.1:8003" +nitro = "11.0.1.0:55000" + +[keys.signing] +secret = "3hzb3bRzn3dXSV1iEVE6mU4BF2aS725s8AboRxLwULPp" +public = "eiwaGN1NNaQdbnR9FsjKzUeLghQZsTLPjiL4RcQgfLoX" + +[keys.dh] +secret = "BB3zUfFQGfw3sL6bpp1JH1HozK6ehEDmRGoiCpQH62rZ" +public = "AZrLbV37HAGhBWh49JHzup6Wfpu2AAGWGJJnxCDJibiY" + +[keys.dkg] +secret = "BW8gq8MARtDkSJL6daobPtGQm22TKkXdbLNrNGngNGTB" +public = "7PdmfTS45d2hTXB8NcrTmvDwUVBimpYBbrBaGnu3i5Ne65krVfUpbe7bYRHS3AEg7H" + +[chain] +namespace = 10101 + +[chain.parent] +id = 31337 +rpc-url = "http://11.0.1.0:8545/" +ws-url = "ws://11.0.1.0:8545/" +ibox-contract = "0xa0f3a1a4e2b2bcb7b48c8527c28098f207572ec1" +block-tag = "finalized" +key-manager-contract = "0x2bbf15bc655c4cc157b769cfcb1ea9924b9e1a35" + +[espresso] +base-url = "https://query.decaf.testnet.espresso.network/v1/" +websockets-base-url = "wss://query.decaf.testnet.espresso.network/v1/" +max-transaction-size = 1048576 diff --git a/test-configs/linux/node_1.toml b/test-configs/linux/node_1.toml new file mode 100644 index 00000000..fa3222e8 --- /dev/null +++ b/test-configs/linux/node_1.toml @@ -0,0 +1,37 @@ +stamp = "/tmp/timeboost.1.stamp" + +[net.public] +address = "11.0.0.10:8010" +http-api = "11.0.0.10:8014" + +[net.internal] +address = "11.0.0.10:8013" +nitro = "11.0.1.0:55000" + +[keys.signing] +secret = "FWJzNGvEjFS3h1N1sSMkcvvroWwjT5LQuGkGHu9JMAYs" +public = "vGKKAxVNfkSCdn8qh36nXdSZqyhPq644sQBoeZtcEUCR" + +[keys.dh] +secret = "4hjtciEvuoFVT55nAzvdP9E76r18QwntWwFoeginCGnP" +public = "FHTJAk6oyt3jefEp1ZrPEn2MkqRt2LibEFd57AnEUZdb" + +[keys.dkg] +secret = "ARtqWGmRWrBqZUr4MmiLaPgzjsiKp5USsC9iQNRMZYy4" +public = "7p1BtEz7WnFMt6Hr28X3Rngqza6i8hRoswhzZRFd6GzgkspLKHBfDocHP8DwzXiNiZ" + +[chain] +namespace = 10101 + +[chain.parent] +id = 31337 +rpc-url = "http://11.0.1.0:8545/" +ws-url = "ws://11.0.1.0:8545/" +ibox-contract = "0xa0f3a1a4e2b2bcb7b48c8527c28098f207572ec1" +block-tag = "finalized" +key-manager-contract = "0x2bbf15bc655c4cc157b769cfcb1ea9924b9e1a35" + +[espresso] +base-url = "https://query.decaf.testnet.espresso.network/v1/" +websockets-base-url = "wss://query.decaf.testnet.espresso.network/v1/" +max-transaction-size = 1048576 diff --git a/test-configs/linux/node_2.toml b/test-configs/linux/node_2.toml new file mode 100644 index 00000000..dc9006c2 --- /dev/null +++ b/test-configs/linux/node_2.toml @@ -0,0 +1,37 @@ +stamp = "/tmp/timeboost.2.stamp" + +[net.public] +address = "11.0.0.20:8020" +http-api = "11.0.0.20:8024" + +[net.internal] +address = "11.0.0.20:8023" +nitro = "11.0.1.0:55000" + +[keys.signing] +secret = "2yWTaC6MWvNva97t81cd9QX5qph68NnB1wRVwAChtuGr" +public = "264jMLf85hfufg4ck97Hw2jiL6i1PHNoGUqxUqfhtssaE" + +[keys.dh] +secret = "Fo2nYV4gE9VfoVW9bSySAJ1ZuKT461x6ovZnr3EecCZg" +public = "63eYNKoW2PsWZFhHHj3eZwHTdPE7gEjEDM7gGeDf9Uaj" + +[keys.dkg] +secret = "77r7T3En7NNQvRA81G5hLhJD3VpnigJdkPonX3oAwWkX" +public = "62bnAAbU58zZUcGqy9JKGZRZHkm3g7JZB2DtJGQyChXQBPGvXSS6fF21yoxiVuD1eb" + +[chain] +namespace = 10101 + +[chain.parent] +id = 31337 +rpc-url = "http://11.0.1.0:8545/" +ws-url = "ws://11.0.1.0:8545/" +ibox-contract = "0xa0f3a1a4e2b2bcb7b48c8527c28098f207572ec1" +block-tag = "finalized" +key-manager-contract = "0x2bbf15bc655c4cc157b769cfcb1ea9924b9e1a35" + +[espresso] +base-url = "https://query.decaf.testnet.espresso.network/v1/" +websockets-base-url = "wss://query.decaf.testnet.espresso.network/v1/" +max-transaction-size = 1048576 diff --git a/test-configs/linux/node_3.toml b/test-configs/linux/node_3.toml new file mode 100644 index 00000000..020e81e0 --- /dev/null +++ b/test-configs/linux/node_3.toml @@ -0,0 +1,37 @@ +stamp = "/tmp/timeboost.3.stamp" + +[net.public] +address = "11.0.0.30:8030" +http-api = "11.0.0.30:8034" + +[net.internal] +address = "11.0.0.30:8033" +nitro = "11.0.1.0:55000" + +[keys.signing] +secret = "CUpkbkn8bix7ZrbztPKJwu66MRpJrc1Wr2JfdrhetASk" +public = "v6UBdLT5BvMhLW7iKv7M2xYeaW2SCAsnZ5PiSg6AaKfA" + +[keys.dh] +secret = "5KpixkV7czZTDVh7nV7VL1vGk4uf4kjKidDWq34CJx1T" +public = "Do2GmAexW5MUdD8nToDiBWGbDgk1AwXoxtLTyirDtKQh" + +[keys.dkg] +secret = "7vWcVJDAhfSvmtm1L7KZvoD9agx6hyy9FvA75xWpjxK7" +public = "7aZBFZUEbXxFH9SiGJeUyjzas4mYJ1R13mTPsPeawVU7JFuocfvX9XsRT8qgr17RCe" + +[chain] +namespace = 10101 + +[chain.parent] +id = 31337 +rpc-url = "http://11.0.1.0:8545/" +ws-url = "ws://11.0.1.0:8545/" +ibox-contract = "0xa0f3a1a4e2b2bcb7b48c8527c28098f207572ec1" +block-tag = "finalized" +key-manager-contract = "0x2bbf15bc655c4cc157b769cfcb1ea9924b9e1a35" + +[espresso] +base-url = "https://query.decaf.testnet.espresso.network/v1/" +websockets-base-url = "wss://query.decaf.testnet.espresso.network/v1/" +max-transaction-size = 1048576 diff --git a/test-configs/linux/node_4.toml b/test-configs/linux/node_4.toml new file mode 100644 index 00000000..7998e6fd --- /dev/null +++ b/test-configs/linux/node_4.toml @@ -0,0 +1,37 @@ +stamp = "/tmp/timeboost.4.stamp" + +[net.public] +address = "11.0.0.40:8040" +http-api = "11.0.0.40:8044" + +[net.internal] +address = "11.0.0.40:8043" +nitro = "11.0.1.0:55000" + +[keys.signing] +secret = "6LMMEuoPRCkpDsnxnANCRCBC6JagdCHs2pjNicdmQpQE" +public = "tV66KknkDH47hRSNzwJtt7Q7EZtxVxQsNnUGoAJdDn6J" + +[keys.dh] +secret = "39wAn3bQzpn19oa8CiaNUFd8GekQAJMMuzrbp8Jt3FKz" +public = "HXaesvEGFiDgrVTix1fKzSLTarFexTZSJD6ymSrF7vPL" + +[keys.dkg] +secret = "GFvv2wcQmiGpk5rFp1FGpeUjnVUyZmGM9k8VHb1Jn7EG" +public = "6R69TzDg3jo1MTex9Uter9XQud458YPpSvXirkYdS295PV81CRvgz5jVWQbkCpZnYV" + +[chain] +namespace = 10101 + +[chain.parent] +id = 31337 +rpc-url = "http://11.0.1.0:8545/" +ws-url = "ws://11.0.1.0:8545/" +ibox-contract = "0xa0f3a1a4e2b2bcb7b48c8527c28098f207572ec1" +block-tag = "finalized" +key-manager-contract = "0x2bbf15bc655c4cc157b769cfcb1ea9924b9e1a35" + +[espresso] +base-url = "https://query.decaf.testnet.espresso.network/v1/" +websockets-base-url = "wss://query.decaf.testnet.espresso.network/v1/" +max-transaction-size = 1048576 diff --git a/test-configs/local/committee.toml b/test-configs/local/committee.toml index 73a09462..56b14418 100644 --- a/test-configs/local/committee.toml +++ b/test-configs/local/committee.toml @@ -1,3 +1,4 @@ +id = 0 effective_timestamp = "2025-09-01T02:00:00Z" [[members]] diff --git a/test-configs/nitro-ci-committee/committee.toml b/test-configs/nitro-ci-committee/committee.toml index e58e623b..f058606a 100644 --- a/test-configs/nitro-ci-committee/committee.toml +++ b/test-configs/nitro-ci-committee/committee.toml @@ -1,3 +1,4 @@ +id = 0 effective_timestamp = "2025-09-01T02:00:00Z" [[members]] diff --git a/test-utils/Cargo.toml b/test-utils/Cargo.toml index b76236d0..6952a4ec 100644 --- a/test-utils/Cargo.toml +++ b/test-utils/Cargo.toml @@ -10,17 +10,21 @@ default = [ "dep:bincode", "dep:bytes", "dep:either", + "dep:jiff", "dep:multisig", "dep:prost", "dep:robusta", "dep:quick_cache", "dep:sailfish", + "dep:serde", "dep:tokio-util", + "dep:toml", "dep:tonic", "dep:timeboost", "dep:timeboost-utils", "ports" ] +netns = ["dep:ipnet"] ports = ["dep:bincode", "dep:tokio-util"] [dependencies] @@ -35,12 +39,16 @@ alloy = { workspace = true, optional = true } bincode = { workspace = true, optional = true } bytes = { workspace = true, optional = true } either = { workspace = true, optional = true } +ipnet = { workspace = true, optional = true } +jiff = { workspace = true, optional = true } multisig = { path = "../multisig", optional = true } prost = { workspace = true, optional = true } quick_cache = { workspace = true, optional = true } robusta = { path = "../robusta", optional = true } sailfish = { path = "../sailfish", optional = true } +serde = { workspace = true, optional = true } tokio-util = { workspace = true, optional = true } +toml = { workspace = true, optional = true } tonic = { workspace = true, optional = true } timeboost = { path = "../timeboost", optional = true } timeboost-utils = { path = "../timeboost-utils", optional = true } @@ -69,3 +77,9 @@ path = "src/binaries/run.rs" [[bin]] name = "run-committee" path = "src/binaries/run-committee.rs" +required-features = ["netns"] + +[[bin]] +name = "net-setup" +path = "src/binaries/net-setup.rs" +required-features = ["netns"] diff --git a/test-utils/src/binaries/block-checker.rs b/test-utils/src/binaries/block-checker.rs index eeba4c67..7f8d3fc6 100644 --- a/test-utils/src/binaries/block-checker.rs +++ b/test-utils/src/binaries/block-checker.rs @@ -13,13 +13,7 @@ use tracing::{debug, info}; #[derive(Parser, Debug)] struct Args { #[clap(long, short)] - config: PathBuf, - - #[clap(long)] - committee: PathBuf, - - #[clap(long)] - committee_id: u64, + configs: PathBuf, #[clap(long, short)] blocks: usize, @@ -35,16 +29,16 @@ async fn main() -> Result<()> { let args = Args::parse(); let committees = { - let conf = CommitteeConfig::read(&args.committee).await?; + let conf = CommitteeConfig::read(args.configs.join("committee.toml")).await?; let mems = conf .members .into_iter() .enumerate() .map(|(i, m)| (i as u8, m.signing_key)); - CommitteeVec::<1>::new(Committee::new(args.committee_id, mems)) + CommitteeVec::<1>::new(Committee::new(conf.id, mems)) }; - let node = NodeConfig::read(&args.config).await?; + let node = NodeConfig::read(args.configs.join("node_0.toml")).await?; let conf = Config::builder() .https_only(args.https_only) diff --git a/test-utils/src/binaries/block-maker.rs b/test-utils/src/binaries/block-maker.rs index f15f4650..c66cd5d4 100644 --- a/test-utils/src/binaries/block-maker.rs +++ b/test-utils/src/binaries/block-maker.rs @@ -1,5 +1,5 @@ use std::convert::Infallible; -use std::net::Ipv4Addr; +use std::net::SocketAddr; use std::path::PathBuf; use std::process::exit; use std::sync::atomic::{AtomicU64, Ordering}; @@ -26,7 +26,7 @@ use tracing::error; #[derive(Parser, Debug)] struct Args { #[clap(long, short)] - port: u16, + bind: SocketAddr, #[clap(long, short)] committee: PathBuf, @@ -51,10 +51,10 @@ impl Service { } } - async fn serve(self, port: u16) -> Result<()> { + async fn serve(self, addr: SocketAddr) -> Result<()> { tonic::transport::Server::builder() .add_service(ForwardApiServer::new(self)) - .serve((Ipv4Addr::UNSPECIFIED, port).into()) + .serve(addr) .await .map_err(From::from) } @@ -126,5 +126,5 @@ async fn main() -> Result<()> { let uri: Uri = format!("http://{}", member.internal_api).parse()?; spawn(deliver(uri, tx.subscribe())); } - Service::new(tx).serve(args.port).await + Service::new(tx).serve(args.bind).await } diff --git a/test-utils/src/binaries/net-setup.rs b/test-utils/src/binaries/net-setup.rs new file mode 100644 index 00000000..9a415ac7 --- /dev/null +++ b/test-utils/src/binaries/net-setup.rs @@ -0,0 +1,272 @@ +//! This executable configures network namespaces according to a configuration file. +//! +//! It creates a bridge device, *N* namespaces and virtual ethernet device pairs, +//! with one end inside the namespace. It also applies a queuing discipline to +//! the egress traffic of each device using the configured delay and jitter +//! values. Finally, for each namespace a `resolv.conf` file is created in +//! `/etc/netns//` to ensure name resolution works inside the namespace. +//! +//! Here is a sample trace showing the command execution for the bridge device and +//! one namespace plus device, and finally the iptables setup. +//! +//! ``` +//! > ip link add bridge type bridge +//! > ip addr add 11.0.1.0/16 dev bridge +//! > ip link set bridge up +//! > ip netns add ns-dev1 +//! > ip link add dev-dev1 type veth peer name dev1 +//! > ip link set dev1 up +//! > ip link set dev1 master bridge +//! > ip link set dev-dev1 netns ns-dev1 +//! > ip netns exec ns-dev1 ip addr add 11.0.0.1/16 dev dev-dev1 +//! > ip netns exec ns-dev1 ip link set dev-dev1 up +//! > ip netns exec ns-dev1 ip link set lo up +//! > ip netns exec ns-dev1 ip route add default via 11.0.1.0 +//! > ip netns exec ns-dev1 tc qdisc add dev dev-dev1 root netem delay 50ms 50ms +//! > writing "/etc/netns/ns-dev1/resolv.conf" +//! ... +//! > iptables -t nat -A POSTROUTING -s 11.0.0.0/8 -o eth0 -j MASQUERADE +//! > iptables -I FORWARD -i eth0 -o bridge -j ACCEPT +//! > iptables -I FORWARD -i bridge -o eth0 -j ACCEPT +//! ``` +//! +//! Should the setup encounter an error, manual cleanup may be required: +//! +//! 1. To remove any `resolv.conf` files, remove the sub-directories in `/etc/netns` +//! 2. To remove the bridge device, enter (as root): `ip link delete bridge`. +//! 3. To remove a virtual ethernet device, enter (as root): `ip link delete ` +//! 4. To remove a namespace, enter (as root): `ip netns delete `. +//! 5. To remove iptables settings, enter (as root): +//! - `iptables -t nat -D POSTROUTING -s -o -j MASQUERADE` +//! - `iptables -D FORWARD -i -o bridge -j ACCEPT` +//! - `iptables -D FORWARD -o -i bridge -j ACCEPT` + +use anyhow::Result; + +#[cfg(not(target_os = "linux"))] +fn main() -> Result<()> { + Ok(()) +} + +#[cfg(target_os = "linux")] +fn main() -> Result<()> { + setup::go() +} + +#[cfg(target_os = "linux")] +#[rustfmt::skip] +mod setup { + use std::{fs, path::PathBuf}; + + use anyhow::{Result, ensure}; + use clap::Parser; + use ipnet::Ipv4Net; + use jiff::Span; + use test_utils::{ + net::{Config, DeviceConfig}, + process::run_command, + }; + + const TRACE: bool = true; + + pub fn go() -> Result<()> { + match Command::parse() { + Command::System { forward_ipv4 } => { + let v = u8::from(forward_ipv4); + run_command(TRACE, ["sysctl", "-w", &format!("net.ipv4.ip_forward={v}")])? + } + Command::Create { config } => { + let t = fs::read(&config)?; + let c: Config = toml::from_slice(&t)?; + let b = Bridge::new(&c.bridge.name, c.bridge.cidr); + b.create()?; + for d in c.device { + let dev = Device::new(&d); + dev.create(&b)?; + if !d.delay.is_zero() { + dev.delay(&d.delay, &d.jitter)? + } + if c.nat.is_some() { + dev.add_resolv_conf()? + } + } + if let Some(nat) = c.nat { + run_command(TRACE, ["iptables", + "-t", "nat", + "-A", "POSTROUTING", + "-s", &nat.cidr.to_string(), + "-o", &nat.device, + "-j", "MASQUERADE" + ])?; + run_command(TRACE, ["iptables", + "-I", "FORWARD", + "-i", &nat.device, + "-o", "bridge", + "-j", "ACCEPT" + ])?; + run_command(TRACE, ["iptables", + "-I", "FORWARD", + "-i", "bridge", + "-o", &nat.device, + "-j", "ACCEPT" + ])?; + } + } + Command::Delete { config } => { + let t = fs::read(&config)?; + let c: Config = toml::from_slice(&t)?; + let b = Bridge::new(&c.bridge.name, c.bridge.cidr); + for d in c.device { + let dev = Device::new(&d); + if c.nat.is_some() { + dev.del_resolv_conf()? + } + dev.delete()?; + } + if let Some(nat) = c.nat { + run_command(TRACE, ["iptables", + "-t", "nat", + "-D", "POSTROUTING", + "-s", &nat.cidr.to_string(), + "-o", &nat.device, + "-j", "MASQUERADE" + ])?; + run_command(TRACE, ["iptables", + "-D", "FORWARD", + "-i", &nat.device, + "-o", "bridge", + "-j", "ACCEPT" + ])?; + run_command(TRACE, ["iptables", + "-D", "FORWARD", + "-i", "bridge", + "-o", &nat.device, + "-j", "ACCEPT" + ])?; + } + b.delete()? + } + } + Ok(()) + } + + #[derive(Debug, Parser)] + enum Command { + System { + #[clap(long, action = clap::ArgAction::Set)] + forward_ipv4: bool, + }, + Create { + #[clap(long, short)] + config: PathBuf, + }, + Delete { + #[clap(long, short)] + config: PathBuf, + }, + } + + #[derive(Debug)] + struct Device { + space: String, + name: String, + dev: String, + cidr: Ipv4Net, + } + + impl Device { + fn new(cfg: &DeviceConfig) -> Self { + Self { + space: cfg.namespace(), + dev: cfg.device(), + cidr: cfg.cidr, + name: cfg.name.clone(), + } + } + + fn create(&self, b: &Bridge) -> Result<()> { + ensure!(b.net.contains(&self.cidr)); + run_command(TRACE, ["ip", "netns", "add", &self.space])?; + run_command(TRACE, ["ip", "link", "add", &self.dev, "type", "veth", "peer", "name", &self.name])?; + run_command(TRACE, ["ip", "link", "set", &self.name, "up"])?; + run_command(TRACE, ["ip", "link", "set", &self.name, "master", &b.name])?; + run_command(TRACE, ["ip", "link", "set", &self.dev, "netns", &self.space])?; + run_command(TRACE, ["ip", "netns", "exec", &self.space, "ip", "addr", "add", &self.cidr.to_string(), "dev", &self.dev])?; + run_command(TRACE, ["ip", "netns", "exec", &self.space, "ip", "link", "set", &self.dev, "up"])?; + run_command(TRACE, ["ip", "netns", "exec", &self.space, "ip", "link", "set", "lo", "up"])?; + run_command(TRACE, ["ip", "netns", "exec", &self.space, "ip", "route", "add", "default", "via", &b.net.addr().to_string()]) + } + + fn delay(&self, delay: &Span, jitter: &Span) -> Result<()> { + let d = format!("{}ms", delay.get_milliseconds()); + let j = format!("{}ms", jitter.get_milliseconds()); + run_command(TRACE, [ + "ip", "netns", "exec", &self.space, + "tc", "qdisc", "add", "dev", &self.dev, "root", "netem", "delay", &d, &j + ]) + } + + fn add_resolv_conf(&self) -> Result<()> { + const RESOLV_CONF: &str = "nameserver 1.1.1.1\nnameserver 8.8.8.8\n"; + + let dir = PathBuf::from(format!("/etc/netns/{}", self.space)); + if !dir.exists() { + if TRACE { + eprintln!("> creating {dir:?}") + } + fs::create_dir_all(&dir)? + } + let file = dir.join("resolv.conf"); + if TRACE { + eprintln!("> writing {file:?}") + } + fs::write(file, RESOLV_CONF)?; + Ok(()) + } + + fn del_resolv_conf(&self) -> Result<()> { + let dir = PathBuf::from(format!("/etc/netns/{}", self.space)); + if dir.exists() { + let file = dir.join("resolv.conf"); + if file.exists() { + if TRACE { + eprintln!("> removing {file:?}") + } + fs::remove_file(file)? + } + if TRACE { + eprintln!("> removing {dir:?}") + } + fs::remove_dir(dir)? + } + Ok(()) + } + + fn delete(self) -> Result<()> { + run_command(TRACE, ["ip", "link", "delete", &self.name])?; + run_command(TRACE, ["ip", "netns", "delete", &self.space]) + } + } + + #[derive(Debug)] + struct Bridge { + name: String, + net: Ipv4Net, + } + + impl Bridge { + fn new(name: &str, ip: Ipv4Net) -> Self { + Self { name: name.to_string(), net: ip } + } + + fn create(&self) -> Result<()> { + run_command(TRACE, ["ip", "link", "add", &self.name, "type", "bridge"])?; + run_command(TRACE, ["ip", "addr", "add", &self.net.to_string(), "dev", "bridge"])?; + run_command(TRACE, ["ip", "link", "set", &self.name, "up"]) + } + + fn delete(self) -> Result<()> { + run_command(TRACE, ["ip", "link", "delete", &self.name]) + } + } +} diff --git a/test-utils/src/binaries/run-committee.rs b/test-utils/src/binaries/run-committee.rs index bc20322a..d49847cd 100644 --- a/test-utils/src/binaries/run-committee.rs +++ b/test-utils/src/binaries/run-committee.rs @@ -1,8 +1,18 @@ -use std::{ffi::OsStr, path::PathBuf}; - -use anyhow::{Result, bail}; +use std::collections::BTreeMap; +use std::process::Command as StdCommand; +use std::{ + ffi::OsStr, + path::{Path, PathBuf}, +}; + +use anyhow::{Result, bail, ensure}; use clap::Parser; -use tokio::{fs::read_dir, process::Command}; +use test_utils::net::Config; +use timeboost::config::CommitteeConfig; +use tokio::{ + fs::{self, read_dir}, + process::Command, +}; use tokio_util::task::TaskTracker; #[derive(Parser, Debug)] @@ -10,13 +20,16 @@ struct Args { #[clap(long, short)] configs: PathBuf, - #[clap(long, short)] - committee_id: u64, - #[clap(long, short, default_value = "target/release/timeboost")] timeboost: PathBuf, - #[clap(long, short, default_value = "/tmp")] + #[clap(long, short)] + uid: Option, + + #[clap(long, short)] + gid: Option, + + #[clap(long, default_value = "/tmp")] tmp: PathBuf, #[clap(long)] @@ -42,37 +55,68 @@ async fn main() -> Result<()> { bail!("{:?} is not a file", args.timeboost) } - let mut commands = Vec::new(); + let committee = CommitteeConfig::read(args.configs.join("committee.toml")).await?; + + let mut netconf: Option = None; + let mut commands = BTreeMap::new(); let mut entries = read_dir(&args.configs).await?; while let Some(entry) = entries.next_entry().await? { - if Some(OsStr::new("toml")) != entry.path().extension() { - continue; - } - if Some(OsStr::new("committee.toml")) == entry.path().file_name() { - continue; + match ConfigType::read(&entry.path()) { + ConfigType::Network => { + ensure!(netconf.is_none()); + let bytes = fs::read(&entry.path()).await?; + netconf = Some(toml::from_slice(&bytes)?); + } + ConfigType::Node(name) => { + let mut cmd = StdCommand::new(args.timeboost.as_os_str()); + cmd.arg("--committee-id") + .arg(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.insert(name.to_string(), cmd); + } + ConfigType::Committee | ConfigType::Unknown => continue, } - let mut cmd = Command::new(args.timeboost.as_os_str()); - cmd.arg("--committee-id") - .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()); + } + + #[cfg(target_os = "linux")] + if let Some(conf) = netconf { + ensure!(conf.device.len() == commands.len()); + for d in conf.device { + let Some(c) = commands.get_mut(&d.node) else { + eprintln!("no command for device {} node {}", d.name, d.node); + continue; + }; + let mut cmd = StdCommand::new("ip"); + cmd.args(["netns", "exec", &d.namespace()]); + if let Some((uid, gid)) = args.uid.zip(args.gid) { + cmd.args([ + "setpriv", + "--reuid", + &uid.to_string(), + "--regid", + &gid.to_string(), + "--clear-groups", + ]); + } + cmd.arg(c.get_program()).args(c.get_args()); + *c = cmd; } - commands.push(cmd); } let tasks = TaskTracker::new(); - for mut cmd in commands { + for cmd in commands.into_values() { tasks.spawn(async move { - let mut child = cmd.spawn()?; + let mut child = Command::from(cmd).spawn()?; child.wait().await }); } @@ -82,3 +126,31 @@ async fn main() -> Result<()> { Ok(()) } + +enum ConfigType<'a> { + Committee, + Node(&'a str), + Network, + Unknown, +} + +impl<'a> ConfigType<'a> { + fn read(p: &'a Path) -> Self { + if p.extension() != Some(OsStr::new("toml")) { + return ConfigType::Unknown; + } + let Some(name) = p.file_stem().and_then(|n| n.to_str()) else { + return ConfigType::Unknown; + }; + if name.starts_with("node") { + return ConfigType::Node(name); + } + if name == "committee" { + return ConfigType::Committee; + } + if name == "net" { + return ConfigType::Network; + } + ConfigType::Unknown + } +} diff --git a/test-utils/src/binaries/run.rs b/test-utils/src/binaries/run.rs index 035f022b..6d5b5feb 100644 --- a/test-utils/src/binaries/run.rs +++ b/test-utils/src/binaries/run.rs @@ -3,7 +3,7 @@ use std::ops::{Deref, DerefMut}; use std::process::ExitStatus; use std::time::Duration; -use anyhow::{Result, anyhow, bail}; +use anyhow::{Result, anyhow, bail, ensure}; use clap::Parser; use futures::FutureExt; use rustix::process::{Pid, Signal, kill_process_group}; @@ -25,6 +25,20 @@ struct Args { #[clap(long, short, value_parser = parse_command_line)] spawn: Vec, + /// Commands to run as root to completion. + #[clap(long, short, value_parser = parse_command_line)] + run_as_root: Vec, + + /// Commands to run concurrently as root user. + #[clap(long, short, value_parser = parse_command_line)] + spawn_as_root: Vec, + + #[clap(long, short)] + env: Vec, + + #[clap(long)] + clear_env: bool, + /// Optional timeout main command in seconds. #[clap(long, short)] timeout: Option, @@ -32,6 +46,14 @@ struct Args { #[clap(long, short)] verbose: bool, + /// Optional user ID to run processes. + #[clap(long)] + uid: Option, + + /// Optional group ID to run processes. + #[clap(long)] + gid: Option, + /// Main command to execute. main: Vec, } @@ -39,37 +61,58 @@ struct Args { #[tokio::main] async fn main() -> Result<()> { let mut args = Args::parse(); + ensure!(!args.main.is_empty()); + args.run_as_root.iter_mut().for_each(|c| c.root = true); args.spawn.iter_mut().for_each(|c| c.sync = false); + args.spawn_as_root.iter_mut().for_each(|c| { + c.root = true; + c.sync = false + }); let mut commands = args.run; + commands.append(&mut args.run_as_root); commands.append(&mut args.spawn); + commands.append(&mut args.spawn_as_root); commands.sort(); let mut term = signal(SignalKind::terminate())?; let mut intr = signal(SignalKind::interrupt())?; let mut helpers = JoinSet::>::new(); - for commandline in commands { + for cmd in commands { + let uid = cmd.root.then_some(0).or(args.uid); + let gid = cmd.root.then_some(0).or(args.gid); if args.verbose { - if commandline.sync { - eprintln!("running command: {}", commandline.args) + if cmd.sync { + eprintln!("running command: {}", cmd.args) } else { - eprintln!("spawning command: {}", commandline.args) + eprintln!("spawning command: {}", cmd.args) } } - - if commandline.sync { - let mut pg = ProcessGroup::spawn(commandline.args.split_whitespace())?; + if cmd.sync { + let mut pg = ProcessGroup::spawn( + uid, + gid, + args.clear_env, + &args.env, + cmd.args.split_whitespace(), + )?; let status = select! { s = pg.wait() => s?, _ = term.recv() => return Ok(()), _ = intr.recv() => return Ok(()), }; if !status.success() { - bail!("{:?} failed with {:?}", commandline.args, status.code()); + bail!("{:?} failed with {:?}", cmd.args, status.code()); } } else { - let mut pg = ProcessGroup::spawn(commandline.args.split_whitespace())?; + let mut pg = ProcessGroup::spawn( + uid, + gid, + args.clear_env, + &args.env, + cmd.args.split_whitespace(), + )?; helpers.spawn(async move { let status = pg.wait().await?; Ok(status) @@ -81,7 +124,7 @@ async fn main() -> Result<()> { eprintln!("spawning command: {}", args.main.join(" ")) } - let mut main = ProcessGroup::spawn(args.main)?; + let mut main = ProcessGroup::spawn(args.uid, args.gid, args.clear_env, &args.env, args.main)?; let timeout = if let Some(d) = args.timeout { sleep(Duration::from_secs(d)).boxed() @@ -113,6 +156,7 @@ struct Commandline { prio: u8, args: String, sync: bool, + root: bool, } fn parse_command_line(s: &str) -> Result { @@ -121,6 +165,7 @@ fn parse_command_line(s: &str) -> Result { prio: p.parse()?, args: a.to_string(), sync: true, + root: false, }) } @@ -128,7 +173,13 @@ fn parse_command_line(s: &str) -> Result { struct ProcessGroup(Child, Pid); impl ProcessGroup { - fn spawn(it: I) -> Result + fn spawn( + uid: Option, + gid: Option, + clear: bool, + env: &[String], + it: I, + ) -> Result where I: IntoIterator, S: Into + AsRef, @@ -159,6 +210,23 @@ impl ProcessGroup { } } cmd.process_group(0); + if let Some(id) = uid { + cmd.uid(id); + } + if let Some(id) = gid { + cmd.gid(id); + } + if clear { + cmd.env_clear(); + for e in env { + match std::env::var(e) { + Ok(v) => { + cmd.env(e, v); + } + Err(err) => eprintln!("error getting env var {e}: {err}"), + } + } + } let child = cmd.spawn()?; let id = child.id().ok_or_else(|| anyhow!("child already exited"))?; let pid = Pid::from_raw(id.try_into()?).ok_or_else(|| anyhow!("invalid pid"))?; diff --git a/test-utils/src/lib.rs b/test-utils/src/lib.rs index e7af0902..1cfe2d0a 100644 --- a/test-utils/src/lib.rs +++ b/test-utils/src/lib.rs @@ -1,2 +1,7 @@ +pub mod process; + #[cfg(feature = "ports")] pub mod ports; + +#[cfg(feature = "netns")] +pub mod net; diff --git a/test-utils/src/net.rs b/test-utils/src/net.rs new file mode 100644 index 00000000..279b5507 --- /dev/null +++ b/test-utils/src/net.rs @@ -0,0 +1,45 @@ +use ipnet::Ipv4Net; +use jiff::Span; +use serde::Deserialize; + +#[derive(Deserialize)] +pub struct Config { + pub bridge: BridgeConfig, + pub device: Vec, + pub nat: Option, +} + +#[derive(Deserialize)] +pub struct BridgeConfig { + pub name: String, + pub cidr: Ipv4Net, +} + +#[derive(Deserialize)] +pub struct DeviceConfig { + pub node: String, + pub name: String, + pub cidr: Ipv4Net, + + #[serde(default)] + pub delay: Span, + + #[serde(default)] + pub jitter: Span, +} + +#[derive(Deserialize)] +pub struct NatConfig { + pub device: String, + pub cidr: Ipv4Net, +} + +impl DeviceConfig { + pub fn namespace(&self) -> String { + format!("ns-{}", self.name) + } + + pub fn device(&self) -> String { + format!("dev-{}", self.name) + } +} diff --git a/test-utils/src/process.rs b/test-utils/src/process.rs new file mode 100644 index 00000000..751714da --- /dev/null +++ b/test-utils/src/process.rs @@ -0,0 +1,30 @@ +use anyhow::{Result, anyhow, bail}; +use std::{ffi::OsStr, iter::once, process::Command}; + +pub fn run_command(trace: bool, it: I) -> Result<()> +where + I: IntoIterator, + S: AsRef, +{ + let mut args = it.into_iter(); + let exe = args.next().ok_or_else(|| anyhow!("invalid command"))?; + let mut cmd = Command::new(exe); + for a in args { + cmd.arg(a); + } + if trace { + eprintln!( + "> {}", + once(cmd.get_program()) + .chain(cmd.get_args()) + .map(|os| os.to_string_lossy()) + .collect::>() + .join(" ") + ); + } + let status = cmd.status()?; + if !status.success() { + bail!("command not successful, status = {status:?}") + } + Ok(()) +} diff --git a/timeboost-config/src/binaries/mkconfig.rs b/timeboost-config/src/binaries/mkconfig.rs index 78f5a3de..fdb5d485 100644 --- a/timeboost-config/src/binaries/mkconfig.rs +++ b/timeboost-config/src/binaries/mkconfig.rs @@ -11,7 +11,7 @@ use ark_std::rand::SeedableRng as _; use clap::{Parser, ValueEnum}; use cliquenet::Address; use jiff::{SignedDuration, Timestamp}; -use multisig::x25519; +use multisig::{CommitteeId, x25519}; use secp256k1::rand::SeedableRng as _; use timeboost_config::{ChainConfig, ParentChain}; use timeboost_config::{ @@ -35,6 +35,9 @@ struct Args { #[clap(long)] seed: Option, + #[clap(long)] + committee_id: CommitteeId, + /// The effective timestamp for this new committee. /// /// The timestamp format corresponds to RFC 3339, section 5.6, for example: @@ -235,6 +238,7 @@ impl Args { } let committee_config = CommitteeConfig { + id: self.committee_id, effective_timestamp: self.timestamp.0, members, }; diff --git a/timeboost-config/src/committee.rs b/timeboost-config/src/committee.rs index 4feaaca7..e28734f6 100644 --- a/timeboost-config/src/committee.rs +++ b/timeboost-config/src/committee.rs @@ -2,7 +2,7 @@ use core::fmt; use std::{path::Path, str::FromStr}; use cliquenet::Address; -use multisig::x25519; +use multisig::{CommitteeId, x25519}; use serde::{Deserialize, Serialize}; use timeboost_crypto::prelude::DkgEncKey; @@ -10,6 +10,7 @@ use crate::ConfigError; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CommitteeConfig { + pub id: CommitteeId, pub effective_timestamp: jiff::Timestamp, #[serde(default)] pub members: Vec, diff --git a/yapper/src/main.rs b/yapper/src/main.rs index 44d4560f..c163f732 100644 --- a/yapper/src/main.rs +++ b/yapper/src/main.rs @@ -32,8 +32,8 @@ struct Cli { /// Path to file containing the committee member's public info. /// /// The file contains backend urls and public key material. - #[clap(long)] - keyset_file: PathBuf, + #[clap(long, short)] + config: PathBuf, /// Specify how many transactions per second to send to each node #[clap(long, short, default_value_t = 100)] @@ -65,9 +65,9 @@ async fn main() -> Result<()> { let cli = Cli::parse(); // Unpack the keyset file which has the urls - let keyset = CommitteeConfig::read(&cli.keyset_file) + let keyset = CommitteeConfig::read(&cli.config) .await - .with_context(|| format!("opening the keyfile at path {:?}", cli.keyset_file))?; + .with_context(|| format!("opening the config at path {:?}", cli.config))?; let mut addresses = Vec::new(); for node in keyset.members {