Repository navigation
test(svm): add real Gateway V5 conformance (ACP-184 Step 5) - #1544
Conversation
droplet-rl
left a comment
There was a problem hiding this comment.
Summary
Reviewed at 2d4155ab against base reinis/acp-184-step-4-destination-fill (13 files, +1211/-0). This is genuinely test/docs/CI-only: the programs/svm-spoke/src/v5.rs addition sits inside the #[cfg(test)] mod tests block at line 341, package.json only gains a script, and no packaged IDL/client is touched. Scope matches the description.
The suite is strong. What I like in particular:
- It derives destination relays from actual origin
FundsDepositedevents rather than hand-builtRelayData, so the deposit-ID derivation, theV5_MAGIC_PREFIX || stepIdwitness, andget_relay_hashare exercised end-to-end against the real Gateway instead of being restated in the test. - The account-authentication cases are meaningful, not tautological.
relayJit(relay, wrong)/relayJit(relay, status, wrong)fail withMissingAccountbecauseload_v5_fill_accountsre-derivesV5FillStatusPdasand the real PDA is not in the forwarded set — which is precisely the property that makes the outer remaining-account pool safe. Addingwrongto the pool viaextraand still getting a rejection is the right way to pin "pool membership does not equal forwarding." - Deliberately keeping the unsafe primitives (short consumption, two fills over one balance, floor-of-committed-minima with larger JIT outputs) as passing counterexamples rather than pretending the program enforces delivery is the honest framing, and the spec/README text around it is accurate w.r.t.
v5_adapter.rs:196(source.amount >= output_amountis an assertion, not a debit). - Good separation from the mock lane:
InsufficientVaultBalance, allowance, pause, Token-2022 extension and exclusivity cases already live intest/svm/SvmSpoke.V5Fill.ts/.V5Source.ts, so this lane doesn't duplicate them.
Independent verification of the new fixture
I re-derived programs/svm-spoke/fixtures/v5_gateway_path.json from scratch (pure-Python Keccak-256, no repo code):
pathId,siblingPathId,stepRoot,witness— all four match.executorbase58-encodes to34trBszXuqhRjWaMxXWsunJNmyUsBvDNPxAwTzbPTm4p, i.e.constants::GATEWAY_PROGRAM_ID. Sincereference.ts::pathIdhardcodesGATEWAY.toBuffer(), the TS assertion transitively pins that too.V5_MAGIC_PREFIX=keccak256("AcrossV5MessagePrefix.V1"), matching the comment atconstants.rs:27.- The
messageblob decodes byte-for-byte as the Borsh tape[BALANCE_REQ(mint, 500000), TRANSFER(mint, recipient, u64::MAX, 10000bps)]— op vector02000000 0011, then two length-prefixed inputs of0x28and0x4abytes.
The fixture is self-consistent and correctly cross-checked in all three languages.
Main thing I'd like addressed
Nothing here runs in CI, and one piece of it easily could. The lane is workflow_dispatch-only and blocked on an environment/secret that doesn't exist yet (correctly disclosed in the PR body). But PathVectors.ts needs no validator and no private dependency — it's pure hashing — while its Rust and Solidity counterparts do run on every PR. That leaves reference.ts, the encoder most likely to be reused downstream by ACB-637/ENG-320, as the only unpinned side of the vector. Note also that pr.yml's paths-filter matches test/svm/**, which does not match test/svm-gateway/**, so a future PR touching only this directory triggers no SVM job at all. Details inline.
Everything else below is non-blocking: CI hygiene and a few assertions that could be tightened. Not requesting changes — this is test infrastructure with no production surface, and the substance is sound.
Caveat on validation: I could not execute the lane (private solana-v5 + Solana toolchain unavailable here), so I'm relying on your reported 14/14 · 16/16 · 5/5. My review is static analysis plus the independent fixture re-derivation above.
droplet-rl
left a comment
There was a problem hiding this comment.
Approving
Re-reviewed the delta 2d4155ab..1176c013 (3 commits, 10 files, +109/−108) and re-checked the full PR against base. All ten comments from my previous review are addressed, several more thoroughly than I asked. LGTM.
Fixes verified
| Previous comment | Resolution |
|---|---|
PathVectors.ts never runs in CI |
New test-svm-gateway-vectors script wired into lint-and-check-generated (pr.yml:104) |
paths-filter misses test/svm-gateway/** |
Added at pr.yml:147 |
| Hardcoded toolchain versions | Workflow deleted |
| Unverified binary download | Workflow deleted |
| Params-buffer leak on setup failure | Setup moved inside try — plus a new regression test |
FilledRelay event in failed tx not asserted |
Now decoded from the failed receipt |
assert.throws on a local helper |
assert.equal(delivered, consumed) added |
| Missing status-PDA assertions after JIT rejections | Added at both sites |
| Hardcoded spoke program ID | Read from target/idl/svm_spoke.json |
Glob picked up reference.ts/provider.ts |
Explicit spec file list |
Two fixes that went beyond the ask
The failed-receipt event decoding (RealGateway.ts:558-560) is now the real thing. Rather than grepping logs, it calls processEventFromTx(receipt, [spoke]) — which walks meta.innerInstructions and decodes the emit_cpi! self-invoke — then asserts exactly one attempted filledRelay and matches its depositId against the relay. That is precisely the hazard V5_ADAPTER_SPEC.md warns about ("Failed transaction logs may contain attempted fill events"), and the test is now the evidence for it rather than a proxy. I confirmed the signature at solanaProgramUtils.ts:55 accepts a VersionedTransactionResponse, which is what getTransaction(..., {maxSupportedTransactionVersion: 0}) returns.
The new retry test (RealGateway.ts:405-427) is self-verifying, which is the part I like. It patches provider.sendAndConfirm to let the setup instruction land on-chain and then throw, so the buffer genuinely exists when finally runs. Because it wraps expectFailure, a patch that silently failed to match would surface as expected injected setup confirmation failure rather than a false pass. The close instruction correctly escapes the injection (its discriminator doesn't match failAfter), so cleanup still runs inside the patched window, and path([floor(mint, 0n)]) encodes to 177 bytes — one fragment — so both loop branches are reachable. Restoration is in a finally.
Checks I ran
- OZ
Hashes.commutativeKeccak256—@openzeppelin/contractsis pinned at5.5.0andHashes.sollanded in 5.1.0, so the new import resolves. Swapping the hand-rolled ternary for the canonical helper also documents that the Gateway's sorted-pair rule is the standard commutative hash, and deriving the prefix viakeccak256("AcrossV5MessagePrefix.V1")instead of the literal means the Solidity vector now catches constant drift the way the Rust one already did. Both orders asserted in Rust (v5.rs:405-412) and Solidity. - The new CI step works on a fresh checkout.
lint-and-check-generateddoes not download SVM artifacts, so I traced the import graph:PathVectors.ts→reference.ts→src/types/svm.ts, which imports only@coral-xyz/anchor,@solana/web3.jsandethers. Notarget/or generated-client dependency, andresolveJsonModuleis already set. It will run green. - No dangling references. Grepped the tree for
SVM_GATEWAY_READ_TOKEN,svm-gateway-integration,svm-gateway.ymlandlinear.app— all clean. Removing the private Linear links from a public repo is the right call; the replacement prose keeps the policy without the unresolvable pointers. v5_gateway_path.jsonandreference.tsare byte-identical to what I reviewed last round, so my independent Keccak re-derivation ofpathId/siblingPathId/stepRoot/witnessstill holds.
Carrying forward
The toolchain-pinning and binary-integrity concerns I raised were resolved by deleting the workflow rather than by fixing it. They'll be live again when solana-v5 goes public and CI returns — flagged inline on the README so they aren't lost with the file.
Same caveat as last time: I could not execute the real-Gateway lane here (private solana-v5, no Solana toolchain), so the reported 15/15 · 1/1 · 12/12 · 16/16 · 5/5 is taken from the PR description. My verification is static analysis plus the independent fixture derivation.
79c0b5d to
deb977f
Compare
935b270 to
638afe1
Compare
deb977f to
7d7daaa
Compare
638afe1 to
0c25db4
Compare
7d7daaa to
6e37e12
Compare
0c25db4 to
5052d26
Compare
6e37e12 to
b20cf27
Compare
e5dd925 to
3f78703
Compare
b20cf27 to
5e18c22
Compare
3f78703 to
e8c4ad5
Compare
5e18c22 to
5237315
Compare
8dad3f4 to
f04c2c3
Compare
12a0845 to
c1f01e2
Compare
fe3a55d to
50fb8e3
Compare
c1f01e2 to
93a5d29
Compare
50fb8e3 to
2215794
Compare
93a5d29 to
4ea94b0
Compare
2215794 to
d646de9
Compare
4ea94b0 to
3683986
Compare
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
d646de9 to
23277c0
Compare
|
|
||
| // Set this before SvmSpoke.common constructs anchor.workspace.SvmSpoke; its | ||
| // default provider otherwise points at port 8899, not our isolated validator. | ||
| setProvider(AnchorProvider.env()); |
There was a problem hiding this comment.
Remove provider.ts and make provider initialization self-contained.
This creates an import-order dependency: ./provider must run before SvmSpoke.common.ts constructs anchor.workspace.SvmSpoke. The shared fixture should initialize its provider before accessing the workspace:
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.SvmSpoke as Program<SvmSpoke>;That removes the need for this file and its side-effect import. Explicitly passing the provider to a Program constructor would further reduce reliance on global state.
There was a problem hiding this comment.
Fixed in 4eb7bbb8. The shared fixture now sets its provider before accessing the workspace. Removed provider.ts and its side-effect import.
Sent from Reinis Martinsons's Codex CLI Agent using gpt-6-astra 🤖
| export const fillInput = (recipient: PublicKey, mint: PublicKey, minimum: bigint) => | ||
| Buffer.concat([Buffer.from([1]), recipient.toBuffer(), mint.toBuffer(), u64(minimum)]); | ||
| export const relayBytes = (r: RelayData) => |
There was a problem hiding this comment.
Consolidate duplicated Spoke encoders.
fillInput, relayBytes, and fillJit repeat the encoding already implemented by encodeFill, encodeRelay, and encodeJit in test/svm/SvmSpoke.V5Fill.ts. depositInput similarly repeats the deposit layout in test/svm/SvmSpoke.V5Source.ts.
Extract shared test-only Spoke encoding helpers and reuse them in the mock and real-Gateway suites so wire changes have one implementation to update. Keep the independent golden-vector calculations independent, since those are intended to catch encoding drift.
There was a problem hiding this comment.
Fixed in 4eb7bbb8. Mock and real-Gateway suites now share test/svm/v5Encoding.ts. Independent golden-vector calculations are unchanged; a new check compares the shared encoders against the frozen wire fixture.
Sent from Reinis Martinsons's Codex CLI Agent using gpt-6-astra 🤖
| const ix = (programId: PublicKey, name: string, data: Buffer, keys: AccountMeta[]) => | ||
| new TransactionInstruction({ programId, keys, data: Buffer.concat([discriminator(name), data]) }); |
There was a problem hiding this comment.
Use IDL-based encoding for ordinary instructions when available.
For the pinned Gateway/Prefunded release, prefer Anchor instruction builders backed by its IDLs for ordinary instructions such as initialization, parameter-buffer initialization/writes/closure, execute, and store. This would avoid maintaining instruction discriminators, argument serialization, and fixed account ordering by hand here.
Keep explicit encoders for command tapes and JIT payloads where the IDL exposes opaque bytes. Consuming versioned IDL artifacts would not require importing the foreign Rust workspace.
There was a problem hiding this comment.
Fixed in 4eb7bbb8. Initialization, buffer initialization/writes/closure, execute, and store now use IDL-backed Anchor builders. For now, IDLs are generated locally from the same pinned checkout as the binaries; public artifact distribution is a separate follow-up.
Sent from Reinis Martinsons's Codex CLI Agent using gpt-6-astra 🤖
| The suite covers StepDelegate and prefunded source deposits, standard deposit identities and witnesses, external and | ||
| in-place destination delivery, first-fill-wins siblings, root mismatch and reuse, account/dispatch rejection, payer | ||
| funding/reclaim/withdrawal, and downstream rollback. Root reuse examples fund and fully deliver each execution. | ||
|
|
There was a problem hiding this comment.
Consolidate documentation.
The delivery invariants and integration boundary are repeated across this README, programs/svm-spoke/V5_ADAPTER_SPEC.md, and AGENTS.md. Keep the authoritative delivery/security rules in the adapter spec, execution instructions and test coverage in this README, and a brief architectural summary with links in AGENTS.md. Link to the spec for the detailed invariant instead of maintaining multiple versions of the same explanation.
There was a problem hiding this comment.
Done in 4eb7bbb8. The adapter spec owns delivery/security rules; the README covers execution and test coverage; AGENTS.md has a short summary and links.
Sent from Reinis Martinsons's Codex CLI Agent using gpt-6-astra 🤖
|
Overall, I think this PR could benefit a lot from having a public repo with compiled SVM Gateway & co. contracts + IDLs. I'd try to set that up next and drop the code that works with the 'private repo checkout' from the tests here |
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
|
Addressed the four inline comments in 4eb7bbb8. Validation: real-Gateway 16/16, mock/foundation 18/18, focused typecheck and formatting pass. Agree on public Gateway/Prefunded binaries and matching IDLs as the next step. Keeping artifact hosting/publication separate so these fixes do not depend on that decision. The README is hosting-neutral; the existing pinned local build remains until we can consume a public release with reviewed checksums and provenance. Sent from Reinis Martinsons's Codex CLI Agent using gpt-6-astra 🤖 |
|
@droplet-rl , please recheck if all reviews have been properly addressed |
|
@Reinis-FRP Yes. All 16 review threads are addressed at grasphoper's 4 threads: fixed. I left these threads unresolved for grasphoper to close.
My 12 threads: all addressed. I resolved the 2 that were still open:
Run locally at
I still can't run the real-Gateway lane (private Optional nits (none blocking):
grasphoper's point about public Gateway binaries and IDLs is recorded as a follow-up in the README. |
Scope
ACP-184 Step 5, targeting
epic-v5/svm: real-Gateway integration tests, cross-VM fixtures, and docs. No production contract or packaged IDL/client changes.solana-v5@457cf693and run an isolated validator.Follow-ups
yarn test-svm-gatewaylocally with access to the pinned checkout. Ordinary CI runs the two dependency-free wire/hash checks and includestest/svm-gateway/**in the SVM path filter; it does not run the real-Gateway lane or use cross-repository credentials.Validation
At 4eb7bbb8:
Review fixes include self-contained provider initialization, shared Spoke encoders with independent golden-vector checks, IDL-backed Gateway/Prefunded instructions, and consolidated docs. Earlier buffer-cleanup, failed-receipt, replay, and malformed-input assertions remain covered.
The local compiler reports stack-size diagnostics in unchanged legacy fill handlers; this V5 lane does not exercise or certify those handlers. Verified-build checks remain separate.