AI agent instructions for working on this codebase.
npm run build # tsc → dist/
npm test # vitest (single run)
npm run typecheck # tsc --noEmitAlways run npm test after changes. Always run npm run typecheck before committing.
| File | Purpose |
|---|---|
src/sag.ts |
SAG (Spontaneous Anonymous Group) ring signatures — ringSign, ringVerify |
src/lsag.ts |
LSAG (Linkable SAG) — lsagSign, lsagVerify, computeKeyImage, hasDuplicateKeyImage |
src/utils.ts |
Shared crypto helpers — hashToScalar, hashToPoint, randomScalar, safeMultiply, constantTimeEqual, scalarEqual |
src/errors.ts |
Error hierarchy: RingSignatureError → ValidationError, CryptoError |
src/index.ts |
Public API re-exports |
- Public keys are x-only hex (32 bytes, BIP-340 convention). Internally converted to curve points via
'02' + hex(even y). - Private keys are 32-byte hex scalars reduced mod N.
- SAG: Schnorr-based ring — cyclic challenge chain
c_0 → c_1 → ... → c_0. - LSAG: Extends SAG with a second chain through
H_p(P || electionId)and the key imageI = x * H_p(P || electionId).
- Domain separators (
'sag-v1','lsag-v1','secp256k1-hash-to-point-v1') are protocol constants. Changing them breaks all existing signatures. - Nonces must be random (via
secp256k1.utils.randomPrivateKey()). Never use deterministic nonces — reusing a nonce leaks the private key. - BIP-340 parity fix (negating
xwhenx*Ghas odd y) is critical for x-only pubkey compatibility. Do not remove or reorder. - Length-prefixed hashing in
hashToScalaris intentional — it prevents domain separation ambiguity when concatenating variable-length fields. - Constant-time comparisons (
constantTimeEqual,scalarEqual) prevent timing side-channels. Do not replace with===orBuffer.equals. safeMultiplyhandles the0nedge case that@noble/curvesrejects. Do not replace with bare.multiply().- Key image format is enforced as compressed point (02/03 prefix) to prevent duplicate images via different encodings of the same point.
- British English in all prose (colour, initialise, behaviour, licence).
- ESM-only — all imports use
.jsextensions. - Commit messages:
type: description(e.g.feat: add batch verification,fix: reject identity point as key image). - No
Co-Authored-Bylines in commits. - Anvil auto-release on main — every push to main runs
forgesworn/anvil@v0and auto-publishes. Work on branches; merge to main only when complete.
Automated via forgesworn/anvil -- auto-release.yml reads conventional commits on push to main, bumps the version, and creates a GitHub Release; release.yml then runs the pre-publish gates and publishes to npm via OIDC trusted publishing.
| Type | Version Bump |
|---|---|
fix: |
Patch (1.0.x) |
feat: |
Minor (1.x.0) |
BREAKING CHANGE: (in commit body) |
Major (x.0.0) |
chore:, docs:, refactor: |
None |
Tests must pass before release. Work on branches -- merge to main only when a logical chunk is complete.
Tests are in tests/. Vitest is the runner. Tests cover:
- Round-trip sign/verify for both SAG and LSAG
- Input validation (ring size, index bounds, duplicates)
- Key image determinism and duplicate detection
- Signature tampering detection
- Edge cases (wrong key, modified ring, altered message)
When adding new functionality, add corresponding tests. PRs touching signature logic require crypto review.