Skip to content

chore: housekeeping sweep across the cryptography package - #8

Merged
mattgle merged 19 commits into
mainfrom
chore/cryptography-housekeeping
May 12, 2026
Merged

mattgle merged 19 commits into
mainfrom
chore/cryptography-housekeeping

Conversation

@mattgle

@mattgle mattgle commented May 7, 2026

Copy link
Copy Markdown
Contributor

Context

@railgun-reloaded/cryptography was shipping a stack of broken infrastructure: a console.log(privateKey) debug line, a npx mocha test script with no mocha installed, tests that always passed via nested-it() and empty-catch, an EDDSA dispatch that depended on a circomlibjs patch that was never applied, dead code, an in-place mutation of caller arrays in poseidonFunc, AES helpers returning Buffer typed as Uint8Array, a misspelled primatives/ folder, and a placeholder README.

This PR is the housekeeping pass. The diff has the full list — this description focuses on the load-bearing decisions and the cross-package verification.

Why the circomlibjs patch is removed

The patch made circomlibjs.buildEddsa() accept an injectPoseidon argument so eddsa could share wallet-node's already-loaded poseidon. It was a no-op in production: patch-package was never in cryptography's deps and there was no postinstall, so on a clean install the .patch file was never applied. circomlibjs always built its own poseidon and ignored the argument.

The behavior was also asymmetric: merkletree-manager's postinstall did walk into its own copy of cryptography and apply the patch, so the patch was effective when cryptography was reached via merkletree-manager but not when wallet-node installed it directly. Same package, different behavior depending on install path.

Three options: wire patch-package in properly (adds a dep + postinstall hook + maintenance burden), upstream the change (out of scope), or drop the patch. Picked drop. No correctness change — both poseidons are the same circomlibjs poseidon over the same BabyJubJub field; signatures are bit-identical. wallet-node drops the now-unused argument in railgun-reloaded/wallet-node#19.

Why @noble/secp256k1 is removed

Used by exactly one file: signature.ts, which exported rawSignature (Ethereum signed-message signing), formatEthMessage, and pointConversion. No consumer in the workspace imports any of them, and engine has no equivalent code path either — verified by grepping engine for Ethereum Signed Message / secp256k1, zero matches. The exports were dead code with no engine basis.

Verification across consumers

Ran every consumer's relevant test suite against this branch via file:.. substitution. All passed:

Consumer Tests Result What's exercised
cryptography 34 34/34 ✅ full primitives + engine poseidonHex vectors
wallet-sdk wallet-id 7 7/7 ✅ sha256 wallet-ID vectors (load-bearing determinism check)
wallet-sdk wallet-crypto 7 7/7 ✅ AES-256-GCM roundtrip + random IV
wallet-node 93 93/93 ✅ AES, EDDSA, poseidon, key derivation, shared-key vector
merkletree-manager 4 4/4 ✅ poseidon + keccak in real merkle ops

145 tests, all green. Existing wallet IDs and ciphertexts continue to decrypt and verify identically.

Follow-up

  • wallet-node#19 drops the now-redundant initializeEddsa(poseidonBuild.pure) arg.
  • merkletree-manager's postinstall still runs npx patch-package against an empty patches/ folder — harmless no-op, could be simplified later.

mattgle added 3 commits May 7, 2026 10:56
Combines a security fix, several correctness fixes, public-API tightening,
test-framework migration, and structural cleanup so that the package no
longer ships with broken / dishonest infrastructure.

Security:
- Remove debug `console.log(privateKey)` from `rawSignature` (file later
  deleted along with the rest of the unused signature module)

Correctness:
- Stop mutating the caller's input array in `poseidonFunc`
- Fix `initPoseidon`'s always-false `typeof === 'function'` check
- Drop `autoInitializeEddsa` (had inverted poseidon dispatch and depended on
  a `circomlibjs` patch that was never applied because `patch-package` was
  not in devDependencies)
- Make AES helpers return real `Uint8Array`s instead of `Buffer` instances
  typed as Uint8Array
- Add proper init guards on `eddsa.*` operations with descriptive errors
- Type the circomlibjs eddsa build object instead of relying on `any`

Test framework:
- Migrate from a `npx mocha` script that never worked (mocha not installed,
  files use `node:test`) to `brittle`, the workspace standard
- Drop nested-`it()` and empty-`catch` test patterns that always passed
- Drop in-suite micro-benchmarks
- Add the missing AES coverage (encrypt/decrypt roundtrip, key-length
  validation, GCM tag tampering)

