Skip to content

feat: Validate signature expiration ledger before submitting auth ent…#202

Merged
KevinMB0220 merged 1 commit intoGalaxy-KJ:mainfrom
Bosun-Josh121:feat/validate-signature-expiration-ledger
Mar 29, 2026
Merged

feat: Validate signature expiration ledger before submitting auth ent…#202
KevinMB0220 merged 1 commit intoGalaxy-KJ:mainfrom
Bosun-Josh121:feat/validate-signature-expiration-ledger

Conversation

@Bosun-Josh121
Copy link
Copy Markdown
Contributor

@Bosun-Josh121 Bosun-Josh121 commented Mar 29, 2026

Pull Request

📋 Description

Validates signatureExpirationLedger on Soroban auth entries before assembling and returning signed transaction XDR. A new SignatureExpiredException is thrown early (at signing time) when a signature has already expired or falls within a configurable ledger buffer, giving developers a clear, actionable error instead of a cryptic on-chain rejection.

Changes:

  • SignatureExpiredException added to the types module with expirationLedger and currentLedger fields
  • New private validateSignatureExpiration() helper in SmartWalletService
  • Validation runs in all five signing paths: sign(), signWithSessionKey(), addSigner(), addSessionSigner(), removeSigner()
  • New optional constructor parameter expirationBufferLedgers (default: 10) for configurable buffer

🔗 Related Issues

Closes #188

🧪 Testing

  • Unit tests added/updated
  • Integration tests added/updated
  • Manual testing completed
  • All tests passing locally

9 new unit tests added covering:

  • Valid expiration (well beyond buffer) → passes
  • Expired signature (ledger already passed) → SignatureExpiredException
  • Nearly-expired signature (within buffer boundary) → SignatureExpiredException
  • Error shape (expirationLedger + currentLedger on thrown instance)
  • Source-account credentials (no expiry field) → skipped, no throw
  • Custom expirationBufferLedgers constructor value respected

All 76 tests pass.

📚 Documentation Updates (Required)

  • Updated docs/AI.md with new patterns/examples
  • Updated API reference in relevant package README
  • Added/updated code examples
  • Updated ARCHITECTURE.md (if architecture changed)
  • Added inline JSDoc/TSDoc comments
  • Updated ROADMAP.md progress (mark issue as completed)

Documentation Checklist by Component:

If you modified packages/core/invisible-wallet/:

  • Updated packages/core/invisible-wallet/README.md
  • Added security notes to docs/SECURITY.md
  • Updated wallet flow diagrams in docs/ARCHITECTURE.md

🤖 AI-Friendly Documentation

New Files Created

No new files. Changes are confined to existing wallet module files:
- packages/core/wallet/src/types/smart-wallet.types.ts - Added SignatureExpiredException
- packages/core/wallet/src/smart-wallet.service.ts     - Added validation logic
- packages/core/wallet/src/tests/smart-wallet.service.test.ts - Added expiration tests

Key Functions/Classes Added

// packages/core/wallet/src/types/smart-wallet.types.ts

export class SignatureExpiredException extends Error {
  constructor(
    public readonly expirationLedger: number,
    public readonly currentLedger: number
  ) {}
}

// packages/core/wallet/src/smart-wallet.service.ts

class SmartWalletService {
  constructor(
    webAuthnProvider: SmartWalletWebAuthnProvider,
    rpcUrl: string,
    factoryId?: string,
    network?: string,
    credentialBackend?: CredentialBackend,
    expirationBufferLedgers?: number   // NEW — default: 10
  )

  // Private — called internally before signing in all five signing methods
  private validateSignatureExpiration(
    authEntry: xdr.SorobanAuthorizationEntry,
    currentLedger: number
  ): void
}

Patterns Used

  • Early validation before signing: After simulation returns auth entries, the current ledger is fetched and each auth entry's signatureExpirationLedger is checked before the WebAuthn or Ed25519 signing step. This prevents users from completing a biometric gesture only to have the transaction fail on-chain.
  • Credential type guard: credentials().switch().name is checked before accessing .address(), so source-account credentials (which carry no expiry) are silently skipped — matching the same pattern used by validateDeFiAuthorization().
  • Reuse existing ledger fetch: Methods that already call getLatestLedger() (addSigner, addSessionSigner, removeSigner) reuse the sequence value to avoid an extra RPC round-trip. Methods that did not (sign, signWithSessionKey) now call it once after simulation.
  • Configurable buffer via constructor: expirationBufferLedgers is an optional last constructor parameter (default 10, ~50 seconds), preserving full backward compatibility for existing callers.

📸 Screenshots (if applicable)

N/A — server-side SDK change, no UI.

⚠️ Breaking Changes

  • No breaking changes

expirationBufferLedgers is an optional 6th constructor parameter with a default of 10. All existing call sites compile and behave identically unless a signature is expiring within 10 ledgers, in which case they now receive SignatureExpiredException instead of a silent on-chain failure.

🔄 Deployment Notes

