Skip to content

Commit 58fa7cd

Browse files
authored
Merge pull request #434 from drips-projects/Add-an-end-to-end-Soroban-withdrawal
feat: Harden WithdrawHarness scenarios and implement structured logging
2 parents 4200fea + 1bd166d commit 58fa7cd

45 files changed

Lines changed: 11682 additions & 121 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
name: Harness Tests
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- 'sdk/src/test/harness/**'
7+
- 'sdk/src/proof.ts'
8+
- 'sdk/src/withdraw.ts'
9+
- 'sdk/src/public_inputs.ts'
10+
- 'sdk/src/backends/**'
11+
- 'artifacts/zk/**'
12+
- '.github/workflows/harness-tests.yml'
13+
push:
14+
branches:
15+
- main
16+
paths:
17+
- 'sdk/src/test/harness/**'
18+
- 'sdk/src/proof.ts'
19+
- 'sdk/src/withdraw.ts'
20+
- 'sdk/src/public_inputs.ts'
21+
- 'sdk/src/backends/**'
22+
- 'artifacts/zk/**'
23+
workflow_dispatch:
24+
inputs:
25+
backend_type:
26+
description: 'Backend type (mock or real)'
27+
required: false
28+
default: 'mock'
29+
type: choice
30+
options:
31+
- mock
32+
- real
33+
34+
permissions:
35+
contents: read
36+
checks: write
37+
38+
jobs:
39+
# Fast feedback with mock backend (< 5 seconds)
40+
fast-tests:
41+
name: Fast Tests (Mock Backend)
42+
runs-on: ubuntu-latest
43+
timeout-minutes: 5
44+
steps:
45+
- name: Check out repository
46+
uses: actions/checkout@v4
47+
48+
- name: Set up Node.js
49+
uses: actions/setup-node@v4
50+
with:
51+
node-version: '20'
52+
cache: 'npm'
53+
cache-dependency-path: sdk/package-lock.json
54+
55+
- name: Install dependencies
56+
working-directory: sdk
57+
run: npm ci --no-fund --no-audit
58+
59+
- name: Run fast harness tests
60+
working-directory: sdk
61+
env:
62+
BACKEND_TYPE: mock
63+
TIMEOUT_MS: 5000
64+
JEST_TIMEOUT: 10000
65+
CI: true
66+
run: npm run test:harness:ci
67+
68+
- name: Upload test results
69+
if: always()
70+
uses: actions/upload-artifact@v4
71+
with:
72+
name: test-results-fast
73+
path: sdk/test-results/junit.xml
74+
retention-days: 7
75+
76+
- name: Publish test results
77+
if: always()
78+
uses: EnricoMi/publish-unit-test-result-action@v2
79+
with:
80+
files: sdk/test-results/junit.xml
81+
check_name: Fast Tests Results
82+
comment_mode: off
83+
84+
# Comprehensive tests with real backend (< 60 seconds)
85+
comprehensive-tests:
86+
name: Comprehensive Tests (Real Backend)
87+
runs-on: ubuntu-latest
88+
timeout-minutes: 10
89+
# Only run comprehensive tests on main branch, manual dispatch, or when explicitly requested
90+
if: |
91+
github.event_name == 'push' ||
92+
github.event_name == 'workflow_dispatch' ||
93+
contains(github.event.pull_request.labels.*.name, 'comprehensive-tests')
94+
steps:
95+
- name: Check out repository
96+
uses: actions/checkout@v4
97+
98+
- name: Set up Node.js
99+
uses: actions/setup-node@v4
100+
with:
101+
node-version: '20'
102+
cache: 'npm'
103+
cache-dependency-path: sdk/package-lock.json
104+
105+
- name: Install Noir toolchain
106+
shell: bash
107+
run: |
108+
set -euo pipefail
109+
curl -L https://raw.githubusercontent.com/noir-lang/noirup/main/install | bash
110+
export PATH="$HOME/.nargo/bin:$PATH"
111+
noirup
112+
echo "$HOME/.nargo/bin" >> "$GITHUB_PATH"
113+
114+
- name: Verify Noir installation
115+
run: nargo --version
116+
117+
- name: Install dependencies
118+
working-directory: sdk
119+
run: npm ci --no-fund --no-audit
120+
121+
- name: Verify circuit artifacts exist
122+
run: |
123+
if [ ! -f "artifacts/zk/manifest.json" ]; then
124+
echo "Error: Circuit artifacts not found at artifacts/zk/manifest.json"
125+
exit 1
126+
fi
127+
echo "Circuit artifacts found"
128+
129+
- name: Run comprehensive harness tests
130+
working-directory: sdk
131+
env:
132+
BACKEND_TYPE: ${{ github.event.inputs.backend_type || 'real' }}
133+
CIRCUIT_ARTIFACTS_PATH: ../artifacts/zk
134+
TIMEOUT_MS: 60000
135+
JEST_TIMEOUT: 120000
136+
CI: true
137+
run: npm run test:harness:ci:comprehensive
138+
139+
- name: Upload test results
140+
if: always()
141+
uses: actions/upload-artifact@v4
142+
with:
143+
name: test-results-comprehensive
144+
path: sdk/test-results/junit.xml
145+
retention-days: 7
146+
147+
- name: Publish test results
148+
if: always()
149+
uses: EnricoMi/publish-unit-test-result-action@v2
150+
with:
151+
files: sdk/test-results/junit.xml
152+
check_name: Comprehensive Tests Results
153+
comment_mode: off
154+
155+
# Summary job that depends on both test jobs
156+
test-summary:
157+
name: Test Summary
158+
runs-on: ubuntu-latest
159+
needs: [fast-tests, comprehensive-tests]
160+
if: always()
161+
steps:
162+
- name: Check test results
163+
run: |
164+
if [ "${{ needs.fast-tests.result }}" != "success" ]; then
165+
echo "Fast tests failed"
166+
exit 1
167+
fi
168+
169+
# Comprehensive tests are optional, only fail if they ran and failed
170+
if [ "${{ needs.comprehensive-tests.result }}" == "failure" ]; then
171+
echo "Comprehensive tests failed"
172+
exit 1
173+
fi
174+
175+
echo "All tests passed or were skipped"

