Skip to content

Implemented Example Program Using Ed25519 Signature Verification #352

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -13,6 +13,8 @@ members = [
"basics/create-account/native/program",
"basics/create-account/anchor/programs/create-system-account",
"basics/cross-program-invocation/anchor/programs/*",
"basics/ed25519-verification/native/program",
"basics/ed25519-verification/anchor/programs/*",
"basics/hello-solana/native/program",
"basics/hello-solana/anchor/programs/*",
"basics/pda-rent-payer/native/program",
128 changes: 128 additions & 0 deletions basics/ed25519-verification/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# Ed25519 Signature Verification for Custodied Funds

This example demonstrates how to implement Ed25519 signature verification to manage custodied funds on Solana. The program verifies Ed25519 signatures before allowing transfers from custodial accounts.

## Overview

The example shows how to:
- Verify Ed25519 signatures using Solana's native Ed25519 program
- Transfer funds from custodial accounts after signature verification
- Implement secure authorization checks
- Handle signature verification errors

## Quick Start

The example is implemented in multiple frameworks:

### Native
```bash
cd native
pnpm install
pnpm build-and-test
```

### Anchor
```bash
cd anchor
pnpm install
anchor build
pnpm test
```

### Steel
```bash
cd steel
pnpm install
steel build
steel test
```

### Poseidon (TypeScript)
```bash
cd poseidon
pnpm install
pnpm test
```

## Program Structure

The program consists of the following key components:

1. **Signature Verification**: Uses Solana's Ed25519 program to verify signatures
2. **Fund Transfer**: Handles secure transfer of funds after verification
3. **Account Validation**: Ensures proper account permissions and ownership

### Account Structure
- Custodial Account: Holds the funds
- Recipient: Account receiving the funds
- Signer: Account authorized to initiate transfers
- Ed25519 Program: Solana's native signature verification program

### Instruction Data
```rust
pub struct TransferInstruction {
signature: [u8; 64], // Ed25519 signature
public_key: [u8; 32], // Signer's public key
amount: u64, // Transfer amount in lamports
message: Vec<u8>, // Message that was signed
}
```

## Usage

### Creating a Transfer

```typescript
// Create and sign the transfer message
const message = Buffer.from(`Transfer ${amount} lamports to ${recipient.toBase58()}`);
const signature = await sign(message, signerKeypair.secretKey.slice(0, 32));

// Create the instruction
const instruction = new TransactionInstruction({
keys: [
{ pubkey: custodialAccount, isSigner: false, isWritable: true },
{ pubkey: recipient, isSigner: false, isWritable: true },
{ pubkey: signerKeypair.publicKey, isSigner: true, isWritable: false },
{ pubkey: ed25519ProgramId, isSigner: false, isWritable: false },
],
programId,
data: Buffer.concat([signature, publicKey, amount, message]),
});
```

### Security Considerations

1. **Signature Verification**: Always verify signatures before transferring funds
2. **Account Validation**: Check account ownership and permissions
3. **Error Handling**: Properly handle all error cases
4. **Amount Validation**: Verify sufficient funds before transfer

## Testing

Each implementation includes comprehensive tests demonstrating:
- Successful signature verification and transfer
- Handling of invalid signatures
- Error cases for insufficient funds
- Account permission checks

## Framework-Specific Details

### Native Implementation
- Direct Solana program implementation
- Manual account and instruction handling
- Bankrun tests for verification

### Anchor Implementation
- Uses Anchor's account validation
- Structured instruction handling
- Type-safe client interface

### Steel Implementation
- Separated API and program logic
- Steel-specific optimizations
- Integrated testing tools

### Poseidon Implementation
- TypeScript client implementation
- Modern Solana practices
- Versioned transaction support
16 changes: 16 additions & 0 deletions basics/ed25519-verification/anchor/Anchor.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[features]
seeds = false
skip-lint = false

[programs.localnet]
ed25519_custodial = "Ed25519CustodiaLXXXXXXXXXXXXXXXXXXXXXXXXXXX"

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

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

[scripts]
test = "pnpm ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
23 changes: 23 additions & 0 deletions basics/ed25519-verification/anchor/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"scripts": {
"test": "pnpm ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts",
"build": "anchor build",
"deploy": "anchor deploy"
},
"dependencies": {
"@coral-xyz/anchor": "^0.30.0",
"@solana/web3.js": "^1.95.2",
"@noble/ed25519": "^1.7.1"
},
"devDependencies": {
"anchor-bankrun": "^0.4.0",
"solana-bankrun": "^0.3.0",
"@types/bn.js": "^5.1.0",
"@types/chai": "^4.3.0",
"@types/mocha": "^9.0.0",
"chai": "^4.3.4",
"mocha": "^9.0.3",
"ts-mocha": "^10.0.0",
"typescript": "^4.3.5"
}
}
20 changes: 20 additions & 0 deletions basics/ed25519-verification/anchor/programs/ed25519-/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "ed25519-custodial"
version = "0.1.0"
description = "Created with Anchor"
edition = "2021"

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

