Skip to content

Commit fa20762

Browse files
committed
fix(factory): require evm confirmation depth
1 parent a5d68a1 commit fa20762

10 files changed

Lines changed: 2895 additions & 246 deletions

File tree

IMPLEMENTATION_LEARNINGS.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,14 @@
4848
- Process improvement: lease/version ownership is part of every nested async function contract. Recheck it immediately after each await and in the same atomic mutation as every result write, including errors. Tests must force takeover inside nested send and receipt awaits and return both stale success and stale error. Recovery must reconstruct all signing inputs from the persisted draft or reject configuration drift.
4949
- Final cycle-4 review: accepted and fixed guard propagation through persisted transaction resume, send, and receipt awaits; guarded CAS evidence/error/finalization writes; stale-owner-safe cleanup; and complete draft authority including the optional ECDSA key name. Factory 110/110, artifact/Candid parity, shared 25/25, indexer 96/96, web 95/95, and all lints passed. No P0/P1 or implementation-caused/plan-required P2 findings remain.
5050
- Follow-up risk for Plan 004: use the factory's persisted transaction draft and generation-CAS discipline for confirmation depth; an observed receipt must never finalize after a lease/version loss or configuration drift.
51+
52+
## Plan 004 — Factory EVM confirmation depth (2026-07-16)
53+
54+
- Repository convention: release, refund, and reproduction flows share typed receipt evidence and must use the same canonical block and confirmation-count helper; persisted transaction hashes are authoritative and must never fall through to a second broadcast.
55+
- Constraint: every caller of a confirmation helper must branch on its returned `confirmed` flag. Persisting receipt evidence is not equivalent to finality, and `debug_assert!` cannot enforce a financial invariant in release builds.
56+
- ICP/Candid/stable storage: the implementation uses additive serde-defaulted receipt and per-block payment-evidence fields plus an optional stable confirmation-depth field, preserving old snapshot decoding while regenerating the factory Candid interface.
57+
- Effective gates: 19 confirmation tests, 6 escrow tests, 146 full factory tests, fmt, clippy with warnings denied, warning-free release Wasm, generated Candid, and artifact parity all passed. Parent diff inspection still caught no-rebroadcast and bytes32 fixture regressions; final review caught host refund finalization below depth, demonstrating that green gates did not cover every caller contract.
58+
- Review findings accepted and fixed before the final review: canonicality-before-depth, per-block deposit evidence, guard propagation across async RPC stages, pending receipt representation, overflow-safe arithmetic, local-chain defaults, spawn no-rebroadcast behavior, pending-state preservation, and exact bytes32 fixtures.
59+
- Authorized final-cycle finding accepted and fixed: `claim_escrow_refund_inner` in the non-Wasm path had finalized both resumed and newly broadcast refunds without rejecting `receipt.confirmed == false`; one branch relied only on `debug_assert!`. Both branches now persist evidence, return a pending error below depth, and finalize only after runtime-confirmed depth. Three host tests cover fresh pending, resumed pending, and exact-depth idempotent finalization.
60+
- Process improvement: map every boolean/enum result from a shared financial helper to every call site and add a caller-contract test matrix, including host-only adapters. Keep Spark executor/reviewer prompts bounded, use high reasoning, require runtime model provenance, and stop broad diagnostic loops that can overflow context or generate unbounded logs.
61+
- Final review: the same Spark/high reviewer approved the extra authorized cycle with no unresolved P0/P1 or implementation-caused P2 findings. Final independent gates passed with 3 focused refund tests, 20 confirmation tests, 9 escrow tests, 149 full factory tests, clean fmt/clippy, warning-free release Wasm, regenerated Candid, and artifact parity.

