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

Update sending #9

Merged
merged 5 commits into from
May 1, 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
81 changes: 51 additions & 30 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import ecc from "./noble_ecc";
const ECPair = ECPairFactory(ecc);
bitcoin.initEccLib(ecc);

export type UTXOType = 'p2wpkh' | 'p2sh-p2wpkh' | 'p2pkh' | 'p2tr' | 'non-eligible';
type UTXO = {
txid: string;
vout: number;
WIF: string;
isTaproot?: boolean;
utxoType: UTXOType;
};

type Target = {
Expand All @@ -25,6 +26,13 @@ type SilentPaymentGroup = {
BmValues: Array<[Buffer, number | undefined]>;
};

function taggedHash(tag: string, data: Buffer): Buffer {
const hash = crypto.createHash('sha256');
const tagHash = hash.update(tag, 'utf-8').digest();
const ss = Buffer.concat([tagHash, tagHash, data]);
return crypto.createHash('sha256').update(ss).digest();
}

const G = Buffer.from("0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", "hex");

export class SilentPayment {
Expand Down Expand Up @@ -69,47 +77,45 @@ export class SilentPayment {
if (silentPaymentGroups.length === 0) return ret; // passthrough

const a = SilentPayment._sumPrivkeys(utxos);
const outpoint_hash = SilentPayment._outpointsHash(utxos);
const A = Buffer.from(ecc.pointFromScalar(a) as Uint8Array);
const outpoint_hash = SilentPayment._outpointsHash(utxos, A);

// Generating Pmn for each Bm in the group
// Generating Pmk for each Bm in the group
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);

let n = 0;
let k = 0;
for (const [Bm, amount] of group.BmValues) {
const tn = crypto
.createHash("sha256")
.update(Buffer.concat([ecdh_shared_secret!, SilentPayment._ser32(n)]))
.digest();
const tk = taggedHash(
"BIP0352/SharedSecret",
Buffer.concat([ecdh_shared_secret!, SilentPayment._ser32(k)])
Overtorment marked this conversation as resolved.
Show resolved Hide resolved
);

// Let Pmn = tn·G + Bm
const Pmn = Buffer.from(ecc.pointAdd(ecc.pointMultiply(G, tn) as Uint8Array, Bm) as Uint8Array);
// Let Pmk = tk·G + Bm
const Pmk = Buffer.from(ecc.pointAdd(ecc.pointMultiply(G, tk) as Uint8Array, Bm) as Uint8Array);

// Encode Pmn as a BIP341 taproot output
const address = Pmn.slice(1).toString("hex");
// Encode Pmk as a BIP341 taproot output
const address = Pmk.slice(1).toString("hex");
const newTarget: Target = { address };
newTarget.value = amount;
ret.push(newTarget);
n += 1;
k += 1;
}
n += 1;
}
return ret;
}

static _outpointsHash(parameters: UTXO[]): Buffer {
static _outpointsHash(parameters: UTXO[], A: Buffer): Buffer {
let bufferConcat = Buffer.alloc(0);
const outpoints: Array<Buffer> = [];
for (const parameter of parameters) {
outpoints.push(Buffer.concat([Buffer.from(parameter.txid, "hex").reverse(), SilentPayment._ser32(parameter.vout).reverse()]));
}
outpoints.sort(Buffer.compare);
for (const outpoint of outpoints) {
bufferConcat = Buffer.concat([bufferConcat, outpoint]);
}
return crypto.createHash("sha256").update(bufferConcat).digest();
const smallest_outpoint = outpoints[0];
Overtorment marked this conversation as resolved.
Show resolved Hide resolved
return taggedHash("BIP0352/Inputs", Buffer.concat([Buffer.from(smallest_outpoint), Buffer.from(A)]));
Overtorment marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand Down Expand Up @@ -138,21 +144,36 @@ export class SilentPayment {
const keys: Array<Buffer> = []
for (const utxo of utxos) {
let key = ECPair.fromWIF(utxo.WIF).privateKey;

if (key === undefined) {
throw new Error("No private key found for UTXO");
switch (utxo.utxoType) {
case 'non-eligible':
// Non-eligible UTXOs can be spent in the transaction, but are not used for the
// shared secret derivation. Note: we don't check that the private key is valid
// for non-eligible utxos because its possible the sender is following a different
// signing protocol for these utxos. For silent payments eligible utxos, we require
// access to the private key.
break;
case 'p2tr':
if (key === undefined) {
throw new Error("No private key found for eligible UTXO");
}

// For taproot, check if the seckey results in an odd y-value and negate if so
if (ecc.pointFromScalar(key)![0] === 0x03) {
key = Buffer.from(ecc.privateNegate(key));
}
case 'p2wpkh':
case 'p2sh-p2wpkh':
case 'p2pkh':
if (key === undefined) {
throw new Error("No private key found for eligible UTXO");
}
keys.push(key);
break;
}

// If taproot, check if the seckey results in an odd y-value and negate if so
if (utxo.isTaproot && ecc.pointFromScalar(key)![0] === 0x03) {
key = Buffer.from(ecc.privateNegate(key));
}

keys.push(key);
}

if (keys.length === 0) {
throw new Error("No UTXOs with private keys found");
throw new Error("No eligible UTXOs with private keys found");
}

// summary of every item in array
Expand Down
Loading
Loading