Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ the processor per request with `X-Payment-Processor: sandbox` (or
`NODE_ENV=production`. See [docs/SANDBOX_MODE.md](docs/SANDBOX_MODE.md) for the
available scenarios, configuration and limitations.

For oracle confirmation thresholds, stale RPC heads, retry recovery, and
ledger entry expiry boundaries, use the fixed offline corpus documented in
[Ledger sequence fixtures](docs/LEDGER_SEQUENCE_FIXTURES.md).

### Environment Configuration

The API validates all required secrets at startup. Invalid or placeholder values
Expand Down Expand Up @@ -268,4 +272,4 @@ If you're interested in becoming a maintainer, reach out to the core team! We're

---

Happy coding! 🚀
Happy coding! 🚀
54 changes: 54 additions & 0 deletions docs/LEDGER_SEQUENCE_FIXTURES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Deterministic ledger sequence fixtures (#346)

`src/testing/fixtures/ledger-sequence.fixtures.ts` supplies reusable oracle
transactions and explicit confirmation/TTL boundary cases. The transaction uses
a fixed test identity, account sequence, Testnet passphrase, invocation arguments,
and time bounds. Rebuilding it produces the same XDR and transaction/payload
hashes without a live RPC, secrets, database, or wall clock. The deterministic
identity is public test data: never fund it or use it in production. The envelope
is unsigned; these fixtures test off-chain verification, not on-chain signature
authorization or contract execution.

Run the executable verification locally:

```bash
npm test -- --runInBand src/testing/fixtures/ledger-sequence.spec.ts
```

Import `createLedgerSequenceFixture`, `CONFIRMATION_FIXTURES`, or
`LEDGER_TTL_FIXTURES` into another test to reuse the same inputs. Each factory
call creates a fresh transaction and submission reference. The existing
`fixtures:generate` output and its five scenario contracts remain unchanged.

## Boundaries and recovery

| Case | Existing behavior | Operational response |
| --- | --- | --- |
| Inclusion ledger | Counts as confirmation one | Wait for `ORACLE_MIN_CONFIRMATIONS`; do not add an extra ledger to the threshold. |
| One ledger short / stale RPC head | Retryable `INSUFFICIENT_CONFIRMATIONS` | Keep the original submission pending and verify it again after the head advances or the RPC catches up. Do not resubmit the transaction. |
| Exact threshold / above threshold | Verification succeeds | The fixture asserts the existing hash, timestamp, and ledger response. |
| uint32 upper boundary | Confirmation arithmetic stays exact | The corpus covers 4294967294–4294967295; it assumes no sequence wraparound. |
| Transaction not found | Retryable `TRANSACTION_NOT_FOUND` | Recheck the original hash; if outside RPC retention, use an archival RPC. Missing data alone does not prove failure. |
| Transaction failed | Nonretryable `TRANSACTION_FAILED` | Investigate the ledger failure before preparing a new transaction. |
| HTTP 429 / transient head read failure | Bounded exponential retry | Retry the read; the transaction is not submitted or reread within that attempt. Exhaustion returns retryable `RPC_UNAVAILABLE`. |
| RPC HTTP 403 | Nonretryable adapter `RPC_ERROR`; no immediate retry | Repair RPC access credentials. The verifier aborts the cycle without rejecting the pending submission for an infrastructure error. |
| Different registered submitter | Nonretryable `PAYLOAD_MISMATCH` | Investigate the registered reference; confirmations never override submitter binding. |
| TTL equals current ledger | Entry remains live | Expiry is inclusive. |
| Persistent TTL below current ledger (including placeholder zero) | Archived | Restore through the authorized operational workflow before new contract calls. |
| Temporary TTL below current ledger | Missing, not archived | Recreate through the application workflow; restoration cannot recover expired temporary data. |

The integration test runs the actual adapter inside `SubmissionVerifierService`
with in-memory history/audit doubles: a short confirmation count calls `defer`
without marking success or rejection; a later cycle verifies the same hash at
the threshold. RPC methods are stubbed at the SDK boundary. Authorization tests
cover RPC access refusal and submitter reference binding; they do not claim to
exercise HTTP guards or the on-chain submitter allow-list.

## Compatibility and rollback

This is a test-only addition. No routes, DTOs, database schemas, production retry
policies, or public responses change, and no migration is required. Tests never
submit transactions or write production state. Rollback consists of reverting
the fixture, test, and documentation additions; no ledger or database rollback
is needed. Do not treat the fixture's historical time bounds as a transaction
template suitable for submission.
140 changes: 140 additions & 0 deletions src/testing/fixtures/ledger-sequence.fixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import {
Account,
Address,
Contract,
Keypair,
Networks,
nativeToScVal,
StrKey,
TransactionBuilder,
} from "@stellar/stellar-sdk";
import { hashOracleArguments } from "src/blockchain/oracle/services/stellar-oracle.adapter";

/** Public test identity only; never fund or use this deterministic key on-chain. */
export const LEDGER_FIXTURE_SUBMITTER = Keypair.fromRawEd25519Seed(
Buffer.alloc(32, 42),
).publicKey();
export const LEDGER_FIXTURE_CONTRACT = StrKey.encodeContract(
Buffer.alloc(32, 1),
);

/** Fixed time bounds and account sequence make XDR and hashes replayable. */
export function createLedgerSequenceFixture() {
const args = [
new Address(LEDGER_FIXTURE_SUBMITTER).toScVal(),
nativeToScVal("XLM_USD", { type: "symbol" }),
nativeToScVal(100n, { type: "i128" }),
nativeToScVal(2, { type: "u32" }),
nativeToScVal(1700000000n, { type: "u64" }),
nativeToScVal(1n, { type: "u64" }),
];
const transaction = new TransactionBuilder(
new Account(LEDGER_FIXTURE_SUBMITTER, "429496729600"),
{ fee: "100", networkPassphrase: Networks.TESTNET },
)
.addOperation(
new Contract(LEDGER_FIXTURE_CONTRACT).call("submit_price", ...args),
)
.setTimebounds(1700000000, 1700000030)
.build();
return {
transaction,
submission: {
submitter: LEDGER_FIXTURE_SUBMITTER,
payloadHash: hashOracleArguments(args),
transactionHash: Buffer.from(transaction.hash()).toString("hex"),
},
};
}

/** Ledger inclusion counts as the first confirmation. Max is Stellar uint32. */
export const CONFIRMATION_FIXTURES = [
{ name: "first ledger", ledger: 1, latest: 1, minimum: 1, confirmed: true },
{
name: "inclusion ledger",
ledger: 100,
latest: 100,
minimum: 2,
confirmed: false,
},
{
name: "exact threshold",
ledger: 100,
latest: 101,
minimum: 2,
confirmed: true,
},
{
name: "above threshold",
ledger: 100,
latest: 102,
minimum: 2,
confirmed: true,
},
{
name: "stale RPC head",
ledger: 100,
latest: 99,
minimum: 1,
confirmed: false,
},
{
name: "uint32 boundary pending",
ledger: 4294967294,
latest: 4294967294,
minimum: 2,
confirmed: false,
},
{
name: "uint32 boundary confirmed",
ledger: 4294967294,
latest: 4294967295,
minimum: 2,
confirmed: true,
},
] as const;

export const LEDGER_TTL_FIXTURES = [
{
name: "before expiry",
latest: 99,
ttl: 100,
temporary: false,
expected: "live",
},
{
name: "expiry ledger inclusive",
latest: 100,
ttl: 100,
temporary: false,
expected: "live",
},
{
name: "persistent expired",
latest: 101,
ttl: 100,
temporary: false,
expected: "archived",
},
{
name: "temporary expired",
latest: 101,
ttl: 100,
temporary: true,
expected: "missing",
},
{
name: "archived placeholder",
latest: 100,
ttl: 0,
temporary: false,
expected: "archived",
},
{
name: "maximum live ledger",
latest: 4294967295,
ttl: 4294967295,
temporary: false,
expected: "live",
},
] as const;
Loading
Loading