[features]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
cpi = ["no-entrypoint"]
default = []

[dependencies]
anchor-lang = "0.30.0"
solana-program = "1.16"
60 changes: 60 additions & 0 deletions basics/ed25519-verification/anchor/programs/ed25519-/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use anchor_lang::prelude::*;
use anchor_lang::solana_program::ed25519_program;

declare_id!("Ed25519CustodiaLXXXXXXXXXXXXXXXXXXXXXXXXXXX");

#[program]
pub mod ed25519_custodial {
use super::*;

pub fn transfer(
ctx: Context<Transfer>,
signature: [u8; 64],
public_key: [u8; 32],
message: Vec<u8>,
amount: u64,
) -> Result<()> {
// Verify Ed25519 signature
let verification_instruction = ed25519_program::instruction::new_ed25519_instruction(
&public_key,
&message,
&signature,
);

// Invoke the Ed25519 program to verify the signature
solana_program::program::invoke(
&verification_instruction,
&[ctx.accounts.ed25519_program.to_account_info()],
)?;

msg!("Signature verification successful!");

// Transfer funds
**ctx.accounts.custodial_account.try_borrow_mut_lamports()? = ctx
.accounts
.custodial_account
.lamports()
.checked_sub(amount)
.ok_or(ProgramError::InsufficientFunds)?;

**ctx.accounts.recipient.try_borrow_mut_lamports()? = ctx
.accounts
.recipient
.lamports()
.checked_add(amount)
.ok_or(ProgramError::Overflow)?;

Ok(())
}
}

#[derive(Accounts)]
pub struct Transfer<'info> {
#[account(mut)]
pub custodial_account: AccountInfo<'info>,
#[account(mut)]
pub recipient: AccountInfo<'info>,
pub signer: Signer<'info>,
/// CHECK: This is the Ed25519 program ID
pub ed25519_program: AccountInfo<'info>,
}
56 changes: 56 additions & 0 deletions basics/ed25519-verification/anchor/tests/ed25519-custodial.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { describe, it } from 'node:test';
import * as anchor from '@coral-xyz/anchor';
import { Program } from '@coral-xyz/anchor';
import { sign } from '@noble/ed25519';
import { Keypair, PublicKey } from '@solana/web3.js';
import { BankrunProvider } from 'anchor-bankrun';
import { startAnchor } from 'solana-bankrun';
import type { Ed25519Custodial } from '../target/types/ed25519_custodial';

describe('Ed25519 Custodial', async () => {
const context = await startAnchor(
'',
[
{
name: 'ed25519_custodial',
programId: new PublicKey('Ed25519CustodiaLXXXXXXXXXXXXXXXXXXXXXXXXXXX'),
},
],
[],
);
const provider = new BankrunProvider(context);
anchor.setProvider(provider);

const program = anchor.workspace.Ed25519Custodial as Program<Ed25519Custodial>;

it('Verifies signature and transfers funds', async () => {
const custodialAccount = Keypair.generate();
const recipient = Keypair.generate();
const signerKeypair = Keypair.generate();
const amount = 1000000; // lamports

// Message to sign
const message = Buffer.from(`Transfer ${amount} lamports to ${recipient.publicKey.toBase58()}`);

// Sign the message with Ed25519
const signature = await sign(message, signerKeypair.secretKey.slice(0, 32));

try {
await program.methods
.transfer(Array.from(signature), Array.from(signerKeypair.publicKey.toBytes()), Array.from(message), new anchor.BN(amount))
.accounts({
custodialAccount: custodialAccount.publicKey,
recipient: recipient.publicKey,
signer: signerKeypair.publicKey,
ed25519Program: new PublicKey('Ed25519SigVerify111111111111111111111111111'),
})
.signers([signerKeypair])
.rpc();

console.log('Transaction processed successfully');
} catch (error) {
console.error('Error:', error);
throw error;
}
});
});
8 changes: 8 additions & 0 deletions basics/ed25519-verification/native/cicd.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/bin/bash

