From b57e9de6f2b10682c2c3e0183930db98e65a49af Mon Sep 17 00:00:00 2001 From: grumbach Date: Mon, 27 Apr 2026 13:02:46 +0900 Subject: [PATCH 1/2] test(e2e): cover the merkle batch payment path The existing e2e script never exercised payForMerkleTree: - Default node count was 5, below CANDIDATES_PER_POOL = 16, so pay_for_merkle_batch returned InsufficientPeers and Auto-mode silently fell back to per-chunk payForQuotes (which has no pool-count validation). - Test files in ugly_files/ are capped at 1 MB, well below the 64-chunk merkle threshold. - No step used --merkle, so even with bigger files Auto fallback would mask any contract-side failure. Net effect: zero coverage for the merkle path. The recent WrongPoolCount(16, 8) production revert against odd-depth merkle trees would not have been caught here. This adds: - DEVNET_NODES default bumped from 5 to 16 (+ BOOTSTRAP_COUNT 2 -> 3), with a comment explaining the threshold. Override via ANT_TEST_DEVNET_NODES still works. - New Step 5b that: * synthesizes a 280 MiB random file (depth-7 chunk band, the exact production failure mode), * uploads with --merkle to disable Auto fallback, * asserts the client's "Submitting merkle batch payment on-chain" log line is present, so a future silent-fallback regression fails the test loudly, * downloads via the saved datamap and SHA256-compares the round trip. Override file size with ANT_TEST_MERKLE_FILE_MB; skip the step entirely with ANT_TEST_SKIP_MERKLE=1 (for disk-constrained CI). Verified locally on a 16-node Anvil-backed devnet: PASS: Merkle batch upload (74 chunks, depth=7) PASS: Merkle batch round-trip (SHA256 match) --- scripts/test_e2e.sh | 110 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 108 insertions(+), 2 deletions(-) diff --git a/scripts/test_e2e.sh b/scripts/test_e2e.sh index 38e90da5..c6041219 100755 --- a/scripts/test_e2e.sh +++ b/scripts/test_e2e.sh @@ -8,6 +8,7 @@ # 3. Uploads each file in ./ugly_files/ with payment # 4. Verifies on-chain payment via Anvil RPC # 5. Downloads and verifies file integrity (SHA256 checksum) +# 5b. Exercises the merkle batch payment path with a synthesized large file # 6. Tests client-side payment rejection (CLI rejects without SECRET_KEY) # 7. Tests server-side payment rejection (node rejects unpaid PUT) # 8. Stops the devnet and reports results @@ -101,8 +102,16 @@ if [ ! -f "${ANT_CLI}" ]; then fi # Step 2: Start devnet with EVM -DEVNET_NODES="${ANT_TEST_DEVNET_NODES:-5}" -BOOTSTRAP_COUNT="${ANT_TEST_BOOTSTRAP_COUNT:-2}" +# +# Default node count is 16 to clear the merkle batch payment threshold. +# `pay_for_merkle_batch` (ant-core) requires CANDIDATES_PER_POOL = 16 peers +# per pool. With fewer nodes the merkle path returns InsufficientPeers and +# Auto payment mode silently falls back to per-chunk `payForQuotes` — which +# has no pool-count validation, so contract-side bugs in `payForMerkleTree` +# would never surface in this script. The Step 5b merkle test below relies +# on the devnet being merkle-capable. +DEVNET_NODES="${ANT_TEST_DEVNET_NODES:-16}" +BOOTSTRAP_COUNT="${ANT_TEST_BOOTSTRAP_COUNT:-3}" echo "=== Step 2: Starting devnet with EVM (${DEVNET_NODES} nodes, ${BOOTSTRAP_COUNT} bootstrap) ===" mkdir -p "${DOWNLOAD_DIR}" @@ -331,6 +340,103 @@ fi echo "" +# Step 5b: Merkle batch payment path +# +# The standard test files in ugly_files/ are kept small (< 1 MB by default), +# so they only produce one or two chunks and never trigger the merkle batch +# payment threshold (DEFAULT_MERKLE_THRESHOLD = 64 chunks). This step uploads +# a synthesized large file with `--merkle` to exercise `payForMerkleTree` on +# the deployed contract, including the on-chain `WrongPoolCount` validation. +# +# `--merkle` disables Auto-mode fallback, so InsufficientPeers (or any other +# merkle-path failure, including a contract revert) fails the upload rather +# than silently dropping back to per-chunk `payForQuotes`. With the default +# 16-node devnet this should always cross the CANDIDATES_PER_POOL threshold. +# +# Default file size is 280 MiB to land in the depth-7 chunk band (65-128 +# chunks) — the production failure band for the WrongPoolCount(16, 8) bug. +# Override with ANT_TEST_MERKLE_FILE_MB. Set ANT_TEST_SKIP_MERKLE=1 to skip +# (e.g. on disk-constrained CI). +echo "=== Step 5b: Merkle batch payment test ===" + +if [ "${ANT_TEST_SKIP_MERKLE:-0}" = "1" ]; then + echo " Skipping (ANT_TEST_SKIP_MERKLE=1)" +else + MERKLE_TEST_SIZE_MB="${ANT_TEST_MERKLE_FILE_MB:-280}" + MERKLE_FILE="/tmp/ant_e2e_merkle_${TEST_RUN_ID}.bin" + MERKLE_DOWNLOAD="${DOWNLOAD_DIR}/merkle_${TEST_RUN_ID}.bin" + MERKLE_LOG="/tmp/ant_e2e_merkle_${TEST_RUN_ID}.log" + + echo " Generating ${MERKLE_TEST_SIZE_MB} MiB of random data..." + if ! dd if=/dev/urandom of="${MERKLE_FILE}" bs=1m count="${MERKLE_TEST_SIZE_MB}" 2>/dev/null; then + # Fall back to GNU dd block syntax if BSD dd unavailable (Linux CI). + dd if=/dev/urandom of="${MERKLE_FILE}" bs=1M count="${MERKLE_TEST_SIZE_MB}" status=none + fi + + echo " Uploading with --merkle (no Auto fallback, surfaces merkle errors)..." + SECRET_KEY="${WALLET_KEY}" RUST_LOG=info "${ANT_CLI}" \ + --devnet-manifest "${MANIFEST_FILE}" \ + --evm-network local \ + --allow-loopback \ + --quote-timeout-secs 60 \ + --store-timeout-secs 300 \ + -v \ + file upload --merkle "${MERKLE_FILE}" \ + > "${CLI_STDOUT}" 2>"${MERKLE_LOG}" || { + fail "Merkle batch upload" "Upload command failed (exit $?)" + echo " Last log lines:" + tail -20 "${MERKLE_LOG}" 2>/dev/null || true + } + + if grep -q "Upload complete" "${CLI_STDOUT}" 2>/dev/null; then + MERKLE_CHUNKS=$(grep "Chunks:" "${CLI_STDOUT}" | head -1 | grep -oE '[0-9]+' | head -1) + # The fixed client logs "Submitting merkle batch payment on-chain (depth=N)" at info level + # before calling payForMerkleTree. Confirms we did not hit some other code path + # (e.g. a silent Auto-mode fallback to per-chunk `payForQuotes`). + if grep -q "Submitting merkle batch payment on-chain" "${MERKLE_LOG}" 2>/dev/null; then + MERKLE_DEPTH=$(grep -oE "depth=[0-9]+" "${MERKLE_LOG}" | head -1 | cut -d= -f2) + pass "Merkle batch upload (${MERKLE_CHUNKS:-?} chunks, depth=${MERKLE_DEPTH:-?})" + else + fail "Merkle batch upload" "Upload reported success but merkle log line not found in stderr — possible silent fallback" + echo " Stderr tail:" + tail -10 "${MERKLE_LOG}" 2>/dev/null || true + fi + + # Round-trip: download and SHA256-compare. Client writes the datamap + # alongside the source by replacing the extension (foo.bin -> foo.datamap). + DATAMAP_PATH="${MERKLE_FILE%.bin}.datamap" + if [ -f "${DATAMAP_PATH}" ]; then + SECRET_KEY="${WALLET_KEY}" "${ANT_CLI}" \ + --devnet-manifest "${MANIFEST_FILE}" \ + --evm-network local \ + --allow-loopback \ + --store-timeout-secs 300 \ + file download --datamap "${DATAMAP_PATH}" -o "${MERKLE_DOWNLOAD}" \ + > /dev/null 2>"${MERKLE_LOG}" || { + fail "Merkle batch download" "Download command failed" + tail -10 "${MERKLE_LOG}" 2>/dev/null || true + } + + if [ -f "${MERKLE_DOWNLOAD}" ]; then + ORIG_HASH=$(shasum -a 256 "${MERKLE_FILE}" | cut -d' ' -f1) + DOWN_HASH=$(shasum -a 256 "${MERKLE_DOWNLOAD}" | cut -d' ' -f1) + if [ "${ORIG_HASH}" = "${DOWN_HASH}" ]; then + pass "Merkle batch round-trip (SHA256 match)" + else + fail "Merkle batch round-trip" "SHA256 mismatch" + fi + fi + fi + fi + + # Clean up the large file even on failure to avoid filling /tmp + [ -f "${MERKLE_FILE}" ] && rm -f "${MERKLE_FILE}" + [ -f "${MERKLE_FILE%.bin}.datamap" ] && rm -f "${MERKLE_FILE%.bin}.datamap" + [ -f "${MERKLE_DOWNLOAD}" ] && rm -f "${MERKLE_DOWNLOAD}" +fi + +echo "" + # Step 6: Test client-side payment rejection (upload without SECRET_KEY) echo "=== Step 6: Client-side payment rejection test ===" From 266a1908e23a99b6347e03ddf1203394adbae50b Mon Sep 17 00:00:00 2001 From: grumbach Date: Mon, 27 Apr 2026 15:47:26 +0900 Subject: [PATCH 2/2] test(e2e): tighten Step 5b pass/fail bookkeeping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address Copilot review on PR #82: the original Step 5b gated all downstream subtests on a stdout substring, so an upload that exited 0 without the expected "Upload complete" marker would silently pass without recording anything in the test counters. This restructures the step around an explicit `UPLOAD_OK` flag: - Track the upload command's exit status directly into UPLOAD_OK. - Check both markers (stdout "Upload complete", stderr merkle log line) independently and `fail` for each missing marker rather than treating their absence as "skip". - Gate the round-trip download on UPLOAD_OK rather than on the marker grep, and report concrete failures when the datamap is missing or the downloaded file fails to materialise. Manually exercised against a 16-node Anvil-backed devnet: Happy path → 2 PASS (upload + round-trip) Exit 0, no markers → 2 FAIL (one per missing marker, no silent skip) Exit nonzero → 1 FAIL, no follow-up subtests run --- scripts/test_e2e.sh | 73 +++++++++++++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 19 deletions(-) diff --git a/scripts/test_e2e.sh b/scripts/test_e2e.sh index c6041219..95eda689 100755 --- a/scripts/test_e2e.sh +++ b/scripts/test_e2e.sh @@ -374,6 +374,7 @@ else fi echo " Uploading with --merkle (no Auto fallback, surfaces merkle errors)..." + UPLOAD_OK=true SECRET_KEY="${WALLET_KEY}" RUST_LOG=info "${ANT_CLI}" \ --devnet-manifest "${MANIFEST_FILE}" \ --evm-network local \ @@ -383,29 +384,58 @@ else -v \ file upload --merkle "${MERKLE_FILE}" \ > "${CLI_STDOUT}" 2>"${MERKLE_LOG}" || { + UPLOAD_OK=false fail "Merkle batch upload" "Upload command failed (exit $?)" echo " Last log lines:" tail -20 "${MERKLE_LOG}" 2>/dev/null || true } - if grep -q "Upload complete" "${CLI_STDOUT}" 2>/dev/null; then - MERKLE_CHUNKS=$(grep "Chunks:" "${CLI_STDOUT}" | head -1 | grep -oE '[0-9]+' | head -1) - # The fixed client logs "Submitting merkle batch payment on-chain (depth=N)" at info level - # before calling payForMerkleTree. Confirms we did not hit some other code path - # (e.g. a silent Auto-mode fallback to per-chunk `payForQuotes`). - if grep -q "Submitting merkle batch payment on-chain" "${MERKLE_LOG}" 2>/dev/null; then - MERKLE_DEPTH=$(grep -oE "depth=[0-9]+" "${MERKLE_LOG}" | head -1 | cut -d= -f2) - pass "Merkle batch upload (${MERKLE_CHUNKS:-?} chunks, depth=${MERKLE_DEPTH:-?})" + # Verify the merkle path actually ran. We assert two independent markers so + # a future change that swallows the contract revert (or relabels the success + # message) fails this step loudly rather than passing silently. + # + # Marker 1: stdout "Upload complete" line — proves the CLI itself reported + # success rather than the upload bailing without saying so. + # Marker 2: stderr "Submitting merkle batch payment on-chain" log line — + # proves we actually called `payForMerkleTree` and didn't drop + # into Auto-mode per-chunk fallback. + if [ "${UPLOAD_OK}" = true ]; then + MERKLE_CHUNKS="" + MERKLE_DEPTH="" + + if ! grep -q "Upload complete" "${CLI_STDOUT}" 2>/dev/null; then + fail "Merkle batch upload" "Upload exited 0 but no 'Upload complete' marker in stdout" + echo " Stdout tail:" + tail -10 "${CLI_STDOUT}" 2>/dev/null || true + UPLOAD_OK=false else - fail "Merkle batch upload" "Upload reported success but merkle log line not found in stderr — possible silent fallback" + MERKLE_CHUNKS=$(grep "Chunks:" "${CLI_STDOUT}" | head -1 | grep -oE '[0-9]+' | head -1) + fi + + if ! grep -q "Submitting merkle batch payment on-chain" "${MERKLE_LOG}" 2>/dev/null; then + fail "Merkle batch upload" "Merkle log line not found in stderr — possible silent fallback" echo " Stderr tail:" tail -10 "${MERKLE_LOG}" 2>/dev/null || true + UPLOAD_OK=false + else + MERKLE_DEPTH=$(grep -oE "depth=[0-9]+" "${MERKLE_LOG}" | head -1 | cut -d= -f2) fi - # Round-trip: download and SHA256-compare. Client writes the datamap - # alongside the source by replacing the extension (foo.bin -> foo.datamap). + if [ "${UPLOAD_OK}" = true ]; then + pass "Merkle batch upload (${MERKLE_CHUNKS:-?} chunks, depth=${MERKLE_DEPTH:-?})" + fi + fi + + # Round-trip: download and SHA256-compare. Client writes the datamap + # alongside the source by replacing the extension (foo.bin -> foo.datamap). + # Only attempted when the upload claim has been corroborated by both + # markers — a failed upload has nothing to download. + if [ "${UPLOAD_OK}" = true ]; then DATAMAP_PATH="${MERKLE_FILE%.bin}.datamap" - if [ -f "${DATAMAP_PATH}" ]; then + if [ ! -f "${DATAMAP_PATH}" ]; then + fail "Merkle batch round-trip" "Datamap not found at ${DATAMAP_PATH}" + else + DOWNLOAD_OK=true SECRET_KEY="${WALLET_KEY}" "${ANT_CLI}" \ --devnet-manifest "${MANIFEST_FILE}" \ --evm-network local \ @@ -413,17 +443,22 @@ else --store-timeout-secs 300 \ file download --datamap "${DATAMAP_PATH}" -o "${MERKLE_DOWNLOAD}" \ > /dev/null 2>"${MERKLE_LOG}" || { - fail "Merkle batch download" "Download command failed" + DOWNLOAD_OK=false + fail "Merkle batch round-trip" "Download command failed" tail -10 "${MERKLE_LOG}" 2>/dev/null || true } - if [ -f "${MERKLE_DOWNLOAD}" ]; then - ORIG_HASH=$(shasum -a 256 "${MERKLE_FILE}" | cut -d' ' -f1) - DOWN_HASH=$(shasum -a 256 "${MERKLE_DOWNLOAD}" | cut -d' ' -f1) - if [ "${ORIG_HASH}" = "${DOWN_HASH}" ]; then - pass "Merkle batch round-trip (SHA256 match)" + if [ "${DOWNLOAD_OK}" = true ]; then + if [ ! -f "${MERKLE_DOWNLOAD}" ]; then + fail "Merkle batch round-trip" "Download exited 0 but output file missing" else - fail "Merkle batch round-trip" "SHA256 mismatch" + ORIG_HASH=$(shasum -a 256 "${MERKLE_FILE}" | cut -d' ' -f1) + DOWN_HASH=$(shasum -a 256 "${MERKLE_DOWNLOAD}" | cut -d' ' -f1) + if [ "${ORIG_HASH}" = "${DOWN_HASH}" ]; then + pass "Merkle batch round-trip (SHA256 match)" + else + fail "Merkle batch round-trip" "SHA256 mismatch" + fi fi fi fi