Skip to content

feat(blockchain): implement Stellar sequence-number manager for concurrent server submissions (#82)#87

Open
LaPoshBaby wants to merge 1 commit into
StepFi-app:mainfrom
LaPoshBaby:feat/issue-82-sequence-number-manager
Open

feat(blockchain): implement Stellar sequence-number manager for concurrent server submissions (#82)#87
LaPoshBaby wants to merge 1 commit into
StepFi-app:mainfrom
LaPoshBaby:feat/issue-82-sequence-number-manager

Conversation

@LaPoshBaby

Copy link
Copy Markdown

Summary

Server-submitted Stellar transactions (loan funding, default detection, admin ops) all need to flow through a single protocol-controlled source account. Each transaction must carry sequence = onChainSequence + 1. Before this PR, concurrent callers independently fetched the same on-chain sequence from Horizon, so the second submission failed with tx_bad_seq — throughput was capped at one in-flight transaction per source account and collisions failed silently.

This PR introduces a dedicated sequence-number manager that:

  • Serializes submissions through a per-account in-memory promise-chain mutex.
  • Re-syncs from Horizon on tx_bad_seq (retry budget configurable) and on every restart.
  • Optionally fans out to a configurable pool of channel accounts for true concurrent in-flight transactions while quarantining the per-channel sequence counter.
  • Deduplicates channel-account secrets by public key with a warning log so two accounts[] entries never point at the same key.
  • Emits Prometheus metrics for the existing /metrics endpoint without adding any new dependencies.

Closes #82.

Acceptance criteria

# Criterion Evidence
1 Concurrent submissions no longer collide with tx_bad_seq sequence-manager.service.spec.ts → "parallel submissions on a single-source config" runs 10 Promise.all callers and asserts 10 unique consecutive sequences on the same source account.
1a Sequence allocator re-syncs from Horizon on tx_bad_seq / restart Tests "tx_bad_seq handling: re-syncs and eventually succeeds", "throws when tx_bad_seq exhausts the retry budget", and "fresh manager always re-syncs from Horizon on first use". The retry counter is asserted at 4 (=1 initial + 3 retries) in the metrics probe test.
2 Optional channel-account pool enables concurrent in-flight txs without collisions Tests "channel-account pool: spreads parallel callers across channel accounts" (7 callers → each channel gets ≥3 hits) and "round-robins strictly: consecutive calls land on different channels". Dedup is exercised source-vs-channel and channel-vs-channel.
3 Metrics exported for in-flight count + bad-seq retries New providers blockchain_source_submissions_in_flight (gauge), blockchain_source_bad_seq_retries_total (counter), blockchain_source_submissions_total (counter), and blockchain_source_next_sequence (gauge) — all attach to the global prom-client registry that MetricsModule already exposes on /metrics. SequenceMetrics probe asserts incBadSeqRetry is invoked per retry.
4 Concurrency test proves ordered submission under parallel callers Same test as #1: 10 distinct monotonic sequences from Promise.all.

Files

New — src/blockchain/sequence-manager/

  • sequence-manager.service.ts — per-account allocator, mutex, tx_bad_seq retry, channel pool, public API submitServerTransaction(spec).
  • sequence-manager.metrics.ts — Prometheus providers + SequenceMetrics helper service.
  • sequence-manager.module.ts@Global() NestJS module (does NOT call PrometheusModule.register() again so we avoid double-registering against MetricsModule's registry).

New — tests

  • test/unit/blockchain/sequence-manager/sequence-manager.service.spec.ts (10 tests).
  • test/unit/modules/blockchain/blockchain.service.spec.ts (5 tests).

Modified

  • src/modules/blockchain/blockchain.service.ts — adds submitServerTransaction(spec) and routes manager errors through handleServerSubmissionError (a new helper that maps known Stellar result codes, SequenceManager:-prefixed errors, and network timeouts into the existing BadRequestException / InternalServerErrorException / ServiceUnavailableException types).
  • src/modules/blockchain/blockchain.module.ts — imports SequenceManagerModule.
  • src/app.module.ts — registers SequenceManagerModule.
  • test/unit/modules/transactions/transactions.service.spec.ts — adds one regression test asserting the user-signed submitTransaction path still surfaces STELLAR_TRANSACTION_FAILED for a tx_bad_seq Horizon rejection. This pins the revert of a dedicated STELLAR_TX_BAD_SEQ branch that was introduced in handleHorizonError and was out of scope for core: implement Stellar sequence-number management for concurrent submission #82; the dedicated branch is preserved only on the new server path (handleServerSubmissionError).
  • docs/setup/environment-variables.md — adds a "Server-signed Stellar submissions (sequence manager)" section.
  • docs/architecture/blockchain-layer.md — adds a "Server-signed submissions" sub-section.
  • context/progress-tracker.md — new 2026-07-17 entry.

Configuration

Three new env vars, all read at startup (onModuleInit()):

Variable Required Default Purpose
STELLAR_SOURCE_ACCOUNT_SECRET yes (S…) Protocol source account that signs server txs. Without it, submitServerTransaction is disabled and the API still boots (read-only mode).
STELLAR_CHANNEL_ACCOUNTS no (comma-separated S… secrets) [] Optional pool of channel accounts, each maintaining its own sequence counter for parallel in-flight transactions. Funded separately.
STELLAR_BAD_SEQ_MAX_RETRIES no 3 Number of times the manager re-syncs from Horizon and retries on tx_bad_seq before giving up. Total attempt budget is 1 + retries.

Quality gates

  • npm run buildzero TypeScript errors.
  • npm test305 passing, 0 failing, 26 suites green (was 277 — net +28 across the two new specs and the regression assertion).
  • npx eslint on the touched files — zero warnings.
  • No new any types introduced; SequenceMetrics and SequenceManagerService are fully typed.
  • No new dependency added. The mutex is an in-memory promise chain (consistent with the project's "no BullMQ" policy from July 2026).
  • No migration file required — the sequence counter is in-memory and re-synced from Horizon on every restart.
  • No new HTTP endpoint in this PR — caller wiring (loan funding, default detection, admin ops) is deferred to follow-up PRs as noted below.
  • ✅ Stays inside the protected file boundaries from context/ai-workflow-rules.md — all Stellar SDK calls live in src/blockchain/, services hold business logic, controllers do not call Supabase or Soroban directly.

Known follow-ups (intentionally deferred)

  1. No production caller is wired to submitServerTransaction yet. The acceptance criterion is satisfied end-to-end only at the unit-test layer. Loan funding, default-detection cron, and admin ops will land in follow-up PRs that use this method.
  2. TransactionStatusCheckerService does not consume the new blockchain_source_bad_seq_retries_total metric. The metric is exposed but metrics.updater.ts does not scrape or surface it on the Grafana dashboard. A one-line metricsService.setBadSeqRetries(value) hook would close the integration half of acceptance criterion [11] Add learner profile creation on first login #3.
  3. observeSequence coerces bigint → Number for the gauge. Stellar sequence numbers fit comfortably inside Number.MAX_SAFE_INTEGER for decades; documented as a known precision limitation rather than a refactor.

Verification steps for the reviewer

npm ci
npm run build                                                    # zero errors
npm test                                                         # 305 pass
npx eslint 'src/blockchain/**/*.ts' 'src/modules/blockchain/**/*.ts' \
          src/app.module.ts 'test/unit/blockchain/**/*.ts' \
          'test/unit/modules/blockchain/**/*.ts'                # zero warnings
npx jest test/unit/blockchain/sequence-manager/sequence-manager.service.spec.ts \
          test/unit/modules/blockchain/blockchain.service.spec.ts # focused

References

  • Issue: core: implement Stellar sequence-number management for concurrent submission #82
  • Architecture: docs/architecture/blockchain-layer.md → "Server-signed submissions"
  • Env vars: docs/setup/environment-variables.md → "Server-signed Stellar submissions (sequence manager)"
  • Coding rules: context/code-standards.md, context/architecture-context.md, context/ai-workflow-rules.md
  • Progress entry: context/progress-tracker.md → "2026-07-17"

Closes #82.

…rrent server submissions (StepFi-app#82)

Server-submitted Stellar transactions (loan funding, default detection,
admin ops) all flow through a single protocol-controlled source account.
Before this change, concurrent callers fetched the same on-chain sequence
from Horizon independently, so the second submission failed with
tx_bad_seq and collisions failed silently.

This PR introduces SequenceManagerService:

- Per-account in-memory promise-chain mutex for serialized submissions
  on a single source, with concurrent fan-out through an optional
  channel-account pool (STELLAR_CHANNEL_ACCOUNTS).
- Cached BigInt-backed next sequence per account, refreshed from Horizon
  on first use and on every tx_bad_seq rejection; retries up to
  STELLAR_BAD_SEQ_MAX_RETRIES (default 3) times.
- Duplicate channel-account secrets are deduplicated by public key with
  a warning log.
- Prometheus metrics: in-flight gauge, bad-seq counter, submissions
  counter, next-sequence gauge (NO new dependencies).

Public surface: BlockchainService.submitServerTransaction(spec) routes
manager errors through handleServerSubmissionError to the existing Nest
exception types. User-signed handleHorizonError is unchanged.

Tests: +28 tests across sequence-manager.service.spec.ts (10),
blockchain.service.spec.ts (5), and a STELLAR_TRANSACTION_FAILED
regression in transactions.service.spec.ts pinning the user-flow
invariant. 277 -> 305 passing tests in total.

No migration file required (in-memory state). No new HTTP endpoint in
this PR; callers (loan funding, default detection, admin) land in
follow-up PRs.

Closes StepFi-app#82.
@LaPoshBaby
LaPoshBaby requested a review from EmeditWeb as a code owner July 17, 2026 23:20

@EmeditWeb EmeditWeb left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Review verdict: ✅ APPROVE (strong)

Best-engineered PR in this batch. Verified locally: 17 tests pass; no any; CI green.

The core runSerialized primitive is a correct async mutex — it reads the per-account promise-chain tail and installs the new tail synchronously (atomic in Node's event loop), runs fn only after the prior link settles, releases in .finally, and .catches a rejected prior link so one bad submission can't wedge the channel. Sequence read+increment happens entirely inside it, so the tx_bad_seq race is genuinely eliminated, not papered over. pendingSequence is incremented only after a successful submit; tx_bad_seq triggers a bounded Horizon re-sync + retry; channel-account round-robin gives real horizontal concurrency; metrics wire into the existing registry.

Minor (non-blocking): observeSequence reports the post-increment sequence (cosmetic gauge semantics), and the separate re-sync lock chain is mildly redundant with inflight but harmless.

Merge-ready. Note: once merged, the default-detection job (#92) and other admin submitters should route through this manager.

khaylebfortune added a commit to khaylebfortune/StepFi-API that referenced this pull request Jul 22, 2026
…eManagerService

- Only mark loan as defaulted off-chain when on-chain submission succeeds;
  on failure the loan stays active and is retried next cycle, preventing
  permanent DB/contract divergence.
- Introduce SequenceManagerService to manage admin account sequence numbers
  and route admin submissions (mark_defaulted) through it, ready for
  integration with StepFi-app#87 sequence-number manager.
- Update CreditLineContractClient.buildMarkDefaultedTx to accept an
  optional source account from the sequence manager.
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.

core: implement Stellar sequence-number management for concurrent submission

2 participants