Skip to content

Implement stealth payment scanning service#33

Open
jskoiz wants to merge 1 commit into
feature/14-of-15-payment-scanning-servicefrom
cursor/AVM-26-implement-stealth-payment-scanning-service-1464
Open

Implement stealth payment scanning service#33
jskoiz wants to merge 1 commit into
feature/14-of-15-payment-scanning-servicefrom
cursor/AVM-26-implement-stealth-payment-scanning-service-1464

Conversation

@jskoiz

@jskoiz jskoiz commented Oct 31, 2025

Copy link
Copy Markdown
Owner

This pull request contains changes generated by a Cursor Cloud Agent

Open in Cursor Open in Web

Co-authored-by: jkoizum <jkoizum@wgu.edu>
@cursor

cursor Bot commented Oct 31, 2025

Copy link
Copy Markdown

Cursor Agent can help with this pull request. Just @cursor in comments and I'll start working on changes in this branch.
Learn more about Cursor Agents

@linear

linear Bot commented Oct 31, 2025

Copy link
Copy Markdown
AVM-26 `[14/15] Implement Payment Scanning Service`

## Objective
Implement background payment scanning service that allows users to discover incoming stealth payments. Users scan the blockchain to find transactions sent to their stealth addresses.

## Branch
`feature/14-of-15-payment-scanning-service`

## Dependencies
- ✅ Issue [13/15] MUST be merged (stealth addresses working)
- ⚠️ DO NOT start until stealth address protocol is tested

## What to Build

### 1. Create `sdk/src/privacy/payment-scanner.ts`
Implement PaymentScanner class:

