Summary
After #1832 / #1843 shipped, verity_contract now accepts struct-array parameters whose elements carry nested dynamic members (e.g. Array Transaction where each Transaction contains uint256[] nullifierHashes), and projects single-word static leaves from those elements via Expr.arrayElementDynamicWord / Expr.paramDynamicHeadWord.
That is strictly less than what writing the UnlinkPool.transfer / withdraw / emergencyWithdraw bodies actually needs. The roadmap (docs/ROADMAP.md "Unlink Audit Readiness") currently claims that promotion to build_green only requires writing the bodies; an empirical pilot against verity-benchmark@1e9b631 shows three additional macro capabilities are still missing.
Reproducer
UnlinkPool.transfer (UnlinkPool.sol:328-359, pinned commit 4bc46c1f) needs to:
for (uint256 i = 0; i < _transactions.length; i++) {
Transaction calldata txn = _transactions[i];
if (txn.nullifierHashes.length != c.inputCount) revert ...; // (1)
if (txn.ciphertexts.length != _countNonZero(txn.newCommitments, ...)) // (2)
_validateContext(..., _computeContextHash(txn.ciphertexts)); // (3)
_verifyProof(c.verifier, txn.proof, txn.merkleRoot, txn.contextHash,
txn.nullifierHashes, txn.newCommitments); // (3)
emit Transferred(newRoot, startIndex, leaves,
_realNullifiers(txn.nullifierHashes), txn.ciphertexts); // (3)
}
Each of (1), (2), (3) is rejected by Verity/Macro/Translate.lean on b2f2ee40:
Specific gaps
G1. arrayLength on a struct-element dynamic member
txn.nullifierHashes.length. arrayLength only resolves direct param refs via lookupNamedValueType? (Translate.lean:2383-2387):
| `(term| arrayLength $name:term) =>
match lookupNamedValueType? constDecls immutableDecls params locals
(← expectStringOrIdent name) with
| some (.array _) => pure .uint256
| _ => throwErrorAt name "unknown array value"
There is no path for arrayLength (arrayElement _transactions i).nullifierHashes.
G2. Element indexing on struct-element dynamic members
txn.nullifierHashes[k]. arrayElement is gated by requireDirectParamRef name "arrayElement" params at Translate.lean:2390, which rejects anything other than a direct parameter ident.
G3. Passing dynamic arrays to tryExternalCall / externalCall / emit argument lists
tryExternalCall, externalCall, and emit argument lists all require requireWordLikeType (Translate.lean:2411, :2623). There is no way to pass txn.nullifierHashes (or any Array-typed local) as an external-call argument, an event component, or a custom-error payload component.
Combined with #1824, these gaps make the body impossible to write line-by-line: every internal helper that takes dynamic arrays would have to be inlined, and the inlined code still cannot read element counts or individual elements out of the struct-array members.
Desired behavior
Three macro-level lifts, in increasing order of scope:
-
G1: Allow arrayLength (arrayElement <param> <index>).<dynamicField> to lower through a new Expr.arrayElementDynamicMemberLength (or extend arrayElementDynamicWord) — the encoded layout already carries the head pointer; resolving it to the underlying length word is one extra mload/calldataload step.
-
G2: Allow arrayElement (arrayElement <param> <index>).<dynamicField> <innerIndex> similarly — one further bounds-checked word read at headPtr + 32 + innerIndex*32.
-
G3: A whitelisted "pass-through" for Array <wordLike> arguments to tryExternalCall / emit / revertError so the macro can hand off an already-encoded calldata slice (the dynamic member already lives in calldata at a known offset/length) without forcing the caller into word-level encoding.
G1 is the smallest unblocker that lets the macro emit shape-check reverts (PoolInvalidInputShape / PoolInvalidOutputShape / PoolCiphertextCountMismatch). G2 unblocks the per-tx nullifier loop. G3 plus #1824 is the final piece.
Scope
This issue is the macro-level prerequisite for the verity-benchmark cases/unlink_xyz/pool/ promotion to build_green, which the ROADMAP currently lists as the trailing Unlink-audit work item under #1760. The maintainer-facing roadmap and the unlink_xyz/pool Contract.lean footer / case.yaml unsupported_feature_codes will be updated in a paired benchmark PR to reference this issue once filed, replacing the current over-optimistic "only requires writing the bodies" wording.
Summary
After #1832 / #1843 shipped,
verity_contractnow accepts struct-array parameters whose elements carry nested dynamic members (e.g.Array Transactionwhere eachTransactioncontainsuint256[] nullifierHashes), and projects single-word static leaves from those elements viaExpr.arrayElementDynamicWord/Expr.paramDynamicHeadWord.That is strictly less than what writing the
UnlinkPool.transfer/withdraw/emergencyWithdrawbodies actually needs. The roadmap (docs/ROADMAP.md"Unlink Audit Readiness") currently claims that promotion tobuild_greenonly requires writing the bodies; an empirical pilot againstverity-benchmark@1e9b631shows three additional macro capabilities are still missing.Reproducer
UnlinkPool.transfer(UnlinkPool.sol:328-359, pinned commit4bc46c1f) needs to:Each of (1), (2), (3) is rejected by
Verity/Macro/Translate.leanonb2f2ee40:Specific gaps
G1.
arrayLengthon a struct-element dynamic membertxn.nullifierHashes.length.arrayLengthonly resolves direct param refs vialookupNamedValueType?(Translate.lean:2383-2387):| `(term| arrayLength $name:term) => match lookupNamedValueType? constDecls immutableDecls params locals (← expectStringOrIdent name) with | some (.array _) => pure .uint256 | _ => throwErrorAt name "unknown array value"There is no path for
arrayLength (arrayElement _transactions i).nullifierHashes.G2. Element indexing on struct-element dynamic members
txn.nullifierHashes[k].arrayElementis gated byrequireDirectParamRef name "arrayElement" paramsatTranslate.lean:2390, which rejects anything other than a direct parameter ident.G3. Passing dynamic arrays to
tryExternalCall/externalCall/emitargument liststryExternalCall,externalCall, andemitargument lists all requirerequireWordLikeType(Translate.lean:2411,:2623). There is no way to passtxn.nullifierHashes(or anyArray-typed local) as an external-call argument, an event component, or a custom-error payload component.Combined with #1824, these gaps make the body impossible to write line-by-line: every internal helper that takes dynamic arrays would have to be inlined, and the inlined code still cannot read element counts or individual elements out of the struct-array members.
Desired behavior
Three macro-level lifts, in increasing order of scope:
G1: Allow
arrayLength (arrayElement <param> <index>).<dynamicField>to lower through a newExpr.arrayElementDynamicMemberLength(or extendarrayElementDynamicWord) — the encoded layout already carries the head pointer; resolving it to the underlying length word is one extramload/calldataloadstep.G2: Allow
arrayElement (arrayElement <param> <index>).<dynamicField> <innerIndex>similarly — one further bounds-checked word read atheadPtr + 32 + innerIndex*32.G3: A whitelisted "pass-through" for
Array <wordLike>arguments totryExternalCall/emit/revertErrorso the macro can hand off an already-encoded calldata slice (the dynamic member already lives in calldata at a known offset/length) without forcing the caller into word-level encoding.G1 is the smallest unblocker that lets the macro emit shape-check reverts (
PoolInvalidInputShape/PoolInvalidOutputShape/PoolCiphertextCountMismatch). G2 unblocks the per-tx nullifier loop. G3 plus #1824 is the final piece.Scope
This issue is the macro-level prerequisite for the verity-benchmark
cases/unlink_xyz/pool/promotion tobuild_green, which the ROADMAP currently lists as the trailing Unlink-audit work item under #1760. The maintainer-facing roadmap and theunlink_xyz/poolContract.leanfooter /case.yamlunsupported_feature_codeswill be updated in a paired benchmark PR to reference this issue once filed, replacing the current over-optimistic "only requires writing the bodies" wording.