artifacts/zk/manifest.json

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
"amount",
2424
"relayer",
2525
"fee"
26-
]
26+
],
27+
"schema_version": "1.20680.19972"
2728
},
2829
"commitment": {
2930
"circuit_id": "commitment",
@@ -32,13 +33,18 @@
3233
"bytecode_sha256": "0xdfbdca2108939b0184db56a381088d4698702988647507242598869290c5d5e0",
3334
"abi_sha256": "0xe084f973bddc1be32518abc050b9f03bcb06ffbef43034b653e9319485a4bb9d",
3435
"name": "commitment",
35-
"backend": "nargo/noir"
36+
"backend": "nargo/noir",
37+
"public_input_schema": [
38+
"pool_id",
39+
"commitment"
40+
],
41+
"schema_version": "1.63027.29691"
3642
}
3743
},
3844
"files": {
3945
"commitment_vectors": {
4046
"path": "commitment_vectors.json",
41-
"sha256": "0x4f68c123c9d40fb34d03019f4e72a5cee64e27e0fd1c4d2e9f0113de7fa54f3d",
47+
"sha256": "0xc62db444d6d857455cf39813149804d3df13eb2897c964aa068c3ee4f9c2e255",
4248
"version": 1
4349
}
4450
}

artifacts/zk/manifest_old.json

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"version": 2,
3+
"backend": {
4+
"name": "nargo/noir",
5+
"nargo_version": "nargo version = 1.0.0-beta.20",
6+
"noirc_version": "noirc version = 1.0.0-beta.20+b4236c1957d0c26cb65d82adc9e5447b6ff1d629"
7+
},
8+
"circuits": {
9+
"withdraw": {
10+
"circuit_id": "withdraw",
11+
"path": "withdraw.json",
12+
"artifact_sha256": "0xc17a200f981dc8f8e60c4966947beae65b447b03df6c53c1df3d8ec29e200dfe",
13+
"bytecode_sha256": "0xf00f05d4d66c318b5431358606330634e5afa4e0683b396bc275da38e23c7d1c",
14+
"abi_sha256": "0x7df703d28f134039b2a91f933dee13a695639def3a18b3117c5a1ef9918d48aa",
15+
"name": "withdraw",
16+
"backend": "nargo/noir",
17+
"root_depth": 20,
18+
"public_input_schema": [
19+
"pool_id",
20+
"root",
21+
"nullifier_hash",
22+
"recipient",
23+
"amount",
24+
"relayer",
25+
"fee"
26+
]
27+
},
28+
"commitment": {
29+
"circuit_id": "commitment",
30+
"path": "commitment.json",
31+
"artifact_sha256": "0xc664b4c5e4bb06f7d54c4e86fd6a81d42a968899941e6f42d91656ddf9f9a661",
32+
"bytecode_sha256": "0xdfbdca2108939b0184db56a381088d4698702988647507242598869290c5d5e0",
33+
"abi_sha256": "0xe084f973bddc1be32518abc050b9f03bcb06ffbef43034b653e9319485a4bb9d",
34+
"name": "commitment",
35+
"backend": "nargo/noir"
36+
}
37+
},
38+
"files": {
39+
"commitment_vectors": {
40+
"path": "commitment_vectors.json",
41+
"sha256": "0x4f68c123c9d40fb34d03019f4e72a5cee64e27e0fd1c4d2e9f0113de7fa54f3d",
42+
"version": 1
43+
}
44+
}
45+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"version": "1",
3+
"backend": "barretenberg",
4+
"circuits": {}
5+
}

contracts/privacy_pool/src/crypto/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,6 @@
88

99
pub mod merkle;
1010
pub mod verifier;
11+
12+
#[cfg(test)]
13+
mod verifier_test;

contracts/privacy_pool/src/crypto/verifier.rs

Lines changed: 38 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use soroban_sdk::{
2020
};
2121

2222
use crate::types::errors::Error;
23-
use crate::types::state::{Proof, PublicInputs, VerifyingKey};
23+
use crate::types::state::{Proof, PublicInputs, SchemaVersion, VerifyingKey};
2424