No deployment changes required. This is a pure SDK-level validation; no contract changes, no new RPC endpoints, no configuration migrations.

✅ Final Checklist

  • Code follows project style guidelines
  • Self-review completed
  • No console.log or debug code left
  • Error handling implemented
  • Performance considered
  • Security reviewed
  • Documentation updated (required)
  • ROADMAP.md updated with progress

By submitting this PR, I confirm that:

  • ✅ I have updated all relevant documentation
  • ✅ AI.md includes new patterns from my changes
  • ✅ Examples are provided for new features
  • ✅ The documentation is accurate and helpful for AI assistants and developers

Summary by CodeRabbit

Release Notes

  • New Features

    • Added signature expiration validation to wallet signing operations with a configurable expiration buffer parameter (default: 10 ledgers)
    • Signatures that have expired or fall within the buffer window are now automatically rejected with detailed error information
  • Tests

    • Added comprehensive test coverage for signature expiration validation scenarios

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 29, 2026

📝 Walkthrough

Walkthrough

Introduces signature expiration validation to SmartWalletService by checking signatureExpirationLedger against current ledger + configurable buffer before signing. Adds SignatureExpiredException error type and integrates validation checks into five signing methods (sign, signWithSessionKey, addSigner, addSessionSigner, removeSigner).

Changes

Cohort / File(s) Summary
Exception Type Definition
packages/core/wallet/src/types/smart-wallet.types.ts
Added new SignatureExpiredException class with expirationLedger and currentLedger properties for error reporting.
Core Service Implementation
packages/core/wallet/src/smart-wallet.service.ts
Added validateSignatureExpiration() private method, extended constructor with expirationBufferLedgers parameter (default 10), and integrated expiration checks into sign(), signWithSessionKey(), addSigner(), addSessionSigner(), and removeSigner() methods using getLatestLedger() for current ledger lookup.
Test Suite
packages/core/wallet/src/tests/smart-wallet.service.test.ts
Updated makeAuthEntry() helper to accept configurable expiration ledger; added comprehensive test suite validating expiration checks for sign() and signWithSessionKey() including buffer boundary conditions, credential type discrimination, and constructor-configured buffer behavior.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • KevinMB0220

Poem

🐰 A signature that's ripe with age,
Won't slip past this careful page!
The buffer guards with gentle care,
Expiration? None shall pass from there!
Hop along with fresh-signed peace,
Let stale credentials finally cease! 🔐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main feature added: early validation of signature expiration ledger before auth entry submission.
Description check ✅ Passed The description is comprehensive, covering changes made, testing results, documentation status, and includes detailed AI-friendly patterns and examples.
Linked Issues check ✅ Passed All coding requirements from issue #188 are met: SignatureExpiredException added, validation logic implemented across five signing paths, configurable buffer parameter added, tests cover all specified cases.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #188: signature expiration validation. No unrelated modifications to other features or systems detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
packages/core/wallet/src/smart-wallet.service.ts (3)

487-519: ⚠️ Potential issue | 🟠 Major

Re-read currentLedger after simulation in these three flows.

The value passed into validateSignatureExpiration() here was fetched before the RPC simulation and is also being used to fake the source-account sequence. If the ledger advances during that round-trip, auth entries that are already inside the configured buffer can still pass validation here and then fail on-chain. Keep the pre-simulation sequence for TransactionBuilder, but fetch getLatestLedger() again after simulateTransaction() succeeds and validate against that fresh ledger.

🛠️ Suggested change
 const simResult = await this.server.simulateTransaction(invokeTx);
 if (Api.isSimulationError(simResult)) {
   throw new Error(`...`);
 }
 if (!simResult.result?.auth?.length) {
   throw new Error('...');
 }

 const authEntry: xdr.SorobanAuthorizationEntry = simResult.result.auth[0];
-this.validateSignatureExpiration(authEntry, sequence);
+const { sequence: currentLedger } = await this.server.getLatestLedger();
+this.validateSignatureExpiration(authEntry, currentLedger);

Apply the same post-simulation refresh in addSigner(), addSessionSigner(), and removeSigner().

