Technical Overview
anon0mesh enables offline-first Solana transaction workflows through two complementary approaches: Direct RPC with Arcium Escrow for immediate confidential execution and BLE Mesh Relay for offline transaction queuing. Both methods preserve transaction privacy and enable trustless settlement.
Multi-Signature Transaction Flow with Durable Nonce
Overview
For transactions requiring multiple signatures (escrow, multi-party agreements), anon0mesh supports a coordinated signing flow using durable nonce accounts for offline compatibility.
Sequence Diagram
User A User B Application/Backend Solana Network
(A_keypair) (B_keypair) │
│ │ │ │
│ │ Build message │ │
│ │ (incl. Durable │ │
│ │ Nonce) │ │
│ ├───────────────────>│ │
│ │ │ │
│ │ Send unsigned │ │
│ │ VersionedTx │ │
│<────────────────┴────────────────────┤ │
│ │ │ │
│ tx.sign(A_keypair) │ │
│ → Partial sig │ │ │
├─────────┐ │ │ │
│ │ │ │ │
│<────────┘ │ │ │
│ │ │ │
│ Send tx.serialize() (signed by A) │ │
├─────────────────────────────────────>│ │
│ │ │ │
│ │ Forward partial │ │
│ │ bytes │ │
│ │<───────────────────┤ │
│ │ │ │
│ │ VersionedTx │ │
│ │ .deserialize() │ │
│ │ → add signature │ │
│ │ tx.sign(B_keypair) │
│ ├─────────┐ │ │
│ │ │ │ │
│ │<────────┘ │ │
│ │ │ │
│ │ sendRawTransaction(tx.serialize()) │
│ │ → with merged signatures │
│ ├───────────────────────────────────────>│
│ │ │ ├─ Validate
│ │ │ ├─ Execute
│ │ │ └─ Confirm
│ │ │ │
│ │ Confirm transaction │
│ │<───────────────────────────────────────┤
│ │ │ │
│ Notify TX validated │ │
│<────────────────┴────────────────────┤ │
│ │ │ │Implementation
import {
Transaction,
VersionedTransaction,
TransactionMessage,
Connection,
Keypair,
SystemProgram,
PublicKey,
NONCE_ACCOUNT_LENGTH
} from '@solana/web3.js';
// Step 1: Application builds unsigned transaction with durable nonce
async function buildMultiSigTransaction(
connection: Connection,
nonceAccount: PublicKey,
nonceAuthority: PublicKey,
signerA: PublicKey,
signerB: PublicKey,
recipient: PublicKey,
amount: number
): Promise<VersionedTransaction> {
// Get current nonce value
const nonceAccountInfo = await connection.getAccountInfo(nonceAccount);
const nonceData = NonceAccount.fromAccountData(nonceAccountInfo.data);
const currentNonce = nonceData.nonce;
console.log('Building multi-sig transaction with nonce:', currentNonce);
// Build transaction message
const message = new TransactionMessage({
payerKey: signerA, // Primary signer pays fees
recentBlockhash: currentNonce, // Use nonce instead of blockhash
instructions: [
// Advance nonce (required first instruction)
SystemProgram.nonceAdvance({
noncePubkey: nonceAccount,
authorizedPubkey: nonceAuthority,
}),
// Main transfer instruction
SystemProgram.transfer({
fromPubkey: signerA,
toPubkey: recipient,
lamports: amount,
}),
// Additional instruction requiring signerB
SystemProgram.transfer({
fromPubkey: signerB,
toPubkey: recipient,
lamports: amount,
}),
],
}).compileToV0Message();
// Create versioned transaction (unsigned)
const transaction = new VersionedTransaction(message);
return transaction;
}
// Step 2: User A signs transaction partially
async function userASignsTransaction(
transaction: VersionedTransaction,
userAKeypair: Keypair
): Promise<Uint8Array> {
console.log('User A signing transaction...');
// Sign with User A's keypair
transaction.sign([userAKeypair]);
console.log('User A signature added');
console.log('Signatures:', transaction.signatures.map(s =>
Buffer.from(s).toString('base64')
));
// Serialize partially signed transaction
const serialized = transaction.serialize();
return serialized;
}
// Step 3: Application forwards to User B
async function forwardToUserB(
serializedTx: Uint8Array,
backend: ApplicationBackend
): Promise<void> {
console.log('Forwarding partially signed transaction to User B...');
// Send via application backend (could be BLE mesh, HTTP, etc.)
await backend.sendToUserB({
transaction: Buffer.from(serializedTx).toString('base64'),
status: 'awaiting_signature',
});
}
// Step 4: User B deserializes, adds signature
async function userBSignsTransaction(
serializedTx: Uint8Array,
userBKeypair: Keypair
): Promise<VersionedTransaction> {
console.log('User B deserializing transaction...');
// Deserialize transaction (already has User A's signature)
const transaction = VersionedTransaction.deserialize(serializedTx);
console.log('Existing signatures:', transaction.signatures.length);
// Add User B's signature
transaction.sign([userBKeypair]);
console.log('User B signature added');
console.log('Total signatures:', transaction.signatures.length);
return transaction;
}
// Step 5: Submit fully signed transaction
async function submitMultiSigTransaction(
transaction: VersionedTransaction,
connection: Connection
): Promise<string> {
console.log('Submitting fully signed transaction...');
// Verify all signatures present
const requiredSignatures = transaction.message.staticAccountKeys.length;
const providedSignatures = transaction.signatures.filter(
sig => sig.some(byte => byte !== 0)
).length;
console.log(`Signatures: ${providedSignatures}/${requiredSignatures}`);
if (providedSignatures < requiredSignatures) {
throw new Error('Missing required signatures');
}
// Submit to Solana
const signature = await connection.sendRawTransaction(
transaction.serialize(),
{
skipPreflight: false,
maxRetries: 3,
}
);
console.log('Transaction submitted:', signature);
// Wait for confirmation
const confirmation = await connection.confirmTransaction(
signature,
'confirmed'
);
if (confirmation.value.err) {
throw new Error('Transaction failed: ' + JSON.stringify(confirmation.value.err));
}
console.log('✅ Transaction confirmed on-chain');
return signature;
}
// Step 6: Notify User A of success
async function notifyUserA(
signature: string,
backend: ApplicationBackend
): Promise<void> {
console.log('Notifying User A of transaction confirmation...');
await backend.notifyUserA({
signature: signature,
status: 'confirmed',
explorerUrl: `https://solscan.io/tx/${signature}`,
});
console.log('✅ User A notified');
}
// Complete flow orchestration
async function executeMultiSigFlow(
connection: Connection,
userAKeypair: Keypair,
userBKeypair: Keypair,
nonceAccount: PublicKey,
recipient: PublicKey,
amount: number,
backend: ApplicationBackend
): Promise<string> {
console.log('=== Multi-Signature Transaction Flow ===\n');
// 1. Build unsigned transaction
const unsignedTx = await buildMultiSigTransaction(
connection,
nonceAccount,
userAKeypair.publicKey, // Nonce authority
userAKeypair.publicKey,
userBKeypair.publicKey,
recipient,
amount
);
// 2. User A signs
const partiallySigned = await userASignsTransaction(
unsignedTx,
userAKeypair
);
// 3. Forward to User B
await forwardToUserB(partiallySigned, backend);
// 4. User B signs
const fullySigned = await userBSignsTransaction(
partiallySigned,
userBKeypair
);
// 5. Submit to Solana
const signature = await submitMultiSigTransaction(
fullySigned,
connection
);
// 6. Notify User A
await notifyUserA(signature, backend);
return signature;
}BLE Mesh Integration
For offline multi-signature coordination via BLE:
// User A broadcasts partially signed transaction via BLE
async function broadcastPartialSignatureBLE(
partiallySigned: Uint8Array,
userBPubkey: PublicKey,
bleAdapter: BLEAdapter
): Promise<void> {
const packet = createPacket({
type: PacketType.PARTIAL_SIGNATURE,
payload: partiallySigned,
ttl: 8,
priority: Priority.HIGH,
metadata: {
requiredSigner: userBPubkey.toBase58(),
signatureCount: 1,
totalRequired: 2,
network: 'mainnet-beta',
},
});
await bleAdapter.broadcast(packet);
console.log('Partially signed transaction broadcasted via BLE');
}
// User B receives, signs, and broadcasts complete transaction
class MultiSigBLECoordinator {
private bleAdapter: BLEAdapter;
private userKeypair: Keypair;
async listenForPartialSignatures(): Promise<void> {
this.bleAdapter.on('packet', async (packet) => {
if (packet.type !== PacketType.PARTIAL_SIGNATURE) return;
// Check if this signature request is for us
const metadata = packet.metadata;
if (metadata.requiredSigner !== this.userKeypair.publicKey.toBase58()) {
console.log('Signature not required from us, relaying...');
return;
}
console.log('Received partial signature request');
// Deserialize and sign
const partiallySigned = packet.payload;
const transaction = VersionedTransaction.deserialize(partiallySigned);
transaction.sign([this.userKeypair]);
console.log('Added our signature, broadcasting complete transaction');
// Broadcast fully signed transaction
const fullySignedPacket = createPacket({
type: PacketType.TRANSACTION,
payload: transaction.serialize(),
ttl: 8,
priority: Priority.HIGH,
metadata: {
signatureCount: 2,
complete: true,
network: 'mainnet-beta',
},
});
await this.bleAdapter.broadcast(fullySignedPacket);
});
}
}Security Considerations
Signature Order
Order matters! Signatures must be added in the same order as signers in the transaction
Use
transaction.sign([keypair])to add signatures correctly
Nonce Validity
Durable nonce remains valid until used
Once transaction is submitted, nonce advances automatically
Must fetch new nonce value for subsequent transactions
Replay Protection
Each nonce can only be used once
After successful submission, previous signature is invalid
Prevents replay attacks
Partial Signature Validation
function validatePartialSignature(
transaction: VersionedTransaction,
expectedSigners: PublicKey[]
): boolean {
const providedSignatures = transaction.signatures.filter(
sig => sig.some(byte => byte !== 0)
).length;
console.log(`Signatures provided: ${providedSignatures}/${expectedSigners.length}`);
// Verify at least one signature present
if (providedSignatures === 0) {
throw new Error('No signatures provided');
}
// Check signature count doesn't exceed expected
if (providedSignatures > expectedSigners.length) {
throw new Error('Too many signatures');
}
return true;
}Transaction Relay Modes
Mode 1: Direct Transaction via RPC → Arcium Escrow
Standard flow for users with internet connectivity who want confidential escrow execution.
Use Case: User has connectivity and wants to execute a confidential escrow transaction with privacy guarantees.
Flow:
User Wallet Solana RPC Arcium Escrow Solana Chain
│ │ │ │
├─ Create tx │ │ │
├─ Sign tx │ │ │
│ │ │ │
├─ Submit ──────────>│ │ │
│ ├─ Validate │ │
│ ├─ Process │ │
│ └─ Forward ──────────────>│ │
│ ├─ Initialize escrow │
│ ├─ Lock funds in PDA │
│ ├─ Create MPC circuit │
│ ├─ Execute conditions │
│ └─ Submit settlement ─────>
│ │ ├─ Verify
│ │ ├─ Execute
│ │ └─ Confirm
│ │ │
│ <─────────────────────────┴──────────────────────┘
│ Confirmation
│<─ Settlement event─┤Architecture:
┌─────────────────────────────────────────────────────────────┐
│ Arcium Escrow System │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Escrow PDA │ │ Arcium Network │ │
│ │ │ │ │ │
│ │ • Locked Funds │◄────────┤ • Condition │ │
│ │ • Conditions │ │ Verification │ │
│ │ • Parties │ │ • Secret Shares │ │
│ │ • Timeout │ │ • MPC Execution │ │
│ └──────────────────┘ └──────────────────┘ │
│ │ │ │
│ └─────────────┬───────────────┘ │
│ ▼ │
│ Settlement Logic │
│ (Executed in Circuit) │
└─────────────────────────────────────────────────────────────┘Implementation:
import {
Transaction,
SystemProgram,
PublicKey,
Connection
} from '@solana/web3.js';
import { ArciumEscrow } from '@arcium/solana-sdk';
// 1. Initialize escrow transaction
async function createEscrowTransaction(
sender: Keypair,
recipient: PublicKey,
amount: number,
conditions: EscrowConditions
): Promise<Transaction> {
const connection = new Connection('https://api.mainnet-beta.solana.com');
// Create escrow PDA
const [escrowPDA] = await PublicKey.findProgramAddress(
[
Buffer.from('escrow'),
sender.publicKey.toBuffer(),
recipient.toBuffer(),
],
ArciumEscrow.PROGRAM_ID
);
// Build escrow initialization transaction
const transaction = new Transaction().add(
ArciumEscrow.initialize({
escrow: escrowPDA,
sender: sender.publicKey,
recipient: recipient,
amount: amount,
conditions: {
timeout: conditions.timeout,
requiresAttestation: conditions.requiresAttestation,
circuitId: conditions.circuitId,
},
}),
SystemProgram.transfer({
fromPubkey: sender.publicKey,
toPubkey: escrowPDA,
lamports: amount,
})
);
// Get recent blockhash and sign
transaction.recentBlockhash = (
await connection.getRecentBlockhash()
).blockhash;
transaction.feePayer = sender.publicKey;
transaction.sign(sender);
return transaction;
}
// 2. Submit to RPC and monitor escrow
async function submitEscrowTransaction(
transaction: Transaction,
connection: Connection
): Promise<EscrowResult> {
// Submit transaction
const signature = await connection.sendRawTransaction(
transaction.serialize(),
{ skipPreflight: false }
);
console.log('Escrow initialized:', signature);
// Wait for confirmation
const confirmation = await connection.confirmTransaction(
signature,
'confirmed'
);
if (confirmation.value.err) {
throw new Error('Escrow initialization failed');
}
// Monitor escrow state
const escrowState = await monitorEscrowExecution(
transaction,
connection
);
return escrowState;
}
// 3. Arcium MPC circuit validates and settles
async function monitorEscrowExecution(
transaction: Transaction,
connection: Connection
): Promise<EscrowResult> {
const escrowPDA = transaction.instructions[0].keys[0].pubkey;
// Listen for Arcium circuit events
const subscriptionId = connection.onAccountChange(
escrowPDA,
async (accountInfo) => {
const escrowData = ArciumEscrow.decode(accountInfo.data);
console.log('Escrow state:', escrowData.state);
if (escrowData.state === 'SETTLED') {
console.log('Settlement confirmed:', escrowData.settlementTx);
return {
settled: true,
signature: escrowData.settlementTx,
timestamp: escrowData.settledAt,
};
}
if (escrowData.state === 'CANCELLED') {
console.log('Escrow cancelled, funds returned');
return {
settled: false,
reason: 'CANCELLED',
};
}
},
'confirmed'
);
// Timeout after 1 hour
await new Promise(resolve => setTimeout(resolve, 3600000));
connection.removeAccountChangeListener(subscriptionId);
}
// 4. Escrow conditions interface
interface EscrowConditions {
timeout: number; // Unix timestamp
requiresAttestation: boolean; // Needs external proof
circuitId: string; // Arcium circuit to execute
metadata?: any; // Custom conditions
}
// Example usage
const conditions: EscrowConditions = {
timeout: Date.now() + 86400000, // 24 hours
requiresAttestation: true,
circuitId: 'escrow-settlement-v1',
metadata: {
deliveryConfirmation: true,
oracleVerification: true,
},
};
const escrowTx = await createEscrowTransaction(
senderKeypair,
recipientPubkey,
1000000000, // 1 SOL
conditions
);
const result = await submitEscrowTransaction(escrowTx, connection);Arcium Circuit Logic:
// Pseudocode for Arcium escrow settlement circuit
circuit EscrowSettlement {
// Private inputs (encrypted)
private sender: Pubkey
private recipient: Pubkey
private amount: u64
private conditions: EscrowConditions
// Public inputs
public attestation: Signature
public timestamp: i64
// Computation
fn execute() -> SettlementDecision {
// 1. Verify attestation signature
if !verify_signature(attestation, conditions.oracle_pubkey) {
return SettlementDecision::Cancel;
}
// 2. Check timeout
if timestamp > conditions.timeout {
return SettlementDecision::Refund;
}
// 3. Validate conditions
if conditions.requires_attestation && attestation.valid {
return SettlementDecision::Settle {
to: recipient,
amount: amount,
};
}
SettlementDecision::Cancel
}
// Output (public)
public settlement: SettlementDecision
}Privacy Guarantees:
Sender/Recipient Privacy: Encrypted in circuit, not visible on-chain
Amount Privacy: Hidden in escrow PDA until settlement
Condition Privacy: Circuit evaluates conditions
Oracle Privacy: Attestation verified without revealing source
Mode 2: BLE Mesh Relay (Offline-First)
For users without connectivity who want to queue transactions for relay.
Use Case: User is offline, creates and signs transaction, broadcasts via BLE mesh, online peer relays to Solana.
Flow:
Offline User BLE Mesh Online Peer Solana RPC
│ │ │ │
├─ Create tx │ │ │
├─ Add 2% (0.6% fee goes to relayer│ │ │
├─ Sign with wallet │ │ │
├─ Serialize packet │ │ │
│ (includes relay │ │ │
│ fee instruction) │ │ │
│ │ │ │
├─ Broadcast BLE ───>│ │ │
│ ├─ Relay (TTL) │ │
│ └───────────────────────>│ │
│ ├─ Deserialize │
│ ├─ Validate sig │
│ ├─ Insert pubkey │
│ ├─ Check blockhash │
│ └─ Submit ──────────>
│ │ ├─ Process
│ │ ├─ Pay 2% fee
│ │ └─ Confirm
│ │ │
│ <─ Confirmation ─────────┴─────────────────┘
│ │ (with relay fee) │
│<─ Status update ───┤ │Packet Structure:
// BLE packet format for Solana transactions with relay fee
interface SolanaTransactionPacket {
header: {
magic: 0xAB0D; // Protocol identifier
version: 0x01; // Protocol version
type: PacketType.TRANSACTION;
ttl: number; // Remaining hops
flags: PacketFlags; // Priority, encryption, etc.
length: number; // Payload size
};
packetId: Uint8Array; // 32 bytes - SHA256 hash
payload: {
transaction: Uint8Array; // Serialized Solana transaction with placeholder
totalAmount: number; // Original transfer amount
relayFeePercent: number; // 2% (200 basis points)
metadata: {
network: 'mainnet' | 'devnet' | 'testnet';
priority: TransactionPriority;
expiresAt: number; // Blockhash expiry
createdAt: number;
requiresRelayFee: boolean; // Must pay relay fee
};
};
signature: Uint8Array; // 64 bytes - ed25519 signature
}Implementation:
import { Transaction, Connection, Keypair, SystemProgram, PublicKey } from '@solana/web3.js';
import { BLEAdapter } from './infrastructure/ble/BLEAdapter';
import { createPacket, PacketType } from './domain/Packet';
// Relay fee configuration
const RELAY_FEE_PERCENT = 0.006; // 0.6%
const RELAY_PLACEHOLDER_PUBKEY = new PublicKey('11111111111111111111111111111111');
// 1. Create transaction offline with relay fee
async function createOfflineTransactionWithRelayFee(
sender: Keypair,
recipient: PublicKey,
amount: number,
cachedBlockhash?: string
): Promise<{ transaction: Transaction; totalAmount: number }> {
// Calculate relay fee (2% of amount)
const relayFee = Math.floor(amount * RELAY_FEE_PERCENT);
const recipientAmount = amount - relayFee;
console.log(`Original amount: ${amount} lamports`);
console.log(`Relay fee (0.6%): ${relayFee} lamports`);
console.log(`Recipient gets: ${recipientAmount} lamports`);
const transaction = new Transaction().add(
// Main transfer to recipient
SystemProgram.transfer({
fromPubkey: sender.publicKey,
toPubkey: recipient,
lamports: recipientAmount,
}),
// Relay fee transfer (placeholder pubkey, will be replaced)
SystemProgram.transfer({
fromPubkey: sender.publicKey,
toPubkey: RELAY_PLACEHOLDER_PUBKEY, // Replaced by first relayer
lamports: relayFee,
})
);
// Use cached blockhash if available, or placeholder
transaction.recentBlockhash = cachedBlockhash ||
'11111111111111111111111111111111';
transaction.feePayer = sender.publicKey;
transaction.sign(sender);
return { transaction, totalAmount: amount };
}
// 2. Serialize and broadcast via BLE
async function broadcastViaBLE(
transaction: Transaction,
totalAmount: number,
bleAdapter: BLEAdapter
): Promise<void> {
// Serialize transaction
const serialized = transaction.serialize({
requireAllSignatures: false,
verifySignatures: true,
});
// Create mesh packet with relay fee metadata
const packet = createPacket({
type: PacketType.TRANSACTION,
payload: serialized,
ttl: 8, // Maximum 8 hops
priority: Priority.HIGH,
metadata: {
network: 'mainnet-beta',
totalAmount: totalAmount,
relayFeePercent: RELAY_FEE_PERCENT,
requiresRelayFee: true,
expiresAt: Date.now() + 120000, // 2 minutes
createdAt: Date.now(),
},
});
// Broadcast to mesh
await bleAdapter.broadcast(packet);
console.log('Transaction broadcasted via BLE mesh with relay fee');
console.log(`First relayer will receive ${Math.floor(totalAmount * RELAY_FEE_PERCENT)} lamports`);
}
// 3. Online peer receives, inserts pubkey, and relays
class OnlinePeerRelayWithFee {
private connection: Connection;
private bleAdapter: BLEAdapter;
private relayerKeypair: Keypair;
private seenTransactions: Set<string>;
constructor(rpcUrl: string, bleAdapter: BLEAdapter, relayerKeypair: Keypair) {
this.connection = new Connection(rpcUrl);
this.bleAdapter = bleAdapter;
this.relayerKeypair = relayerKeypair;
this.seenTransactions = new Set();
}
async startRelayService(): Promise<void> {
console.log(`Relay service started. Relayer pubkey: ${this.relayerKeypair.publicKey.toBase58()}`);
// Listen for transaction packets
this.bleAdapter.on('packet', async (packet) => {
if (packet.type !== PacketType.TRANSACTION) return;
try {
await this.relayTransactionWithFee(packet);
} catch (error) {
console.error('Relay failed:', error);
}
});
}
private async relayTransactionWithFee(packet: Packet): Promise<void> {
// 1. Check for duplicates (first relayer wins)
const txHash = hashPacket(packet);
if (this.seenTransactions.has(txHash)) {
console.log('Transaction already relayed by another peer');
return;
}
this.seenTransactions.add(txHash);
// 2. Deserialize transaction
const transaction = Transaction.from(packet.payload);
// 3. Validate signature
if (!transaction.verifySignatures()) {
throw new Error('Invalid transaction signature');
}
// 4. Check blockhash validity
const metadata = packet.metadata;
if (Date.now() > metadata.expiresAt) {
console.log('Transaction expired, not relaying');
return;
}
// 5. Check if relay fee is required
if (!metadata.requiresRelayFee) {
throw new Error('Transaction does not include relay fee');
}
// 6. Insert relayer pubkey into fee instruction
const relayFeeAmount = Math.floor(metadata.totalAmount * metadata.relayFeePercent);
// Find the relay fee instruction (second transfer instruction)
const feeInstruction = transaction.instructions[1];
if (feeInstruction.programId.equals(SystemProgram.programId)) {
// Replace placeholder pubkey with actual relayer pubkey
const keys = feeInstruction.keys;
const recipientKey = keys.find(k =>
k.pubkey.equals(RELAY_PLACEHOLDER_PUBKEY)
);
if (recipientKey) {
recipientKey.pubkey = this.relayerKeypair.publicKey;
console.log(`✅ Inserted relayer pubkey: ${this.relayerKeypair.publicKey.toBase58()}`);
console.log(`💰 Relay fee: ${relayFeeAmount} lamports (${metadata.relayFeePercent * 100}%)`);
}
}
// 7. Update blockhash if needed
if (transaction.recentBlockhash === '11111111111111111111111111111111') {
const { blockhash } = await this.connection.getRecentBlockhash();
transaction.recentBlockhash = blockhash;
}
// 8. Submit to Solana RPC
const signature = await this.connection.sendRawTransaction(
transaction.serialize(),
{
skipPreflight: false,
maxRetries: 3,
}
);
console.log('Transaction submitted:', signature);
console.log(`Relay fee will be paid to: ${this.relayerKeypair.publicKey.toBase58()}`);
// 9. Wait for confirmation
const confirmation = await this.connection.confirmTransaction(
signature,
'confirmed'
);
// 10. Broadcast confirmation back to mesh
if (!confirmation.value.err) {
await this.broadcastConfirmation(txHash, signature, relayFeeAmount);
}
}
private async broadcastConfirmation(
txHash: string,
signature: string,
relayFee: number
): Promise<void> {
const confirmationPacket = createPacket({
type: PacketType.TRANSACTION_CONFIRMATION,
payload: encodeConfirmation({
txHash,
signature,
relayerPubkey: this.relayerKeypair.publicKey.toBase58(),
relayFee: relayFee,
}),
ttl: 8,
priority: Priority.NORMAL,
});
await this.bleAdapter.broadcast(confirmationPacket);
console.log('Confirmation broadcasted to mesh');
}
}
// 4. Offline user receives confirmation with relay fee info
class OfflineUserMonitor {
private bleAdapter: BLEAdapter;
private pendingTransactions: Map<string, TransactionMetadata>;
async monitorConfirmations(): Promise<void> {
this.bleAdapter.on('packet', (packet) => {
if (packet.type !== PacketType.TRANSACTION_CONFIRMATION) return;
const { txHash, signature, relayerPubkey, relayFee } = decodeConfirmation(packet.payload);
const metadata = this.pendingTransactions.get(txHash);
if (metadata) {
console.log('✅ Transaction confirmed:', signature);
console.log(`💰 Relay fee paid: ${relayFee} lamports to ${relayerPubkey}`);
metadata.status = 'confirmed';
metadata.signature = signature;
metadata.relayerPubkey = relayerPubkey;
metadata.relayFee = relayFee;
// Notify user
this.notifyUser(metadata);
}
});
}
private notifyUser(metadata: TransactionMetadata): void {
// Update UI with relay fee information
console.log('Transaction Details:');
console.log(` Signature: ${metadata.signature}`);
console.log(` Relayed by: ${metadata.relayerPubkey}`);
console.log(` Relay fee: ${metadata.relayFee} lamports (0.6%)`);
console.log(` Status: ${metadata.status}`);
}
}
// Example usage
const bleAdapter = new BLEAdapter();
// Offline user creates transaction with relay fee
const { transaction, totalAmount } = await createOfflineTransactionWithRelayFee(
userKeypair,
recipientPubkey,
1000000, // 1,000,000 lamports (0.001 SOL)
cachedBlockhash
);
// Recipient will receive: 994,000 lamports (99.4%)
// Relayer will receive: 6,000 lamports (0.6%)
await broadcastViaBLE(transaction, totalAmount, bleAdapter);
// Online peer (relayer) with their keypair
const relayerKeypair = Keypair.generate(); // Or load existing keypair
const relay = new OnlinePeerRelayWithFee(
'https://api.mainnet-beta.solana.com',
bleAdapter,
relayerKeypair
);
await relay.startRelayService();
// Monitor confirmations with relay fee info
const monitor = new OfflineUserMonitor(bleAdapter);
await monitor.monitorConfirmations();Relay Fee Economics
Fee Structure
interface RelayFeeStructure {
percentage: number; // 0.6% (60 basis points)
minimumFee: number; // Minimum fee in lamports
maximumFee: number; // Maximum fee cap (optional)
priorityMultiplier: number; // 1.5x for high priority
}
const RELAY_FEE_CONFIG: RelayFeeStructure = {
percentage: 0.006, // 0.6%
minimumFee: 5000, // 0.000005 SOL minimum
maximumFee: 1000000, // 0.001 SOL maximum (optional)
priorityMultiplier: 1.0, // No multiplier for standard
};
function calculateRelayFee(
amount: number,
priority: Priority = Priority.NORMAL
): number {
let fee = Math.floor(amount * RELAY_FEE_CONFIG.percentage);
// Apply priority multiplier
if (priority === Priority.HIGH) {
fee = Math.floor(fee * 1.5);
} else if (priority === Priority.URGENT) {
fee = Math.floor(fee * 2.0);
}
// Enforce minimum and maximum
fee = Math.max(fee, RELAY_FEE_CONFIG.minimumFee);
if (RELAY_FEE_CONFIG.maximumFee) {
fee = Math.min(fee, RELAY_FEE_CONFIG.maximumFee);
}
return fee;
}Example Fee Calculations
0.001 SOL (1M lamports)
6,000 lamports
994,000 lamports
6,000 lamports
0.01 SOL (10M lamports)
60,000 lamports
9,940,000 lamports
60,000 lamports
0.1 SOL (100M lamports)
600,000 lamports
99,400,000 lamports
600,000 lamports
1 SOL (1B lamports)
6,000,000 lamports
994,000,000 lamports
6,000,000 lamports
Race Condition Handling
Multiple peers may receive the transaction simultaneously. First to submit wins:
class RelayCompetition {
private submissionAttempts: Map<string, SubmissionAttempt[]>;
async handleRaceCondition(txHash: string, relayerPubkey: PublicKey): Promise<boolean> {
const attempts = this.submissionAttempts.get(txHash) || [];
// Record attempt with timestamp
attempts.push({
relayerPubkey: relayerPubkey.toBase58(),
timestamp: Date.now(),
});
this.submissionAttempts.set(txHash, attempts);
// Try to submit
try {
await this.submitTransaction(txHash);
console.log(`✅ Won relay race! Fee earned by ${relayerPubkey.toBase58()}`);
return true;
} catch (error) {
if (error.message.includes('already processed')) {
console.log(`❌ Lost relay race. Another peer was faster.`);
return false;
}
throw error;
}
}
}Anti-Spam Protection
class RelaySpamProtection {
private relayHistory: Map<string, RelayHistory>;
canRelayTransaction(
peerId: string,
transactionSize: number
): boolean {
const history = this.relayHistory.get(peerId) || {
successfulRelays: 0,
failedRelays: 0,
totalFeesEarned: 0,
lastRelayTime: 0,
};
// Rate limit: max 100 relays per hour
const hourAgo = Date.now() - 3600000;
if (history.lastRelayTime > hourAgo) {
const recentRelays = this.countRecentRelays(peerId, hourAgo);
if (recentRelays >= 100) {
return false;
}
}
// Reputation check: success rate > 80%
const totalRelays = history.successfulRelays + history.failedRelays;
if (totalRelays > 10) {
const successRate = history.successfulRelays / totalRelays;
if (successRate < 0.8) {
return false;
}
}
return true;
}
}BLE-Specific Optimizations
Transaction Compression
import { compress, decompress } from 'lz4';
function compressTransaction(tx: Transaction): Uint8Array {
const serialized = tx.serialize();
const compressed = compress(serialized);
console.log(`Original: ${serialized.length} bytes`);
console.log(`Compressed: ${compressed.length} bytes`);
console.log(`Savings: ${((1 - compressed.length / serialized.length) * 100).toFixed(1)}%`);
return compressed;
}
function decompressTransaction(data: Uint8Array): Transaction {
const decompressed = decompress(data);
return Transaction.from(decompressed);
}Fragmentation for Large Transactions
// BLE MTU typically 512 bytes, need to fragment larger transactions
class TransactionFragmenter {
private readonly FRAGMENT_SIZE = 400; // Leave room for headers
fragment(tx: Transaction): Uint8Array[] {
const serialized = tx.serialize();
const fragments: Uint8Array[] = [];
for (let i = 0; i < serialized.length; i += this.FRAGMENT_SIZE) {
const fragment = serialized.slice(i, i + this.FRAGMENT_SIZE);
fragments.push(fragment);
}
return fragments;
}
reassemble(fragments: Uint8Array[]): Transaction {
const totalLength = fragments.reduce((sum, f) => sum + f.length, 0);
const reassembled = new Uint8Array(totalLength);
let offset = 0;
for (const fragment of fragments) {
reassembled.set(fragment, offset);
offset += fragment.length;
}
return Transaction.from(reassembled);
}
}Hybrid Approach: BLE + Arcium
Combine offline BLE relay with confidential Arcium execution.
Flow:
Offline User BLE Mesh Online Peer Arcium Escrow Solana
│ │ │ │ │
├─ Create │ │ │ │
├─ Encrypt │ │ │ │
├─ BLE ────────>│ │ │ │
│ ├─ Relay │ │ │
│ └─────────────────>│ │ │
│ ├─ Submit ───────────>│ │
│ │ ├─ Execute │
│ │ └─────────────────>│
│ │ │ ├─ Confirm
│ <─ Confirmation ───┴─────────────────┴──────────────┘Implementation:
// Create encrypted intent for Arcium
const intent = {
type: 'escrow_transfer',
params: {
amount: 1000000,
recipient: recipientPubkey.toBase58(),
conditions: {
timeout: Date.now() + 86400000,
requiresAttestation: true,
},
},
};
// Encrypt for Arcium
const encrypted = await arciumClient.encrypt(
JSON.stringify(intent),
arciumNetworkPublicKey
);
// Broadcast via BLE
const packet = createPacket({
type: PacketType.ARCIUM_INTENT,
payload: encrypted,
ttl: 12,
priority: Priority.HIGH,
});
await bleAdapter.broadcast(packet);Best Practices
Transaction Validation
function validateTransactionPacket(packet: Packet): boolean {
// 1. Check packet structure
if (!packet.header || !packet.payload) {
return false;
}
// 2. Verify TTL
if (packet.header.ttl <= 0) {
return false;
}
// 3. Check expiry
if (packet.metadata.expiresAt < Date.now()) {
return false;
}
// 4. Validate transaction signature
const transaction = Transaction.from(packet.payload);
if (!transaction.verifySignatures()) {
return false;
}
return true;
}Rate Limiting
class RelayRateLimiter {
private counters = new Map<string, { count: number; reset: number }>();
private readonly LIMIT = 100; // 100 tx per hour
private readonly WINDOW = 3600000; // 1 hour
canRelay(peerId: string): boolean {
const now = Date.now();
const counter = this.counters.get(peerId);
if (!counter || now > counter.reset) {
this.counters.set(peerId, { count: 1, reset: now + this.WINDOW });
return true;
}
if (counter.count >= this.LIMIT) {
return false;
}
counter.count++;
return true;
}
}Summary
Direct RPC → Arcium
Online
High
Low
Confidential escrow with immediate execution
BLE Mesh Relay
Offline
High (w/ Noise Protocol)
Medium
Offline transaction queuing and relay
Choose the appropriate mode based on your connectivity, privacy requirements, and latency tolerance.
Last updated
