Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Small updates #14

Merged
merged 3 commits into from
May 31, 2024
Merged
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
19 changes: 10 additions & 9 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export type Target = {

export type SilentPaymentGroup = {
Bscan: Buffer;
BmValues: Array<[Buffer, number | undefined]>;
BmValues: Array<[Buffer, number | undefined, number]>;
};

function taggedHash(tag: string, data: Buffer): Buffer {
Expand All @@ -45,12 +45,13 @@ export class SilentPayment {
* Numeric values (if present) for targets are passed through.
*/
createTransaction(utxos: UTXO[], targets: Target[]): Target[] {
const ret: Target[] = [];
const ret: Target[] = new Array(targets.length);

const silentPaymentGroups: Array<SilentPaymentGroup> = [];
for (const target of targets) {
for (let i = 0; i < targets.length; i++) {
const target = targets[i];
if (!target.address?.startsWith("sp1")) {
ret.push(target); // passthrough
ret[i] = target; // passthrough
continue;
}

Expand All @@ -66,11 +67,11 @@ export class SilentPayment {
// Addresses with the same Bscan key all belong to the same recipient
const recipient = silentPaymentGroups.find((group) => Buffer.compare(group.Bscan, Bscan) === 0);
if (recipient) {
recipient.BmValues.push([Bm, target.value]);
recipient.BmValues.push([Bm, target.value, i]);
} else {
silentPaymentGroups.push({
Bscan: Bscan,
BmValues: [[Bm, target.value]],
BmValues: [[Bm, target.value, i]],
});
}
}
Expand All @@ -84,10 +85,10 @@ export class SilentPayment {
for (const group of silentPaymentGroups) {
// Bscan * a * outpoint_hash
const ecdh_shared_secret_step1 = Buffer.from(ecc.privateMultiply(outpoint_hash, a) as Uint8Array);
const ecdh_shared_secret = ecc.pointMultiply(group.Bscan, ecdh_shared_secret_step1);
const ecdh_shared_secret = Buffer.from(ecc.getSharedSecret(ecdh_shared_secret_step1, group.Bscan) as Uint8Array);
Copy link
Member

Choose a reason for hiding this comment

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

I'm curious, how is this better?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Currently, it doesn't change anything since getSharedSecret is calling Point.multiply under the hood (which was surprising to me). But ideally, getSharedSecret should be doing a constant time multiplication. If getSharedSecret is updated in the future to do constant time multiplication, we won't have to change anything here. I seem to remember seeing a todo in the noble crypto repos about changing this to constant time, but will need to go back and find it.


let k = 0;
for (const [Bm, amount] of group.BmValues) {
for (const [Bm, amount, i] of group.BmValues) {
const tk = taggedHash("BIP0352/SharedSecret", Buffer.concat([ecdh_shared_secret!, SilentPayment._ser32(k)]));

// Let Pmk = tk·G + Bm
Expand All @@ -97,7 +98,7 @@ export class SilentPayment {
const address = SilentPayment.pubkeyToAddress(Pmk.slice(1).toString("hex"));
const newTarget: Target = { address };
newTarget.value = amount;
ret.push(newTarget);
ret[i] = newTarget;
k += 1;
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/noble_ecc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ const ecc = {
return { parity, xOnlyPubkey: P.slice(1) };
}),

getSharedSecret: (sk: Uint8Array, pk: Uint8Array, compressed?: boolean): Uint8Array => {
return necc.getSharedSecret(sk, pk, defaultTrue(compressed));
},

pointFromScalar: (sk: Uint8Array, compressed?: boolean): Uint8Array | null => throwToNull(() => necc.getPublicKey(sk, defaultTrue(compressed))),

pointCompress: (p: Uint8Array, compressed?: boolean): Uint8Array => {
Expand Down
50 changes: 50 additions & 0 deletions tests/silent-payment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,56 @@ it("2 inputs - 0 SP outputs (just a passthrough)", () => {
);
});

it("2 inputs - 1 SP output, 1 legacy, 1change (should not rearrange order of inputs )", () => {
const sp = new SilentPayment();
assert.deepStrictEqual(
sp.createTransaction(
[
{
txid: "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16",
vout: 0,
wif: ECPair.fromPrivateKey(Buffer.from("1cd5e8f6b3f29505ed1da7a5806291ebab6491c6a172467e44debe255428a192", "hex")).toWIF(),
utxoType: "p2wpkh",
},
{
txid: "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d",
vout: 0,
wif: ECPair.fromPrivateKey(Buffer.from("7416ef4d92e4dd09d680af6999d1723816e781c030f4b4ecb5bf46939ca30056", "hex")).toWIF(),
utxoType: "p2wpkh",
},
],
[
{
address: "3FiYaHYHQTmD8n2SJxVYobDeN1uQKvzkLe",
value: 11_111,
},
{
address: "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv",
value: 22_222,
},
{
// no address, which should be interpreted as change
value: 33_333,
},
]
),
[
{
address: "3FiYaHYHQTmD8n2SJxVYobDeN1uQKvzkLe",
value: 11_111,
},
{
address: "bc1pszgngkje7t5j3mvdw8xc5l3q7n28awdwl8pena6hrvxgg83lnpmsme6u6j", // unwrapped from SP
value: 22_222,
},
{
// no address, which should be interpreted as change
value: 33_333,
},
]
);
});

it("SilentPayment._outpointHash() works", () => {
const A = ECPair.fromWIF("L4cJGJp4haLbS46ZKMKrjt7HqVuYTSHkChykdMrni955Fs3Sb8vq").publicKey;
assert.deepStrictEqual(
Expand Down
Loading