Skip to content

fix(server): read the signers of real testnet accounts in wallet sign-in - #158

Merged
XxHugheadxX merged 2 commits into
mainfrom
fix/136-sep10-real-account-signers
Oct 10, 2026
Merged

XxHugheadxX merged 2 commits into
mainfrom
fix/136-sep10-real-account-signers

Conversation

@moises-cisneros

Copy link
Copy Markdown
Contributor

Refs #136

Summary

Since #147, a wallet signs in with a SEP-10 challenge and the server checks the signature against the account's signers on the ledger. That check could never pass for a real testnet account: RpcChainAccounts.authorityOf decoded the account entry with the general LedgerEntryData decoder of stellar_dart, which throws on a real entry (it carries ledger extensions). The server mapped the failure to ChainUnavailable, so verifyChallenge answered AuthenticationUnavailable: The account signers could not be read., and the app showed "Sign-in with your wallet is not available yet". The tests passed because they only used a minimal synthetic entry.

This PR reads the fixed AccountEntry layout by hand, up to the signers, and ignores the extension tail. Thresholds and ed25519 signers are read exactly as before. Anything that does not fit the layout is still unreadable (ChainUnavailable), never a master-key account.

It is server-only and does not touch the app PRs (#149, #150). Without it, wallet sign-in from the app fails for any real account, so #149 needs it to work end to end.

Acceptance criteria

Of #136 (this PR does not complete the issue, hence Refs):

Verification evidence

cd puls3_server
dart analyze --fatal-infos          -> exit code 0
dart test                           -> +900: All tests passed! (unit + integration, test Postgres/Redis up)

Regression test first (red), against the recorded real entry of a funded testnet account (test/fixtures/escrow_relay/get_ledger_entries_account.json):

a real testnet account entry, with its ledger extensions, decodes too   FAILED (ChainUnavailable)

After the fix it decodes to master weight 1, medium threshold 0, no extra signers.

Manual, local server against Stellar testnet, funded account GABKNX5HWXUYTWF6ORIKYO2NHTAPJ67OIF46TPP2IEMVGWXGBQXIHF5H, challenge signed with its key:

before: verifyChallenge  -> 400 Puls3ApiException AuthenticationUnavailable "The account signers could not be read."
after:  createChallenge  -> 200 WalletChallenge
        verifyChallenge  -> 200 AuthSuccess
        createHire       -> 200 CreateHireResult (prepared create_job returned)

Server log after the fix, from the browser flow: walletAuth ok code=ok for createChallenge and verifyChallenge, then hire.createHire and hire.submitEscrowCall with the session user.

Notes for reviewers

  • The decoder walks: entry type, account id, balance, sequence, sub entries, optional inflation destination, flags, home domain (max 32), thresholds, signers (max 20, all four signer key types, including the variable-length signed payload). It stops before the extension union on purpose.
  • No models or endpoints change, so serverpod generate is not affected. No new environment variable or secret.
  • The new unit test uses public chain data only.

Wallet sign-in failed for every real testnet account with
AuthenticationUnavailable ("The account signers could not be read").
The general-purpose LedgerEntryData decoder of stellar_dart throws on a
real account entry, which carries ledger extensions, and the failure was
mapped to ChainUnavailable. The tests only used a minimal synthetic entry.

Read the fixed AccountEntry layout up to the signers by hand and ignore
the extension tail. A truncated or foreign entry is still unreadable and
never falls back to the master key.

Add regression tests with the recorded real entry of a funded testnet
account, and with truncated and non-account entries.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Deploying puls3 with  Cloudflare Pages  Cloudflare Pages

Latest commit: c9f7acd
Status: ✅  Deploy successful!
Preview URL: https://71b596c7.puls3-4lw.pages.dev
Branch Preview URL: https://fix-136-sep10-real-account-s.puls3-4lw.pages.dev

View logs

@XxHugheadxX XxHugheadxX left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approved. Without this fix, wallet sign-in from the app (#149) can never work against a real testnet account. The diagnosis matches the code: the general LedgerEntryData decoder throws on the extension tail, and that error ended as AuthenticationUnavailable.

What I checked on 0f75a3d:

  • Layout against the XDR spec. The fields come in this order: entry type, PublicKey account id, balance, sequence number, numSubEntries, optional inflationDest, flags, string32 home domain (padded), thresholds[4], then Signer<20> (key union + weight). The decoder stops before ext on purpose. All four SignerKey arms are handled, including the variable payload<64> of ed25519SignedPayload.

  • Failure handling. A truncated, foreign or oversized entry throws inside _authorityOf, and authorityOf turns that into ChainUnavailable. It never falls back to the master key.

  • Locally:

    • dart analyze --fatal-infos: no issues.
    • dart test test/unit: 561 passed, including the new real-entry and truncated-entry tests and the SEP-10 verifier tests.
    • CI is green.
  • Paths the PR's tests do not reach. I ran a throwaway probe, not committed, that read back hand-built entries:

    • an inflationDest that is set;
    • home domains of length 1, 2, 3, 4 and 9, which covers every padding case;
    • a hashX signer;
    • an ed25519SignedPayload signer with payloads of 0, 1, 3, 4, 5 and 64 bytes.

    All 14 cases decode to the right thresholds, and to only the ed25519 signer with its weight.

Non-blocking, for a follow-up:

  • stellar_dart 2.3.0 serializes SignerKeyEd25519SignedPayload without its payload. Signer.toXDR() gives 40 bytes: type, key and weight, with no payload length. So a fixture built with it would encode a wrong entry. I had to write the signed-payload bytes by hand from the spec. This is one more reason the hand decoder is the right call.
  • One test with an inflationDest, a non-empty home domain and a signed-payload signer would pin those three branches. Today only the probe above covers them.

@XxHugheadxX
XxHugheadxX merged commit d111740 into main Oct 10, 2026
10 checks passed
@XxHugheadxX
XxHugheadxX deleted the fix/136-sep10-real-account-signers branch October 10, 2026 06:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: backend Serverpod endpoints and persistence P0 Blocks a deadline deliverable type: feat New functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants