feat: mpp-channel Anchor program and session method (PR #201 spec)#20
Open
alexanderattar wants to merge 4 commits intosolana-foundation:mainfrom
Open
feat: mpp-channel Anchor program and session method (PR #201 spec)#20alexanderattar wants to merge 4 commits intosolana-foundation:mainfrom
alexanderattar wants to merge 4 commits intosolana-foundation:mainfrom
Conversation
Implements the on-chain payment channel escrow program and aligns the TypeScript session method with PR #201 of the mpp-specs. ## Anchor program (programs/mpp-channel) PaymentChannel account tracks: payer, payee, token mint, authorized signer, deposit, settled amount, grace period, forced-close timestamp, finalization status, and salt. PDA seeds: ["mpp-channel", payer, payee, token, salt_le, authorized_signer] This binds all five spec-required components into the channel address. Instructions: - open: creates channel PDA and transfers SPL tokens into vault ATA - settle: verifies Ed25519 voucher signature on-chain via instructions sysvar introspection, transfers cumulative delta to payee - close: like settle but also refunds remaining deposit to payer and marks channel finalized - top_up: increases vault deposit, resets any pending forced-close - request_close: payer initiates forced close, records timestamp - withdraw: payer recovers deposit after grace period expires On-chain voucher verification parses the raw JCS JSON bytes (the same bytes signed for the HTTP credential) to extract channelId and cumulativeAmount. The Ed25519 precompile is not CPI-callable; the program validates it by reading the instructions sysvar at the index specified in the settle/close args. Program ID: 21fLdahqKtVAt4V2JLwVrRb7tuqPADjjPVCU9bK3MFPQ (deployed to devnet) ## TypeScript session method Aligned to PR #201 (Ludo Galabru's session spec): - Voucher schema simplified to three fields: channelId, cumulativeAmount, expiresAt - Signing uses raw JCS bytes with no domain separator prefix - Credential actions renamed: update->voucher, topup->topUp - Open/topUp actions carry a partially-signed transaction (pull mode) - Close voucher is optional (supports refund-only cooperative close) - Channel state uses acceptedCumulative/spentAmount (not lastSequence) - Idempotent voucher handling: cumulativeAmount <= acceptedCumulative succeeds silently - Server rejects vouchers when channel has a pending forced close New files: - src/anchor/MppChannelClient.ts: instruction builders and PDA derivation for mpp-channel - src/anchor/TransactionHandler.ts: server-side on-chain transaction verification - src/utils/ed25519.ts: Ed25519 instruction serialization (DATA_START=16, no padding) - src/session/BinaryVoucher.ts: compact 48-byte voucher for on-chain use ## Tests - anchor-channel.test.ts: 5 localnet integration tests covering open, settle with Ed25519 verification, full lifecycle, requestClose/withdraw, and topUp with forced-close cancellation. Starts solana-test-validator with the compiled .so loaded. - binary-voucher.test.ts: round-trip tests for the JCS parser and compact voucher format - session.test.ts: 62 unit tests for server/client session logic - vitest.config.anchor.ts: separate vitest config for anchor tests (120s timeout, single worker, excluded from main test run) ## Devnet verification Program deployed and all instructions exercised with real transactions: Setup: - Fund payee: https://explorer.solana.com/tx/4aenktQ6G5yos1ioZr18cC1RLVu7w3CLEDWdVtMQH8HNjTGcvhaG1yoYNh7ynR6mY6WUkepM4Q9DmVEfhBHFW445?cluster=devnet - Create mint: https://explorer.solana.com/tx/2bSreTn1vyfgYQWoRT4ghyQYC1Bnte2dmUmVjfkgzd1BXrGv5Ab1p312MpGiWBecChuCRE4Yvef55wSY4c2WFKMK?cluster=devnet - Create ATAs: https://explorer.solana.com/tx/4CGn6jmXia2JJQUhmDMzvjKmUnoA5m7x4Ykm5d6TotPmZNNN7dXtg4uVNBG3a84v69GgiWGQA5AFiTLsM4grgjNL?cluster=devnet - Mint tokens: https://explorer.solana.com/tx/2DVvN1QmuMnPfJwqCQUYSjFAniaqoTDB5oP7JjLiQqFPHDetGumNWSgDVJbRPJBbuQnZDymffhfMnzg9z1cRczjd?cluster=devnet Channel lifecycle (open -> settle -> settle -> close): - Open (deposit 1M): https://explorer.solana.com/tx/35rVZ7FVaQHTft3fFrr8YMyjoxcuWCKAqyTs9crW7qnQxXQhj8sgm3XkzyNdCPXpTgN8Vj5BNzG5G26TDEEERwLk?cluster=devnet - Settle 300k: https://explorer.solana.com/tx/5J9YWGkPtp9SRDaFZ7MxUkxf2xZfHz26g8tN1YmBf2hNQFjRPvMk1zbiekRKmxx8nrcJEYQBRwHJzoP6bZKdxh6h?cluster=devnet - Settle 600k: https://explorer.solana.com/tx/3g5GvmQZ7zpZPVXu17LHE6N1RQcgC2gGhFpft1VvLmNRejg6SK8Lur9DNfNWK1cWjLeVzc7PUEQqisyLrNvJQrCc?cluster=devnet - Close 800k: https://explorer.solana.com/tx/2DLg8MicA4PJMv7LJQxeydHVE4RP1mJFmvFD3DznuUKGMFmymjnh6FVA41DJ6gDm1rp2Ew6HPA8SmkeZq1JwGUSx?cluster=devnet Forced close (requestClose -> grace period -> withdraw): - Open (deposit 500k): https://explorer.solana.com/tx/4hcU5ckjRg4BnwFjvPxHmn3Z7B434ndT6Xrd83qshvoogLVY6Z9z5k25M2GsYvD4u1SLneTGkbnoQY1vh3UtqGuK?cluster=devnet - Request close: https://explorer.solana.com/tx/3hxMmZV74WbvRDVUGCombfBEMP856T5JZMp5YvhvZMRgpCTwx9AZ5MY54nxNb3ARCCFXeZ9S3NTwcyaNJPgBFSrz?cluster=devnet - Withdraw: https://explorer.solana.com/tx/5isuUTzhaLad422pU8m24UK2wn4dJa7fXBDZTRK4fEN1zVmUGTUYgMMZrXqQwEFm1zuL92gLwQJQ25EwnsHPVzFT?cluster=devnet Balances verified: payee received exactly 800,000 tokens, payer holds 9,200,000 (started 10M, deposited 1M, refunded 200k). Forced close recovered the full 500,000 deposit.
Five blockers: - TransactionHandler now verifies the Anchor discriminator, deposit/amount via u64 LE, payee (accounts[1]), and mint (accounts[2]) instead of only checking that the channel program appeared in the transaction. DISCRIMINATOR_OPEN and DISCRIMINATOR_TOP_UP are now exported from MppChannelClient so TransactionHandler can import them. - ChannelStore.fromStore() documents that the in-process Map lock is single-instance only; notes what a distributed-safe implementation needs (Redis WATCH/MULTI/EXEC or Lua CAS). - assertSignerAuthorized removes the `|| signer !== channel.payer` fallback. For standard channels authorizedSigner IS the payer (set at open time); accepting the payer as an alternative bypass silently widened trust. Added comment explaining the delegated-key model. - closeTx removed from the model entirely: Types.ts, UnboundedAuthorizer, SwigBudgetAuthorizer, SwigSessionAuthorizer, makeSessionAuthorizer. The close path is server-initiated; clients never submit a close transaction. - handleVoucher now distinguishes equal (idempotent retry, return cached result) from less-than (error: "Voucher cumulative amount must not decrease"). Previously both cases returned success, letting stale replays authorize service without new value. Three secondary: - BinaryVoucher.ts and its tests deleted (48-byte format superseded by JCS JSON signing). Export removed from session/index.ts. - programs/mpp-channel/src/binary_voucher.rs renamed to jcs_voucher.rs; imports updated in settle.rs, close.rs, and lib.rs to reflect that the file parses JCS JSON, not a binary format. - BudgetAuthorizer renamed to SwigBudgetAuthorizer everywhere (class, interface, filename, exports) to surface the Swig dependency. native SOL rejected at session init with a clear error; state.rs comment updated to remove the misleading "or system program for native SOL" note.
…a instructions verifyOpenInstruction now checks accounts[3] (channel PDA) against the session channelId. verifyTopUpInstruction checks accounts[1] (channel PDA). Both handlers stop ignoring the channelId argument. findChannelInstruction switched from find() to filter() with an explicit length assertion. Transactions containing more than one channel-program instruction are now rejected outright — a mixed tx with top_up followed by request_close can no longer slip through by matching only on the first ix.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
mpp-channelAnchor escrow program for on-chain payment channel settlementAnchor program
PaymentChannelaccount binds payer, payee, token mint, authorized signer, deposit, settled amount, grace period, forced-close timestamp, finalization status, and salt.PDA seeds:
["mpp-channel", payer, payee, token, salt_le, authorized_signer]Instructions:
opensettleclosetop_uprequest_closewithdrawThe Ed25519 precompile is not CPI-callable. The program validates signatures by reading the instructions sysvar at the index passed in
settle/closeargs, then parsing the raw JCS JSON bytes to extractchannelIdandcumulativeAmount. This is the same byte sequence signed for the HTTP credential, so no format translation is needed.Program ID:
21fLdahqKtVAt4V2JLwVrRb7tuqPADjjPVCU9bK3MFPQSession method changes
Aligned to PR #201:
channelId,cumulativeAmount,expiresAt)update→voucher,topup→topUpacceptedCumulative/spentAmountcumulativeAmount <= acceptedCumulativesucceeds silently with no state changeNew files:
src/anchor/MppChannelClient.ts— instruction builders and PDA derivationsrc/anchor/TransactionHandler.ts— server-side on-chain transaction verificationsrc/utils/ed25519.ts— Ed25519 instruction serialization (layout: 2-byte header + 14-byte descriptor, DATA_START=16)src/session/BinaryVoucher.ts— compact 48-byte voucher representationTests
anchor-channel.test.ts: 5 localnet integration tests (open, settle with Ed25519, full lifecycle, requestClose/withdraw, topUp + forced-close cancellation). Startssolana-test-validatorwith the compiled.so.binary-voucher.test.ts: round-trip tests for the JCS parser and compact voucher formatsession.test.ts: 62 unit tests for server/client session logicvitest.config.anchor.ts: separate config for anchor tests (120s timeout, single worker)Run anchor tests:
anchor test(requires Anchor CLI and Rust toolchain)Run unit tests:
pnpm exec vitest run --config vitest.config.tsDevnet verification
All instructions exercised against the deployed program with real transactions.
Setup
Channel lifecycle (open → settle → settle → close)
Forced close (requestClose → grace period → withdraw)
Balances verified: payee received exactly 800,000 tokens, payer holds 9,200,000 (started 10M, deposited 1M, refunded 200k). Forced close recovered the full 500,000 deposit.