Skip to content

feat: mpp-channel Anchor program and session method (PR #201 spec)#20

Open
alexanderattar wants to merge 4 commits intosolana-foundation:mainfrom
alexanderattar:main
Open

feat: mpp-channel Anchor program and session method (PR #201 spec)#20
alexanderattar wants to merge 4 commits intosolana-foundation:mainfrom
alexanderattar:main

Conversation

@alexanderattar
Copy link
Copy Markdown

Summary

  • Implements the mpp-channel Anchor escrow program for on-chain payment channel settlement
  • Aligns the TypeScript session method with PR #201 of the mpp-specs
  • Adds localnet integration tests and devnet verification with real transactions

Anchor program

PaymentChannel account 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:

Instruction Description
open Creates channel PDA, transfers SPL tokens into vault ATA
settle Verifies Ed25519 voucher on-chain, transfers delta to payee
close Like settle, but also refunds remaining deposit and finalizes
top_up Increases vault deposit, cancels any pending forced close
request_close Payer initiates forced close, records timestamp
withdraw Payer recovers deposit after grace period expires

The Ed25519 precompile is not CPI-callable. The program validates signatures by reading the instructions sysvar at the index passed in settle/close args, then parsing the raw JCS JSON bytes to extract channelId and cumulativeAmount. This is the same byte sequence signed for the HTTP credential, so no format translation is needed.

Program ID: 21fLdahqKtVAt4V2JLwVrRb7tuqPADjjPVCU9bK3MFPQ

Session method changes

Aligned to PR #201:

  • Voucher schema: three fields only (channelId, cumulativeAmount, expiresAt)
  • Signing: raw JCS bytes, no domain separator
  • Credential actions: updatevoucher, topuptopUp
  • Open and topUp carry a partially-signed transaction (pull mode)
  • Close voucher is optional — supports refund-only cooperative close
  • Channel state uses acceptedCumulative/spentAmount
  • Idempotent vouchers: cumulativeAmount <= acceptedCumulative succeeds silently with no state change
  • Server rejects vouchers when a forced close is pending

New files:

  • src/anchor/MppChannelClient.ts — instruction builders and PDA derivation
  • src/anchor/TransactionHandler.ts — server-side on-chain transaction verification
  • src/utils/ed25519.ts — Ed25519 instruction serialization (layout: 2-byte header + 14-byte descriptor, DATA_START=16)
  • src/session/BinaryVoucher.ts — compact 48-byte voucher representation

Tests

  • anchor-channel.test.ts: 5 localnet integration tests (open, settle with Ed25519, full lifecycle, requestClose/withdraw, topUp + forced-close cancellation). Starts solana-test-validator with the compiled .so.
  • 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 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.ts

Devnet 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.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant