diff --git a/crates/malachite-app/src/handlers/restream_proposal.rs b/crates/malachite-app/src/handlers/restream_proposal.rs index 6b7323b..aa87dc2 100644 --- a/crates/malachite-app/src/handlers/restream_proposal.rs +++ b/crates/malachite-app/src/handlers/restream_proposal.rs @@ -32,9 +32,9 @@ use crate::store::repositories::UndecidedBlocksRepository; /// Handles the `RestreamProposal` message from the consensus engine. /// /// This is called when the consensus engine requests to restream a proposal for a specific height and round. -/// If a valid round is provided, the application first fetches the block from the valid round, -/// updates its round to the new round, and stores it. Then, it fetches the -/// block for the specified height and round, streams the proposal parts, and sends them over the network. +/// The block is looked up by height and block hash (ignoring round), so it will be found +/// regardless of which round it was originally stored under. The block's round and valid_round +/// are updated to match the current proposal context before restreaming. /// /// ## Errors /// - If no block is found for the specified height and round @@ -107,18 +107,17 @@ async fn get_block_to_restream( valid_round: Round, block_hash: BlockHash, ) -> eyre::Result> { - if valid_round.is_defined() - && let Some(mut block) = undecided_blocks - .get(height, valid_round, block_hash) - .await - .wrap_err_with(|| { - format!( - "Failed to fetch block from valid round for restreaming it \ - (height={height}, valid_round={valid_round}, block_hash={block_hash})" - ) - })? - { - // Update the block for the new round and store it + let block = undecided_blocks + .get_first(height, block_hash) + .await + .wrap_err_with(|| { + format!( + "Failed to fetch block for restreaming \ + (height={height}, round={round}, block_hash={block_hash})" + ) + })?; + + if let Some(mut block) = block { block.round = round; block.valid_round = valid_round; @@ -127,24 +126,14 @@ async fn get_block_to_restream( .await .wrap_err_with(|| { format!( - "Failed to store updated undecided block from valid round before restreaming it \ - (height={height}, valid_round={valid_round}, block_hash={block_hash})" - ) - })?; - - Ok(Some(block)) - } else { - let block_to_restream = undecided_blocks - .get(height, round, block_hash) - .await - .wrap_err_with(|| { - format!( - "Failed to fetch block from round for restreaming it \ + "Failed to store updated block before restreaming \ (height={height}, round={round}, block_hash={block_hash})" ) })?; - Ok(block_to_restream) + Ok(Some(block)) + } else { + Ok(None) } } @@ -179,25 +168,22 @@ mod tests { } #[tokio::test] - async fn get_block_from_valid_round_and_store_update() { + async fn get_block_found_and_updated() { let mut mock_repo = MockUndecidedBlocksRepository::new(); let height = Height::new(10); - let round = Round::new(5); // The new round we are proposing in - let valid_round = Round::new(3); // The previous round where we saw the block + let round = Round::new(5); + let valid_round = Round::new(3); let block_hash = BlockHash::default(); - // Original block proposed in round 3 - let original_block = create_dummy_block(height, valid_round, Round::Nil); + let original_block = create_dummy_block(height, Round::new(0), Round::Nil); - // Expectation: Fetch from valid_round (3) mock_repo - .expect_get() - .with(eq(height), eq(valid_round), eq(block_hash)) + .expect_get_first() + .with(eq(height), eq(block_hash)) .times(1) - .returning(move |_, _, _| Ok(Some(original_block.clone()))); + .returning(move |_, _| Ok(Some(original_block.clone()))); - // Expectation: Store the block updated with the NEW round (5) and valid_round (3) mock_repo .expect_store() .withf(move |b| b.round == round && b.valid_round == valid_round) @@ -207,46 +193,13 @@ mod tests { let result = get_block_to_restream(&mock_repo, height, round, valid_round, block_hash).await; - assert!(result.is_ok()); - let block = result.unwrap(); - assert!(block.is_some()); - - let b = block.unwrap(); - assert_eq!(b.round, round); // Ensure returned block has updated round - assert_eq!(b.valid_round, valid_round); - } - - #[tokio::test] - async fn get_block_no_valid_round_fetch_current() { - let mut mock_repo = MockUndecidedBlocksRepository::new(); - - let height = Height::new(10); - let round = Round::new(5); - let valid_round = Round::Nil; // No valid round - let block_hash = BlockHash::default(); - - // A block proposed for the first time at round 5, no valid round - let current_block = create_dummy_block(height, round, valid_round); - - // Expectation: we are restreaming this block because we received it at round 5. - mock_repo - .expect_get() - .with(eq(height), eq(round), eq(block_hash)) - .times(1) - .returning(move |_, _, _| Ok(Some(current_block.clone()))); - - // Expectation: Store should NOT be called - mock_repo.expect_store().times(0); - - let result = - get_block_to_restream(&mock_repo, height, round, valid_round, block_hash).await; - - assert!(result.is_ok()); - assert_eq!(result.unwrap().unwrap().round, round); + let block = result.unwrap().expect("block should be found"); + assert_eq!(block.round, round); + assert_eq!(block.valid_round, valid_round); } #[tokio::test] - async fn fallback_when_valid_round_block_missing() { + async fn get_block_not_found() { let mut mock_repo = MockUndecidedBlocksRepository::new(); let height = Height::new(10); @@ -254,32 +207,16 @@ mod tests { let valid_round = Round::new(3); let block_hash = BlockHash::default(); - let block_at_current = create_dummy_block(height, round, valid_round); - - // 1. First fetch at valid_round returns None - // The proposed value can be valid for the proposer but not for us. - mock_repo - .expect_get() - .with(eq(height), eq(valid_round), eq(block_hash)) - .times(1) - .returning(|_, _, _| Ok(None)); - - // 2. Fallback: fetch at current round - // Since it was restreamed by the proposer, we have it as current's round value. mock_repo - .expect_get() - .with(eq(height), eq(round), eq(block_hash)) + .expect_get_first() + .with(eq(height), eq(block_hash)) .times(1) - .returning(move |_, _, _| Ok(Some(block_at_current.clone()))); - - // Store should NOT be called in the fallback path - mock_repo.expect_store().times(0); + .returning(|_, _| Ok(None)); let result = get_block_to_restream(&mock_repo, height, round, valid_round, block_hash).await; - assert!(result.is_ok()); - assert!(result.unwrap().is_some()); + assert!(result.unwrap().is_none()); } #[tokio::test] @@ -290,8 +227,8 @@ mod tests { let valid_round = Round::Nil; mock_repo - .expect_get() - .returning(|_, _, _| Err(std::io::Error::other("DB connection failed"))); + .expect_get_first() + .returning(|_, _| Err(std::io::Error::other("DB connection failed"))); let result = get_block_to_restream(&mock_repo, height, round, valid_round, BlockHash::default()) diff --git a/crates/quake/src/infra/remote.rs b/crates/quake/src/infra/remote.rs index f725035..4f6de0f 100644 --- a/crates/quake/src/infra/remote.rs +++ b/crates/quake/src/infra/remote.rs @@ -215,7 +215,7 @@ impl RemoteInfra { .map(|node| self.node_private_ip(node).map(String::as_str)) .collect::>>()?; - let pssh_cmd = format!("./pssh.sh \"{cmd}\" {}", node_ips.join(" ")); + let pssh_cmd = format!("./pssh.sh '{cmd}' {}", node_ips.join(" ")); self.ssh_cc(&pssh_cmd, false) .wrap_err_with(|| format!("Failed to run '{cmd}' on {nodes:?}")) } diff --git a/crates/quake/terraform/cc-data.yaml b/crates/quake/terraform/cc-data.yaml index 9224387..6817f74 100644 --- a/crates/quake/terraform/cc-data.yaml +++ b/crates/quake/terraform/cc-data.yaml @@ -39,23 +39,23 @@ write_files: # Usage: pssh.sh "command" ip1 ip2 ip3 ... # Executes command on all IPs in parallel, fails if any command fails. set -euo pipefail - CMD="$$1" + CMD="$1" shift SSH_OPTS="-o StrictHostKeyChecking=no -o LogLevel=ERROR" USER="${username}" - TMPDIR=$$(mktemp -d) - trap "rm -rf $$TMPDIR" EXIT + TMPDIR=$(mktemp -d) + trap "rm -rf $TMPDIR" EXIT i=0 - for IP in "$$@"; do - (ssh $$SSH_OPTS $$USER@$$IP "$$CMD"; echo $$? > "$$TMPDIR/$$i") & - ((i++)) + for IP in "$@"; do + (ssh $SSH_OPTS $USER@$IP "$CMD"; echo $? > "$TMPDIR/$i") & + i="$((i+1))" done wait FAILED=0 - for f in "$$TMPDIR"/*; do - [ -f "$$f" ] && [ "$$(cat "$$f")" != "0" ] && FAILED=1 + for f in "$TMPDIR"/*; do + [ -f "$f" ] && [ "$(cat "$f")" != "0" ] && FAILED=1 done - exit $$FAILED + exit $FAILED runcmd: # Set up 4 GiB swap to prevent hard OOM kills (dd instead of fallocate for XFS compatibility) - dd if=/dev/zero of=/swapfile bs=1M count=4096 diff --git a/deployments/blockscout.yaml b/deployments/blockscout.yaml new file mode 100644 index 0000000..80379a3 --- /dev/null +++ b/deployments/blockscout.yaml @@ -0,0 +1,72 @@ +version: '3.9' + +name: blockscout + +services: + proxy: + image: nginx + container_name: proxy + extra_hosts: + - 'host.docker.internal:host-gateway' + volumes: + - "../deployments/monitoring/config-blockscout/proxy:/etc/nginx/templates:ro" + environment: + BACK_PROXY_PASS: ${BACK_PROXY_PASS:-http://backend:4000} + FRONT_PROXY_PASS: ${FRONT_PROXY_PASS:-http://frontend:3000} + ports: + - target: 80 + published: 80 + + frontend: + image: ghcr.io/blockscout/frontend:${BLOCKSCOUT_FRONTEND_TAG:-latest} + pull_policy: always + restart: always + container_name: 'frontend' + volumes: + - ../deployments/monitoring/config-blockscout/frontend/.curlrc:/home/nextjs/.curlrc:ro + env_file: + - ../deployments/monitoring/config-blockscout/frontend/frontend.env + + backend: + image: ghcr.io/blockscout/${BLOCKSCOUT_REPO:-blockscout}:${BLOCKSCOUT_TAG:-latest} + pull_policy: always + restart: always + stop_grace_period: 5m + container_name: 'backend' + command: sh -c "bin/blockscout eval \"Elixir.Explorer.ReleaseTasks.create_and_migrate()\" && bin/blockscout start" + extra_hosts: + - 'host.docker.internal:host-gateway' + env_file: + - ../deployments/monitoring/config-blockscout/backend.env + volumes: + - ../deployments/volumes/blockscout/logs/:/app/logs/ + - ../deployments/volumes/blockscout/dets/:/app/dets/ + + db-init: + image: postgres:17 + entrypoint: + - sh + - -c + - | + chown -R 2000:2000 /var/lib/postgresql/data + + db: + image: postgres:17 + user: 2000:2000 + shm_size: 256m + restart: always + container_name: 'db' + command: postgres -c 'max_connections=200' -c 'client_connection_check_interval=60000' + environment: + POSTGRES_DB: 'blockscout' + POSTGRES_USER: 'blockscout' + POSTGRES_HOST_AUTH_METHOD: 'trust' + ports: + - target: 5432 + published: 7432 + healthcheck: + test: ["CMD-SHELL", "pg_isready -U blockscout -d blockscout"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s \ No newline at end of file diff --git a/deployments/monitoring/config-blockscout/backend.env b/deployments/monitoring/config-blockscout/backend.env new file mode 100644 index 0000000..d903db1 --- /dev/null +++ b/deployments/monitoring/config-blockscout/backend.env @@ -0,0 +1,35 @@ +ETHEREUM_JSONRPC_VARIANT=geth +DATABASE_URL=postgresql://blockscout:@db:5432/blockscout +ETHEREUM_JSONRPC_TRANSPORT=http +ETHEREUM_JSONRPC_DISABLE_ARCHIVE_BALANCES=false +SECRET_KEY_BASE=0000000000000000000000000000000000000000000000000000000000000000 +PORT=4000 +COIN_NAME= +COIN= +DISABLE_MARKET=true +POOL_SIZE=80 +POOL_SIZE_API=10 +ECTO_USE_SSL=false +HEART_BEAT_TIMEOUT=30 +RELEASE_LINK= +ADMIN_PANEL_ENABLED=false +API_V1_READ_METHODS_DISABLED=false +API_V1_WRITE_METHODS_DISABLED=false +TXS_STATS_DAYS_TO_COMPILE_AT_INIT=10 +COIN_BALANCE_HISTORY_DAYS=90 +RE_CAPTCHA_DISABLED=false +MICROSERVICE_SC_VERIFIER_TYPE=eth_bytecode_db +MICROSERVICE_VISUALIZE_SOL2UML_ENABLED=true +MICROSERVICE_VISUALIZE_SOL2UML_URL=http://visualizer:8050/ +MICROSERVICE_SIG_PROVIDER_ENABLED=true +MICROSERVICE_SIG_PROVIDER_URL=http://sig-provider:8050/ +MICROSERVICE_ACCOUNT_ABSTRACTION_URL=http://user-ops-indexer:8050/ +DECODE_NOT_A_CONTRACT_CALLS=true +ACCOUNT_CLOAK_KEY= +ACCOUNT_ENABLED=false +ACCOUNT_REDIS_URL=redis://redis-db:6379 +NFT_MEDIA_HANDLER_ENABLED=true +NFT_MEDIA_HANDLER_REMOTE_DISPATCHER_NODE_MODE_ENABLED=true +RELEASE_NODE=producer@172.18.0.4 +RELEASE_DISTRIBUTION=name +RELEASE_COOKIE=secret_cookie \ No newline at end of file diff --git a/docs/installation.md b/docs/installation.md index 65bea79..11322fc 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -27,7 +27,6 @@ https://github.com/circlefin/arc-node repository: git clone https://github.com/circlefin/arc-node.git cd arc-node git checkout $VERSION -git submodule update --init --recursive ``` `$VERSION` is a tag for a released version. @@ -43,9 +42,17 @@ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source ~/.cargo/env ``` +With Rust installed, install the dependencies for your operating system: + +- **Ubuntu:** `sudo apt-get install libclang-dev pkg-config build-essential` +- **macOS:** `brew install llvm pkg-config` +- **Windows:** `choco install llvm` or `winget install LLVM.LLVM` + +These are needed to build bindings for Arc node execution's database. + **3. Build and install:** -The following commands produce three Arc node binaries: +The following commands produce three Arc node binaries: `arc-node-execution`, `arc-node-consensus`, and `arc-snapshots`: ```sh @@ -67,4 +74,3 @@ arc-snapshots --version arc-node-execution --version arc-node-consensus --version ``` - diff --git a/scripts/scenarios/nightly-chaos-testing.sh b/scripts/scenarios/nightly-chaos-testing.sh index fdfce0c..15356f7 100755 --- a/scripts/scenarios/nightly-chaos-testing.sh +++ b/scripts/scenarios/nightly-chaos-testing.sh @@ -60,6 +60,9 @@ echo " Quake dir: $QUAKE_DIR" echo "" echo "To reproduce this run locally:" echo " bash scripts/scenarios/nightly-chaos-testing.sh $SCENARIO $SPAM_DURATION_SECS $SPAM_RATE $SEED" +echo " Note: each 'quake perturb chaos' iteration uses its own generated seed;" +echo " see $RESULTS_DIR/chaos_loop.log for the exact seeds needed to replay the" +echo " chaos loop." echo "" mkdir -p "$RESULTS_DIR" @@ -96,7 +99,7 @@ current_height() { # # If some nodes are down it can print "conn refused" in place of numbers; ignore those. local line max - line="$("$QUAKE" --seed "$SEED" -f "$SCENARIO" info heights -n 1 2>/dev/null | tail -n 1)" + line="$("$QUAKE" -f "$SCENARIO" info heights -n 1 2>/dev/null | tail -n 1)" max="$( printf '%s\n' "$line" | tr '|' ' ' | @@ -211,16 +214,16 @@ chaos_loop() { echo "=== Chaos testing iteration #$iter ($(date -u)) ===" # 1) perturb chaos (runs ~5 minutes) - echo "Running: $QUAKE --seed $SEED -f $SCENARIO perturb chaos -d 5m" + echo "Running: $QUAKE -f $SCENARIO perturb chaos -d 5m" - "$QUAKE" --seed "$SEED" -f "$SCENARIO" perturb chaos -d 5m + "$QUAKE" -f "$SCENARIO" perturb chaos -d 5m # 2) wait for a random height a bit in the future target_height="$(random_wait_height)" echo "Waiting for height $target_height..." - echo "Running: $QUAKE --seed $SEED -f $SCENARIO wait height $target_height --timeout 180" + echo "Running: $QUAKE -f $SCENARIO wait height $target_height --timeout 180" - "$QUAKE" --seed "$SEED" -f "$SCENARIO" wait height "$target_height" --timeout 180 + "$QUAKE" -f "$SCENARIO" wait height "$target_height" --timeout 180 # 3) random validator set update updates=() @@ -233,8 +236,8 @@ chaos_loop() { done <<< "$rand_valset" echo "Valset updates: ${updates[*]}" - echo "Running: $QUAKE --seed $SEED -f $SCENARIO valset ${updates[@]}" - "$QUAKE" --seed "$SEED" -f "$SCENARIO" valset "${updates[@]}" + echo "Running: $QUAKE -f $SCENARIO valset ${updates[@]}" + "$QUAKE" -f "$SCENARIO" valset "${updates[@]}" sleep 5 done