Skip to content

RPC method getSignaturesForAddress implemented#95

Open
sebatustra wants to merge 6 commits intomainfrom
feat/implement-get-signatures-for-address
Open

RPC method getSignaturesForAddress implemented#95
sebatustra wants to merge 6 commits intomainfrom
feat/implement-get-signatures-for-address

Conversation

@sebatustra
Copy link
Copy Markdown
Collaborator

@sebatustra sebatustra commented Apr 9, 2026

feat(rpc): implement getSignaturesForAddress

Implements the getSignaturesForAddress Solana JSON-RPC method end-to-end.

What changed

  • address_signatures table: new table (address, slot, signature) indexed via composite PK. Written atomically with transactions so they're always consistent.
  • get_signatures_for_address.rs: single LEFT JOIN query, slot DESC ordering, before/until cursor pagination for Postgres (Redis returns an error for cursor params — non-trivial with score-only sorted sets).
  • get_signatures_for_address_impl.rs: address validation, limit clamped to [1, 1000], typed RpcSignaturesForAddressConfig.
  • Gateway: method added to KNOWN_RPC_METHODS and ACCOUNT_GATED_METHODS (JWT required, Phase 2 ownership check for user-role callers).

Tests

Location Coverage
core/src/accounts/traits.rs found, empty, same-slot ordering, before/until cursors, unknown cursor → error
core/src/rpc/mod.rs found, empty, invalid address, limit clamping, newest-first ordering
gateway/src/auth.rs + gateway/tests/auth_integration.rs no token → 401, operator proceeds, user role → ownership check, missing pubkey → 400

Notes

  • Unknown cursor signatures return an explicit error (Solana mainnet silently falls back — we consider that worse behavior).
  • mint.rs idempotency check now actually works (previously hit -32601 method not found).

Coverage Report

Component Lines Hit Lines Total Coverage Artifact
Core 7,817 9,089 86.0% rust-unit-coverage-reports
Indexer 13,462 15,715 85.7% rust-unit-coverage-reports
Gateway 991 1,115 88.9% rust-unit-coverage-reports
Auth 541 596 90.8% rust-unit-coverage-reports
Withdraw Program 118 230 51.3% unit-coverage-reports
Escrow Program 1,170 1,951 60.0% unit-coverage-reports
E2E Integration 8,072 11,759 68.6% e2e-coverage-reports
Total 32,171 40,455 79.5%

Last updated: 2026-04-16 18:22:32 UTC by Withdraw Program

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 9, 2026

Greptile Summary

This PR implements the getSignaturesForAddress Solana JSON-RPC method end-to-end: a new address_signatures table indexed per account key during write_batch, a single LEFT JOIN query handler in Postgres, a clamped-limit RPC impl, and gateway auth gating via ACCOUNT_GATED_METHODS.

  • The Redis backend (get_signatures_for_address_redis) silently returns Ok(vec![]) with no indexing in write_batch_redis, making the idempotency guarantee in mint.rs unreliable when Redis is the active backend — the PR's own rationale for using Result over Vec is defeated on this path.
  • before / until cursor fields from the config are silently swallowed with no error or documentation; callers passing a cursor receive more results than requested with no indication.

Confidence Score: 4/5

Safe to merge for Postgres-only deployments; the Redis path has a P1 issue that silently defeats the idempotency guarantee mint.rs relies on.

The Postgres implementation is clean, well-tested, and correctly atomic. The P1 finding (Redis returning Ok(vec![]) instead of Err) is a real correctness defect on the Redis code path: it directly contradicts the PR's stated design rationale and could cause duplicate mints if Redis is the active backend. The P2 findings on silent cursor-field dropping and non-deterministic same-slot order are non-blocking for the current use case.

core/src/accounts/get_signatures_for_address.rs (Redis path), core/src/rpc/get_signatures_for_address_impl.rs (cursor fields)

Vulnerabilities

  • The ACCOUNT_GATED_METHODS check correctly requires a valid JWT for getSignaturesForAddress; no token returns 401, user-role callers go through the Phase 2 ownership check, operator-role callers bypass it — no privilege escalation paths identified.
  • No SQL injection risk: all query parameters are bound via sqlx parameterized queries ($1, $2).
  • No secrets or sensitive data exposure beyond what the caller is authorized to see by the ownership check.
  • Address bytes are stored as BYTEA (not as printable strings), reducing the risk of encoding-based injection.

Important Files Changed