# This script is for quick building & deploying of the program.
# It also serves as a reference for the commands used for building & deploying Solana programs.
# Run this bad boy with "bash cicd.sh" or "./cicd.sh"

cargo build-sbf --manifest-path=./program/Cargo.toml --bpf-out-dir=./program/target/so
solana program deploy ./program/target/so/program.so
21 changes: 21 additions & 0 deletions basics/ed25519-verification/native/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"scripts": {
"test": "pnpm ts-mocha -p ./tests/tsconfig.test.json -t 1000000 ./tests/test.ts",
"build-and-test": "cargo build-sbf --manifest-path=./program/Cargo.toml --sbf-out-dir=./tests/fixtures && pnpm test",
"build": "cargo build-sbf --manifest-path=./program/Cargo.toml --sbf-out-dir=./program/target/so",
"deploy": "solana program deploy ./program/target/so/program.so"
},
"dependencies": {
"@solana/web3.js": "^1.95.2",
"@noble/ed25519": "^1.7.1",
"buffer": "^6.0.3",
"solana-bankrun": "^0.3.0"
},
"devDependencies": {
"@types/node": "^18.0.0",
"typescript": "^4.7.4",
"ts-mocha": "^10.0.0",
"@types/mocha": "^9.0.0",
"mocha": "^9.0.3"
}
}
1,263 changes: 1,263 additions & 0 deletions basics/ed25519-verification/native/pnpm-lock.yaml

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions basics/ed25519-verification/native/program/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "ed25519-custodial"
version = "0.1.0"
edition = "2021"
description = "Example Solana program demonstrating Ed25519 signature verification for custodied funds"

[features]
no-entrypoint = []

[dependencies]
solana-program = "1.16"
thiserror = "1.0"

[lib]
crate-type = ["cdylib", "lib"]
83 changes: 83 additions & 0 deletions basics/ed25519-verification/native/program/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
msg,
program_error::ProgramError,
pubkey::Pubkey,
ed25519_program,
};

entrypoint!(process_instruction);

#[derive(Debug)]
pub struct TransferInstruction {
pub amount: u64,
}

pub fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();

// Get account info
let custodial_account = next_account_info(accounts_iter)?;
let recipient = next_account_info(accounts_iter)?;
let signer = next_account_info(accounts_iter)?;
let ed25519_program_id = next_account_info(accounts_iter)?;

// Verify this is the expected Ed25519 program
if ed25519_program_id.key != &ed25519_program::id() {
return Err(ProgramError::InvalidArgument);
}

// First 64 bytes are the signature
let signature = &instruction_data[..64];
// Next 32 bytes are the public key
let public_key = &instruction_data[64..96];
// Next 8 bytes are the amount
let amount = u64::from_le_bytes(instruction_data[96..104].try_into().unwrap());
// Remaining data is the message to verify
let message = &instruction_data[104..];

// Verify the Ed25519 signature
let verification_instruction = ed25519_program::instruction::new_ed25519_instruction(
public_key,
message,
signature,
);

// Invoke the Ed25519 program to verify the signature
solana_program::program::invoke(
&verification_instruction,
&[ed25519_program_id.clone()],
)?;

msg!("Signature verification successful!");

// Transfer funds from custodial account to recipient
**custodial_account.try_borrow_mut_lamports()? = custodial_account
.lamports()
.checked_sub(amount)
.ok_or(ProgramError::InsufficientFunds)?;

**recipient.try_borrow_mut_lamports()? = recipient
.lamports()
.checked_add(amount)
.ok_or(ProgramError::Overflow)?;

Ok(())
}

