SilentPayment.sumPubKeys() appears to be order-dependent when an intermediate point sum reaches infinity, even though the final sum is non-infinity.
I first noticed this while differential-testing Silent Payments implementations, then reduced it to the standalone repro below against current master.
This may also affect createTransaction(), but the smallest repro is in sumPubKeys().
Minimal case
Let the three input keys be [A, -A, A] modulo secp256k1 order.
The final sum should be:
which is non-zero, so the result should not depend on input order.
Concrete scalars:
A = a6df6a0bb448992a301df4258e06a89fe7cf7146f59ac3bd5ff26083acb22ceb
-A mod n = 592095f44bb766d5cfe20bda71f9575ed2df6b9fb9addc7e5fdffe0923841456
Minimal repro
From the repo root:
npm install
npx tsx repro.ts
import { ECPairFactory } from "ecpair";
import * as ecc from "tiny-secp256k1";
import { SilentPayment } from "./src/index.ts";
const ECPair = ECPairFactory(ecc);
const a = Buffer.from("a6df6a0bb448992a301df4258e06a89fe7cf7146f59ac3bd5ff26083acb22ceb", "hex");
const minusA = Buffer.from("592095f44bb766d5cfe20bda71f9575ed2df6b9fb9addc7e5fdffe0923841456", "hex");
const Apub = ECPair.fromPrivateKey(a).publicKey;
const minusApub = ECPair.fromPrivateKey(minusA).publicKey;
function fmt(x: Uint8Array | null) {
return x ? Buffer.from(x).toString("hex") : "null";
}
console.log("[A,-A,A]", fmt(SilentPayment.sumPubKeys([Apub, minusApub, Apub])));
console.log("[A,A,-A]", fmt(SilentPayment.sumPubKeys([Apub, Apub, minusApub])));
Actual output
[A,-A,A] null
[A,A,-A] 02557ef3e55b0a52489b4454c1169e06bdea43687a69c1f190eb50781644ab6975
The successful result above is exactly A*G, so both orders appear to represent the same final key sum.
This appears to also affect the user-facing send path. With the same keys as three p2wpkh inputs, createTransaction() behaves differently by order:
[A,-A,A] -> ERR Expected Private
[A,A,-A] -> OK
Expected behavior
I would expect both orders to succeed, since the final scalar sum is A in both cases.
Likely cause
sumPubKeys() aggregates left-to-right with ecc.pointAdd(...):
let result = pubkeys[0];
for (let i = 1; i < pubkeys.length; i++) {
const sum = ecc.pointAdd(result, pubkeys[i], compressed);
if (!sum) return null;
result = sum;
}
and _sumPrivkeys() similarly aggregates left-to-right with ecc.privateAdd(...):
const ret = keys.reduce((acc, key) => {
return new Uint8Array(ecc.privateAdd(acc, key) as Uint8Array);
});
So if the intermediate sum hits infinity/zero at A + (-A), the function returns null or throws immediately, even though the remaining + A would make the final sum non-zero again.
Question
Is this input class intentionally unsupported, or is this a bug?
I could not find a restriction against this, and the repo's own test data already includes cases with multiple UTXOs from the same public key. If I'm missing an intended restriction here, I'd appreciate a pointer.
SilentPayment.sumPubKeys()appears to be order-dependent when an intermediate point sum reaches infinity, even though the final sum is non-infinity.I first noticed this while differential-testing Silent Payments implementations, then reduced it to the standalone repro below against current
master.This may also affect
createTransaction(), but the smallest repro is insumPubKeys().Minimal case
Let the three input keys be
[A, -A, A]modulo secp256k1 order.The final sum should be:
which is non-zero, so the result should not depend on input order.
Concrete scalars:
Minimal repro
From the repo root:
Actual output
The successful result above is exactly
A*G, so both orders appear to represent the same final key sum.This appears to also affect the user-facing send path. With the same keys as three
p2wpkhinputs,createTransaction()behaves differently by order:Expected behavior
I would expect both orders to succeed, since the final scalar sum is
Ain both cases.Likely cause
sumPubKeys()aggregates left-to-right withecc.pointAdd(...):and
_sumPrivkeys()similarly aggregates left-to-right withecc.privateAdd(...):So if the intermediate sum hits infinity/zero at
A + (-A), the function returnsnullor throws immediately, even though the remaining+ Awould make the final sum non-zero again.Question
Is this input class intentionally unsupported, or is this a bug?
I could not find a restriction against this, and the repo's own test data already includes cases with multiple UTXOs from the same public key. If I'm missing an intended restriction here, I'd appreciate a pointer.