Filename Overview
core/src/accounts/get_signatures_for_address.rs New file implementing the DB query layer; Postgres path is solid (single JOIN, proper error propagation), but the Redis path silently returns Ok(vec![]) despite no indexing, breaking the idempotency guarantee that is the PR's stated design rationale.
core/src/accounts/write_batch.rs Indexes every account key into address_signatures within the existing atomic Postgres transaction; ON CONFLICT DO NOTHING is correct. Redis write_batch has no corresponding index, consistent with the silent empty-vec return issue.
core/src/accounts/postgres.rs Adds the address_signatures table with composite PK (address, slot, signature) using IF NOT EXISTS; schema is correct and idempotent.
core/src/rpc/get_signatures_for_address_impl.rs Parses address and clamps limit correctly; before/until cursor fields from the config are silently dropped with no error, which could mislead callers expecting pagination behavior.
gateway/src/auth.rs Correctly adds getSignaturesForAddress to ACCOUNT_GATED_METHODS; auth gating follows the same pattern as getAccountInfo with operator bypass and user ownership check.
core/src/accounts/traits.rs Adds get_signatures_for_address method to AccountsDB; signature and delegation are consistent with other methods.
gateway/tests/auth_integration.rs Adds 4 integration tests covering 401 (no token), 200 (owned wallet), 403 (unowned), and 200 (operator); coverage matches the auth scenarios.
core/src/rpc/api.rs Declares getSignaturesForAddress on the ContraRpc trait; signature matches the impl.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Gateway
    participant RPC as Core RPC
    participant DB as AccountsDB

    Client->>Gateway: POST getSignaturesForAddress {address, params}
    Gateway->>Gateway: JWT validation (ACCOUNT_GATED_METHODS)
    alt No token
        Gateway-->>Client: 401 Unauthorized
    else User role
        Gateway->>RPC: getAccountInfo(address) [Phase 2 ownership check]
        RPC-->>Gateway: account data
        alt Ownership check fails
            Gateway-->>Client: 403 Forbidden
        else Ownership OK
            Gateway->>RPC: proxy getSignaturesForAddress
        end
    else Operator role
        Gateway->>RPC: proxy getSignaturesForAddress (unrestricted)
    end
    RPC->>RPC: parse address, clamp limit [1,1000]
    RPC->>DB: get_signatures_for_address(pubkey, limit)
    alt Postgres backend
        DB->>DB: SELECT sig, slot, tx.data FROM address_signatures LEFT JOIN transactions ORDER BY slot DESC LIMIT $2
        DB-->>RPC: Vec of RpcConfirmedTransactionStatusWithSignature
    else Redis backend
        DB-->>RPC: Ok(vec![]) [unimplemented — silent empty]
    end
    RPC-->>Client: JSON-RPC result
Loading

Reviews (1): Last reviewed commit: "RPC method getSignaturesForAddress imple..." | Re-trigger Greptile

Comment thread core/src/accounts/get_signatures_for_address.rs
Comment thread core/src/rpc/get_signatures_for_address_impl.rs
Comment thread core/src/accounts/get_signatures_for_address.rs Outdated
Comment thread core/src/accounts/traits.rs Outdated
@sebatustra sebatustra force-pushed the feat/implement-get-signatures-for-address branch from e9e9f54 to 4e6f78b Compare April 15, 2026 21:49
@sebatustra sebatustra requested review from amilz and dev-jodee April 15, 2026 22:12
Copy link
Copy Markdown
Collaborator

@dev-jodee dev-jodee left a comment

Choose a reason for hiding this comment

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

two questions / discussions

Comment thread gateway/src/auth.rs
const ACCOUNT_GATED_METHODS: &[&str] = &[
"getAccountInfo",
"getTokenAccountBalance",
"getSignaturesForAddress",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think there's a corner case that's going to fail if we set on getSignaturesForAddress the current auth flow:

Here the check for account exists is before account owned by user

Thinking out loud here, but if some accounts get closed, like closes TokenAccount, then that first if would fail, and user couldn't get signatures for an account he used to have ? Might not be an issue but curious to see what you think

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch, you're right. Once a TokenAccount is closed, check_account_data_ownership falls back to is_wallet_owned_by_user on the TA address directly, which always fails.

I see three options:

  1. Accept the limitation. No real incentive for users to close these accounts in our context (no rent to claim).
  2. Optional mint param. Derive the ATA and check if it matches (partial solution, ATAs only).
  3. Snapshot ownership at index time. Store (address -> owner_wallet) in write_batch.rs during ingest. Complete solution but requires schema changes and extra work at ingest.

I'm leaning toward 1 for now, but curious about your thoughts?

let key = format!("addr_sigs:{}", address);

// ZRANGE ... BYSCORE REV returns members with the highest score (most recent
// slot) first, matching the Postgres ORDER BY slot DESC behaviour.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is this going to have a different behavior than postgres since postgres has on slot and then on signature (https://github.com/solana-foundation/contra/pull/95/changes#diff-e1e9b8a701448e47decc131fc65967e2bc6d0b951829b2877bf49c2abc573736R138)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thanks, fixed by storing members as hex instead of base58, which preserves byte ordering and matches Postgres's ORDER BY signature DESC

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.

3 participants