backend/factory/factory.did

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,13 @@ type EscrowClaim = record {
111111
created_at : nat64;
112112
payment_status : PaymentStatus;
113113
refundable : bool;
114+
payment_evidence : vec PaymentEvidenceBlock;
115+
payment_evidence_block_hash : opt text;
114116
refunded_at : opt nat64;
115117
refund_broadcast : opt ReleaseBroadcastRecord;
116118
payment_address : text;
119+
payment_evidence_increment : opt text;
120+
payment_evidence_block_number : opt nat64;
117121
last_scanned_block : opt nat64;
118122
};
119123
type FactoryArtifactSnapshot = record {
@@ -123,6 +127,7 @@ type FactoryArtifactSnapshot = record {
123127
version_commit : opt text;
124128
};
125129
type FactoryConfigSnapshot = record {
130+
evm_confirmation_depth : nat64;
126131
base_rpc_fallback_endpoint : opt text;
127132
escrow_contract_address : text;
128133
session_ttl_ms : nat64;
@@ -171,6 +176,7 @@ type FactoryError = variant {
171176
ManagementCallFailed : record { method : text; message : text };
172177
InvalidChildRuntimeConfig : record { field : text; message : text };
173178
InvalidDeathReport : record { reason : text };
179+
InvalidOperationalConfig : record { field : text; message : text };
174180
SessionNotFound : record { session_id : text };
175181
InvalidStewardProof : record { reason : text };
176182
RepositoryStrategyAlreadyExists : record { strategy_id : text };
@@ -251,6 +257,7 @@ type FactoryInitArgs = record {
251257
admin_principals : vec principal;
252258
};
253259
type FactoryOperationalConfig = record {
260+
evm_confirmation_depth : nat64;
254261
cycles_per_spawn : nat64;
255262
estimated_outcall_cycles_per_interval : nat64;
256263
min_pool_balance : nat64;
@@ -333,6 +340,11 @@ type ListRepositoryStrategiesResponse = record {
333340
};
334341
type MemoryDowryFact = record { key : text; value : text };
335342
type OpenRouterReasoningLevel = variant { Low; High; Medium; Default };
343+
type PaymentEvidenceBlock = record {
344+
block_hash : text;
345+
block_number : nat64;
346+
amount : text;
347+
};
336348
type PaymentStatus = variant { Refunded; Paid; Unpaid; Partial };
337349
type PostRoomMessageRequest = record {
338350
body : text;
@@ -372,6 +384,7 @@ type ReleaseBroadcastFailure = record {
372384
rpc_category : opt RpcFailureCategory;
373385
};
374386
type ReleaseBroadcastRecord = record {
387+
receipt_status : opt bool;
375388
last_error : opt ReleaseBroadcastFailure;
376389
signature : opt ReleaseSignatureRecord;
377390
rpc_tx_hash : opt text;
@@ -388,6 +401,8 @@ type ReleaseBroadcastRecord = record {
388401
signing_payload_hash : opt text;
389402
gas_limit : nat64;
390403
raw_transaction_hash : opt text;
404+
receipt_block_number : opt nat64;
405+
receipt_block_hash : opt text;
391406
calldata_hex : text;
392407
};
393408
type ReleaseBroadcastStage = variant {

backend/factory/src/api/admin.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,28 @@ pub fn set_operational_config(
8585
caller: &str,
8686
config: FactoryOperationalConfig,
8787
) -> Result<FactoryOperationalConfig, FactoryError> {
88+
if config.evm_confirmation_depth == 0 {
89+
return Err(FactoryError::InvalidOperationalConfig {
90+
field: "evm_confirmation_depth".to_string(),
91+
message: "must be greater than zero".to_string(),
92+
});
93+
}
94+
if config.evm_confirmation_depth > crate::types::MAX_EVM_CONFIRMATION_DEPTH {
95+
return Err(FactoryError::InvalidOperationalConfig {
96+
field: "evm_confirmation_depth".to_string(),
97+
message: format!(
98+
"exceeds maximum supported confirmation depth {}",
99+
crate::types::MAX_EVM_CONFIRMATION_DEPTH
100+
),
101+
});
102+
}
103+
88104
write_state(|state| -> Result<(), FactoryError> {
89105
ensure_admin_in_state(state, caller)?;
90106
state.cycles_per_spawn = config.cycles_per_spawn;
91107
state.min_pool_balance = config.min_pool_balance;
92108
state.estimated_outcall_cycles_per_interval = config.estimated_outcall_cycles_per_interval;
109+
state.evm_confirmation_depth = config.evm_confirmation_depth;
93110
Ok(())
94111
})?;
95112

@@ -177,6 +194,7 @@ pub fn get_factory_config(caller: &str) -> Result<FactoryConfigSnapshot, Factory
177194
cycles_per_spawn: state.cycles_per_spawn,
178195
min_pool_balance: state.min_pool_balance,
179196
estimated_outcall_cycles_per_interval: state.estimated_outcall_cycles_per_interval,
197+
evm_confirmation_depth: state.evm_confirmation_depth,
180198
session_ttl_ms: state.session_ttl_ms,
181199
version_commit: state.version_commit.clone(),
182200
wasm_sha256: state.wasm_sha256.clone(),

0 commit comments

Comments
 (0)