```typescript
class PaymentScanner {
  constructor(
    private connection: Connection,
    private metaAddress: StealthMetaAddress
  )
  
  // Scan recent transactions for incoming stealth payments
  async scanForPayments(
    startSlot?: number,
    endSlot?: number
  ): Promise<StealthPayment[]> {
    const payments: StealthPayment[] = [];
    
    // 1. Get recent transactions (or specific slot range)
    const signatures = await this.connection.getSignaturesForAddress(
      // Scan all transactions (no specific address filter)
      new PublicKey('11111111111111111111111111111111'), // System program
      { limit: 1000 }
    );
    
    // 2. For each transaction, check if it's for me
    for (const sig of signatures) {
      const tx = await this.connection.getTransaction(sig.signature);
      if (!tx) continue;
      
      const payment = await this.checkTransaction(tx);
      if (payment) {
        payments.push(payment);
      }
    }
    
    return payments;
  }
  
  // Check if single transaction is stealth payment for me
  private async checkTransaction(tx: Transaction): Promise<StealthPayment | null> {
    // 1. Extract ephemeral public key from transaction
    const ephemeralKey = this.extractEphemeralKey(tx);
    if (!ephemeralKey) return null;
    
    // 2. Compute shared secret
    const sharedSecret = this.ecdh(
      this.metaAddress.viewingSecretKey,
      ephemeralKey
    );
    
    // 3. Derive expected stealth address
    const expectedStealthAddress = this.deriveStealthAddress(
      this.metaAddress.spendingPublicKey,
      sharedSecret
    );
    
    // 4. Check if transaction destination matches
    const destination = this.extractDestination(tx);
    if (destination.equals(expectedStealthAddress)) {
      return {
        signature: tx.signature,
        amount: this.extractAmount(tx),
        stealthAddress: expectedStealthAddress,
        blockTime: tx.blockTime,
        ephemeralKey
      };
    }
    
    return null;
  }
  
  // Start background scanning (continuous)
  async startBackgroundScan(
    onPaymentFound: (payment: StealthPayment) => void,
    intervalMs: number = 30000 // Scan every 30 seconds
  ): Promise<() => void> {
    let lastScannedSlot = await this.connection.getSlot();
    
    const interval = setInterval(async () => {
      const currentSlot = await this.connection.getSlot();
      const payments = await this.scanForPayments(lastScannedSlot, currentSlot);
      
      payments.forEach(onPaymentFound);
      lastScannedSlot = currentSlot;
    }, intervalMs);
    
    // Return stop function
    return () => clearInterval(interval);
  }
  
  // Optimized: Scan only specific program (faster)
  async scanProgramTransactions(
    programId: PublicKey,
    limit: number = 1000
  ): Promise<StealthPayment[]> {
    // Scan only transactions involving specific program
    // Much faster than scanning all transactions
  }
}

2. Integration with GhostSolPrivacy

Update: sdk/src/privacy/ghost-sol-privacy.ts

class GhostSolPrivacy {
  private paymentScanner?: PaymentScanner;
  
  // Enable automatic payment scanning
  async enablePaymentScanning(
    onPaymentReceived: (payment: StealthPayment) => void
  ): Promise<void> {
    this.paymentScanner = new PaymentScanner(
      this.connection,
      this.stealthMetaAddress
    );
    
    // Start background scan
    await this.paymentScanner.startBackgroundScan(onPaymentReceived);
    
    console.log('Payment scanning enabled. You will be notified of incoming payments.');
  }
  
  // Manual scan
  async scanForPayments(): Promise<StealthPayment[]> {
    if (!this.paymentScanner) {
      throw new Error('Payment scanning not enabled');
    }
    
    return await this.paymentScanner.scanForPayments();
  }
}

3. Create sdk/test/privacy/payment-scanner.test.ts

Integration tests for scanning:

describe('Payment Scanner', () => {
  it('should detect incoming stealth payment', async () => {
    // Setup: Generate stealth meta-address
    const recipient = await stealthManager.generateStealthMetaAddress();
    const scanner = new PaymentScanner(connection, recipient);
    
    // Sender generates stealth address and sends payment
    const stealthAddress = await stealthManager.generateStealthAddress(recipient);
    await sendToStealthAddress(stealthAddress, 0.5);
    
    // Wait for transaction to confirm
    await sleep(5000);
    
    // Recipient scans for payments
    const payments = await scanner.scanForPayments();
    
    // Should find the payment
    expect(payments.length).toBeGreaterThan(0);
    expect(payments[0].amount).toBe(0.5);
  });
  
  it('should not detect payments for other users', async () => {
    const alice = await stealthManager.generateStealthMetaAddress();
    const bob = await stealthManager.generateStealthMetaAddress();
    
    // Send payment to Alice
    const aliceStealth = await stealthManager.generateStealthAddress(alice);
    await sendToStealthAddress(aliceStealth, 0.5);
    
    // Bob scans (should not find Alice's payment)
    const bobScanner = new PaymentScanner(connection, bob);
    const bobPayments = await bobScanner.scanForPayments();
    
    expect(bobPayments.length).toBe(0);
  });
  
  it('should scan efficiently (<10s for 1000 transactions)', async () => {
    const recipient = await stealthManager.generateStealthMetaAddress();
    const scanner = new PaymentScanner(connection, recipient);
    
    const start = Date.now();
    await scanner.scanForPayments();
    const duration = Date.now() - start;
    
    // Should be fast enough for good UX
    expect(duration).toBeLessThan(10000); // <10 seconds
  });
  
  it('should handle background scanning', async () => {
    const recipient = await stealthManager.generateStealthMetaAddress();
    const scanner = new PaymentScanner(connection, recipient);
    
    const paymentsFound: StealthPayment[] = [];
    
    // Start background scan
    const stop = await scanner.startBackgroundScan(
      (payment) => paymentsFound.push(payment),
      5000 // Scan every 5 seconds
    );
    
    // Send payment while scanning
    const stealthAddress = await stealthManager.generateStealthAddress(recipient);
    await sendToStealthAddress(stealthAddress, 0.3);
    
    // Wait for scan to detect it
    await sleep(10000);
    
    // Should have found payment
    expect(paymentsFound.length).toBeGreaterThan(0);
    
    // Stop scanning
    stop();
  });
});

Success Criteria

  • Can scan for incoming stealth payments
  • Scanning is reasonably fast (<10s per 1000 tx)
  • Background scanning works continuously
  • Only detects payments for correct recipient
  • Does not detect other users' payments
  • Integration tests pass
  • Memory usage acceptable (no leaks)
  • CPU usage acceptable (<10% during scan)

Performance Targets

  • Scan 1000 transactions: <10 seconds
  • Background scan interval: 30 seconds (configurable)
  • Memory usage: <100MB for scanner
  • CPU usage: <10% average during scan

Optimization Strategies

1. Filter by Program ID

Only scan transactions involving privacy program (faster):

// Instead of scanning ALL transactions
await connection.getSignaturesForAddress(systemProgram); // Slow

// Scan only privacy program transactions
await connection.getSignaturesForAddress(privacyProgramId); // Fast

2. Parallel Scanning

Scan multiple transaction batches in parallel:

const batches = chunkArray(transactions, 100);
const results = await Promise.all(
  batches.map(batch => scanBatch(batch))
);

3. Caching

Cache scanned slots to avoid re-scanning:

const lastScannedSlot = localStorage.getItem('lastScannedSlot');
// Only scan new transactions

Reference Documentation

  • /workspace/GHOSTSOL_IMPLEMENTATION_PLAN.md (Phase 4, Week 7-8)
  • /workspace/docs/research/confidential-transfers.md (lines 787-807: Scanning)

Time Estimate

3-4 days (Week 7-8, Days 1-4)

Notes

  • Scanning is necessary trade-off for unlinkability
  • Optimize for performance (users won't wait >10s)
  • Background scanning should be optional (battery/CPU)
  • Document scanning overhead clearly

@jskoiz jskoiz marked this pull request as ready for review October 31, 2025 02:57
@jskoiz

jskoiz commented May 6, 2026

Copy link
Copy Markdown
Owner Author

@codex review please run a code review on the latest PR head.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 55d21cbb5f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


// Use X25519 (Curve25519 ECDH)
// nacl.box.before computes the shared secret
const sharedSecret = nacl.box.before(publicKey, secretKeySeed);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use matching X25519 keypairs for shared-secret derivation

When a sender uses generateStealthAddress() and the recipient later scans, nacl.box.before is being fed Solana Keypair.generate() Ed25519 public keys/seeds rather than a Curve25519 keypair. The sender computes with the ephemeral seed plus viewing public key, while the scanner computes with the viewing seed plus ephemeral public key, so the shared secrets generally differ; in the normal payment flow deriveStealthAddressFromEphemeral() derives a different address than the one paid and PaymentScanner never reports the payment.

Useful? React with 👍 / 👎.

// TODO: Replace with proper Ed25519 point addition
const result = new Uint8Array(32);
for (let i = 0; i < 32; i++) {
result[i] = pubkey1[i] ^ pubkey2[i];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Derive a spendable stealth address keypair

When SOL is sent to the address returned by generateStealthAddress(), the recipient must later sign with deriveStealthSpendingKey(), but this address is constructed by bytewise XORing two Ed25519 public keys while the spending key XORs secret bytes. That secret key's Ed25519 public key will not equal this XORed destination address, so signatures for funds sent to these stealth addresses will be rejected and the funds are effectively unrecoverable.

Useful? React with 👍 / 👎.

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.

2 participants