-
Notifications
You must be signed in to change notification settings - Fork 11
added tweak calculation & incoming UTXO detection #21
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
Changes from all commits
945211a
3873d95
cc006f5
727f6a9
6613838
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,3 @@ | ||
| node_modules | ||
| dist | ||
| .idea/ |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,11 +2,16 @@ import * as crypto from "crypto"; | |
| import { ECPairFactory } from "ecpair"; | ||
| import { bech32m } from "bech32"; | ||
| import * as bitcoin from "bitcoinjs-lib"; | ||
| import { Stack, Transaction, script, address, networks } from "bitcoinjs-lib"; | ||
| import { BIP32Factory } from 'bip32'; | ||
| import * as bip39 from 'bip39'; | ||
|
|
||
| import ecc from "./noble_ecc"; | ||
| import { compareUint8Arrays, concatUint8Arrays, hexToUint8Array, uint8ArrayToHex } from "./uint8array-extras"; | ||
|
|
||
| const ECPair = ECPairFactory(ecc); | ||
| bitcoin.initEccLib(ecc); | ||
| const bip32 = BIP32Factory(ecc); | ||
|
|
||
| export type UTXOType = "p2wpkh" | "p2sh-p2wpkh" | "p2pkh" | "p2tr" | "non-eligible"; | ||
|
|
||
|
|
@@ -27,14 +32,7 @@ export type SilentPaymentGroup = { | |
| BmValues: Array<[Uint8Array, number | undefined, number]>; | ||
| }; | ||
|
|
||
| function taggedHash(tag: string, data: Uint8Array): Uint8Array { | ||
| const hash = crypto.createHash("sha256"); | ||
| const tagHash = hash.update(tag, "utf-8").digest(); | ||
| const ss = concatUint8Arrays([tagHash, tagHash, data]); | ||
| return crypto.createHash("sha256").update(ss).digest(); | ||
| } | ||
|
|
||
| const G = hexToUint8Array("0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"); | ||
| export const G = hexToUint8Array("0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"); | ||
|
|
||
| export class SilentPayment { | ||
| /** | ||
|
|
@@ -90,7 +88,7 @@ export class SilentPayment { | |
|
|
||
| let k = 0; | ||
| for (const [Bm, amount, i] of group.BmValues) { | ||
| const tk = taggedHash("BIP0352/SharedSecret", concatUint8Arrays([ecdh_shared_secret, SilentPayment._ser32(k)])); | ||
| const tk = SilentPayment.taggedHash("BIP0352/SharedSecret", concatUint8Arrays([ecdh_shared_secret, SilentPayment._ser32(k)])); | ||
|
|
||
| // Let Pmk = tk·G + Bm | ||
| const Pmk = new Uint8Array(ecc.pointAdd(ecc.pointMultiply(G, tk) as Uint8Array, Bm) as Uint8Array); | ||
|
|
@@ -106,6 +104,13 @@ export class SilentPayment { | |
| return ret; | ||
| } | ||
|
|
||
| static taggedHash(tag: "BIP0352/Inputs" | "BIP0352/SharedSecret", data: Uint8Array): Uint8Array { | ||
| const hash = crypto.createHash("sha256"); | ||
| const tagHash = hash.update(tag, "utf-8").digest(); | ||
| const ss = concatUint8Arrays([tagHash, tagHash, data]); | ||
| return crypto.createHash("sha256").update(ss).digest(); | ||
| } | ||
|
|
||
| static _outpointsHash(parameters: UTXO[], A: Uint8Array): Uint8Array { | ||
| const outpoints: Array<Uint8Array> = []; | ||
| for (const parameter of parameters) { | ||
|
|
@@ -115,7 +120,7 @@ export class SilentPayment { | |
| } | ||
| outpoints.sort((a, b) => compareUint8Arrays(a, b)); | ||
| const smallest_outpoint = outpoints[0]; | ||
| return taggedHash("BIP0352/Inputs", concatUint8Arrays([smallest_outpoint, A])); | ||
| return SilentPayment.taggedHash("BIP0352/Inputs", concatUint8Arrays([smallest_outpoint, A])); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -209,4 +214,155 @@ export class SilentPayment { | |
| static addressToPubkey(address: string): string { | ||
| return uint8ArrayToHex(bitcoin.address.toOutputScript(address).subarray(2)); | ||
| } | ||
|
|
||
| static getPubkeysFromTransactionInputs(tx: Transaction): Uint8Array[] { | ||
| const result: Uint8Array[] = []; | ||
|
|
||
| const stackToPubkeys = (stack: Stack): Uint8Array[] => { | ||
| return stack | ||
| .filter((elem) => typeof elem !== "number") // filtering out numbers, leaving only Uint8Array | ||
| .filter((elem) => ecc.isXOnlyPoint(elem as Uint8Array) || script.isCanonicalPubKey(elem as Uint8Array)) as Uint8Array[]; | ||
| }; | ||
|
|
||
| for (const input of tx.ins) { | ||
| const inScript = script.decompile(input.script); | ||
| if (inScript) { | ||
| // push any pubkeys in the scriptSig | ||
| result.push(...stackToPubkeys(inScript)); | ||
| if (inScript.length > 1) { | ||
| const lastItem = inScript[inScript.length - 1]; | ||
| if (typeof lastItem !== "number") { | ||
| // If the last item is a buffer, treat as redeemScript and check if we can decompile | ||
| // and if it has any pubkeys (it might not) | ||
| const redeemScript = script.decompile(lastItem); | ||
| if (redeemScript) { | ||
| result.push(...stackToPubkeys(redeemScript)); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| // Find any raw pubkeys in the witness stack | ||
| result.push(...input.witness.filter(script.isCanonicalPubKey)); | ||
| for (const item of input.witness) { | ||
| const maybeScript = script.decompile(item); | ||
| if (maybeScript) { | ||
| result.push(...stackToPubkeys(maybeScript)); | ||
| } | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
| /** | ||
| * takes decoded bitcoin transaction and computes tweak. some transactions must be augmented with prevout data | ||
| * so the method can successfully discover all pubkeys from inputs (example: `tx.ins[0].script = txPrevout0.outs[0].script;`) | ||
| */ | ||
| static computeTweakForTx(tx: Transaction): Uint8Array | null { | ||
| // you need the sum of the (eligible) input public keys (call it A), multiplied by the input_hash, i.e, | ||
| // hash(A|smallest_outpoint). this is a public key (33bytes) so this 33 bytes per tx is sent to the client. | ||
| // that would be a tweak (per tx) | ||
| let A = SilentPayment.sumPubKeys(SilentPayment.getPubkeysFromTransactionInputs(tx)); | ||
|
|
||
| // looking for smallest outpoint: | ||
| const outpoints: Array<Uint8Array> = []; | ||
| for (const inn of tx.ins) { | ||
| const txidBuffer = inn.hash;//.reverse(); | ||
| const voutBuffer = new Uint8Array(SilentPayment._ser32(inn.index).reverse()); | ||
| outpoints.push(new Uint8Array([...txidBuffer, ...voutBuffer])); | ||
| } | ||
| outpoints.sort((a, b) => compareUint8Arrays(a, b)); | ||
| const smallest_outpoint = outpoints[0]; | ||
| const input_hash = SilentPayment.taggedHash("BIP0352/Inputs", concatUint8Arrays([smallest_outpoint, A])); | ||
|
|
||
| // finally, computing tweak: | ||
| return ecc.pointMultiply(A, input_hash); | ||
| } | ||
|
|
||
| static sumPubKeys(pubkeys: Uint8Array[], compressed: boolean = true): Uint8Array | null { | ||
| if (pubkeys.length === 0) return null; | ||
|
|
||
| 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; | ||
| } | ||
|
|
||
| if (result.length === 32) { | ||
| // its x-only...? | ||
| return concatUint8Arrays([new Uint8Array([2]), result]); // not sure about this `2` | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Incorrect Public Key ConversionThe |
||
|
|
||
| return result; | ||
| } | ||
|
|
||
|
|
||
| /** | ||
| * takes BIP-39 mnemonic seed and returns shareable static payment code; also: Bscan, bscan, Bspend, bspend | ||
| */ | ||
| static seedToCode(bip39seed: string, accountNum = 0, passphrase = ''): { address: string; Bscan: Uint8Array; bscan: Uint8Array; Bspend: Uint8Array, bspend: Uint8Array } { | ||
| const root = bip32.fromSeed(bip39.mnemonicToSeedSync(bip39seed, passphrase)); | ||
| const scanXprv = root.derivePath(`m/352'/0'/${accountNum}'/1'/0`); | ||
| const spendXprv = root.derivePath(`m/352'/0'/${accountNum}'/0'/0`); | ||
| const Bscan = scanXprv.publicKey; | ||
| const bscan = scanXprv.privateKey; | ||
| const Bspend = spendXprv.publicKey; | ||
| const bspend = spendXprv.privateKey; | ||
|
|
||
| const bech32Version = 0; | ||
| const words = [bech32Version].concat(bech32m.toWords(Buffer.concat([Bscan, Bspend]))); | ||
| const address = bech32m.encode('sp', words, 1023); | ||
| return { address, Bscan, bscan, Bspend, bspend }; | ||
| } | ||
|
|
||
| /** | ||
| * takes a decoded transaction (`bitcoinjs.Transaction.fromHex()` will do fine), | ||
| * takes computed tweak for this transaction, your mnemonic seed, and gives you UTXOs from this transaction | ||
| * that you own. tweak is _not_ calculated here because theoretically it can come from a tweak-indexing backend | ||
| * service. | ||
| */ | ||
| static detectOurUtxos(tx: Transaction, seed: string, tweakHex: string) { | ||
| const ret: UTXO[] = []; | ||
| const code = SilentPayment.seedToCode(seed); | ||
| const sharedSecret = ecc.getSharedSecret(code.bscan, hexToUint8Array(tweakHex)); | ||
|
|
||
| // todo: iterate k (aka label), cause it might be non-zero | ||
| const k = 0; | ||
| const t_k = SilentPayment.taggedHash("BIP0352/SharedSecret", concatUint8Arrays([sharedSecret, SilentPayment._ser32(k)])); | ||
|
|
||
| // Compute the expected output pubkey | ||
| const P_k = ecc.pointAdd(ecc.pointMultiply(G, t_k), code.Bspend); | ||
|
|
||
| let pubkeyHex = uint8ArrayToHex(P_k); | ||
| if (pubkeyHex.startsWith("02") || pubkeyHex.startsWith("03")) pubkeyHex = pubkeyHex.substring(2); | ||
|
|
||
| let vout = 0; | ||
| for (const o of tx.outs) { | ||
| if (uint8ArrayToHex(o.script) === "5120" + pubkeyHex) { | ||
| // match, that means this output is spendable by us; | ||
| // alternatively, could compare addresses: SilentPayment.pubkeyToAddress(pubkeyHex) === SilentPayment.pubkeyToAddress(o.script) | ||
|
|
||
| // deriving spending privkey for this utxo: d = b_spend + t_k (mod n) | ||
| const d = ecc.privateAdd(code.bspend, t_k); | ||
| const keyPair = ECPair.fromPrivateKey(d); | ||
| const wif = keyPair.toWIF(); | ||
|
|
||
| if (!d) { | ||
| console.log("SilentPayment: Invalid private‐key tweak addition"); | ||
| continue; | ||
| } | ||
|
|
||
| const u: UTXO = { | ||
| txid: tx.getId(), | ||
| vout, | ||
| wif, | ||
| utxoType: "p2tr" | ||
| } | ||
|
|
||
| ret.push(u); | ||
| } | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: UTXO Detection and Key Generation ErrorsIn |
||
|
|
||
| return ret; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Byte Order Mismatch in Tweak Calculation
The
computeTweakForTxmethod usestxids without reversing them, unlike_outpointsHashwhich explicitly reverses thetxidbuffer for outpoint hash calculation. This byte order inconsistency can lead to incorrect tweak computations and potential incompatibility with the BIP-352 specification.