Page MenuHomePhabricator

[alp-fusion] Add protocol primitives (Pedersen, blind Schnorr, hashing)
ClosedPublic

Authored by bytesofman on Jul 16 2026, 16:46.

Details

Summary

Pedersen commitments with separate sats/token H domains, CashFusion-style blind Schnorr, session/round hashing, and constants (num_components = 23, ALP_MAX_OUTPUTS = 29). Mocha unit tests only — no coordinator, client, or tx assembly yet.

Crypto uses ecash-lib only (Uint8Array): sha256/sha256Hasher, fromHex/toHex, and new Ecc helpers pubkeyMul / pubkeyCombine / uncompressPk exposed via ecash-lib-wasm (local secp256k1).

Add tests to CI

We do not exactly match the Cash Fusion test vectors from Electrum but we cover the same bases

Test Plan

pnpm --filter alp-fusion test
pnpm --filter alp-fusion run build

Diff Detail

Repository
rABC Bitcoin ABC
Lint
Lint Not Applicable
Unit
Tests Not Applicable

Event Timeline

There are a very large number of changes, so older changes are hidden. Show Older Changes
apps/alp-fusion/test/constants.test.ts
14 ↗(On Diff #60360)

stop testing constants

modules/ecash-lib/README.md
120 ↗(On Diff #60360)

don't add release notes if there is no release...

modules/ecash-lib/src/blindSchnorr.ts
73 ↗(On Diff #60360)

this is misplaced

79 ↗(On Diff #60360)

Since you can't sign twice you'd better remove the signed flag, make k optional, and zero it (explicitly so it doesn't leak in memory) before destroying it. Then if k is undefined, you know you can't sign anymore.

modules/ecash-lib/src/eccScalar.ts
20 ↗(On Diff #60360)

this seems overkill to use an intermediate string conversion to work on numbers

27–28 ↗(On Diff #60360)

Don't do that, it's adding bias. You trust your random generator is uniform, so keep that property: check the value is within range and if not repeat the whole process, summoning the RNG again

modules/ecash-lib/src/io/array.ts
6 ↗(On Diff #60360)

one option for non constant time operations is to suffix with _var. That's how it's done internally in libsecp256k1.
But really it's trivial to make this constant-time for this one.

Edit: after asking AI it's not possible to have strict constant time functions in typescript (e.g. there is a GC). Also TIL there is a Node crypto.timingSafeEqual() function if you don't need it in the browser

33 ↗(On Diff #60360)

that's named scalarToBytes the other way around, it's inconsistently named

44 ↗(On Diff #60360)

This can throw ?

49 ↗(On Diff #60360)

This can throw?

modules/ecash-lib/src/pedersen.ts
16–17 ↗(On Diff #60360)

same as above, consider using Uint8Array and avoid back and forth conversions

42 ↗(On Diff #60360)

style nit, but not sure if that works

62 ↗(On Diff #60360)

It's useless as per your above check

66 ↗(On Diff #60360)

don't do that! Electrum implementation explicitely tells you why it's bad. you should port their amount blinding method instead

89 ↗(On Diff #60360)

Style nit: you could use reduce

This revision now requires changes to proceed.Thu, Jul 23, 10:06

Address review: Electrum amount blinding; wasm seckeyMul/seckeyNegate for blind Schnorr; Uint8Array secrets with zeroing; rejection-sampled randomScalar; rename modCurveOrder/bytesToScalar; drop constants tests and unreleased changelog.

Done — updated this diff with the review fixes. Sorry about the stray __draft-probe-do-not-keep__ comment from API probing; please ignore it.

Inline replies:

  • blindSchnorr signed / zero k: Removed the flag. k is now Uint8Array | undefined; length checks run first; after one sign(), k is zeroed and cleared.
  • signed = true misplaced: Fixed (validate before consuming k).
  • bigint ↔ bytes on secrets: Added wasm seckeyMul / seckeyNegate; blind signing uses seckeyMul + seckeyAdd instead of JS bigint for secret scalars.
  • blinding factors as Uint8Array: a / b / e are byte scalars; zeroed after use.
  • Pedersen amount=0 shortcut: Ported Electrum amount blinding: P = (a-k)*H + k*(H+G).
  • useless modN(k) after range check: Removed.
  • Pedersen nonce/amount as bytes: Nonce is Uint8Array; EC ops use byte scalars (amount still accepted as bigint at the API for ergonomics, converted once).
  • addCommitmentPoints reduce: Done.
  • empty catch style: Split compressed/uncompressed parse paths with explicit errors.
  • u32ToBe / bigintToU64Be throw?: Now validate ranges and throw clear errors.
  • bytesToBigint vs scalarToBytes naming: Renamed to bytesToScalar (pairs with scalarToBytes).
  • equalBytes CT / _var: Compare loop has no early exit; noted JS cannot give strict CT. Left the name as equalBytes (not _var) since it's the best-effort form.
  • randomScalar bias: Rejection sampling in [1, n) (no % n on RNG output).
  • scalarToBytes hex string: Direct big-endian byte write.
  • modN name: Renamed to modCurveOrder.
  • stop testing constants: Removed constants.test.ts and the CURVE_ORDER equality test.
  • unreleased changelog: Removed (no release notes without a release).

General note on secret zeroing: applied on the obvious paths (BlindSigner.k, blinding factors, Pedersen intermediates). Happy to extend further if you want a stricter pass.

The build failed due to an unexpected infrastructure outage. The administrators have been notified to investigate. Sorry for the inconvenience.
The build failed due to an unexpected infrastructure outage. The administrators have been notified to investigate. Sorry for the inconvenience.
bytesofman added inline comments.
modules/ecash-lib/src/eccScalar.ts
13 ↗(On Diff #60360)

x

Clarify blindSchnorr file comment: describe Uint8Array + wasm ECC scalar ops without negative phrasing.

The build failed due to an unexpected infrastructure outage. The administrators have been notified to investigate. Sorry for the inconvenience.
The build failed due to an unexpected infrastructure outage. The administrators have been notified to investigate. Sorry for the inconvenience.

Tail of the build log:

  └────────────────────────────────────────────────────────────────────────────────────────────────┘


================================================================================

  (Run Finished)


       Spec                                              Tests  Passing  Failing  Pending  Skipped  
  ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
  │ ✔  bip21-payment-request.cy.ts              00:09        6        6        -        -        - │
  ├────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ ✔  history.cy.ts                            00:15       10       10        -        -        - │
  ├────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ ✔  main.cy.ts                               00:10        7        7        -        -        - │
  ├────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ ✔  mnemonic.cy.ts                           00:17        7        7        -        -        - │
  ├────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ ✖  send.cy.ts                               01:52       24       23        1        -        - │
  ├────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ ✔  shared-image-qr.cy.ts                    00:06        5        5        -        -        - │
  ├────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ ✔  transitional-balance.cy.ts               00:16        9        9        -        -        - │
  └────────────────────────────────────────────────────────────────────────────────────────────────┘
    ✖  1 of 7 failed (14%)                      03:09       68       67        1        -        -  

--------------------------------------------------------------------------------

  Debug faster with full visibility.

  Record to Cypress Cloud and get instant access to full test details and replays.
  Inspect the DOM, network events, and console logs exactly as they ran in CI.

  >> https://on.cypress.io/cloud-get-started

--------------------------------------------------------------------------------
 ELIFECYCLE  Command failed with exit code 1.
<i> [webpack-dev-server] Gracefully shutting down. To force exit, press ^C again. Please wait...
Error: Command failed with exit code 1: pnpm run cypress:run
    at makeError (/work/node_modules/.pnpm/execa@5.1.1/node_modules/execa/lib/error.js:60:11)
    at handlePromise (/work/node_modules/.pnpm/execa@5.1.1/node_modules/execa/index.js:118:26)
    at process.processTicksAndRejections (node:internal/process/task_queues:105:5) {
  shortMessage: 'Command failed with exit code 1: pnpm run cypress:run',
  command: 'pnpm run cypress:run',
  escapedCommand: '"pnpm run cypress:run"',
  exitCode: 1,
  signal: undefined,
  signalDescription: undefined,
  stdout: undefined,
  stderr: undefined,
  failed: true,
  timedOut: false,
  isCanceled: false,
  killed: false
}
/work/apps/marlin-wallet/web:
 ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL  marlin-wallet-web@2.2.1 test:e2e: `start-server-and-test 'pnpm run dev' http://localhost:3000 'pnpm run cypress:run'`
Exit status 1
 ELIFECYCLE  Command failed.
Build marlin-wallet-e2e-tests failed with exit code 1

Tail of the build log:

  └────────────────────────────────────────────────────────────────────────────────────────────────┘


================================================================================

  (Run Finished)


       Spec                                              Tests  Passing  Failing  Pending  Skipped  
  ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
  │ ✔  bip21-payment-request.cy.ts              00:09        6        6        -        -        - │
  ├────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ ✔  history.cy.ts                            00:15       10       10        -        -        - │
  ├────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ ✔  main.cy.ts                               00:10        7        7        -        -        - │
  ├────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ ✔  mnemonic.cy.ts                           00:17        7        7        -        -        - │
  ├────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ ✖  send.cy.ts                               01:52       24       23        1        -        - │
  ├────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ ✔  shared-image-qr.cy.ts                    00:06        5        5        -        -        - │
  ├────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ ✔  transitional-balance.cy.ts               00:16        9        9        -        -        - │
  └────────────────────────────────────────────────────────────────────────────────────────────────┘
    ✖  1 of 7 failed (14%)                      03:09       68       67        1        -        -  

--------------------------------------------------------------------------------

  Debug faster with full visibility.

  Record to Cypress Cloud and get instant access to full test details and replays.
  Inspect the DOM, network events, and console logs exactly as they ran in CI.

  >> https://on.cypress.io/cloud-get-started

--------------------------------------------------------------------------------
 ELIFECYCLE  Command failed with exit code 1.
<i> [webpack-dev-server] Gracefully shutting down. To force exit, press ^C again. Please wait...
Error: Command failed with exit code 1: pnpm run cypress:run
    at makeError (/work/node_modules/.pnpm/execa@5.1.1/node_modules/execa/lib/error.js:60:11)
    at handlePromise (/work/node_modules/.pnpm/execa@5.1.1/node_modules/execa/index.js:118:26)
    at process.processTicksAndRejections (node:internal/process/task_queues:105:5) {
  shortMessage: 'Command failed with exit code 1: pnpm run cypress:run',
  command: 'pnpm run cypress:run',
  escapedCommand: '"pnpm run cypress:run"',
  exitCode: 1,
  signal: undefined,
  signalDescription: undefined,
  stdout: undefined,
  stderr: undefined,
  failed: true,
  timedOut: false,
  isCanceled: false,
  killed: false
}
/work/apps/marlin-wallet/web:
 ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL  marlin-wallet-web@2.2.1 test:e2e: `start-server-and-test 'pnpm run dev' http://localhost:3000 'pnpm run cypress:run'`
Exit status 1
 ELIFECYCLE  Command failed.
Build marlin-wallet-e2e-tests failed with exit code 1

D20379

Build Bitcoin ABC Diffs / Diff Testing (ai-review) passed.
CodeRabbit Review

Diff : committed changes only
Compare : HEAD → master
Directory : work
────────────────────────────────────────

(\(\
(• .•) The object-oriented model makes it easy to build up programs by accretion. What this often means, in practice, is that it provides a structured way to write spaghetti code. - Paul Graham

────────────────────────────────────────────────────────────────────────

minor [Data Integrity & Integration]
→ ]8;;vscode://file//work/apps/alp-fusion/src/protocol/hash.ts:75apps/alp-fusion/src/protocol/hash.ts:75-80]8;;

Reject malformed pool keys instead of truncating them.

key.split(':') ignores fields after the second, so
tokenHex:10000:extra is parsed as tokenHex:10000. Require exactly two
fields before decoding to prevent malformed persisted or input keys from
resolving to an unintended pool.

────────────────────────────────────────────────────────────────────────

major [Functional Correctness]
→ ]8;;vscode://file//work/apps/alp-fusion/test/hash.test.ts:29apps/alp-fusion/test/hash.test.ts:29-49]8;;

Add golden vectors for protocol hashes.

The test claims hash stability but checks only length and that
round/session hashes differ. Changes to domain tags, field ordering, byte
order, or length framing would still pass. Assert exact reference hex
digests for the session and round hashes, plus a fixed listHash vector.

────────────────────────────────────────────────────────────────────────

major [Functional Correctness]
→ ]8;;vscode://file//work/apps/alp-fusion/test/hash.test.ts:51apps/alp-fusion/test/hash.test.ts:51-63]8;;

Use asymmetric token IDs and assert the serialized key.

'cc'.repeat(32) and 'ab'.repeat(32) are invariant under byte reversal,
so these tests cannot detect an endian regression. Use a non-repeating
32-byte ID and assert the exact pool-key text and encoded bytes.

────────────────────────────────────────
Review complete
3 findings ✔

Major 2
Minor 1

23 files reviewed:

  • apps/alp-fusion/.gitignore
  • apps/alp-fusion/.mocharc.cjs
  • apps/alp-fusion/README.md
  • apps/alp-fusion/package.json
  • apps/alp-fusion/src/protocol/constants.ts
  • apps/alp-fusion/src/protocol/hash.ts
  • apps/alp-fusion/test/hash.test.ts
  • apps/alp-fusion/test/pedersenDomains.test.ts
  • apps/alp-fusion/tsconfig.json
  • contrib/teamcity/build-configurations.yml

... and 13 more files
────────────────────────────────────────

Print all AI prompts: coderabbit review --show-prompts

Fabien requested changes to this revision.Thu, Jul 23, 13:54
Fabien added inline comments.
modules/ecash-lib/src/blindSchnorr.ts
111–155 ↗(On Diff #60542)

Note your solution is missing the zeroBytes(a) in the happy path

169 ↗(On Diff #60542)

if the implementation is correct then the comment above is wrong. Otherwise it's concerning the tests pass because you only computes if c == -1

172 ↗(On Diff #60542)

so -snew is safe but snew is not

modules/ecash-lib/src/pedersen.ts
45 ↗(On Diff #60542)

this changes everything lol

108 ↗(On Diff #60542)

this is wrong because it's calculation on the curve, so there is a modulus involves. This means you can have a != k but (a - k) % order == 0

109 ↗(On Diff #60542)

Note: this has a 1/2**256 chance to happen

115 ↗(On Diff #60542)

please check (and comment accordingly) if this applies the modulo on the result

164–167 ↗(On Diff #60542)
This revision now requires changes to proceed.Thu, Jul 23, 13:54

Address review: modular (a-k) path for Pedersen blinding; clarify HG equivalence and seckeyAdd mod n; zero blind-Schnorr temps on both c signs; stricter poolKey parsing and golden hash vectors. Rebase onto master.

Build Bitcoin ABC Diffs / Diff Testing (ai-review) passed.
CodeRabbit Review

Diff : committed changes only
Compare : HEAD → master
Directory : work
────────────────────────────────────────

(\(\
(• .•) Sending Skynet back to the drawing board.

────────────────────────────────────────────────────────────────────────

major [Data Integrity & Integration]
→ ]8;;vscode://file//work/apps/alp-fusion/src/protocol/hash.ts:71apps/alp-fusion/src/protocol/hash.ts:71-95]8;;

Validate pool-key components at the boundary.

poolKey, parsePoolKey, and the token-ID helpers accept
arbitrary-length IDs and negative or oversized tiers. This conflicts with
the 32-byte txid-shaped IDs used by the tests and the u64 atomTier
encoding used by calcSessionHash, allowing invalid pool state to enter
downstream logic. Enforce a 32-byte token ID, 0 <= atomTier <= 2^64 - 1,
and canonical decimal key text in both directions; add regression tests
for these cases.

────────────────────────────────────────────────────────────────────────

major [Functional Correctness]
→ ]8;;vscode://file//work/modules/ecash-lib/src/pedersen.ts:88modules/ecash-lib/src/pedersen.ts:88-102]8;;

Support zero aggregate nonces.

addScalars(k, n-k) throws because seckeyAdd rejects a zero result, and
verifyCommitmentSum would subsequently reject that zero nonce in
commit. A zero aggregate nonce with a nonzero total amount is a valid
Pedersen opening (C = amount·H).

Return canonical zero from scalar aggregation and handle the zero-nonce
verification path; add a regression test for complementary nonces.






Also applies to: 160-176

────────────────────────────────────────────────────────────────────────

major [Stability & Availability]
→ ]8;;vscode://file//work/modules/ecash-lib/src/blindSchnorr.ts:111modules/ecash-lib/src/blindSchnorr.ts:111-152]8;;

Retry loop's catch-all masks deterministic input errors as probabilistic
failures.

The try/catch at lines 114-147 treats every exception from
pubkeyMul/pubkeyCombine/pubkeyAdd/uncompressPk the same as a
jacobi-symbol retry case. If pubkey or R is malformed (e.g. wrong
length, invalid point), every one of the 32 attempts will fail identically
for the same deterministic reason — the retry loop wastes compute and then
surfaces a misleading "failed to find valid R' after retries" error
instead of the actual root cause (invalid input).

Consider validating pubkey/R up front (fail fast with a clear message)
and only catching the specific rare point-at-infinity/degenerate cases
inside the loop, or at minimum propagate/wrap the last caught error when
retries are exhausted.





💡 Suggested direction

     constructor(pubkey: Uint8Array, R: Uint8Array, messageHash: Uint8Array) {
         if (messageHash.length !== 32) {
             throw new Error('message_hash must be 32 bytes');
         }
+        if (pubkey.length !== 33 || R.length !== 33) {
+            throw new Error('pubkey and R must be 33-byte compressed points');
+        }
         this.messageHash = new Uint8Array(messageHash);
         this.pubkeyCompressed = new Uint8Array(pubkey);

         // R' = R + a*G + b*P; c = jacobi(R'.y) ∈ {±1}.
+        let lastError: unknown;
         for (let attempt = 0; attempt < 32; attempt++) {
             ...
             } catch {
+                lastError = err;
                 zeroBytes(a);
                 zeroBytes(b);
             }
         }
         throw new Error(
-            "BlindSignatureRequest: failed to find valid R' after retries",
+            `BlindSignatureRequest: failed to find valid R' after retries: ${lastError}`,
         );
     }

────────────────────────────────────────
Review complete
3 findings ✔

Major 3

23 files reviewed:

  • apps/alp-fusion/.gitignore
  • apps/alp-fusion/.mocharc.cjs
  • apps/alp-fusion/README.md
  • apps/alp-fusion/package.json
  • apps/alp-fusion/src/protocol/constants.ts
  • apps/alp-fusion/src/protocol/hash.ts
  • apps/alp-fusion/test/hash.test.ts
  • apps/alp-fusion/test/pedersenDomains.test.ts
  • apps/alp-fusion/tsconfig.json
  • contrib/teamcity/build-configurations.yml

... and 13 more files
────────────────────────────────────────

Print all AI prompts: coderabbit review --show-prompts

Fabien requested changes to this revision.Thu, Jul 23, 19:59

The 3 reviews from the rabbit are valid points (though the second one is statistically basically impossible to achieve, but better handle it properly)

modules/ecash-lib/src/blindSchnorr.ts
172 ↗(On Diff #60542)

so -snew is safe but snew is not

You got my comment wrong. You're building the signature, there is no secret anymore at this point

modules/ecash-lib/src/pedersen.ts
141 ↗(On Diff #60547)

Can't you initialize point to kHG and avoid nullability ?

175 ↗(On Diff #60547)

As noted by the rabbit, this can throw if a + b = 0

This revision now requires changes to proceed.Thu, Jul 23, 19:59

Address review: zero-nonce Pedersen aggregation; validate blind-Schnorr inputs before retry; stop zeroing public s'; enforce pool-key tokenId/tier bounds.

Build Bitcoin ABC Diffs / Diff Testing (ai-review) passed.
CodeRabbit Review

Diff : committed changes only
Compare : HEAD → master
Directory : work
────────────────────────────────────────

(\(\
(• .•) I wish to make a complaint.

────────────────────────────────────────────────────────────────────────

minor [Functional Correctness]
→ ]8;;vscode://file//work/apps/alp-fusion/test/pedersenDomains.test.ts:13apps/alp-fusion/test/pedersenDomains.test.ts:13-25]8;;

Add a same-domain positive control.

This negative-only test also passes if commit and verifyCommitmentSum
disagree for unrelated reasons. Verify the same witness succeeds with
satsSetup before asserting that it fails with tokenSetup.






Proposed test addition

         const c = satsSetup.commit(42n);
+        expect(
+            verifyCommitmentSum(
+                satsSetup,
+                [c.pointUncompressed],
+                42n,
+                c.nonce,
+            ),
+        ).to.equal(true);
         expect(
             verifyCommitmentSum(
                 tokenSetup,

────────────────────────────────────────────────────────────────────────

major [Security & Privacy]
→ ]8;;vscode://file//work/modules/ecash-lib/src/pedersen.ts:64modules/ecash-lib/src/pedersen.ts:64-89]8;;

Do not allow caller-selected known generator relations.

Line 85 rejects only H = -G. H = G passes, making `C = (nonce +
amount)G`, so equivalent openings can be constructed and binding is lost.
A public constructor cannot prove an unknown discrete-log relation;
require an audited, domain-separated hash-to-curve derivation for H
rather than arbitrary caller input, and add an H = G regression case.

────────────────────────────────────────────────────────────────────────

major [Functional Correctness]
→ ]8;;vscode://file//work/modules/ecash-lib/src/pedersen.ts:169modules/ecash-lib/src/pedersen.ts:169-190]8;;

Handle valid identity aggregates explicitly.

A commitment sum with total amount and nonce both zero is the group
identity. addCommitmentPoints cannot serialize that result, while Lines
114-119 make setup.commit(0n, zeroNonce) throw. Consequently,
verifyCommitmentSum throws instead of returning a boolean for a valid
balanced aggregate. Represent identity separately from malformed points,
or explicitly exclude this case from the API and its callers.

────────────────────────────────────────────────────────────────────────

minor [Data Integrity & Integration]
→ ]8;;vscode://file//work/modules/ecash-lib/src/blindSchnorr.ts:71modules/ecash-lib/src/blindSchnorr.ts:71-80]8;;

getR()/R expose the live internal buffer — no defensive copy.

Unlike getRequest() (line 171), which returns new Uint8Array(this.e),
getR() returns this.R directly, and the readonly modifier on line 71
only prevents reassigning the reference, not mutating its bytes at
runtime. A caller that mutates the returned array corrupts the signer's
own nonce point permanently.





🛡️ Proposed fix

     getR(): Uint8Array {
-        return this.R;
+        return new Uint8Array(this.R);
     }

────────────────────────────────────────
Review complete
4 findings ✔

Major 2
Minor 2

23 files reviewed:

  • apps/alp-fusion/.gitignore
  • apps/alp-fusion/.mocharc.cjs
  • apps/alp-fusion/README.md
  • apps/alp-fusion/package.json
  • apps/alp-fusion/src/protocol/constants.ts
  • apps/alp-fusion/src/protocol/hash.ts
  • apps/alp-fusion/test/hash.test.ts
  • apps/alp-fusion/test/pedersenDomains.test.ts
  • apps/alp-fusion/tsconfig.json
  • contrib/teamcity/build-configurations.yml

... and 13 more files
────────────────────────────────────────

Print all AI prompts: coderabbit review --show-prompts

Address CodeRabbit: reject H=G; identity (0,0) verify; defensive getR copy; same-domain Pedersen positive control. Skip hash-to-curve H derivation (Electrum NUMS API).

Build Bitcoin ABC Diffs / Diff Testing (ai-review) passed.
CodeRabbit Review

Diff : committed changes only
Compare : HEAD → master
Directory : work
────────────────────────────────────────

(\(\
(• .•) These comments are outdated enough to vote.

────────────────────────────────────────────────────────────────────────

minor [Performance & Scalability]
→ ]8;;vscode://file//work/apps/alp-fusion/src/protocol/hash.ts:112apps/alp-fusion/src/protocol/hash.ts:112-118]8;;

Bound the tier text before parsing it as BigInt.

Line 117 parses arbitrarily long digit strings that pass the regex,
allowing needless CPU and memory work before the uint64-range check.
Reject values longer than 20 digits (and lexically above 2^64 - 1)
first.






Proposed fix

     if (!ATOM_TIER_DEC_RE.test(tierStr)) {
         throw new Error(
             'parsePoolKey: atomTier must be canonical decimal (no leading zeros)',
         );
     }
+    if (
+        tierStr.length > 20 ||
+        (tierStr.length === 20 && tierStr > U64_MAX.toString())
+    ) {
+        throw new Error('parsePoolKey: atomTier exceeds uint64 range');
+    }
     const atomTier = BigInt(tierStr);
-    assertAtomTier(atomTier);
     return { tokenId: fromHex(hex), atomTier };

────────────────────────────────────────────────────────────────────────

major [Functional Correctness]
→ ]8;;vscode://file//work/contrib/teamcity/build-configurations.yml:935contrib/teamcity/build-configurations.yml:935-954]8;;

Trigger ALP Fusion tests when chronik-client changes.

The ecash-lib build configuration depends on chronik-client, but this
job neither watches modules/chronik-client/ nor declares the
corresponding build dependency. A change there can leave this test job
green without exercising the updated dependency.


Suggested fix

         - modules/ecash-lib/
+        - modules/chronik-client/
         - modules/ecash-lib-wasm/
...
         depends:
             - ecashaddrjs
             - b58-ts
             - ecash-lib-wasm
+            - chronik-client
             - ecash-lib

────────────────────────────────────────
Review complete
2 findings ✔

Major 1
Minor 1

23 files reviewed:

  • apps/alp-fusion/.gitignore
  • apps/alp-fusion/.mocharc.cjs
  • apps/alp-fusion/README.md
  • apps/alp-fusion/package.json
  • apps/alp-fusion/src/protocol/constants.ts
  • apps/alp-fusion/src/protocol/hash.ts
  • apps/alp-fusion/test/hash.test.ts
  • apps/alp-fusion/test/pedersenDomains.test.ts
  • apps/alp-fusion/tsconfig.json
  • contrib/teamcity/build-configurations.yml

... and 13 more files
────────────────────────────────────────

Print all AI prompts: coderabbit review --show-prompts

Address CodeRabbit: bound pool-key tier digit length before BigInt; watch/depend on chronik-client for alp-fusion-tests.

Tail of the build log:

/work /work/abc-ci-builds/ai-review
Connecting to CodeRabbit... 0s elapsed
Preparing review... 2s elapsed

  ✗ Review limit reached

  Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).
  You can wait 5 seconds for the limit to reset.
  https://app.coderabbit.ai/settings/billing?tab=usage&orgId=79839cd4-7849-4bd5-8d6d-5ecaf453356e

Build ai-review failed with exit code 1

Build Bitcoin ABC Diffs / Diff Testing (ai-review) passed.
CodeRabbit Review

Diff : committed changes only
Compare : HEAD → master
Directory : work
────────────────────────────────────────

(\(\
(• .•) Review complete: I laughed, I cried, I filed issues.

────────────────────────────────────────────────────────────────────────

minor [Security & Privacy]
→ ]8;;vscode://file//work/apps/alp-fusion/README.md:56apps/alp-fusion/README.md:56]8;;

Clarify that linkage—not amounts or addresses—is obscured.

The protocol requirements establish input↔output unlinkability, but do not
hide the public transaction’s amounts or addresses. Please make the
wording explicit to avoid overstating the privacy guarantee.


Suggested wording

-| What stays visible | That XEC moved in a large fusion                                             | That a given `tokenId` moved in a fusion; amounts↔addresses obscured inside the round |
+| What stays visible | That XEC moved in a large fusion                                             | That a given `tokenId` moved in a fusion; input↔output linkage is obscured inside the round |

────────────────────────────────────────────────────────────────────────

minor [Security & Privacy]
→ ]8;;vscode://file//work/modules/ecash-lib/src/blindSchnorr.ts:171modules/ecash-lib/src/blindSchnorr.ts:171-204]8;;

Zero the intermediate s+a secret material on the schnorrVerify failure
path.

sPlusA/sFinal (built from secret a combined with the
attacker/server-supplied public sbytes) is zeroized only in the `c ===
-1 branch, and only ever intentionally exposed via the returned sig` on
success. If check is true and ecc.schnorrVerify throws (exactly the
"bastardized s response" case exercised in tests), the function throws
without zeroing sFinal/sig, leaving array data that — combined with
the caller-supplied sbytes — trivially reveals the blinding secret a
(a = sFinal - sbytes mod n) lingering in memory. This undermines the
defense-in-depth zeroing pattern established everywhere else in this file.


🔒 Proposed fix

             const sig = concatBytes(this.Rxnew, sFinal);
             if (check) {
-                ecc.schnorrVerify(sig, this.messageHash, this.pubkeyCompressed);
+                try {
+                    ecc.schnorrVerify(sig, this.messageHash, this.pubkeyCompressed);
+                } catch (err) {
+                    zeroBytes(sFinal);
+                    zeroBytes(sig);
+                    throw err;
+                }
             }
             return sig;

────────────────────────────────────────
Review complete
2 findings ✔

Minor 2

23 files reviewed:

  • apps/alp-fusion/.gitignore
  • apps/alp-fusion/.mocharc.cjs
  • apps/alp-fusion/README.md
  • apps/alp-fusion/package.json
  • apps/alp-fusion/src/protocol/constants.ts
  • apps/alp-fusion/src/protocol/hash.ts
  • apps/alp-fusion/test/hash.test.ts
  • apps/alp-fusion/test/pedersenDomains.test.ts
  • apps/alp-fusion/tsconfig.json
  • contrib/teamcity/build-configurations.yml

... and 13 more files
────────────────────────────────────────

Print all AI prompts: coderabbit review --show-prompts

Fabien requested changes to this revision.Mon, Jul 27, 12:46

It's getting there

modules/ecash-lib/src/blindSchnorr.ts
137 ↗(On Diff #60574)

I don't understand this change, this seems strictly worst than the previous implementation

modules/ecash-lib/src/pedersen.ts
116–118 ↗(On Diff #60574)

I think this is a clear code smell: randomScalar doesn't return a scalar. Either rename it or make it return a bigint

131 ↗(On Diff #60574)

Please add this is very unlikely

250 ↗(On Diff #60574)

This is a bad change (and a bad name btw, you're mixing types for scalar). The previous implementation was much better, only you need to make sure you handle the function potentially throwing

This revision now requires changes to proceed.Mon, Jul 27, 12:46

Address review: restore seckeyAdd-based nonce sum (handle a+b≡0); rename randomScalar→randomScalarBytes; note zero-nonce ~2^-256; restore full-attempt try/catch in BlindSignatureRequest after input validation.

Build Bitcoin ABC Diffs / Diff Testing (ai-review) passed.
CodeRabbit Review

Diff : committed changes only
Compare : HEAD → master
Directory : work
────────────────────────────────────────

(\(\
(• .•) My other transformer is Optimus Prime.

────────────────────────────────────────────────────────────────────────

major [Data Integrity & Integration]
→ ]8;;vscode://file//work/modules/ecash-lib/src/pedersen.ts:247modules/ecash-lib/src/pedersen.ts:247-254]8;;

Catch-all masks malformed-scalar errors as "sum ≡ 0".

ecc.seckeyAdd can throw for reasons other than a genuine zero result
(e.g. wrong-length or out-of-range input), but the bare catch here
always returns a canonical all-zero 32-byte array. Since this is an
exported function usable outside this file's tests, a malformed a/b
would silently become "zero" instead of raising an error, corrupting any
downstream nonce/scalar aggregation.


🛡️ Proposed fix

 export function addScalars(a: Uint8Array, b: Uint8Array): Uint8Array {
+    if (a.length !== 32 || b.length !== 32) {
+        throw new Error('Scalars must be 32 bytes');
+    }
     try {
         return ecc.seckeyAdd(a, b);
     } catch {
         // (a + b) ≡ 0 (mod n)
         return new Uint8Array(32);
     }
 }

────────────────────────────────────────────────────────────────────────

major [Security & Privacy]
→ ]8;;vscode://file//work/modules/ecash-lib/src/pedersen.ts:188modules/ecash-lib/src/pedersen.ts:188-204]8;;

Catch-all swallows malformed-input errors as "point at infinity".

Any exception from pubkeyCombine (invalid point length/format, not just
a genuine sum-to-infinity) is converted into ResultAtInfinity. Since
verifyCommitmentSum treats ResultAtInfinity as "the aggregate is the
identity" and returns true when the caller also claims `(amount, nonce)
= (0, 0)` (Lines 223-228), malformed/garbage commitment points combined
with a (0,0) claim could incorrectly verify as valid. There's also no
upfront validation that each point is 33 or 65 bytes before the
length-based compress branch.


🛡️ Proposed fix

 export function addCommitmentPoints(points: Uint8Array[]): Uint8Array {
     if (points.length === 0) {
         throw new Error('No points to add');
     }
+    for (const p of points) {
+        if (p.length !== 33 && p.length !== 65) {
+            throw new Error('Invalid commitment point length');
+        }
+    }
     const compressed = points.map(p =>
         p.length === 65 ? ecc.compressPk(p) : p,
     );

────────────────────────────────────────────────────────────────────────

minor [Functional Correctness]
→ ]8;;vscode://file//work/modules/ecash-lib/src/pedersen.ts:211modules/ecash-lib/src/pedersen.ts:211-241]8;;

Validate totalNonce length before use.

commit() strictly validates nonce.length === 32 (Lines 119-121), but
verifyCommitmentSum passes totalNonce straight into bytesToScalar
without any length check. For consistency and to avoid surprising behavior
with malformed input, add the same guard here.


🛡️ Proposed fix

 export function verifyCommitmentSum(
     setup: PedersenSetup,
     commitments: Uint8Array[],
     totalAmount: bigint,
     totalNonce: Uint8Array,
 ): boolean {
+    if (totalNonce.length !== 32) {
+        return false;
+    }
     const amountMod = modCurveOrder(totalAmount);

────────────────────────────────────────
Review complete
3 findings ✔

Major 2
Minor 1

23 files reviewed:

  • apps/alp-fusion/.gitignore
  • apps/alp-fusion/.mocharc.cjs
  • apps/alp-fusion/README.md
  • apps/alp-fusion/package.json
  • apps/alp-fusion/src/protocol/constants.ts
  • apps/alp-fusion/src/protocol/hash.ts
  • apps/alp-fusion/test/hash.test.ts
  • apps/alp-fusion/test/pedersenDomains.test.ts
  • apps/alp-fusion/tsconfig.json
  • contrib/teamcity/build-configurations.yml

... and 13 more files
────────────────────────────────────────

Print all AI prompts: coderabbit review --show-prompts

Fabien requested changes to this revision.Tue, Jul 28, 09:45
Fabien added inline comments.
modules/ecash-lib/src/pedersen.ts
211 ↗(On Diff #60585)

I disagree with the rabbit regarding input checking on addScalars (this is an exported function for the sake of tests).
This one otoh is a public API and should be hardened. The fact that ill-formed commitments can lead to the function returning true is an error and this should be tested.

This revision now requires changes to proceed.Tue, Jul 28, 09:45

Address review: harden verifyCommitmentSum against ill-formed commitments (validate points before combine; reject bad nonce length); add regression test that garbage+(0,0) never verifies. Leave addScalars unchecked (test helper).

Build Bitcoin ABC Diffs / Diff Testing (ai-review) passed.
CodeRabbit Review

Diff : committed changes only
Compare : HEAD → master
Directory : work
────────────────────────────────────────

(\(\
(• .•) X marks the bug.

────────────────────────────────────────────────────────────────────────

major [Functional Correctness]
→ ]8;;vscode://file//work/apps/alp-fusion/src/protocol/hash.ts:19apps/alp-fusion/src/protocol/hash.ts:19-33]8;;

Reject non-canonical numeric hash inputs.
Math.floor can alias distinct timestamps before hashing, and
covertPort lacks a local range check. Require non-negative safe integers
(and a uint32 bound for the port), or switch timestamp inputs to
bigint for full uint64 support.

────────────────────────────────────────────────────────────────────────

major [Functional Correctness]
→ ]8;;vscode://file//work/modules/ecash-lib/src/pedersen.ts:273modules/ecash-lib/src/pedersen.ts:273-280]8;;

Overly broad catch can silently mask malformed input.

Unlike the internal uses of this same "catch = zero-result" pattern (e.g.
line 158, where operands are already validated in-range), addScalars is
a public API that accepts arbitrary caller-supplied Uint8Arrays with no
length/range validation before calling ecc.seckeyAdd. Any failure —
wrong length, out-of-range scalar, or the genuine (a+b)≡0 case — is
indistinguishable here and all silently become new Uint8Array(32). A
caller passing a malformed nonce would get a plausible-looking but wrong
zero result instead of an error.





🐛 Proposed fix

 export function addScalars(a: Uint8Array, b: Uint8Array): Uint8Array {
+    if (a.length !== 32 || b.length !== 32) {
+        throw new Error('addScalars: inputs must be 32 bytes');
+    }
     try {
         return ecc.seckeyAdd(a, b);
     } catch {
         // (a + b) ≡ 0 (mod n)
         return new Uint8Array(32);
     }
 }

────────────────────────────────────────
Review complete
2 findings ✔

Major 2

23 files reviewed:

  • apps/alp-fusion/.gitignore
  • apps/alp-fusion/.mocharc.cjs
  • apps/alp-fusion/README.md
  • apps/alp-fusion/package.json
  • apps/alp-fusion/src/protocol/constants.ts
  • apps/alp-fusion/src/protocol/hash.ts
  • apps/alp-fusion/test/hash.test.ts
  • apps/alp-fusion/test/pedersenDomains.test.ts
  • apps/alp-fusion/tsconfig.json
  • contrib/teamcity/build-configurations.yml

... and 13 more files
────────────────────────────────────────

Print all AI prompts: coderabbit review --show-prompts

This revision is now accepted and ready to land.Tue, Jul 28, 12:38