Also applies to: 603-638, 744-777

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/wallet/src/smart-wallet.service.ts` around lines 487 - 519, The
pre-simulation ledger sequence is used to build the fake source account but must
be refreshed after simulation before calling validateSignatureExpiration to
avoid race conditions; keep using the original sequence for
TransactionBuilder/Transaction construction, but immediately after
simulateTransaction() succeeds (and before accessing auth/result entries) call
this.server.getLatestLedger() again and pass that fresh ledger.sequence into
validateSignatureExpiration(authEntry, freshSequence); apply this change in the
three flows/functions addSigner, addSessionSigner, and removeSigner, keeping
TransactionBuilder usage unchanged and only swapping the sequence value used for
validateSignatureExpiration.

439-455: ⚠️ Potential issue | 🟠 Major

Don't downgrade SignatureExpiredException to a plain Error in addSigner().

When the legacy session path delegates to addSessionSigner(), this catch wraps every Error in a new Error. That strips instanceof SignatureExpiredException and drops expirationLedger / currentLedger, so callers on the addSigner() path can't observe the new typed failure. Preserve the original object and only rewrite the message text if needed.

🛠️ Suggested change
       } catch (error) {
         if (error instanceof Error) {
-          throw new Error(
-            error.message.replace(/addSessionSigner/g, 'addSigner')
-          );
+          error.message = error.message.replace(/addSessionSigner/g, 'addSigner');
         }
         throw error;
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/wallet/src/smart-wallet.service.ts` around lines 439 - 455, The
catch currently wraps every Error in a new Error which loses typed exceptions
like SignatureExpiredException and their properties; update the catch in the
isLegacySessionPath branch so it preserves the original error object: if error
is an instance of SignatureExpiredException rethrow it as-is; otherwise if error
is an Error mutate its message in-place (e.g. error.message =
error.message.replace(/addSessionSigner/g, 'addSigner')) and then throw the
original error; for non-Error throwables keep throwing them unchanged; this
preserves SignatureExpiredException, its instanceof check and fields while still
normalizing the message for callers of addSigner().

371-384: ⚠️ Potential issue | 🟡 Minor

Validate expirationBufferLedgers before storing it.

This is a new public runtime option, but NaN, negative values, or stringly JS inputs change the currentLedger + expirationBufferLedgers comparison in surprising ways. That can silently disable the safeguard or make it reject everything. A small constructor guard would keep the feature safe for JS/env-configured callers.

🛠️ Suggested change
   ) {
+    if (
+      !Number.isInteger(expirationBufferLedgers) ||
+      expirationBufferLedgers < 0
+    ) {
+      throw new Error(
+        'expirationBufferLedgers must be a non-negative integer'
+      );
+    }
     this.server = new Server(rpcUrl);
     this.credentialBackend = credentialBackend;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/wallet/src/smart-wallet.service.ts` around lines 371 - 384,
Validate and sanitize the constructor parameter expirationBufferLedgers before
storing it: in the SmartWalletService constructor, coerce the incoming
expirationBufferLedgers (from the parameter list) to a safe integer (e.g.,
Number(...) then Math.floor) and ensure it is a finite non-negative number; if
it is NaN, non-finite, negative, or not a number, fallback to the default value
(10) or throw a clear error, and then assign the sanitized value to
this.expirationBufferLedgers so downstream comparisons (e.g., currentLedger +
expirationBufferLedgers) behave predictably.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/core/wallet/src/smart-wallet.service.ts`:
- Around line 487-519: The pre-simulation ledger sequence is used to build the
fake source account but must be refreshed after simulation before calling
validateSignatureExpiration to avoid race conditions; keep using the original
sequence for TransactionBuilder/Transaction construction, but immediately after
simulateTransaction() succeeds (and before accessing auth/result entries) call
this.server.getLatestLedger() again and pass that fresh ledger.sequence into
validateSignatureExpiration(authEntry, freshSequence); apply this change in the
three flows/functions addSigner, addSessionSigner, and removeSigner, keeping
TransactionBuilder usage unchanged and only swapping the sequence value used for
validateSignatureExpiration.
- Around line 439-455: The catch currently wraps every Error in a new Error
which loses typed exceptions like SignatureExpiredException and their
properties; update the catch in the isLegacySessionPath branch so it preserves
the original error object: if error is an instance of SignatureExpiredException
rethrow it as-is; otherwise if error is an Error mutate its message in-place
(e.g. error.message = error.message.replace(/addSessionSigner/g, 'addSigner'))
and then throw the original error; for non-Error throwables keep throwing them
unchanged; this preserves SignatureExpiredException, its instanceof check and
fields while still normalizing the message for callers of addSigner().
- Around line 371-384: Validate and sanitize the constructor parameter
expirationBufferLedgers before storing it: in the SmartWalletService
constructor, coerce the incoming expirationBufferLedgers (from the parameter
list) to a safe integer (e.g., Number(...) then Math.floor) and ensure it is a
finite non-negative number; if it is NaN, non-finite, negative, or not a number,
fallback to the default value (10) or throw a clear error, and then assign the
sanitized value to this.expirationBufferLedgers so downstream comparisons (e.g.,
currentLedger + expirationBufferLedgers) behave predictably.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 31d86f99-e5ff-4daf-ba98-2b61828c4c36

📥 Commits

Reviewing files that changed from the base of the PR and between cc968c0 and ed89ac5.

📒 Files selected for processing (3)
  • packages/core/wallet/src/smart-wallet.service.ts
  • packages/core/wallet/src/tests/smart-wallet.service.test.ts
  • packages/core/wallet/src/types/smart-wallet.types.ts

@KevinMB0220 KevinMB0220 merged commit bcb56f2 into Galaxy-KJ:main Mar 29, 2026
7 checks passed
@KevinMB0220 KevinMB0220 self-requested a review March 29, 2026 18:08
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.

[FEATURE] Validate signatureExpirationLedger before submitting auth entries

2 participants