API + structure:
- Rename misspelled `primatives/` folder to `primitives/` (no consumer
  imports sub-paths, so the package boundary is unchanged)
- Drop the `poseidonLib` namespace re-export that leaked the entire
  `poseidon-lite` library surface
- Eliminate the `primitives/index <-> poseidon-lite` circular import
- Stop running `initializePoseidonFuncs` as a side effect at module load
  (the populated array was dead code)
- Remove unused public exports: `signature.ts`, `hash.ts`,
  `poseidon-module.ts`, `poseidonHex`, `getPoseidonFunc`, plus the dead
  `@noble/secp256k1` dependency
- Delete `patches/` (`patch-package` was never wired up)

Docs:
- Replace the placeholder README with one that documents the actual
  primitives, init order, and usage examples
- verifyEDDSA was calling .reverse() in place on the caller's signature.R8
  and pubkey arrays, silently corrupting them across consecutive verifies.
  Wrap each input in `new Uint8Array(...)` before reversing, matching
  signPoseidon. New test asserts that two consecutive verifies on the same
  inputs both succeed and that no input array is mutated.
- decryptGCM length-validation errors were wrapped inside the generic
  "Unable to decrypt ciphertext" cause and lost in caller logs. Move the
  guards above the try/catch so the specific message surfaces. Tests
  updated and two new negative tests cover wrong-length iv and tag.
- Annotate the 16-byte AES-GCM iv as a deliberate wire-format choice
  (NIST recommends 12; existing at-rest ciphertexts assume 16).
- Add a negative test for decryptCTR's iv-length validation. Mirrors the
  GCM iv/tag tests added earlier; closes the corresponding gap in CTR
  coverage.
- Remove eddsa.genRandomPoint. It was a one-line `poseidon([randomBytes(32)])`
  wrapper with no consumer in the workspace; deleting raises the bar over
  testing dead code.

Coverage: 100% functions, 98.56% lines/statements, 95.77% branches. The
remaining uncovered lines are defensive guards (assertEddsaReady throw
branch, poseidon-lite shape-mismatch checks) that require child-process
isolation or library mocking to exercise honestly.
mattgle added 5 commits May 7, 2026 15:52
- sha256 was being used by wallet-sdk/services/wallet/wallet-id.ts to
  generate deterministic wallet IDs. Removing it would have changed every
  consumer's wallet IDs after upgrade — a critical regression. Restore
  hash.ts and re-add the export to primitives/index. Verified by running
  wallet-sdk's wallet-id vector tests against local cryptography (7/7
  match the canonical IDs).
- Drop the _injectPoseidon parameter from initializeEddsa now that the
  wallet-node call site has been updated in
  railgun-reloaded/wallet-node#19. The transition shim is no longer needed.
- Add a sha256 test file (NIST FIPS-180-4 test vectors).
- Add a single CryptographyError class extending Error with a `code`
  discriminator (matching the BytesError pattern). Codes cover every
  failure mode in the package. All `throw new Error(...)` sites now use
  this class so consumers can branch on `err.code` rather than parsing
  message strings.
- Convert internal `interface` declarations to `type` aliases for
  consistency with the rest of the package.
- Drop unnecessary `as Uint8Array` casts in eddsa/index.ts: the typed
  EddsaBuild interface already declares the right return types.
- Migrate the test suite from brittle to node:test (assertions via
  node:assert/strict). Drops `brittle` and `@types/brittle` from
  devDependencies. The npm test script now invokes `node --test`.
- Tests now assert error codes via the {name, code} matcher form,
  giving a typed contract instead of regex-matching messages.
A deeper read of the engine source showed two of the previously-removed
exports are not actually dead code — they have no current consumer in
reloaded yet but engine relies on them, and reloaded packages will need
them as they catch up to engine functionality:

- poseidonHex: engine uses it in merkletree.ts (sibling-node hashing),
  abstract-wallet.ts, and keys-utils.ts (getRandomScalar).
- initPoseidonPromise: engine awaits it twice in railgun-engine.ts
  startup as the canonical "poseidon is ready" gate.

Restore both with engine-compatible semantics:
- poseidonHex(inputs: string[]): string accepts hex strings with or without
  the 0x prefix and any nibble length, returning a 64-character lowercase
  hex digest. Verified against engine's reference vectors
  (poseidon(0, 1) === 1bd20834...e65e).
- initPoseidon prefers WASM and falls back to pure-JS, matching the
  one-shot init shape engine consumers expect.

