Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,13 @@ dist/
target/
Cargo.lock
.surfpool

# Anchor
.anchor/
test-ledger/
programs/mpp-channel-keypair.json

# Local development files
CLAUDE.md
docs/
scripts/
23 changes: 23 additions & 0 deletions Anchor.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[toolchain]

[features]
resolution = true
skip-lint = false

[programs.localnet]
mpp_channel = "21fLdahqKtVAt4V2JLwVrRb7tuqPADjjPVCU9bK3MFPQ"

[programs.devnet]
mpp_channel = "21fLdahqKtVAt4V2JLwVrRb7tuqPADjjPVCU9bK3MFPQ"

[registry]
url = "https://api.apr.dev"

[provider]
cluster = "localnet"
wallet = "~/.config/solana/id.json"

[scripts]
test = "pnpm exec vitest run --config typescript/vitest.config.anchor.ts"

[hooks]
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,12 +214,19 @@ just ts-fmt # Format and lint
just ts-build # Build
just ts-test # Unit tests (charge + session, no network)
just ts-test-integration # Integration tests (requires Surfpool)
# Rust

# Rust client SDK
cd rust && cargo build

# Anchor program (programs/mpp-channel)
# Prerequisites: Rust stable toolchain, Anchor CLI >=1.0.0-rc.2, solana-test-validator on PATH
just anchor-build # Compile the on-chain program
just anchor-test # Localnet integration tests (starts/stops validator automatically)