#[cfg(test)]
mod tests {
use super::*;
use solana_program::clock::Epoch;

#[test]
fn test_transfer_with_valid_signature() {
// Test implementation here
}
}
71 changes: 71 additions & 0 deletions basics/ed25519-verification/native/tests/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Buffer } from 'node:buffer';
import { describe, it } from 'node:test';
import { sign } from '@noble/ed25519';
import {
Connection,
Keypair,
LAMPORTS_PER_SOL,
PublicKey,
SystemProgram,
Transaction,
TransactionInstruction,
sendAndConfirmTransaction,
} from '@solana/web3.js';
import { start } from 'solana-bankrun';

describe('Ed25519 Custodial Program', async () => {
const PROGRAM_ID = PublicKey.unique();
const context = await start([{ name: 'ed25519_custodial', programId: PROGRAM_ID }], []);
const client = context.banksClient;
const payer = context.payer;

it('should verify signature and transfer funds', async () => {
const custodialAccount = Keypair.generate();
const recipient = Keypair.generate();
const signerKeypair = Keypair.generate();
const amount = 1000000; // lamports

// Message to sign
const message = Buffer.from(`Transfer ${amount} lamports to ${recipient.publicKey.toBase58()}`);

// Sign the message with Ed25519
const signature = await sign(message, signerKeypair.secretKey.slice(0, 32));

// Create instruction data
const instructionData = Buffer.concat([
Buffer.from(signature),
Buffer.from(signerKeypair.publicKey.toBytes()),
Buffer.from(new Uint8Array(new BigUint64Array([BigInt(amount)]).buffer)),
message,
]);

const instruction = new TransactionInstruction({
keys: [
{
pubkey: custodialAccount.publicKey,
isSigner: false,
isWritable: true,
},
{ pubkey: recipient.publicKey, isSigner: false, isWritable: true },
{ pubkey: signerKeypair.publicKey, isSigner: true, isWritable: false },
{
pubkey: new PublicKey('Ed25519SigVerify111111111111111111111111111'),
isSigner: false,
isWritable: false,
},
],
programId: PROGRAM_ID,
data: instructionData,
});

const transaction = new Transaction().add(instruction);

try {
await client.processTransaction(transaction);
console.log('Transaction processed successfully');
} catch (error) {
console.error('Error:', error);
throw error;
}
});
});
10 changes: 10 additions & 0 deletions basics/ed25519-verification/native/tests/tsconfig.test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"types": ["mocha", "node"],
"typeRoots": ["../node_modules/@types"],
"lib": ["es2015"],
"module": "commonjs",
"target": "es6",
"esModuleInterop": true
}
}
25 changes: 25 additions & 0 deletions basics/ed25519-verification/poseidon/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "ed25519-custodial-poseidon",
"version": "1.0.0",
"scripts": {
"test": "jest",
"build": "tsc",
"lint": "eslint src/**/*.ts tests/**/*.ts",
"format": "prettier --write src/ tests/"
},
"dependencies": {
"@solana/web3.js": "^1.95.2",
"@noble/ed25519": "^1.7.1"
},
"devDependencies": {
"@types/jest": "^29.5.0",
"@types/node": "^18.0.0",
"@typescript-eslint/eslint-plugin": "^5.57.0",
"@typescript-eslint/parser": "^5.57.0",
"eslint": "^8.37.0",
"jest": "^29.5.0",
"prettier": "^2.8.7",
"ts-jest": "^29.1.0",
"typescript": "^5.0.3"
}
}
68 changes: 68 additions & 0 deletions basics/ed25519-verification/poseidon/src/program.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { sign } from '@noble/ed25519';
import { Connection, Keypair, PublicKey, SystemProgram, TransactionInstruction, TransactionMessage, VersionedTransaction } from '@solana/web3.js';

