Feature/test infrastructure#106
Conversation
- 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>
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
| let client: StateTransitionClient; | ||
|
|
||
| const ALICE_SECRET = new TextEncoder().encode('Alice'); | ||
| const url = 'http://localhost:3000'; |
There was a problem hiding this comment.
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.
| const url = 'http://localhost:3000'; | |
| const url = process.env.AGGREGATOR_URL || 'http://localhost:3000'; |
| // const aggregatorClient = TestAggregatorClient.create(); | ||
| const aggregatorClient = new AggregatorClient('http://192.168.43.106:3000'); |
There was a problem hiding this comment.
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.
| // 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'); |
| 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; |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
# 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>
#107 Add StateId Shard check
#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>
#105 Update inclusion proof format
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>
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>
No description provided.