# Everything
just build # Build both
just test # Test both
just build # Build TypeScript, Rust, and Anchor
just test # Unit tests (TypeScript + Rust, no network)
just test-all # All tests including integration and Anchor localnet
just pre-commit # Full pre-commit checks
```

Expand Down
14 changes: 12 additions & 2 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,26 @@ rs-fmt:
rs-lint:
cd rust && cargo clippy -- -D warnings

# ── Anchor ──

# Build Anchor program (programs/mpp-channel)
anchor-build:
anchor build --no-idl

# Run Anchor localnet tests (starts solana-test-validator automatically)
anchor-test:
anchor test

# ── Orchestration ──

# Build everything
build: ts-build rs-build
build: ts-build rs-build anchor-build

# Run all unit tests
test: ts-test rs-test

# Run all tests including integration
test-all: ts-test ts-test-integration rs-test
test-all: ts-test ts-test-integration rs-test anchor-test

# Format everything
fmt: ts-fmt rs-fmt
Expand Down
24 changes: 24 additions & 0 deletions programs/mpp-channel/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[package]
name = "mpp-channel"
version = "0.1.0"
edition = "2021"
description = "MPP Solana payment channel escrow program"

[lib]
crate-type = ["cdylib", "lib"]

[dependencies]
anchor-lang = "1.0.0-rc.2"
anchor-spl = "1.0.0-rc.2"
solana-instructions-sysvar = "3"
solana-sdk-ids = "3"

[dev-dependencies]

[features]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
cpi = ["no-entrypoint"]
default = []
idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"]
93 changes: 93 additions & 0 deletions programs/mpp-channel/src/ed25519.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
use anchor_lang::prelude::*;

use crate::errors::MppChannelError;

// Ed25519 instruction data layout offsets (for a single signature).
// See: https://docs.solanalabs.com/runtime/programs#ed25519-program
//
// Header:
// [0..2] num_signatures (u16 LE) = 1
// [2..4] padding (u16 LE) = 0
//
// Per-signature descriptor (16 bytes):
// [4..6] signature_offset (u16 LE)
// [6..8] signature_instruction_index (u16 LE) = 0xFFFF (same instruction)
// [8..10] public_key_offset (u16 LE)
// [10..12] public_key_instruction_index (u16 LE) = 0xFFFF
// [12..14] message_data_offset (u16 LE)
// [14..16] message_data_size (u16 LE)
// [16..18] message_instruction_index (u16 LE) = 0xFFFF
//
// Padding:
// Data (for inline, all in same instruction):
// [16..80] signature (64 bytes)
// [80..112] public_key (32 bytes)
// [112..] message (variable)

const HEADER_SIZE: usize = 2; // num_signatures u16
const DESCRIPTOR_SIZE: usize = 14; // 7 x u16 fields

const SIGNATURE_SIZE: usize = 64;
const PUBKEY_SIZE: usize = 32;

const DATA_START: usize = HEADER_SIZE + DESCRIPTOR_SIZE; // 16
const SIGNATURE_OFFSET: usize = DATA_START;
const PUBKEY_OFFSET: usize = SIGNATURE_OFFSET + SIGNATURE_SIZE;
const MESSAGE_OFFSET: usize = PUBKEY_OFFSET + PUBKEY_SIZE;

/// Validate that a specific instruction in the transaction is an Ed25519
/// precompile verification of the expected public key and message.
///
/// The Ed25519 precompile itself verifies the cryptographic signature.
/// This function verifies that the precompile was asked to check the
/// correct inputs (the payer's public key and the binary voucher bytes).
///
/// If this validation is wrong or missing, anyone could submit a settle/close
/// transaction with an Ed25519 instruction that verifies a different key or
/// message, effectively bypassing signature authorization.
pub fn validate_ed25519_instruction(
instructions_sysvar: &AccountInfo,
expected_signer: &Pubkey,
expected_message: &[u8],
ed25519_instruction_index: u8,
) -> Result<()> {
// Load the instruction at the given index from the instructions sysvar.
let instruction = solana_instructions_sysvar::load_instruction_at_checked(
ed25519_instruction_index as usize,
instructions_sysvar,
)
.map_err(|_| MppChannelError::MissingEd25519Instruction)?;

// Verify the instruction targets the Ed25519 precompile program.
if instruction.program_id != solana_sdk_ids::ed25519_program::ID {
return Err(MppChannelError::InvalidEd25519Program.into());
}

let data = &instruction.data;

// Minimum size: header + descriptor + padding + signature + pubkey + at least 1 byte message
let min_size = MESSAGE_OFFSET + 1;
if data.len() < min_size {
return Err(MppChannelError::MissingEd25519Instruction.into());
}

// Verify num_signatures == 1 (we only support single-signature verification).
let num_signatures = u16::from_le_bytes([data[0], data[1]]);
if num_signatures != 1 {
return Err(MppChannelError::MissingEd25519Instruction.into());
}

// Extract the public key from the instruction data and compare.
let pubkey_bytes = &data[PUBKEY_OFFSET..PUBKEY_OFFSET + PUBKEY_SIZE];
if pubkey_bytes != expected_signer.as_ref() {
return Err(MppChannelError::InvalidEd25519PublicKey.into());
}

// Extract the message from the instruction data and compare.
let message_bytes = &data[MESSAGE_OFFSET..];
if message_bytes != expected_message {
return Err(MppChannelError::InvalidEd25519Message.into());
}

Ok(())
}
35 changes: 35 additions & 0 deletions programs/mpp-channel/src/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use anchor_lang::prelude::*;

#[error_code]
pub enum MppChannelError {
#[msg("Channel is not open (finalized)")]
ChannelNotOpen,
#[msg("Channel is already finalized")]
ChannelFinalized,
#[msg("Cumulative amount must exceed current settled amount")]
AmountNotGreaterThanSettled,
#[msg("Cumulative amount exceeds deposit")]
AmountExceedsDeposit,
#[msg("Missing Ed25519 verify instruction")]
MissingEd25519Instruction,
#[msg("Ed25519 instruction targets wrong program")]
InvalidEd25519Program,
#[msg("Ed25519 instruction verifies wrong public key")]
InvalidEd25519PublicKey,
#[msg("Ed25519 instruction verifies wrong message")]
InvalidEd25519Message,
#[msg("Unauthorized: caller is not the payer")]
UnauthorizedPayer,
#[msg("Unauthorized: caller is not the payee")]
UnauthorizedPayee,
#[msg("Deposit amount must be greater than zero")]
ZeroDeposit,
#[msg("Arithmetic overflow")]
ArithmeticOverflow,
#[msg("Close has not been requested")]
CloseNotRequested,
#[msg("Grace period has not expired yet")]
GracePeriodNotExpired,
#[msg("Close has already been requested")]
CloseAlreadyRequested,
}
45 changes: 45 additions & 0 deletions programs/mpp-channel/src/events.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use anchor_lang::prelude::*;

#[event]
pub struct ChannelOpened {
pub channel: Pubkey,
pub payer: Pubkey,
pub payee: Pubkey,
pub token: Pubkey,
pub authorized_signer: Pubkey,
pub deposit: u64,
pub grace_period_seconds: u64,
}

#[event]
pub struct ChannelSettled {
pub channel: Pubkey,
pub delta: u64,
pub cumulative_settled: u64,
}

#[event]
pub struct ChannelClosed {
pub channel: Pubkey,
pub final_settled: u64,
pub refund: u64,
}

#[event]
pub struct CloseRequested {
pub channel: Pubkey,
pub requested_at: i64,
}

#[event]
pub struct TopUpCompleted {
pub channel: Pubkey,
pub additional: u64,
pub new_deposit: u64,
}

#[event]
pub struct Withdrawn {
pub channel: Pubkey,
pub refund: u64,
}
Loading