Skip to content

feat(rewards): authenticate reward-code creates via signed transactions#822

Merged
raymondjacobson merged 1 commit into
mainfrom
rj-tx-based-reward-auth
May 18, 2026
Merged

feat(rewards): authenticate reward-code creates via signed transactions#822
raymondjacobson merged 1 commit into
mainfrom
rj-tx-based-reward-auth

Conversation

@raymondjacobson
Copy link
Copy Markdown
Member

Summary

Switches POST /v1/rewards/code from signMessage-based auth to authentication via a signed Solana transaction containing a Memo Program instruction whose data is the millisecond-epoch timestamp.

Motivation: Phantom (and other browser wallet extensions) cannot reliably sign off-chain messages with Ledger hardware wallets. The device either rejects (status 0x6a81) or returns bytes that don't verify against any standard SIMD-0048 envelope on the server. Transaction signing is the wallet code path every wallet supports uniformly — hot wallets, Phantom + Ledger, Solflare + Ledger, mobile, etc. — so using a throwaway transaction as the signing envelope eliminates the entire problem class.

The transaction is never submitted on-chain.

Server-side flow

  1. base64-decode the transaction
  2. tx.VerifySignatures() for the cryptographic check
  3. Require the fee-payer (AccountKeys[0]) to be in RewardCodeAuthorizedKeys
  4. Locate the first Memo Program instruction and parse its data as a millisecond timestamp
  5. Enforce the existing ±12h drift window
  6. Replay-prevention keyed on the transaction signature

Removes the now-unused SIMD-0048 wrapping helper, signMessage verification helpers, and the legacy timestamp + signature request fields.

Pairs with

  • AudiusProject/gift-rewards PR (frontend switched to signTransaction + memo)

Test plan

  • go test ./api/ -run 'TestGenerateCode|TestExtractMemoTimestamp' — passes locally; new tests cover memo extraction, invalid memo, missing memo.
  • Integration tests TestV1CreateRewardCode/* (require DB) run in CI; cover happy path, unauthorized signer, missing field, invalid base64, stale timestamp, and replay rejection.
  • Manual: hot-wallet end-to-end create returns 201.
  • Manual: Ledger + Phantom end-to-end create returns 201 (the whole point).

Switches POST /v1/rewards/code from signMessage-based auth to a signed
Solana transaction containing a single Memo Program instruction whose
data is the millisecond-epoch timestamp.

Motivation: Phantom (and other browser wallet extensions) cannot
reliably sign off-chain messages with a Ledger hardware wallet — the
device either rejects with 0x6a81 or returns bytes that don't verify
against any standard SIMD-0048 envelope on the server. Transaction
signing is the wallet code path every wallet supports uniformly (hot
wallets, Phantom + Ledger, Solflare + Ledger, mobile, etc.), so using
it as the signing envelope eliminates the entire problem class.

The transaction is never submitted on-chain. The server:
- decodes the base64 transaction
- cryptographically verifies the signature via tx.VerifySignatures()
- requires the fee-payer (account[0]) to be in RewardCodeAuthorizedKeys
- extracts the millisecond timestamp from the first Memo instruction
- enforces the existing 12h drift window and replay-protection
  (keyed on the transaction signature)

Removes the now-unused SIMD-0048 wrapping helper, signMessage
verification helpers, and the legacy timestamp+signature fields.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@raymondjacobson raymondjacobson merged commit b4ce84e into main May 18, 2026
4 of 5 checks passed
@raymondjacobson raymondjacobson deleted the rj-tx-based-reward-auth branch May 18, 2026 18:58
raymondjacobson added a commit that referenced this pull request May 18, 2026
#825)

## Summary

Removes `9WXR4YNXhG5PYfp4bD1uzFw1TNgrX2yKid5T1mcssKyF` from
`RewardCodeAuthorizedKeys`.

This wallet was added in #821 purely to reproduce the Phantom+Ledger
`signMessage` failure against the prod API while debugging. Now that the
transaction-based auth path (#822) is live in prod and the real partner
wallets are sufficient, the test wallet should not stay on the
allowlist.

The two intentional partner wallets remain authorized.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
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.

1 participant