Skip to content

Release: develop -> main - #32

Open
github-actions[bot] wants to merge 12 commits into
mainfrom
develop
Open

Release: develop -> main#32
github-actions[bot] wants to merge 12 commits into
mainfrom
develop

Conversation

@github-actions

@github-actions github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Automatic Release PR

Commits: 1 new commit(s)

  • Review all changes
  • Verify CI passes
  • Merge to release; tag a vX.Y.Z afterwards to trigger npm publish

TaprootFreak and others added 11 commits June 9, 2026 20:24
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 TaprootFreakAI left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Release develop→main of the v1 TypeScript client (#48 + prettier #49 + dep bumps). Lint/typecheck/tests/cross-tests green. Approving the production SDK cut.

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>
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