2525
// ──────────────────────────────────────────────────────────────
2626
// Public Input Linear Combination
@@ -35,9 +35,9 @@ fn compute_vk_x(
3535
vk: &VerifyingKey,
3636
pub_inputs: &PublicInputs,
3737
) -> Result<Bn254G1Affine, Error> {
38-
// The VK must have exactly 9 IC points: IC[0] + 8 public inputs
39-
// [pool_id, root, nullifier_hash, recipient, amount, relayer, fee, denomination]
40-
if vk.gamma_abc_g1.len() != 9 {
38+
// The VK must have exactly 7 IC points: IC[0] + 6 public inputs
39+
// [root, nullifier_hash, recipient, amount, relayer, fee]
40+
if vk.gamma_abc_g1.len() != 7 {
4141
return Err(Error::MalformedVerifyingKey);
4242
}
4343

@@ -48,15 +48,13 @@ fn compute_vk_x(
4848
let mut acc = Bn254G1Affine::from_bytes(ic0_bytes);
4949

5050
// Public inputs as 32-byte field elements → Fr scalars
51-
let inputs: [&BytesN<32>; 8] = [
52-
&pub_inputs.pool_id,
51+
let inputs: [&BytesN<32>; 6] = [
5352
&pub_inputs.root,
5453
&pub_inputs.nullifier_hash,
5554
&pub_inputs.recipient,
5655
&pub_inputs.amount,
5756
&pub_inputs.relayer,
5857
&pub_inputs.fee,
59-
&pub_inputs.denomination,
6058
];
6159

6260
for (i, input_bytes) in inputs.iter().enumerate() {
@@ -140,52 +138,40 @@ pub fn verify_proof(
140138
}
141139

142140
// ──────────────────────────────────────────────────────────────
143-
// Tests
141+
// Schema Version Validation
144142
// ──────────────────────────────────────────────────────────────
145143

146-
#[cfg(test)]
147-
mod tests {
148-
use super::*;
149-
150-
#[test]
151-
fn test_verifier_schema_parity() {
152-
// ZK-087: Ensure the contract verifier's expectations match the
153-
// authoritative machine-readable schema artifact.
154-
let schema_json = include_str!("../../../../artifacts/zk/v1/verifier_schema.json");
155-
156-
// Count public input names in schema
157-
let input_count = schema_json.matches("\"name\":").count();
158-
159-
// Verifier expects IC[0] + all public inputs
160-
let expected_ic_total = input_count + 1;
161-
162-
// This pins the verifier to the schema
163-
assert_eq!(expected_ic_total, 9, "Schema must define exactly 8 public inputs (plus IC[0])");
164-
}
165-
166-
#[test]
167-
fn test_public_input_order() {
168-
let schema_json = include_str!("../../../../artifacts/zk/v1/verifier_schema.json");
169-
170-
// Names must appear in this order in the JSON
171-
let expected_order = [
172-
"pool_id",
173-
"root",
174-
"nullifier_hash",
175-
"recipient",
176-
"amount",
177-
"relayer",
178-
"fee",
179-
"denomination"
180-
];
181-
182-
let mut last_pos = 0;
183-
for name in expected_order {
184-
let search_str = concat!("\"name\": \"", stringify!(name), "\"");
185-
let pos = schema_json.find(search_str)
186-
.expect(concat!("Field ", stringify!(name), " missing from schema"));
187-
assert!(pos > last_pos, concat!("Field ", stringify!(name), " out of order in schema"));
188-
last_pos = pos;
189-
}
144+
/// Validates that the proof schema version matches the expected version.
145+
///
146+
/// This function ensures that proofs are generated with a compatible schema version,
147+
/// preventing runtime errors from schema mismatches at the verifier boundary.
148+
///
149+
/// # Arguments
150+
/// * `proof_schema` - The schema version from the proof
151+
/// * `expected_version_str` - The expected schema version string (e.g., "1.0.0")
152+
///
153+
/// # Returns
154+
/// * `Ok(())` if the schema versions are compatible
155+
/// * `Err(Error::InvalidSchemaVersion)` if the expected version string is malformed
156+
/// * `Err(Error::SchemaVersionMismatch)` if the versions are incompatible
157+
///
158+
/// # Compatibility Rules
159+
/// Schema versions are compatible if:
160+
/// - Major versions match exactly
161+
/// - Minor versions match exactly
162+
/// - Patch versions can differ (backward compatible bug fixes)
163+
pub fn validate_schema_version(
164+
proof_schema: &SchemaVersion,
165+
expected_version_str: &str,
166+
) -> Result<(), Error> {
167+
// Parse the expected version string
168+
let expected = SchemaVersion::from_string(expected_version_str)
169+
.map_err(|_| Error::InvalidSchemaVersion)?;
170+
171+
// Check compatibility using semantic versioning rules
172+
if !proof_schema.is_compatible_with(&expected) {
173+
return Err(Error::SchemaVersionMismatch);
190174
}
175+
176+
Ok(())
191177
}

0 commit comments

Comments
 (0)