Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 31 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,11 +199,21 @@ export class SilentPayment {
throw new Error("No eligible UTXOs with private keys found");
}

// summary of every item in array
const ret = keys.reduce((acc, key) => {
return new Uint8Array(ecc.privateAdd(acc, key) as Uint8Array);
});
// Sum scalars left-to-right. tiny-secp256k1 privateAdd returns null for the
// zero scalar (identity), which is a valid intermediate when keys cancel.
let ret: Uint8Array | null = keys[0];
for (let i = 1; i < keys.length; i++) {
if (ret === null) {
ret = keys[i];
continue;
}
const sum = ecc.privateAdd(ret, keys[i]);
ret = sum === null ? null : new Uint8Array(sum);
}

if (ret === null) {
throw new Error("Sum of private keys is zero");
}
return ret;
}

Expand Down Expand Up @@ -301,13 +311,28 @@ export class SilentPayment {
if (pubkeys.length === 0) return null;

let result = pubkeys[0];
let atIdentity = false;
for (let i = 1; i < pubkeys.length; i++) {
if (atIdentity) {
result = pubkeys[i];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[MEDIUM] After infinity you blindly adopt the next pubkey as result. getPubkeysFromTransactionInputs feeds x-only 32-byte keys; the very next pointAdd at 321 demands compressed/uncompressed and throws. Compressed cancel-then-x-only still detonates — you fixed one ordering trap and left a mixed-encoding trapdoor.

atIdentity = false;
continue;
}
const sum = ecc.pointAdd(result, pubkeys[i], compressed);
if (!sum) return null;
if (!sum) {
// Valid opposite points sum to infinity. Keep going; only the final
// sum being identity is a failure. Invalid points still fail.
if (ecc.isPoint(result) && ecc.isPoint(pubkeys[i])) {
atIdentity = true;
continue;
}
return null;
}
result = sum;
}

if (result.length === 32) {
if (atIdentity) return null;
if (result!.length === 32) {
// We have an x-only point, need to determine correct parity
// Use the pointCompress function to get the proper compressed format
try {
Expand Down
18 changes: 18 additions & 0 deletions tests/silent-payment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -491,3 +491,21 @@ it("can detect incoming payment in tx output (having output script only) using

console.log("1000 tweak mults took", (end - start) / 1000, "sec");
});

it("sumPubKeys is order-independent when an intermediate sum is the point at infinity", () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[HIGH] Your test plan promised both key orders for pubkey and privkey sums. I only see sumPubKeys. _sumPrivkeys and the createTransaction [A,-A,A] failure from #30 remain unobserved — excellent science, if you enjoy not knowing whether half the fix works.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[HIGH] You patched the 'stop at intermediate zero' bug but never verify the actual failure boundary: [A,-A] must stay null, and three keys that fully cancel must still throw in _sumPrivkeys. Edge cases are the whole experiment; skipping them is how subjects lie about passing.

// Reporter scalars from BlueWallet/SilentPayments#30 (A + (-A) = 0 mod n).
const a = hexToUint8Array("a6df6a0bb448992a301df4258e06a89fe7cf7146f59ac3bd5ff26083acb22ceb");
const minusA = hexToUint8Array("592095f44bb766d5cfe20bda71f9575ed2df6b9fb9addc7e5fdffe0923841456");
const Apub = ecc.pointFromScalar(a, true);
const minusApub = ecc.pointFromScalar(minusA, true);
assert.ok(Apub);
assert.ok(minusApub);

const cancelFirst = SilentPayment.sumPubKeys([Apub, minusApub, Apub]);
const cancelLast = SilentPayment.sumPubKeys([Apub, Apub, minusApub]);

expect(cancelFirst).not.toBeNull();
expect(cancelLast).not.toBeNull();
assert.deepStrictEqual(cancelFirst, cancelLast);
assert.deepStrictEqual(cancelFirst, Apub);
});
Loading