export class Ed25519CustodialProgram {
constructor(
private connection: Connection,
private programId: PublicKey,
) {}

async createTransferInstruction(
custodialAccount: PublicKey,
recipient: PublicKey,
signer: Keypair,
amount: number,
): Promise<TransactionInstruction> {
// Create message to sign
const message = Buffer.from(`Transfer ${amount} lamports to ${recipient.toBase58()}`);

// Sign the message with Ed25519
const signature = await sign(message, signer.secretKey.slice(0, 32));

// Create instruction data
const data = Buffer.concat([
Buffer.from(signature),
Buffer.from(signer.publicKey.toBytes()),
Buffer.from(new Uint8Array(new BigUint64Array([BigInt(amount)]).buffer)),
message,
]);

return new TransactionInstruction({
keys: [
{ pubkey: custodialAccount, isSigner: false, isWritable: true },
{ pubkey: recipient, isSigner: false, isWritable: true },
{ pubkey: signer.publicKey, isSigner: true, isWritable: false },
{
pubkey: new PublicKey('Ed25519SigVerify111111111111111111111111111'),
isSigner: false,
isWritable: false,
},
],
programId: this.programId,
data,
});
}

async transfer(custodialAccount: PublicKey, recipient: PublicKey, signer: Keypair, amount: number, payer: Keypair): Promise<string> {
const instruction = await this.createTransferInstruction(custodialAccount, recipient, signer, amount);

const latestBlockhash = await this.connection.getLatestBlockhash();

const messageV0 = new TransactionMessage({
payerKey: payer.publicKey,
recentBlockhash: latestBlockhash.blockhash,
instructions: [instruction],
}).compileToV0Message();

const transaction = new VersionedTransaction(messageV0);
transaction.sign([payer, signer]);

const signature = await this.connection.sendTransaction(transaction);
await this.connection.confirmTransaction({
signature,
...latestBlockhash,
});

return signature;
}
}
43 changes: 43 additions & 0 deletions basics/ed25519-verification/poseidon/tests/program.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { describe, it } from 'node:test';
import { Connection, Keypair, LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js';
import { Ed25519CustodialProgram } from '../src/program';

describe('Ed25519 Custodial Program', () => {
const connection = new Connection('http://localhost:8899', 'confirmed');
const programId = new PublicKey('your_program_id_here');
const program = new Ed25519CustodialProgram(connection, programId);

it('should create transfer instruction', async () => {
const custodialAccount = Keypair.generate();
const recipient = Keypair.generate();
const signer = Keypair.generate();
const amount = LAMPORTS_PER_SOL;

const instruction = await program.createTransferInstruction(custodialAccount.publicKey, recipient.publicKey, signer, amount);

// Verify instruction structure
expect(instruction.programId).toEqual(programId);
expect(instruction.keys).toHaveLength(4);
expect(instruction.data).toBeDefined();
});

it('should execute transfer', async () => {
const custodialAccount = Keypair.generate();
const recipient = Keypair.generate();
const signer = Keypair.generate();
const payer = Keypair.generate();
const amount = LAMPORTS_PER_SOL;

// Fund accounts for testing
await connection.requestAirdrop(custodialAccount.publicKey, 2 * LAMPORTS_PER_SOL);
await connection.requestAirdrop(payer.publicKey, LAMPORTS_PER_SOL);

const signature = await program.transfer(custodialAccount.publicKey, recipient.publicKey, signer, amount, payer);

expect(signature).toBeDefined();

// Verify balances
const recipientBalance = await connection.getBalance(recipient.publicKey);
expect(recipientBalance).toEqual(amount);
});
});
14 changes: 14 additions & 0 deletions basics/ed25519-verification/poseidon/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"declaration": true
},
"include": ["src", "tests"],
"exclude": ["node_modules"]
}
10 changes: 10 additions & 0 deletions basics/ed25519-verification/steel/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[workspace]
members = [
"api",
"program"
]
resolver = "2"

[workspace.dependencies]
solana-program = "1.16"
steel = "0.3.0"
8 changes: 8 additions & 0 deletions basics/ed25519-verification/steel/api/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "ed25519-custodial-api"
version = "0.1.0"
edition = "2021"

[dependencies]
solana-program.workspace = true
steel.workspace = true
46 changes: 46 additions & 0 deletions basics/ed25519-verification/steel/api/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use solana_program::{
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
system_program,
};
use steel::steel_api;

