Skip to content

Feature/test infrastructure#106

Open
b3y0urs3lf wants to merge 72 commits into
mainfrom
feature/test-infrastructure
Open

Feature/test infrastructure#106
b3y0urs3lf wants to merge 72 commits into
mainfrom
feature/test-infrastructure

Conversation

@b3y0urs3lf

Copy link
Copy Markdown
Contributor

No description provided.

b3y0urs3lf and others added 4 commits March 16, 2026 14:29
- Add Cucumber BDD test framework with 26 feature files and step definitions
- Add e2e and integration test scaffolding
- Add ShardAwareAggregatorClient for sharded aggregator testing
- Add trust-base fixtures and test utilities
- Update package.json with test:bdd, test:single, test:examples scripts
- Include debug console.log statements in source (temporary)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Resolves package.json version conflicts by taking newer dep versions
from issue-92 while keeping @cucumber/cucumber from test-infrastructure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix batch semantics: batchSize = ops per batch, batchCount = repetitions
  (previously inverted: opsPerShard/batchCount produced tiny batches)
- Add ShardBlockMonitor: background block poller that tracks aggregator
  block finalization, commitment counts, and throughput per shard
- Add waitForDrain: after test ops complete, poll until shards produce
  0-commitment blocks to ensure all submissions are finalized
- Add commitment validation: compare client-side successes vs server-side
  finalized commitments per shard, warn on mismatch
- Fix shard mode detection: derive first shard ID from SHARD_ID_LENGTH
  instead of hardcoding SHARD_2_URL (supports SHARD_ID_LENGTH>1)
- Report now includes per-shard block finalization tables and
  commitment validation summary; block data written to separate CSV

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces significant updates to the testing infrastructure, including the addition of BDD coverage plans, new feature files, and supporting test utilities for shard load testing and token operations. Several debugging statements (console.log) and commented-out code were identified across the codebase and test files. Additionally, there are concerns regarding hardcoded URLs in tests and a package version downgrade that requires justification. I have provided specific suggestions to address these issues, including removing debug logs, making URLs configurable via environment variables, and cleaning up test comments.

}

