Release: develop -> main - #32
Open
github-actions[bot] wants to merge 12 commits into
Open
Conversation
Remove the staging integration buffer from the sdk repo. Feature PRs now go directly to develop (feature/* -> develop -> main). Deletes the staging->develop auto-promote workflow, drops staging from the CI push triggers, retargets dependabot at develop, and rewrites the branch-flow sections of CONTRIBUTING.md and README.md to state the develop-only rule exactly. Also brings the project-overview README onto develop.
… 100% unit coverage (#48) * test: make the Node/SDK parity gate able to fail The cross-tests job reported success without checking anything: every step was guarded by an existence condition, so a missing shim skipped the suite instead of failing it. Node/SDK parity is DoD 6(b), and a gate that cannot go red is not a gate. The job now requires the shim sources and fails loudly when they are absent. The suite covers six operations against the real SDK modules: fixed BIP-39 reference vectors plus 100 randomized samples each for mnemonic_from_entropy, account_from_mnemonic, derive_public_keys, derive_signing_key, sign_schnorr and create_commitment. Randomized inputs come from a seeded xorshift32 rather than the system CSPRNG, so a parity break is reproducible: the seed is printed on every run, can be overridden through CROSS_RUST_SEED, and a zero or empty value is rejected instead of silently falling back. Raw 32-byte account_from_seed is deliberately not parity-checked and says so in the README. The SDK keeps accountFromSeed private and only exposes generateAccountKeys and generateAccountKeysFromMnemonic, so a case for it would have compared Rust against a reimplementation living in the test rather than against the SDK. The mnemonic path still covers the same helper through BIP-39's 64-byte seed. * ci: pause hosted CI on pull requests while the v1 work is verified locally The v1 work is developed and checked on the build host; hosted CI is out of the loop until it is finished and is switched back on as the last step before the branch is offered for review. Only the trigger changes. The pull_request event is commented out verbatim so re-enabling is a deletion rather than a rewrite, and every job and step is untouched. push on develop/main stays active — those branches are not touched by the feature work — as does workflow_dispatch for a run started by hand. * feat: reimplement Poseidon over Goldilocks, E(.) and Hc in TypeScript The spec asks the SDK for an independent primitive-level reimplementation so that node and SDK can be shown to agree bit for bit (V.7 parity matrix). test/cross-rust/README.md said plainly what was missing -- V.4 Poseidon digests had no JS implementation, and expanding the gate without one "would only re-test Rust against itself". This closes that. Five pieces, no new runtime dependency: the Goldilocks field (p = 2^64 - 2^32 + 1, bigint-backed because a JS number carries only 53 bits safely), the plonky2 Poseidon permutation at width 12 with 4+22+4 rounds, the section 1.7.2 field encoding E(.), Hc on top of it, and the canonical digest <-> bytes conversion. Two traps the Rust code calls out by name are handled rather than rediscovered. digestToBytes reduces each limb canonically before emitting big-endian bytes, so two mathematically equal elements cannot serialise differently; digestFromBytes rejects a limb >= p instead of reducing it silently, because the obvious library call only checks that in a debug assertion. Byte strings are encoded with a length element and big-endian 7-byte chunks -- an older model packed little-endian without a length prefix, and a test is written so that swapping the direction turns it red. Every target value comes from outside this code: thirteen are reproduced from the vectors the Rust node generated, and five more (nk_sample, ss, epk and two others) are pinned in the specification itself. Not one was authored here. That is what makes a green run mean something -- if the TypeScript disagrees, the TypeScript is wrong. Also fixed, and worth stating because neither came from this work: the branch was already lint-red on test/cross-rust/cross.test.ts (a rethrown error without its cause, confirmed by stashing everything and running eslint again) and test/cross-rust/README.md was already unformatted. Both are repaired here rather than deferred. Verified locally: typecheck, lint, 301 tests, the 100% coverage gate and the build all pass. * feat: the v1 signer and the wallet-side transition flow Node and API were ready for a full run; what was missing was the third side. Nothing in the system held a spend key and produced the §3.2 signature, so every run ended at `awaiting_signature`. The signer lives here rather than in the app's WASM crate: that crate is the legacy wallet (flat-message Schnorr, ash‖ocr commitments) and carries nothing of §3.2. The SDK already had the v1 primitives and — through `test/cross-rust/` — a real Rust shim to check against, so the work is verifiable end to end without a browser or a WASM toolchain. Acceptance is not "our tests pass". It is **V.8**: the spec pins the whole signing and aggregation layer, and the Rust side already runs against it. Every pinned value is reproduced bit for bit — both signers including the odd-y key, `R'`, `t`, `R`, `e`, `s`, and `z`, `a₁`, `a₂`, `s_agg`. The V.9 negative controls are separate tests: swapped `R` in the aggregate, a foreign `H(ProofData)` in a CommVerify opening, and a testnet signature checked against mainnet's `m_state` — the last one is what closes cross-network replay. Two details are the ones a plausible-looking implementation gets wrong: the S2C tweak is used **unreduced** (`int(t) ≥ n` means redraw, never `mod n`) while the aggregation coefficients are explicitly reduced, and `j` starts at 1. Neither shows up in self-written tests; both show up immediately against V.8. The wallet flow is `POST /v1/tx` → poll with `Retry-After` or SSE → `awaiting_signature` → `POST /v1/jobs/<id>/sign`, plus the §5.1 ownership proof and pull session that `GET /v1/account/state` needs. What makes it a wallet rather than a signing automaton are the refusals: it signs only when `derive(A/0'/send_counter).pubkey == current_pubkey == txn_pubkey` — all three, two out of three is not enough — and only when `H(ProofData)` recomputed from the six fields matches the digest the job reports. The server-supplied digest is never signed; that would bind the S2C tweak to an assertion instead of to the values. A third refusal covers a network the node names and the wallet does not expect. The transition request cannot be malformed by construction: the §7.5 presence matrix is closed in the type, so a request carrying `fee_address`, or `fold_coin_ids` on a send, does not compile. `Verify` initially rejected roughly half of all valid signatures. It required `y(s·G)` to be even — a property BIP-340 says nothing about; the even-y rule belongs to the reconstructed nonce point, which here is `lift_x(bytes(R))` and is even by construction. Signer 1 of the fixture happens to have even `y(s·G)` and passed. With one signer in the fixture this would never have surfaced. Four tests used `x = 1` and `0x0101…01` as "off-curve" probes. Both are on the curve; the tests were green for the wrong reason. They now use the BIP-340 not-on-the-curve vector. * fix: refuse terminal job states that arrive without their payload §7.5 requires a terminal job to carry its outcome: `completed` has a `result`, `failed` and `cancelled` have an `error`. The client parsed both fields only when present, so a `completed` job with no result passed straight through and the wallet held a success it could not act on. The SSE path made it worse by being a second, laxer way into the same state: it yielded raw job objects and stopped at a terminal status without running them through the parser the polling path used. The stricter path was avoidable simply by choosing the other one. Both now share a single parser, so a malformed terminal job fails identically whichever transport delivered it. A missing payload is a protocol violation by the server, not something to paper over with a default — the error names which field was absent for which status. The SSE yield type said `V1Job | unknown`, which is just `unknown` with documentation attached: the union absorbed the useful half and promised nothing to callers. It is now discriminated by event name, so a caller can tell from the event whether a job is present instead of asserting it. * feat: carry the delivery credential on send, and check it as the sender §7.5 now defines `OutputTemplate.delivery`, the closed tagged union that carries a recipient's delivery credential across the kernel boundary — an `Invoice`, or a full kind-0 profile event. The client types mirror that union exactly; an unknown tag fails parsing rather than travelling onward. The send flow places each credential at the position of its own output, `output_templates[i]`, because §7.5 binds by position: two outputs to the same recipient for the same amount are otherwise indistinguishable. Mints with third-party outputs carry credentials the same way. Before a credential enters a request the SDK runs the checks §4.3 demands of the sender, in order: the address preimage, `addr_sig` under `pk0` over `invoice_message`, the op signature — and for invoices the byte-exact match of recipient, asset and amount against the output the credential is attached to. A profile is checked down its own chain (event signature, preimage, profile-fixed `addr_sig`, version, network, relay) and matches the recipient only, since it addresses an account rather than authorising a payment. This is the sender checking its counterparty's object, which §4.3 requires — not the wallet second-guessing its own node. A pinned payment identity is compared before anything is sent: a credential disagreeing with the stored `{address, pk0, nk_commit, ivpk}` raises the §4.3 warning path and is not paid silently; first contact carries no pin and is not an error. Credential parsing takes `unknown` and narrows field by field with named errors — no `any` at the one place that handles untrusted input. The fixtures derive their addresses from the spec's V.2/V.4 vector material, so every negative test fails on the property it names, not on an invented address dying at the checksum. * ci: switch the hosted runner back on The v1 work is done and verified locally, so hosted CI comes back — the un-pause is the deletion the paused block was written to be: the PAUSED note goes and the `pull_request:` trigger returns verbatim. `push` and `workflow_dispatch` were left active throughout and are untouched. * fix: harden the crypto edges a review found, and stop exporting a test signer A review of the diff surfaced seven issues; the first was the one that mattered most. `signTransitionWithFixtureNonce` — a deterministic-nonce signer whose `k'` does not bind `mSc`, so two signatures under the same key, network and counter recover the secret scalar — was exported from the package root behind a "test-vector only" comment. A comment does not un-export a function. It and its trace helpers move into the test fixtures; the public surface offers only the CSPRNG path. The hashing primitives were fail-open in two places. `digestFromHex` parsed with `parseInt(_, 16)`, which reads `0g` as `0`, so a malformed digest could be accepted as a different one — it now validates the whole hex alphabet before parsing, with partial-nibble negatives. `Hc` silently skipped an unknown `HcInput.type` (no `default`) and normalised non-canonical limbs through `reduce`, both of which collide distinct malformed inputs; unknown types and out-of-field limbs now throw. The rest are honesty and reproducibility. The cross-Rust README claimed V.2-ext hardening and Poseidon were not implemented — they are, in this PR; the crypto paths not yet in the parity gate are named as open rather than described as absent. CONTRIBUTING's "no rolled crypto" now names the Poseidon/`E(·)`/`Hc` reference implementation as the deliberate, vector-checked exception it always was. The Rust reference shim carries a committed `Cargo.lock` and builds `--locked`. The half-aggregation cross-test runs the promised 100 batches, not 50. The pre-sign gate was left untouched: its refusals are the §7.5 custody boundary the wallet must run before signing, not node-distrust logic. * fix: a second review round on the wallet-facing edges A fresh review pass found six more real gaps, all fixed. An invalid BIP-39 phrase was accepted as a seed: `seedFromMnemonicV1` checked only for an empty string, so a typo derived a different, unrecoverable account while the Rust reference rejected it. The phrase is now validated against the wordlist and checksum before derivation, and rejected otherwise. `assertTransitionRequest` claimed a narrowing it did not perform — it accepted numbers inside `input_coins`/`fold_coin_ids`, skipped the issuance fields entirely, and let a v1 request carry the v2-only `cap_total`/`terms_salt`. It now validates every array element and hex/ decimal/issuance field and forbids the v2 fields under `issuance_version = 1`, returning a normalised wire object. The NIP-01 preimage passed control characters through raw, so U+0000… U+001F produced a JSON string that is not NIP-01-canonical and a different event id than a conforming serialiser; they are now escaped, with independent control-character id vectors. `name_sig` was validated only if it was already a string, so `undefined`/`null`/a number slipped through where §4.3 requires the field; it is now read as a required 64-byte hex. A completed `mint`/`send`/`receive` job could arrive missing its mandatory ProofData digests and still be accepted; the parser now requires all of them per kind, with `attest_balance` the only documented exception. And the SSE reader split frames on `\n\n` only, so a conforming `\r\n\r\n` stream was never processed; it now handles CRLF, LF and CR. * test: bring the coverage floor to 100% and make the package git-installable CI enforces 100% line/function/statement coverage, but the local gate ran `vitest run` without `--coverage`, so the branch had been red on the coverage job while looking green locally. The local gate now runs `test:coverage`, and the uncovered surface is closed with real tests rather than ignore pragmas. Most of the gap was error and edge paths in the delivery-credential, NIP-01 and transition-request code: invalid BIP-340 keys, malformed fields, out-of-range amounts, boundary lengths. Two verify catches (`verifyBip340`, the NIP-01 author check) are now exercised with a 32-byte x-only key that is not a valid curve point, which makes noble's `lift_x` throw — the path a length check alone never reached. The redundant `BigInt` try/catch after the canonical-decimal regex is removed, since the regex already guarantees a parseable value. Only the two u32 wire-length bounds (memo and relay length, mirroring the Rust encoder) keep a narrow documented ignore: a >4 GiB input is not constructible in the runtime. Adds a `prepare` build script so the package can be consumed as a git dependency (`github:zk-coins/sdk#…`): npm runs `prepare` after installing a git dep and builds `dist/`. The published npm tarball is unaffected. * feat(v1): add issueInvoice and the issuance creator_pubkey Add issueInvoice, building a delivery-credential Invoice whose addr_sig/sig match the kernel verifier's message byte-for-byte, and require the issuance creator_pubkey (Pk0) on both token standards, with full test coverage. * feat(v1): validate the genesis-receive Pk0 on the receive request Add genesis_pubkey to the receive variant with presence validation (rejected on mint/send) and a round-trip test, keeping the 100% coverage gate. * feat(v1): balance-attestation and view-grant request helpers Add the client-side crypto and REST surface for the two v1 disclosure controls, as byte-exact ports of the api/node reference (verified against known-answer SHA-256 vectors): - attest.ts: AttestBalance challenge/request-hash/ownership-proof helpers (§5.7 / §7.5) plus client.openAttestBalanceChallenge / attestBalance. - grant.ts: ViewGrant scope encoding, issue-grant request hash and ownership proof, and GrantProof construction (§5.2 / §5.1) plus client.openGrantsChallenge / issueViewGrant / openGrantPullSession; openPullSession now accepts an OwnershipProofJson | GrantProofJson union. - client.getJob: branch the completed-result parser on job kind so an attest_balance job (result carries only `attestation`, no output_coin_ids) parses instead of throwing. - Export the public scope constant and grant/scope types from the package root so consumers of '@zkcoins/sdk' can build a scope without a deep import. 100% statement coverage retained; the issueViewGrant scope is snapshotted before the challenge round-trip so a caller mutating it cannot desync the signed hash from the wire body. * review-fix(sdk): drop the silent internal_error fallback; enforce decimals u8 bound; integer-check send_counter - parseV1ApiError: an unparseable error body no longer silently defaults machineCode to 'internal_error' (which masqueraded as a real server code); it now uses the honest marker 'unparseable_error_body' and keeps rawBody intact. - normalizeIssuance: decimals is now additionally bounded to <= 255 (u8), consistent with the Poseidon assetIdV1 bound. - new requireCounter (Number.isInteger && >= 0) for send_counter from awaiting_signature / account_state; the generic requireNumber is unchanged (progress floats stay valid). - one test adjusted (unparseable-body machineCode). Gate green: tsc --noEmit, lint, tests. * review-fix(sdk): export the grant/attest surface, close grant-scope widening and host binding, harden parsers - src/index.ts re-exports the full attest/grant builder surface (13 symbols) that was reachable only via src/v1/index.ts, so @zkcoins/sdk consumers can actually use it; add barrel-completeness tests. Drop the internal '(Req 9(c))' note from a comment. - grant scope is fail-closed: assertConsistentAssetScope rejects allAssets+assetIds together (no silent '*' widening), and grantScopeToJsonBody runs full encodeGrantAssetIds validation so the pull paths validate before any network call, matching issueViewGrant. - attestBalance/issueViewGrant sign against the client's canonical this.host; host removed from the input. - parseJob rejects mixed terminal payloads (attestation in a transition result, or transition-digest keys in an attest result); parseAttestBalanceAccepted/parseIssueGrantResult reject an empty job_id/grant handle. - tests: the four proof tests now bind against the input public key (not the builder's own output); send_counter -1/0.5 and decimals 255/256 boundaries; empty-handle rejection. * test(sdk): enforce 100% unit coverage on every axis Raise the vitest gate to 100% statements, branches, functions, and lines. Cover the remaining client, delivery, and account paths; mark only structurally unreachable crypto edges with justified v8 ignores. * fix(sdk): fail-closed digest limbs and challenge fields Reject Poseidon digest inputs that are not exactly four field elements, and range-check coinhist leaf states at runtime. Challenge parsers now require a 32-byte lowercase nonce and a canonical u64 expiry before returning the wire strings. Coverage-test lint is clean; CONTRIBUTING documents the 100% branch gate and the structurally unreachable v8-ignore rule. * fix(sdk): reject non-canonical digest lengths in digestToBytes digestToBytes now runs the same four-limb check as hc/encodeDigest instead of silently serializing a prefix. digestsEqual treats a non-four-limb value as unequal. v8 ignores on the unconstructible u32 overflow guards keep an explicit reason. * fix(sdk): fail-closed stream drain and SSE frame boundaries Reject non-canonical digest lengths at the subject/sign gates, drain a terminal SSE frame when the peer closes on the same read, and treat every WHATWG pair of line endings as a blank line. Unit coverage is 100% on statements, branches, functions, and lines (639 tests). * style(sdk): apply Prettier to the stream-drain files * fix(sdk): keep sign-trace helpers off the public surface signTransitionTrace, SignTrace, and runWithRedrawBudget stay in transitionSignature for V.8 parity tests. Package and v1 barrels no longer re-export them, so production callers cannot import the normalised secret scalar. * test(sdk): reject type-only re-export of sign-trace helpers namedExports() now fails if SignTrace, signTransitionTrace, or runWithRedrawBudget appear in either barrel, including as type-only names that a runtime property check would miss. * fix(sdk): reject contradictory grant scopes and mixed job envelopes allAssets together with any assetIds field is now a hard error. Grant pull snapshots the scope before the challenge round-trip. Terminal job objects may not mix result and error, and job_id must be non-empty.
…ory with 2 updates (#36) Bumps the eslint-prettier group with 2 updates in the / directory: [eslint](https://github.com/eslint/eslint) and [prettier](https://github.com/prettier/prettier). Updates `eslint` from 10.4.1 to 10.8.0 - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](eslint/eslint@v10.4.1...v10.8.0) Updates `prettier` from 3.8.3 to 3.9.6 - [Release notes](https://github.com/prettier/prettier/releases) - [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md) - [Commits](prettier/prettier@3.8.3...3.9.6) --- updated-dependencies: - dependency-name: eslint dependency-version: 10.5.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: eslint-prettier - dependency-name: prettier dependency-version: 3.8.4 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: eslint-prettier ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](actions/checkout@v6...v7) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…2 updates (#38) Bumps the vitest group with 1 update in the / directory: [@vitest/coverage-v8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8). Updates `@vitest/coverage-v8` from 4.1.8 to 4.1.10 - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.10/packages/coverage-v8) Updates `vitest` from 4.1.8 to 4.1.10 - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.10/packages/vitest) --- updated-dependencies: - dependency-name: "@vitest/coverage-v8" dependency-version: 4.1.9 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: vitest - dependency-name: vitest dependency-version: 4.1.9 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: vitest ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
) Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 8.60.1 to 8.63.0. - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.63.0/packages/typescript-eslint) --- updated-dependencies: - dependency-name: typescript-eslint dependency-version: 8.63.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [msw](https://github.com/mswjs/msw) from 2.14.6 to 2.15.0. - [Release notes](https://github.com/mswjs/msw/releases) - [Changelog](https://github.com/mswjs/msw/blob/main/CHANGELOG.md) - [Commits](mswjs/msw@v2.14.6...v2.15.0) --- updated-dependencies: - dependency-name: msw dependency-version: 2.15.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* style: prettier DeliveryCredential union * style: prettier TransitionRequest union
TaprootFreakAI
approved these changes
Aug 17, 2026
Same one-line pin as dependabot#40, rebased onto current develop so the Cargo cache step applies cleanly. Co-authored-by: TaprootFreakAI <315477232+TaprootFreakAI@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Automatic Release PR
Commits: 1 new commit(s)