The other engine-checked items (rawSignature/formatEthMessage/secp256k1,
autoInitializeEddsa, getPoseidonFunc, initializePoseidonFuncs, poseidonLib
namespace) all confirmed absent from engine and stay removed.
initPoseidon was the only remaining throw site still using the generic
Error class, leftover from before the CryptographyError migration.

The defensive `if (!(cause instanceof Error))` guard around the wasm
fallback was also paranoia — circomlibjs throws Error instances, and
forcing the caller to distinguish "wasm threw an Error" from "wasm threw
something else" served no real purpose. Drop the guard, simplify the
control flow, and surface the final pure-JS failure as
CryptographyError(PoseidonNotLoaded) with the underlying cause attached.
@mattgle
mattgle marked this pull request as ready for review May 8, 2026 13:36
@mattgle
mattgle requested review from bhflm, mesquka and zy0n May 8, 2026 13:36

@zy0n zy0n 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.

Looks good to me.

@mattgle
mattgle requested a review from simonmasson May 11, 2026 14:18
@simonmasson

Copy link
Copy Markdown
Collaborator

I had to do this in order to make the install and test work:

npm install
cd @railgun-reloaded/bytes/
npm install
npm run build
cd ../../
npm test

Adding "tsc --build" in the bytes repository (package.json file) would fix this but it's another repo.

@mattgle

mattgle commented May 11, 2026

Copy link
Copy Markdown
Contributor Author

I had to do this in order to make the install and test work:

npm install
cd @railgun-reloaded/bytes/
npm install
npm run build
cd ../../
npm test

Adding "tsc --build" in the bytes repository (package.json file) would fix this but it's another repo.

Yes! We're aware, we just merged a PR that handles that a few hours ago. This is just until we have packages released to NPM.

@simonmasson
simonmasson force-pushed the chore/cryptography-housekeeping branch from 5e5eabc to 0154164 Compare May 12, 2026 10:30
@mesquka

mesquka commented May 12, 2026

Copy link
Copy Markdown

While engine doesn't handle secp256k1, it is used by the likes of TokenShielder for the shield message.

That feature is better served by ethersjs/web3js so it's fine to remove here but noting the reasons.

@simonmasson simonmasson 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.

Sounds good to me.

simonmasson and others added 7 commits May 12, 2026 18:10
Lockfile previously pinned @railgun-reloaded/bytes to a commit that
predates its prepare script, so CI installs of cryptography ended up
with an unbuilt bytes (missing src/index.js). Refreshing the lock to
current bytes#dev (4980c5c) brings in the prepare hook and the build
runs on git+https consumers.
mattgle and others added 2 commits May 12, 2026 18:10
Drop inline `bytesToBig`, `bigToBytesBE`, and `fromHex` helpers in
`test/eddsa.test.ts` and `test/aes.test.ts` in favour of the canonical
`bytesToBigInt`, `bigIntToBytes`, and `hexToBytes` exports from
`@railgun-reloaded/bytes`. The bytes-package versions throw on negative
values, overflow, and malformed hex rather than silently producing
malformed output.
@simonmasson
simonmasson force-pushed the chore/cryptography-housekeeping branch from 044c219 to a94ff14 Compare May 12, 2026 16:13
… test

Drop the inline bigToBytes32 helper from test/poseidon.test.ts in favour of
bigIntToBytes(value, 32) from @railgun-reloaded/bytes. Continuation of the
earlier refactor that did the same for the eddsa and aes test files.

The bytes-package version throws NegativeValue on negative input and
BigIntOverflow on values that do not fit in the given byte length, instead
of silently returning a zero-padded or truncated array.
@mattgle
mattgle merged commit 8f4a819 into main May 12, 2026
1 check passed
@mattgle
mattgle deleted the chore/cryptography-housekeeping branch May 12, 2026 16:25
mattgle added a commit to railgun-reloaded/wallet-node that referenced this pull request May 12, 2026
The argument to initializeEddsa(poseidonBuild.pure) was already a no-op:
the cryptography package shipped a circomlibjs patch that would have made
it effective, but patch-package was never wired up so circomlibjs's
upstream buildEddsa was always the unpatched version that ignores its
argument. The injection has been removed from cryptography (see
railgun-reloaded/cryptography#8); drop the call-site argument and the
now-unused poseidonBuild import.

No behavior change: eddsa already builds its own internal poseidon
regardless of what was passed in.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

5 participants