const aliceToken = await mintToken(trustBase, client, {
secret: initialOwnerSecret,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

The variable initialOwnerSecret is used here but is not defined within this file's scope. This will cause a ReferenceError at runtime. It seems the intention was to use ALICE_SECRET, which is defined on line 20.

Suggested change
secret: initialOwnerSecret,
secret: ALICE_SECRET,

let client: StateTransitionClient;

const ALICE_SECRET = new TextEncoder().encode('Alice');
const url = 'http://localhost:3000';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Hardcoding URLs in tests makes them brittle and difficult to run in different environments (e.g., CI, other developers' machines). It's better to make this configurable, for instance, by using an environment variable with a fallback to the localhost default.

Suggested change
const url = 'http://localhost:3000';
const url = process.env.AGGREGATOR_URL || 'http://localhost:3000';

Comment on lines +11 to +12
// const aggregatorClient = TestAggregatorClient.create();
const aggregatorClient = new AggregatorClient('http://192.168.43.106:3000');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Hardcoding an IP address in a test makes it difficult for other developers to run and can cause failures in CI environments. Please consider using an environment variable to make the aggregator URL configurable, with a fallback for local development.

Suggested change
// const aggregatorClient = TestAggregatorClient.create();
const aggregatorClient = new AggregatorClient('http://192.168.43.106:3000');
const aggregatorClient = new AggregatorClient(process.env.AGGREGATOR_URL || 'http://192.168.43.106:3000');

Comment on lines +44 to +47
const url_test = 'https://gateway-test.unicity.network';
//const url_main = 'https://gateway.unicity.network';
// const url_local = 'http://localhost:8080';
const url = url_test;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Hardcoding URLs in tests is not ideal as it makes them less portable across different environments. It would be better to use an environment variable for this configuration, with a sensible default.

Suggested change
const url_test = 'https://gateway-test.unicity.network';
//const url_main = 'https://gateway.unicity.network';
// const url_local = 'http://localhost:8080';
const url = url_test;
const url = process.env.AGGREGATOR_URL || 'https://gateway-test.unicity.network';


const mintTransaction = await MintTransaction.create(
await PayToScriptHash.create(predicate),
await PayToScriptHash.create(predicate), //if here I as a owner can I specify

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

This comment appears to be a leftover question or a personal note. It should be removed or clarified to provide value to other developers.

Suggested change
await PayToScriptHash.create(predicate), //if here I as a owner can I specify
await PayToScriptHash.create(predicate),

dmytro and others added 25 commits April 15, 2026 09:47
# Conflicts:
#	tests/functional/payment/SplitBuilderTest.ts
….0 legacy tests

- Ignore shard-load-*.csv artifacts produced by ShardLoadRunner and the
  local issueFix/ scratch directory used to shuttle fixes between laptops.
- Replace the hardcoded 192.168.43.106 private-LAN IP with a localhost
  default + env-var override (matches the existing BDD TestSetup convention).
- Delete pre-SDK-2.0 test files that reference removed APIs
  (MaskedPredicate, TokenCoinData, CoinId, submitBurnTransactionForSplit,
  @unicitylabs/commons/lib/*, @unicitylabs/prefix-hash-tree): the *2 /
  *Test2 / test*.json tree under tests/token/, tests/integration/token/,
  and tests/e2e/token/TestPrepareApiKeyPaymentTest.ts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Nametag BDD (option-1 scope):
- New feature files: token-nametag, token-nametag-split, token-nametag-negative,
  token-mixed-addressing. Exercise mint + verify + sender-side resolution,
  privacy invariant (nametag bytes absent from downstream CBOR), split children
  via nametag, interleaved pubkey/nametag chains.
- token-nametag-negative is tagged @pending-src-cleanup: scenarios A1/A2/B1/B2/B3
  encode the option-2 removal contracts and turn green once the
  UnicityIdPredicate trio + UnicityIdToken._transactions plumbing are stripped.
  Full rationale: docs/test-findings.md.
- Amended token-minting, token-transfer, token-transfer-chain,
  token-long-transfer-chain, and token-payment-journey with @nametag-standard
  scenario outlines so existing coverage doubles as regression on the lookup path.
- New helpers in support/TestSetup: registerNametag, resolveNametag,
  resolveRecipientAddress, runMixedChain, plus the AddressingMethod union.
- TokenWorld gains nametags / namedUsers / addressingMethod for step sharing.

SDK 2.0 rename migration (rode along with issue-98):
- PayToScriptHash → Address.fromPredicate
- PredicateVerifier → PredicateVerifierService.create(trustBase)
- PayToPublicKeyPredicate.create → .fromSigningService
- PayToPublicKeyPredicate.generateUnlockScript → PayToPublicKeyPredicateUnlockScript.create
- CertificationData.fromTransferTransaction → .fromTransaction
- waitInclusionProof arg reorder to (client, trustBase, verifier, tx)
- new TokenId(random) → TokenId.generate() / same for TokenType
- IPaymentData.toCBOR → .encode
- submitCertificationRequest loses the receipt arg (#92 ride-along)

Other:
- token-id-boundaries: 0-byte-collision scenario expects STATE_ID_EXISTS
  (deterministic stateId is already committed on a seasoned aggregator).
- Refreshed tests/{e2e,functional}/trust-base.json for the current network.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 3 of each shard-load operation (Token.mint / certified transaction
verification) is CPU-heavy crypto work. Running it on the main event loop
serialized all concurrent shard work — throughput was capped regardless of
how many shards were under pressure.

Move the mint step into a worker_threads pool:
- ShardLoadMintWorker: per-thread worker that re-hydrates MintTransaction +
  InclusionProof from CBOR, reconstructs the certified transaction, and runs
  Token.mint against a PredicateVerifierService initialized from the trust base.
- ShardLoadMintPool: fixed-size pool with an in-flight queue. Pool size
  defaults to min(shardCount, availableParallelism() - 2), overridable via
  LOAD_TEST_MINT_POOL_SIZE. Workers are spawned with
  execArgv: ['--import', 'tsx/esm'] so the tsx loader reaches the worker
  context (node doesn't inherit module hooks across worker_threads).
- ShardLoadRunner: initMintPool / destroyMintPool lifecycle; the hot path
  now calls this.mintPool.mint(mintTx.toCBOR(), proof.toCBOR()) instead of
  Token.mint(...) inline. Inline path preserved as a fallback when the pool
  isn't initialized.
- Steps: pool is initialized during prepare and destroyed in the report step.
- Feature: batch sizes bumped 100 → 1000 (10x load) now that the CPU
  bottleneck is lifted.
- World: LOAD_TEST_TIMEOUT raised from 10 min to 60 min to fit the bigger
  runs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- ESLint --fix across BDD steps: import-order, sort-keys.
- Remove unused PayToPublicKeyPredicate / Token / DataTable / ISplitPaymentData / SplitReason / TestAggregatorClient imports.
- TokenWorld: restore alphabetical field ordering after adding
  addressingMethod / namedUsers / nametags.
- IHop: reorder to alphabetical key order.
- resolveNametag / resolveRecipientAddress: add explicit await (satisfies
  require-await without changing behavior).
- mixed-addressing.deliverChild: explicit Promise<Token> return type.

Remaining 8 lint errors are pre-existing in split-advanced.steps.ts and
ShardAwareAggregatorClient.ts (from commit 6aeaecd "Add test infrastructure")
— cosmetic member-ordering / naming-convention / require-await issues. Not
blocking.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…gregatorClient

- split-advanced.steps.ts: reorder augmented interface members
  alphabetically, drop unnecessary async on parseSplitPaymentData
  (wrap in Promise.resolve instead), suppress naming-convention on
  the module augmentation interface (must match the class name).
- ShardAwareAggregatorClient.ts: reorder members (static before
  instance), add explicit await in getInclusionProof.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
File started with 'soFeature:' instead of 'Feature:' — Gherkin parser
rejected it at parse time.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
#103 Update cbor tags for aggregator layer and token
…rastructure

# Conflicts:
#	package-lock.json
#	package.json
#	tests/e2e/trust-base.json
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rt binary, ShardIdMatchesStateIdRule

Adds 4 contract-style feature files exercising SDK-internal logic without
needing a live aggregator:
- shard-id.feature: encode/decode roundtrip, isPrefixOf, getBit
- cbor-envelope-tags.feature: tag/version mismatch rejection across 6 types
- inclusion-certificate-binary.feature: malformed bytes, popcount, verify negatives
- shard-id-matches-state-id-rule.feature: byte- and bit-aligned match/mismatch

41 scenarios, all passing. Closes the largest gap from the post-merge audit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
inclusion-proof-statuses.feature exercises every status branch in
InclusionProofVerificationRule by mutating real proofs in-memory:
- INCLUSION_CERTIFICATE_MISSING / MISSING_CERTIFICATION_DATA (drop fields)
- PATH_INVALID (corrupt sibling byte)
- SHARD_ID_MISMATCH (replace shardTreeCertificate with non-prefix shard)
- TRANSACTION_HASH_MISMATCH (corrupt txhash via CBOR roundtrip)
- Status precedence (txhash + sibling corruption → txhash wins)
- Decision-table outline covering 4 mutation flavours

9 scenarios (5 named + 4 outline rows). All passing against bft-shard mode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
state-id-encoding.feature: 32-byte enforcement at SDK level (5 BVA + 1
legacy-prefix Error Guessing scenario; uses StateId.fromCBOR which is
backed by DataHash's algorithm-length guard).

bft-shard-routing.feature (tagged @bft-shard-only):
- Decision Table outline for MSB routing of synthetic StateIDs
- Wrong-shard submission rejection (any non-SUCCESS counts as rejected)
- Use Case scenario: 4-token mint round reaches both shards

12 scenarios (8 outline + 4 named). All passing against bft-shard mode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
token-cbor-roundtrip.feature: 2 byte-equal idempotence scenarios for
InclusionProof and CertificationData CBOR re-encoding (T4-32, T4-33).

token-id-boundaries.feature: tagged the existing 0-byte scenario @stateful
(it asserts STATE_ID_EXISTS, which only holds after a prior 0-byte mint),
and added a symmetric @fresh-aggregator counterpart asserting SUCCESS.
Default suite runs exclude both via 'not @stateful and not @fresh-aggregator';
operators opt in based on aggregator state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
inclusion-cert-stress.feature (tagged @stress, opt-in):
- Loop Testing: 20 sequential mints, each verifies (T4-35)
- Use Case: mint -> 5 transfer hops -> verify (T4-36)
- State Transition: re-submitting an already-finalised certData returns
  STATE_ID_EXISTS (T4-37)

Reuses existing 'the final token passes verification' step from transfer.steps.ts.
Tagged @stress so default suite runs skip the slow paths; opt in with --tags @stress.

3 scenarios, all passing (~70s on bft-shard, dominated by the 20-mint loop
and the 5-hop chain).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Statistical fix: 4 mints had a 12.5% chance of all landing on one shard
(P = 2 * (1/2)^4). With 16 mints the probability drops to ~0.003%,
small enough to treat as deterministic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…NGTH

Previous T4-29/30/31 hardcoded shardIdLength=1 (2 shards). Updated to read
SHARD_ID_LENGTH from env and assert against the topology dynamically:

- T4-29 (Decision Table): the picked shard is (1<<N) | top-N-bits-of-stateId.
  Examples cover bit patterns 0x00/0x40/0x80/0xC0/0xFF — meaning is preserved
  for any N (top-N bits get extracted, the rest is don't-care).
- T4-30 (Risk-Based): mint a token, ask the helper which shard it routes to,
  resubmit to any other configured shard, expect rejection. Works for N>=1.
- T4-31 (Use Case): mint shardCount * 8 tokens (so P(missing any shard) is
  < 0.05% at 4 shards) and assert every configured shard ID was reached.

Verified on 4-shard bft-shard topology (ports 3001-3004): 7 scenarios pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… byte-0 convention

The pre-#141 aggregator's verifyShardID used big.Int.SetBytes which
inadvertently anchored 'LSB' to the last byte. Post-#141 the convention
unified to byte 0 for both modes:
  - bft-shard mode: top bits of byte 0 (MSB-first within byte)
  - child mode:     low bits of byte 0 (LSB-first within byte)

Both modes now read from byte 0 of the raw 32-byte StateID; only the
bit direction within each byte differs.

The new LSB branch mirrors aggregator-go's pkg/api/shard_match.go
MatchesShardPrefix byte-for-byte:

  for d := 0; d < shardIdLength; d++ {
    bit = (data[d >> 3] >> (d & 7)) & 1
    shardBits |= bit << d
  }

Verified against sharding-compose.yml (parent/child mode, child=2/3 on
ports 3002/3001 respectively): 5/5 minting scenarios pass.

Both routing modes are kept (operator picks via SHARD_ROUTING_MODE);
older aggregator deployments that still validate via the pre-unification
convention may still need the LSB code path, even though current builds
use byte 0 universally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds routing-byte-source.feature with regression tests that explicitly
disagree between data[0] LSB and data[31] LSB, ensuring the helper picks
the byte-0 answer in both modes:

- LSB mode: data[0]=0x00, data[31]=0x01, shardIdLength=1 → shard 2
  (the pre-#141 byte-31-LSB code would have picked shard 3)
- MSB mode: data[0]=0x80, data[31]=0x00, shardIdLength=1 → shard 3
  (symmetric pin so a future regression that reads the wrong byte
  in either direction surfaces)
- 4-row outline asserts the picked shard is always a configured shard ID
  (defense in depth against arithmetic drift)

Audit: neither branch of getShardForStateId touches stateId.imprint
anymore; both consume stateId.data only. So a future v1-shaped 34-byte
StateID couldn't silently route via the algorithm-prefix byte.

6 scenarios, all passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Catches up to main's HEAD now that PR #108 (issue-105 umbrella) has been
merged upstream. No file content change — all of #108's content was
already in this branch via the earlier 'Merge remote-tracking branch
origin/issue-105' commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rastructure

# Conflicts:
#	src/api/bft/UnicityCertificate.ts
Migrates tests/bdd/functional to the issue-110 / PR #112 API surface:
- Address removed: pass IPredicate directly to MintTransaction.create,
  TransferTransaction.create, UnicityIdMintTransaction.create
- TransferTransaction.create signature: drop ownerPredicate (the previous
  state owner is now derived from the token's history), passes recipient
  + stateMask + optional data
- MintTransaction.create signature: insert null for new justification
  arg before data; for empty-payload mints, drop the trailing arg entirely
- Token.mint and Token.verify now take MintJustificationVerifierService
  as their 3rd argument; token.transfer is unchanged (3 args)
- TokenSplit.split: drop ownerPredicate (signature is now token,
  decodePaymentData, splitTokens)
- TokenSplit.verify removed; split-mint justification is now verified
  automatically through the SplitMintJustificationVerifier registered
  in the MintJustificationVerifierService. Test scenarios that called
  TokenSplit.verify now call token.verify which runs the same logic.
- SplitReason renamed to SplitMintJustification (CBOR_TAG = 39044)
- ISplitPaymentData removed; parseSplitVerificationData now returns
  IPaymentData directly (split proofs live in the justification field)
- Aggregator's MatchesShardPrefix LSB convention: SDK's LSB branch was
  already aligned by the byte-0 patch in fdef570 ahead of this merge

Address.fromPredicate -> direct predicate refactor touched 31 files.
Build clean, lint clean. Suite needs an aggregator running to validate;
not yet re-run at the time of this commit.

Note: PR #112's commit 66c8cb7 fixes UnicityCertificate shard-tree
inner-node hash composition with the same approach as our fdef570 —
upstream caught the same bug independently. Conflict resolved by
taking the upstream form (verbatim functionally equivalent).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dmytro and others added 30 commits May 21, 2026 15:17
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…TS or proof TRANSACTION_HASH_MISMATCH)

aggregator-go#151 (now merged to main) skips the finalized-duplicate lookup on
the async-v2 submit path: a re-spend returns SUCCESS at submit and is rejected
at inclusion-proof time as TRANSACTION_HASH_MISMATCH, rather than submit-time
STATE_ID_EXISTS. Double-spend safety is unchanged — only the rejection layer
moved (confirmed intended by the aggregator dev).

Make the 9 affected scenarios tolerant of both aggregator builds:
- token-4level-owner-actions.feature: the 8-row double-spend outline now uses
  'the duplicate transfer is rejected as a double-spend'.
- token-transfer-edge-cases.feature: the stale-token scenario now uses
  'the stale-token re-spend is rejected as a double-spend'.
Each passes if submit == STATE_ID_EXISTS (pre-#151) OR submit == SUCCESS plus a
proof-time TRANSACTION_HASH_MISMATCH (#151+).

Refs #118

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…est (aggregator-go#153)

Add a test-only raw-submit seam (RawCertificationSubmitter) that POSTs arbitrary bytes
as the certification_request JSON-RPC payload, bypassing the SDK's canonical encoder, so
the aggregator's canonical-CBOR rejection (aggregator-go#153, ValidateCoreDeterministic)
can be exercised on the submit path.

New BDD coverage:
- canonical-certification-request.feature (@canonical-cbor): positive control (canonical
  accepted) + 6 envelope-level non-canonical mutations — unsorted map keys, non-minimal
  integer (value), non-minimal length, indefinite-length, trailing bytes, float — each
  asserted rejected with 'CBOR is not canonical: <reason>' and not certified. Validated
  end-to-end through the proxy against a #153 aggregator.
- certification-request-determinism.feature (offline): the same logical mint request built
  twice yields byte-identical CBOR and an equal stateId (malleability guard).

@canonical-cbor is gated out of default runs (needs a #153 aggregator); the determinism
feature is offline and stays in default CI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
# Conflicts:
#	package-lock.json
#	package.json
…nt, …) + SplitTokenRequest API

PR #119 (sdk-js#115 + sdk-js#116) replaces the caller-supplied tokenId with
networkId+salt derivation, reorders MintTransaction.create(), removes STATE_ID_EXISTS
from CertificationStatus, and replaces the [TokenId, PaymentAssetCollection][] split
input with SplitTokenRequest[]. This ports the BDD test infrastructure to the new APIs:

- TestSetup mintTokenToRecipient / mintTokenWithAssets / runMixedChain: now pass
  setup.trustBase.networkId + recipient; defaults for salt/tokenType.
- TestSetup splitToken / splitTokenToOwner / attemptUnauthorizedSplit: build a
  SplitTokenRequest[] internally; the mint loop reads networkId/recipient/tokenType/
  salt/assets/proofs from each SplitToken in splitResult.tokens.
- Step files (addressing, canonical-certification-request, cbor-envelope,
  certification-request-determinism, certification-status, id-boundaries,
  mint-transaction-fields, minting, mixed-addressing, split-boundaries,
  split-combinations, split-edge-cases, split, transaction-data): updated to the new
  signature; TokenSalt/NetworkId imports added where needed.
- transfer-edge-cases + tree-owner-actions: the tolerant-re-spend branch that checked
  for STATE_ID_EXISTS is removed (the enum value no longer exists in PR #119); the
  proof-time TRANSACTION_HASH_MISMATCH path is now the only enforcement layer.
- ShardLoadRunner / ShardLoadTypes: tokenIdBytes → saltBytes (the seed now feeds
  TokenSalt.fromBytes rather than the removed TokenId constructor).
- AggregatorClientTest: NetworkId.LOCAL + default salt/tokenType.
- World.ts: mintTokenSalt added (certification-status uses it to reproduce the same
  derived tokenId across two mints with different tokenTypes).
- src/api/InclusionCertificate.ts: minor prettier-only formatting from lint:fix.

build:check ✔, lint ✔.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tion

PR #119 made the aggregator surface re-spend rejections as a JSON-RPC error rather
than a {status: ...} CertificationResponse, so submitCertificationRequest now throws
('Invalid JSON structure' from CertificationResponse.fromJSON) instead of returning
SUCCESS. The tolerant double-spend assertion was only handling SUCCESS+proof-time
TRANSACTION_HASH_MISMATCH; extend it to also accept submit-side rejection.

Affected:
- tree-owner-actions.steps.ts (the 8 token-4level Outline rows) — catch in the
  duplicate-transfer When step; the Then accepts a null status as 'rejected at submit'.
- transfer-edge-cases.steps.ts (stale-token scenario) — same.
- World.ts: respendSubmitError field for capture; certificationStatus now nullable.

Restores the 9 token-4level/transfer-edge re-spend scenarios that regressed after
the merge; the proof-time path is still asserted when the aggregator accepts the
submit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…in BDD

sdk-js#115 acceptance criteria, hermetic (no aggregator needed):

- NetworkId.fromId rejects 0 / 65536 / -1 (out-of-range 16-bit unsigned).
- NetworkId.fromId resolves the registered constants (MAINNET=1, TESTNET=2, LOCAL=3)
  and accepts arbitrary 16-bit ids with id reported as-is.
- TokenId.fromSalt determinism: same salt + DIFFERENT networkIds → DIFFERENT tokenIds
  (malleability guard); same salt + same networkId → SAME tokenId.
- TokenSalt.fromBytes rejects non-32-byte inputs; TokenSalt.generate produces 32 bytes.
- MintTransaction CBOR round-trip preserves networkId, salt, and derived tokenId.
- MintTransaction.tokenId equals an independent TokenId.fromSalt(networkId, salt)
  derivation of its own fields.
- MintTransaction.create defaults to a 32-byte random salt when not specified.

16 scenarios, 39 steps — all pass offline. Adds World.networkIdSaltStash.

Refs sdk-js#115

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sdk-js#116 acceptance: token and inclusion-proof verification reject mismatched
network ids BEFORE signature/quorum checks. Two rules guard the boundary:
- MintNetworkMatchesTrustBaseRule (genesis.networkId == trustBase.networkId)
- UnicitySealNetworkMatchesTrustBaseRule
  (inclusionProof.unicityCertificate.unicitySeal.networkId == trustBase.networkId)

Both fire FIRST in CertifiedMintTransactionVerificationRule and
UnicityCertificateVerification, so the failure is observable end-to-end with the
specific rule name in the verification result tree.

Scenarios (live against AGGREGATOR_URL, 3 total, all pass):
- positive control: the token verifies OK under its native trust base.
- the token is rejected under a trust base whose networkId is changed to 2,
  with 'MintNetworkMatchesTrustBaseRule' FAIL surfacing in the result tree.
- a transferred token is also rejected under the wrong-network trust base.

The wrong-network trust base is constructed in-test by reading TRUST_BASE_PATH and
overriding only its networkId field; everything else (root nodes / sig keys / quorum
/ signatures) is unchanged, but the network rule fires first so verification
short-circuits there. Adds World.networkConsistencyStash.

Refs sdk-js#116

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ectly

The same-logical-request-twice scenario doesn't need an aggregator, but it tried to read
this.setup.trustBase.networkId — which is unset when the feature has no aggregator-related
Background. Use NetworkId.LOCAL directly; the assertion is about request-bytes/stateId
determinism for a given (networkId, salt, recipient, tokenType), not about any specific
networkId.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three test-side fallouts from PR #119 (NetworkId + salt minting):

* SplitMintJustificationVerifier.verify now reads transaction.networkId.
  The mockCert helper in split-mint-justification.steps.ts was synthesizing
  a partial CertifiedMintTransaction without networkId, crashing 4 mutation
  scenarios with TypeError. Forward base.networkId into the mock.
* wrong-trust-base.steps.ts hard-coded networkId: 0 in its synthetic JSON.
  NetworkId.fromId now rejects 0 (out of 16-bit unsigned range). Use 2,
  which differs from the live LOCAL=3 trust base — same intent, valid id.
* cbor-envelope-tags.feature pinned MintTransaction arity at 6. #119 bumped
  the wire format to 7 elements (networkId + salt). Update the table row.

Also includes updated trust-base.json fixtures for tests/{e2e,functional}
matching the running bft-2sh aggregator at localhost:8080.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds three new feature files covering high-value gaps from the post-#119
coverage audit, plus a tagged repro for a real bug surfaced by adversarial
review of src/payment/SplitMintJustification.ts.

* mint-canonicalization.feature (6 scenarios) — pins MintTransaction CBOR
  byte-stability (encode→decode→re-encode == orig) and two-build determinism
  (two independent create() calls with identical logical inputs produce
  byte-identical CBOR and the same derived tokenId). Both properties are
  load-bearing for the stateId / CertificationData chain after #115.

* mint-wire-mutation.feature (4 scenarios) — adversarial harness: rebuild
  a real MintTransaction CBOR with networkId={0,65536} or salt={31,33} bytes
  and assert the decoder rejects each with the documented error fragment.
  Confirms the new NetworkId/TokenSalt guards fire on the decode path, not
  just the constructor path.

* split-mint-empty-proofs.feature (@known-bug, 2 scenarios) — captures
  B3#1 finding (pre-existing): SplitMintJustification.fromCBOR at
  SplitMintJustification.ts:58 calls `new SplitMintJustification(...)`
  directly, bypassing the proofs.length>0 invariant enforced by create()
  at line 36. A crafted CBOR payload with zero proofs decodes cleanly.
  The bug scenario fails by design with an actionable message; tag
  @known-bug lets the regression filter skip it until fromCBOR is routed
  through create() or duplicates the non-empty check.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds 5 new feature files and extends 1 existing, covering the bulk of the
remaining post-#119 coverage gaps from the audit.

Hermetic (all pass, no aggregator):
* network-id-edge-cases.feature (5 scenarios) — pins NetworkId.fromId
  singleton identity for MAINNET/TESTNET/LOCAL, asserts custom-id
  fromId(42) returns fresh-but-equal instances, and verifies a
  RootTrustBase JSON round-trip preserves equality.
* token-salt-edge-cases.feature (3 scenarios) — mutation safety of
  TokenSalt.fromBytes (input-buffer mutation does not leak) and
  TokenSalt.toBytes (returned slice mutation does not leak), plus
  TokenSalt.generate uniqueness over 100 calls.
* mint-slot-pinning.feature (6 scenarios) — asymmetric per-field
  inputs (networkId=7, salt=0xAA·32, tokenType=0xBB·32, justification=0xCC,
  data=0xDD) round-tripped through CBOR. Each scenario asserts one slot
  decoded to its expected distinguishable value so any encoder/decoder
  slot-swap regression is observable (constructor arg order differs from
  wire-slot order in MintTransaction; this pins both).

Live (real aggregator at localhost:8080):
* mint-respend-tolerance.feature (1 scenario) — second mint at the same
  derived stateId with different `data` collides; tolerated either as a
  submit-side JSON-RPC error OR proof-time TRANSACTION_HASH_MISMATCH (the
  same shape as the transfer re-spend tolerance).
* split-mint-justification-verifier.feature (1 new mutation row,
  Examples table) — "swapping the mint networkId to a different network"
  triggers SplitMintJustificationVerifier.ts:62 cross-network check.
  Extends the existing mutation harness; mockCert helper now forwards
  a networkId override.

Deferred (skeleton + intent documented):
* deferred-coverage.feature (@deferred, 2 placeholders) — captures the
  two scenarios that need invasive fixture work:
  - #7: aggregator-side rejection of arity-6 MintTransaction sent over
    the JSON-RPC seam (extension of RawCertificationSubmitter).
  - #8: UnicitySealNetworkMatchesTrustBaseRule isolated FAIL, requires
    CBOR-level tampering of the inclusion proof's unicityCertificate
    to swap seal.networkId without mutating genesis.networkId.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…vel tampering

Closes gap #8 from the post-#119 coverage audit. The existing
network-id-consistency.feature swaps the trust-base's networkId, which
trips MintNetworkMatchesTrustBaseRule first and short-circuits before
the seal rule is even checked. To observe the seal rule's FAIL in
isolation we needed a CertifiedMintTransaction where:
  - genesis.networkId matches the trust-base (mint rule passes)
  - inclusionProof.unicityCertificate.unicitySeal.networkId differs

seal-network-rule-isolation.feature constructs that shape by CBOR-
tampering the real seal's networkId byte (arity-8 tag 39005 array,
slot [1]) inside a real mint's inclusion proof, then re-packaging via
UnicitySeal.fromCBOR / new UnicityCertificate / new InclusionProof.
The seal's signatures no longer verify after the swap — but per PR
#119 the seal rule fires BEFORE signature verification, so the test
observes the seal-rule FAIL specifically (not a signature failure).

The mock CertifiedMintTransaction forwards just the methods/fields
the verification rule actually reads (calculateTransactionHash,
sourceStateHash, lockScript, networkId, tokenId, recipient, tokenType,
data, justification, inclusionProof). Verifies via
CertifiedMintTransactionVerificationRule.verify directly so we can
walk the result tree and assert MintNetworkMatchesTrustBaseRule=OK
sibling-to UnicitySealNetworkMatchesTrustBaseRule=FAIL.

Also updates deferred-coverage.feature: #7 (arity-6 mint over the wire)
turned out non-testable — CertificationData.toCBOR carries only the
mint's transactionHash, not the mint bytes themselves, so there is no
aggregator-side arity check to test. The arity guard is entirely
SDK-side and is already covered by cbor-envelope-tags.feature +
mint-wire-mutation.feature. Documented in deferred-coverage.feature
as a paper trail.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…@known-bug

PR #119 commit 1dbc4a0 (Martti, 2026-06-04) routes
SplitMintJustification.fromCBOR through SplitMintJustification.create:

    - return new SplitMintJustification(
    + return SplitMintJustification.create(
        await Token.fromCBOR(data[0]),
        CborDeserializer.decodeArray(data[1]).map(p => SplitAssetProof.fromCBOR(p)),
      );

The create() invariant (proofs.length > 0) is now enforced at the decode
path too. Our adversarial repro scenario (commit 075a4b9) now passes —
empty-proofs CBOR is rejected with "proofs cannot be empty.".

Drop the @known-bug feature tag; rename the scenario from
"the bug" to "regression guard for 1dbc4a0"; tighten the Then step
to assert the specific error fragment instead of just "any error".
The hermetic positive control scenario is unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ion guard for e635578)

PR #119 commit e635578 (Martti, 2026-06-04) removed a stray `}` from
the OK branch of UnicitySealQuorumSignaturesVerificationRule.verifySignature:

    - return new VerificationResult(`SignatureVerificationRule[${nodeId}]}`, OK);
    + return new VerificationResult(`SignatureVerificationRule[${nodeId}]`, OK);

Pre-existing bug (introduced in 3e3a7fe "Draft version of sdk 2.0").
Surfaced by this session's adversarial review (B2#1) and reported to
the SDK dev via Discord.

Note on test shape: Token.verify discards UnicityCertificateVerification's
result subtree on OK (InclusionProofVerificationRule:117 returns OK with
no child results), so the per-node `SignatureVerificationRule[<nodeId>]`
names don't surface through the public Token.verify path. The regression
guard instead invokes UnicitySealQuorumSignaturesVerificationRule.verify
directly on a real mint's seal and walks its per-node children:
  - Asserts every child name matches /^SignatureVerificationRule\[[^\]]+]$/
  - Asserts no child name ends with `}`

Live (real aggregator) — needs a passing mint that produces a signed
seal with quorum-passing signatures.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…n-request.feature

Background was missing the "Given a mock aggregator client is set up" step
that initializes this.setup. The "fresh canonical certification_request"
step uses this.setup.trustBase.networkId (added when we ported the harness
to the new MintTransaction.create signature in commit 601a3ed), so without
the setup step it crashes with "Cannot read properties of undefined
(reading 'trustBase')" in all 7 scenarios.

Adding the setup step unblocks the positive control scenario; the 6
mutation scenarios still need a separate reconciliation against the
current aggregator's canonical-CBOR behavior (none of the mutations
return the "CBOR is not canonical" prefix the test expects — likely
aggregator-go ValidateCoreDeterministic isn't active on the current
build despite #153 being merged to main).

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.

2 participants