#[steel_api]
pub mod ed25519_custodial {
use super::*;

#[derive(Debug)]
pub struct TransferAccounts {
pub custodial_account: Pubkey,
pub recipient: Pubkey,
pub signer: Pubkey,
}

pub fn transfer(
program_id: Pubkey,
accounts: TransferAccounts,
signature: [u8; 64],
public_key: [u8; 32],
message: Vec<u8>,
amount: u64,
) -> Instruction {
let accounts = vec![
AccountMeta::new(accounts.custodial_account, false),
AccountMeta::new(accounts.recipient, false),
AccountMeta::new_readonly(accounts.signer, true),
AccountMeta::new_readonly(system_program::id(), false),
];

let mut data = Vec::with_capacity(104 + message.len());
data.extend_from_slice(&signature);
data.extend_from_slice(&public_key);
data.extend_from_slice(&amount.to_le_bytes());
data.extend_from_slice(&message);

Instruction {
program_id,
accounts,
data,
}
}
}
8 changes: 8 additions & 0 deletions basics/ed25519-verification/steel/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"scripts": {
"test": "steel test",
"build-and-test": "steel build && steel test",
"build": "steel build",
"deploy": "solana program deploy ./program/target/so/program.so"
}
}
11 changes: 11 additions & 0 deletions basics/ed25519-verification/steel/program/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "ed25519-custodial-program"
version = "0.1.0"
edition = "2021"

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

[dependencies]
solana-program.workspace = true
steel.workspace = true
61 changes: 61 additions & 0 deletions basics/ed25519-verification/steel/program/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
use solana_program::{
account_info::AccountInfo,
entrypoint::ProgramResult,
msg,
program_error::ProgramError,
pubkey::Pubkey,
ed25519_program,
};
use steel::steel_program;

#[steel_program]
pub mod ed25519_custodial {
use super::*;

pub fn transfer(
program_id: &Pubkey,
accounts: &[AccountInfo],
signature: [u8; 64],
public_key: [u8; 32],
message: Vec<u8>,
amount: u64,
) -> ProgramResult {
let custodial_account = &accounts[0];
let recipient = &accounts[1];
let signer = &accounts[2];
let ed25519_program_id = &accounts[3];

// Verify this is the expected Ed25519 program
if ed25519_program_id.key != &ed25519_program::id() {
return Err(ProgramError::InvalidArgument);
}

// Verify the Ed25519 signature
let verification_instruction = ed25519_program::instruction::new_ed25519_instruction(
&public_key,
&message,
&signature,
);

// Invoke the Ed25519 program to verify the signature
solana_program::program::invoke(
&verification_instruction,
&[ed25519_program_id.clone()],
)?;

msg!("Signature verification successful!");

// Transfer funds
**custodial_account.try_borrow_mut_lamports()? = custodial_account
.lamports()
.checked_sub(amount)
.ok_or(ProgramError::InsufficientFunds)?;

**recipient.try_borrow_mut_lamports()? = recipient
.lamports()
.checked_add(amount)
.ok_or(ProgramError::Overflow)?;

Ok(())
}
}
47 changes: 47 additions & 0 deletions basics/ed25519-verification/steel/program/tests/test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use {
ed25519_custodial_api::ed25519_custodial,
solana_program::{pubkey::Pubkey, system_program},
solana_program_test::*,
solana_sdk::{signature::Keypair, signer::Signer},
steel_test::*,
};

#[tokio::test]
async fn test_ed25519_transfer() {
let program_id = Pubkey::new_unique();
let mut context = program_test()
.add_program("ed25519_custodial", program_id)
.start_with_context()
.await;

let custodial_account = Keypair::new();
let recipient = Keypair::new();
let signer = Keypair::new();
let amount = 1_000_000;

// Create test message and signature
let message = format!("Transfer {} lamports to {}", amount, recipient.pubkey());
let signature = signer.sign_message(message.as_bytes());

let accounts = ed25519_custodial::TransferAccounts {
custodial_account: custodial_account.pubkey(),
recipient: recipient.pubkey(),
signer: signer.pubkey(),
};

let ix = ed25519_custodial::transfer(
program_id,
accounts,
signature.to_bytes(),
signer.pubkey().to_bytes(),
message.as_bytes().to_vec(),
amount,
);

let result = context
.banks_client
.process_transaction(&[ix], &[&signer])
.await;

assert!(result.is_ok());
}