Compare commits

..

1 Commits
master ... bump

Author SHA1 Message Date
Ivan Vershigora
af6609bcbf
chore: bump deps and switch ECC/hash utilities 2026-01-16 23:31:30 +00:00
6 changed files with 278 additions and 411 deletions

View File

@ -1,6 +1,6 @@
# SilentPayments (BIP-352)
Send & receive bitcoins via SilentPayment (aka static payment codes), in pure typescript.
Send bitcoins to SilentPayment static payment codes, in pure typescript.
## Installation
@ -10,7 +10,6 @@ Library is implemented in pure typescript _without_ js-compiled version committe
For example, to use it in `jest` tests:
`package.json`:
```json
"jest": {
"transform": {
@ -20,9 +19,9 @@ For example, to use it in `jest` tests:
"node_modules/(?!((jest-)?react-native(-.*)?|@react-native(-community)?)|silent-payments/)"
],
```
If youre using webpack you might need to add a loader in `webpack.config.js`, something like this:
```js
...
{
@ -47,8 +46,6 @@ If youre using webpack you might need to add a loader in `webpack.config.js`, so
## Usage
### Send
You must provide UTXOs and targets (which might or might not include SilentPayment codes):
```typescript
@ -58,40 +55,39 @@ createTransaction(utxos: UTXO[], targets: Target[]): Target[]
Finally:
```typescript
const sp = new SilentPayment();
const targets = sp.createTransaction(
[
{
txid: "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16",
vout: 0,
wif: ECPair.fromPrivateKey(Buffer.from("1cd5e8f6b3f29505ed1da7a5806291ebab6491c6a172467e44debe255428a192", "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,
},
]
);
[
{
txid: "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16",
vout: 0,
wif: ECPair.fromPrivateKey(Buffer.from("1cd5e8f6b3f29505ed1da7a5806291ebab6491c6a172467e44debe255428a192", "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,
},
]
),
```
Library will unwrap `sp1...` codes into correct receivers address. You _must_ provide correct UTXO types to the library, and you _must_ use the same UTXOs
in an actual transaction you create. Library will _not_ do coin selection for you.
### Receive
TODO
## Development

View File

@ -1,26 +1,26 @@
{
"name": "silent-payments",
"version": "3.0.0",
"version": "2.2.2",
"description": "SilentPayments (BIP-352) in pure typescript",
"main": "src/index.ts",
"scripts": {
"tslint": "tsc src/index.ts --noEmit --skipLibCheck --target esnext --moduleResolution nodenext --module NodeNext ",
"lint:write": "prettier . --write",
"test": "vitest run tests/"
},
"author": "overtorment",
"license": "MIT",
"dependencies": {
"bip32": "5.0.0",
"bip39": "3.1.0",
"bitcoinjs-lib": "7.0.0",
"ecpair": "3.0.0",
"tiny-secp256k1": "^2.2.4"
"@bitcoinerlab/secp256k1": "^1.2.0",
"@noble/hashes": "^1.7.1",
"bip32": "^5.0.0",
"bip39": "^3.1.0",
"bitcoinjs-lib": "^7.0.1",
"ecpair": "^3.0.0"
},
"devDependencies": {
"@tsconfig/recommended": "^1.0.2",
"prettier": "2.8.8",
"typescript": "^5.1.6",
"vitest": "^3.0.8"
"@tsconfig/recommended": "^1.0.13",
"prettier": "^3.8.0",
"typescript": "^5.9.3",
"vitest": "^4.0.17"
}
}

View File

@ -1,13 +1,13 @@
import * as crypto from "crypto";
import { ECPairFactory } from "ecpair";
import { sha256 } from "@noble/hashes/sha2";
import { bech32m } from "bech32";
import * as bitcoin from "bitcoinjs-lib";
import { Stack, Transaction, script } from "bitcoinjs-lib";
import { BIP32Factory } from "bip32";
import * as bip39 from "bip39";
import * as bitcoin from "bitcoinjs-lib";
import { Stack, Transaction, script } from "bitcoinjs-lib";
import { ECPairFactory } from "ecpair";
import * as ecc from "tiny-secp256k1";
import { areUint8ArraysEqual, compareUint8Arrays, concatUint8Arrays, hexToUint8Array, uint8ArrayToHex } from "./uint8array-extras";
import ecc from "./noble_ecc";
import { compareUint8Arrays, concatUint8Arrays, hexToUint8Array, uint8ArrayToHex } from "./uint8array-extras";
const ECPair = ECPairFactory(ecc);
bitcoin.initEccLib(ecc);
@ -83,8 +83,8 @@ export class SilentPayment {
// Generating Pmk for each Bm in the group
for (const group of silentPaymentGroups) {
// Bscan * a * outpoint_hash
const ecdh_shared_secret_step1 = SilentPayment._privateMultiply(outpoint_hash, a);
const ecdh_shared_secret = getSharedSecret(ecdh_shared_secret_step1, group.Bscan);
const ecdh_shared_secret_step1 = new Uint8Array(ecc.privateMultiply(outpoint_hash, a) as Uint8Array);
const ecdh_shared_secret = new Uint8Array(ecc.getSharedSecret(ecdh_shared_secret_step1, group.Bscan) as Uint8Array);
let k = 0;
for (const [Bm, amount, i] of group.BmValues) {
@ -105,10 +105,9 @@ export class SilentPayment {
}
static taggedHash(tag: "BIP0352/Inputs" | "BIP0352/SharedSecret", data: Uint8Array): Uint8Array {
const hash = crypto.createHash("sha256");
const tagHash = new Uint8Array(hash.update(tag, "utf-8").digest());
const tagHash = sha256(new TextEncoder().encode(tag));
const ss = concatUint8Arrays([tagHash, tagHash, data]);
return new Uint8Array(crypto.createHash("sha256").update(ss).digest());
return sha256(ss);
}
static _outpointsHash(parameters: UTXO[], A: Uint8Array): Uint8Array {
@ -138,19 +137,6 @@ export class SilentPayment {
return returnValue;
}
private static _privateMultiply(a: Uint8Array, b: Uint8Array): Uint8Array {
if (a.length !== 32 || b.length !== 32) {
throw new Error("Expected 32-byte scalars for private multiply");
}
const product = (bytesToBigInt(a) * bytesToBigInt(b)) % SECP256K1_N;
if (product === BigInt(0)) {
throw new Error("Invalid private multiply result");
}
return bigIntTo32Bytes(product);
}
/**
* Sums the private keys of the UTXOs
* @param utxos {UTXO[]}
@ -363,7 +349,7 @@ export class SilentPayment {
static detectOurUtxos(tx: Transaction, seed: string, tweakHex: string) {
const ret: UTXO[] = [];
const code = SilentPayment.seedToCode(seed);
const sharedSecret = getSharedSecret(code.bscan, hexToUint8Array(tweakHex));
const sharedSecret = ecc.getSharedSecret(code.bscan, hexToUint8Array(tweakHex));
// todo: iterate k (aka label), cause it might be non-zero
const k = 0;
@ -409,8 +395,9 @@ export class SilentPayment {
return ret;
}
static isOurUtxoUsingTweakbscanBspendAndOutputScript(outputScriptHex: string, tweakHex: string, bscan: string, Bspend: string) {
const sharedSecret = getSharedSecret(hexToUint8Array(bscan), hexToUint8Array(tweakHex));
static detectOurUtxosUsingTweakbscanBspend(tx: Transaction, tweakHex: string, bscan: string, Bspend: string) {
const ret: Omit<UTXO, "wif">[] = [];
const sharedSecret = ecc.getSharedSecret(hexToUint8Array(bscan), hexToUint8Array(tweakHex));
// todo: iterate k (aka label), cause it might be non-zero
const k = 0;
@ -425,41 +412,13 @@ export class SilentPayment {
let pubkeyHex = uint8ArrayToHex(P_k);
if (pubkeyHex.startsWith("02") || pubkeyHex.startsWith("03")) pubkeyHex = pubkeyHex.substring(2);
// match, that means this output is spendable by us;
// alternatively, could compare addresses: SilentPayment.pubkeyToAddress(pubkeyHex) === SilentPayment.pubkeyToAddress(o.script)
return outputScriptHex === "5120" + pubkeyHex;
}
static isOurUtxoUsingTweakbscanBspendAndOutputScriptUint8array(outputScript: Uint8Array, tweak: Uint8Array, bscan: Uint8Array, Bspend: Uint8Array) {
const sharedSecret = getSharedSecret(bscan, tweak);
// 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 tkG = ecc.pointMultiply(G, t_k);
assert(tkG, "Failed to compute tkG");
const P_k = ecc.pointAdd(tkG, Bspend);
assert(P_k, "Failed to compute output pubkey");
if (P_k[0] === 2 || P_k[0] === 3) {
// need to strip first x-only value, and compare only it.
//
// match, that means this output is spendable by us;
// alternatively, could compare addresses: SilentPayment.pubkeyToAddress(pubkeyHex) === SilentPayment.pubkeyToAddress(o.script)
return areUint8ArraysEqual(outputScript.subarray(2), P_k.subarray(1));
}
return areUint8ArraysEqual(outputScript.subarray(2), P_k);
}
static detectOurUtxosUsingTweakbscanBspend(tx: Transaction, tweakHex: string, bscan: string, Bspend: string) {
const ret: Omit<UTXO, "wif">[] = [];
let vout = 0;
for (const o of tx.outs) {
if (SilentPayment.isOurUtxoUsingTweakbscanBspendAndOutputScript(uint8ArrayToHex(o.script), tweakHex, bscan, Bspend)) {
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 u: Omit<UTXO, "wif"> = {
txid: tx.getId(),
vout,
@ -478,23 +437,3 @@ export class SilentPayment {
function assert(condition: any, message: string): asserts condition {
if (!condition) throw new Error(message);
}
const SECP256K1_N = BigInt("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141");
function bytesToBigInt(bytes: Uint8Array): bigint {
return BigInt(`0x${uint8ArrayToHex(bytes)}`);
}
function bigIntTo32Bytes(num: bigint): Uint8Array {
const hex = num.toString(16).padStart(64, "0");
return hexToUint8Array(hex);
}
function getSharedSecret(privateKey: Uint8Array, publicKey: Uint8Array): Uint8Array {
const shared = ecc.pointMultiply(publicKey, privateKey, true);
if (!shared) {
throw new Error("Failed to derive shared secret");
}
return shared;
}

28
src/noble_ecc.ts Normal file
View File

@ -0,0 +1,28 @@
/**
* ECC implementation using @bitcoinerlab/secp256k1
* Extended with privateMultiply and getSharedSecret for Silent Payments (BIP-352).
*
* @see https://github.com/bitcoinerlab/secp256k1
*/
import * as ecc from "@bitcoinerlab/secp256k1";
import { mod } from "@noble/curves/abstract/modular";
import { secp256k1 } from "@noble/curves/secp256k1";
import { bytesToNumberBE, numberToBytesBE } from "@noble/curves/utils";
function privateMultiply(d: Uint8Array, tweak: Uint8Array): Uint8Array | null {
if (!ecc.isPrivate(d)) throw new Error("Expected Private");
try {
const result = numberToBytesBE(mod(bytesToNumberBE(d) * bytesToNumberBE(tweak), secp256k1.CURVE.n), 32);
return secp256k1.utils.isValidSecretKey(result) ? result : null;
} catch {
return null;
}
}
export default {
...ecc,
privateMultiply,
getSharedSecret: (sk: Uint8Array, pk: Uint8Array, compressed = true): Uint8Array => {
return secp256k1.getSharedSecret(sk, pk, compressed);
},
};

View File

@ -3,7 +3,7 @@ import assert from "node:assert";
import { expect, it } from "vitest";
import { Stack, Transaction, script, address, networks } from "bitcoinjs-lib";
import { G, getPubkeys, SilentPayment, UTXOType } from "../src";
import * as ecc from "tiny-secp256k1";
import ecc from "../src/noble_ecc";
import { compareUint8Arrays, concatUint8Arrays, hexToUint8Array, uint8ArrayToHex } from "../src/uint8array-extras";
import { Vin, getUTXOType } from "../tests/utils";
import jsonInput from "./data/sending_test_vectors.json";
@ -174,41 +174,37 @@ it("2 inputs - 1 SP output, 1 legacy, 1change (should not rearrange order of inp
it("SilentPayment._outpointHash() works", () => {
const A = ECPair.fromWIF("L4cJGJp4haLbS46ZKMKrjt7HqVuYTSHkChykdMrni955Fs3Sb8vq").publicKey;
assert.deepStrictEqual(
uint8ArrayToHex(
SilentPayment._outpointsHash(
[
{
txid: "a2365547d16b555593e3f58a2b67143fc8ab84e7e1257b1c13d2a9a2ec3a2efb",
vout: 0,
wif: "",
utxoType: "p2wpkh",
},
],
A
)
),
uint8ArrayToHex(SilentPayment._outpointsHash(
[
{
txid: "a2365547d16b555593e3f58a2b67143fc8ab84e7e1257b1c13d2a9a2ec3a2efb",
vout: 0,
wif: "",
utxoType: "p2wpkh",
},
],
A
)),
"94d5923201f2f239e4d2d5a44239e0377325a343e4c068cfd078217adc663d7c"
);
assert.deepStrictEqual(
uint8ArrayToHex(
SilentPayment._outpointsHash(
[
{
txid: "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16",
vout: 0,
wif: "",
utxoType: "non-eligible",
},
{
txid: "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d",
vout: 0,
wif: "",
utxoType: "p2wpkh",
},
],
A
)
),
uint8ArrayToHex(SilentPayment._outpointsHash(
[
{
txid: "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16",
vout: 0,
wif: "",
utxoType: "non-eligible",
},
{
txid: "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d",
vout: 0,
wif: "",
utxoType: "p2wpkh",
},
],
A
)),
"3ea0693eeb0c7e848ad7b875f1998e9ed02905e88a6f5c45f25fa187b7f073d2"
);
});
@ -252,9 +248,7 @@ it("can get pubkeys from Tx inputs", () => {
"02000000000101e79e2690d05d3589257a5d1094de7f46bb1cfae3fc3fb3b644b790d4337931c5000000000001000000013226000000000000225120e92e6cb44492f87779999fbbc295540eef8a23f42efdebacac001ffa18074c100140692f4e81047496cd755c4a24b54ae36e74f7e303a265b1a9a643774d5699a6723cc66e9cdd395d2e487f7881a74bbb5740241498e70ede269583f862a3d47b4600000000"
);
const txPrevout0 = Transaction.fromHex(
"020000000001018f93f113a3d5f2d3feb7444ab8c8d7de5b2b2d1d9e9a5e2e2de42ad4b622958f0000000000000000800210270000000000002251203361aedbd209998e73f60ce0ea2245fa5c4ba747489c58eaa9222df401fda898ea140f000000000016001420c262ffbfe8be9744d502f421df8e1392f3231b02483045022100efaff08cc56bbe1a2819383ff95be23a6f6ab6acaf6bb75ccb7b1e462c62f1c202206fab1385f91ba4ae4fb7731944c8ebef501c7a16eafa320975cfb7697b8537ca012102fc490ee8b804b85d1b7d4959d0bd153ca5bc12fd82134aabbe96acb21b06a1ca00000000"
);
const txPrevout0 = Transaction.fromHex('020000000001018f93f113a3d5f2d3feb7444ab8c8d7de5b2b2d1d9e9a5e2e2de42ad4b622958f0000000000000000800210270000000000002251203361aedbd209998e73f60ce0ea2245fa5c4ba747489c58eaa9222df401fda898ea140f000000000016001420c262ffbfe8be9744d502f421df8e1392f3231b02483045022100efaff08cc56bbe1a2819383ff95be23a6f6ab6acaf6bb75ccb7b1e462c62f1c202206fab1385f91ba4ae4fb7731944c8ebef501c7a16eafa320975cfb7697b8537ca012102fc490ee8b804b85d1b7d4959d0bd153ca5bc12fd82134aabbe96acb21b06a1ca00000000');
// important, need the locking script from previous transaction, and we put it in place of unlocking
// script so the util that parses pubkeys can find the pubkey
@ -270,9 +264,7 @@ it("can calculate tweak 1", () => {
"02000000000101e79e2690d05d3589257a5d1094de7f46bb1cfae3fc3fb3b644b790d4337931c5000000000001000000013226000000000000225120e92e6cb44492f87779999fbbc295540eef8a23f42efdebacac001ffa18074c100140692f4e81047496cd755c4a24b54ae36e74f7e303a265b1a9a643774d5699a6723cc66e9cdd395d2e487f7881a74bbb5740241498e70ede269583f862a3d47b4600000000"
);
const txPrevout0 = Transaction.fromHex(
"020000000001018f93f113a3d5f2d3feb7444ab8c8d7de5b2b2d1d9e9a5e2e2de42ad4b622958f0000000000000000800210270000000000002251203361aedbd209998e73f60ce0ea2245fa5c4ba747489c58eaa9222df401fda898ea140f000000000016001420c262ffbfe8be9744d502f421df8e1392f3231b02483045022100efaff08cc56bbe1a2819383ff95be23a6f6ab6acaf6bb75ccb7b1e462c62f1c202206fab1385f91ba4ae4fb7731944c8ebef501c7a16eafa320975cfb7697b8537ca012102fc490ee8b804b85d1b7d4959d0bd153ca5bc12fd82134aabbe96acb21b06a1ca00000000"
);
const txPrevout0 = Transaction.fromHex('020000000001018f93f113a3d5f2d3feb7444ab8c8d7de5b2b2d1d9e9a5e2e2de42ad4b622958f0000000000000000800210270000000000002251203361aedbd209998e73f60ce0ea2245fa5c4ba747489c58eaa9222df401fda898ea140f000000000016001420c262ffbfe8be9744d502f421df8e1392f3231b02483045022100efaff08cc56bbe1a2819383ff95be23a6f6ab6acaf6bb75ccb7b1e462c62f1c202206fab1385f91ba4ae4fb7731944c8ebef501c7a16eafa320975cfb7697b8537ca012102fc490ee8b804b85d1b7d4959d0bd153ca5bc12fd82134aabbe96acb21b06a1ca00000000');
// important, need the locking script from previous transaction, and we put it in place of unlocking
// script so the util that parses pubkeys can find the pubkey
@ -280,8 +272,8 @@ it("can calculate tweak 1", () => {
const sum = SilentPayment.sumPubKeys(SilentPayment.getPubkeysFromTransactionInputs(tx));
assert.strictEqual(uint8ArrayToHex(sum), "023361aedbd209998e73f60ce0ea2245fa5c4ba747489c58eaa9222df401fda898");
assert.strictEqual(uint8ArrayToHex(SilentPayment.computeTweakForTx(tx)), "032698de13d4b56f9e5f884daa14eaa1978d599fc4cdcb092c36f15e7498172d64");
assert.strictEqual(uint8ArrayToHex(sum), '023361aedbd209998e73f60ce0ea2245fa5c4ba747489c58eaa9222df401fda898');
assert.strictEqual(uint8ArrayToHex(SilentPayment.computeTweakForTx(tx)), '032698de13d4b56f9e5f884daa14eaa1978d599fc4cdcb092c36f15e7498172d64');
});
it("can not calculate tweak 1 when there is no script from prevout", () => {
@ -293,6 +285,7 @@ it("can not calculate tweak 1 when there is no script from prevout", () => {
assert.throws(() => SilentPayment.computeTweakForTx(tx), /No pubkeys found/);
});
it("can calculate tweak 2", () => {
// 0002593785f4bd80373f36781a02bc9bf091387b1fa12b95811333d5aaab5172
const tx = Transaction.fromHex(
@ -303,12 +296,13 @@ it("can calculate tweak 2", () => {
assert.strictEqual(uint8ArrayToHex(SilentPayment.getPubkeysFromTransactionInputs(tx)[1]), "03ab0f6573cdf40b2a0582565cb5628a46af9f102d568501b20c4ac9e33927fa75");
const sum = SilentPayment.sumPubKeys(SilentPayment.getPubkeysFromTransactionInputs(tx));
assert.strictEqual(uint8ArrayToHex(sum), "0392cccfef96a8fbd21fad2bef9b5a78423f49c87a9b48d420ddc4401ba10f1ed4");
assert.strictEqual(uint8ArrayToHex(sum), '0392cccfef96a8fbd21fad2bef9b5a78423f49c87a9b48d420ddc4401ba10f1ed4');
const tweak = SilentPayment.computeTweakForTx(tx);
assert.strictEqual(uint8ArrayToHex(tweak), "02101bd99f275e575712ad28c697488915f7087074c55a15320799f344f1e8fa5a");
assert.strictEqual(uint8ArrayToHex(tweak), '02101bd99f275e575712ad28c697488915f7087074c55a15320799f344f1e8fa5a');
});
it("can calculate tweak 3", () => {
// txid c0deeef514bc1bcb959e51a414db1dc107ef299d9b140d1a6d7f4efe5f3f50f9
let tx = Transaction.fromHex(
@ -316,17 +310,7 @@ it("can calculate tweak 3", () => {
);
assert.ok(SilentPayment.computeTweakForTx(tx));
assert.strictEqual(uint8ArrayToHex(SilentPayment.computeTweakForTx(tx)), "03363f3e1db6a545fc3a98ce6c55d7bdc288009109442d539c09ebc7a7cb515aa1");
});
it("can calculate tweak 4", () => {
// txid ba7597f306e32836ba0dae64f760b2cb3ec6e5b5681ca93af878e49342016c10 height 933626
let tx = Transaction.fromHex(
"020000000001016a300b3c3750c7aec50aa49fd22d090e1cdb77f9358e852cb0478aa8d35e500e0100000000000000800258020000000000002251203fd5ab8ef219b411bd410e457766ce11057a502e07537e60b44fd7d90836e0d8217c170000000000160014fbc1e7108754d800df0f5d33548b9fdc45274d490247304402207ac8a740eea404ed36c360402fd39e1e41ce1c15211a2d4fa2a82b264f0e5e26022045d55198c899b228964ad4a9c2242ec1be9127a31a856a47450e4cb3523bae790121039000152920443367f00c174c852b5fb3da5bffa47d4789d60535c28bc46ef93d00000000"
);
assert.ok(SilentPayment.computeTweakForTx(tx));
assert.strictEqual(uint8ArrayToHex(SilentPayment.computeTweakForTx(tx)), "02670bbd884161533aefd5248fbe8143e5084dba0a82229094a90377a75fd5cd15");
assert.strictEqual(uint8ArrayToHex(SilentPayment.computeTweakForTx(tx)), '03363f3e1db6a545fc3a98ce6c55d7bdc288009109442d539c09ebc7a7cb515aa1');
});
it("can create payment code out of BIP-39 seed", async () => {
@ -335,7 +319,7 @@ it("can create payment code out of BIP-39 seed", async () => {
assert.strictEqual(uint8ArrayToHex(code.bscan), "8ec7ee5936f993b57dcc4e182eea413136e2a897b76328ae3ca19eca7804b45d");
assert.strictEqual(uint8ArrayToHex(code.Bspend), "02a9a4b5ff061e3c07c3a4979cba003995376601bc4e45160cc4adf1227fd3c9f6");
});
});
it("can detect incoming payment in transaction using seed", async () => {
// txid 511e007f9c96b6d713a72b730506198f61dd96046edee72f0dc636bfe1f3a9cf
@ -343,18 +327,14 @@ it("can detect incoming payment in transaction using seed", async () => {
"02000000000101e79e2690d05d3589257a5d1094de7f46bb1cfae3fc3fb3b644b790d4337931c5000000000001000000013226000000000000225120e92e6cb44492f87779999fbbc295540eef8a23f42efdebacac001ffa18074c100140692f4e81047496cd755c4a24b54ae36e74f7e303a265b1a9a643774d5699a6723cc66e9cdd395d2e487f7881a74bbb5740241498e70ede269583f862a3d47b4600000000"
);
const utxos = SilentPayment.detectOurUtxos(
tx,
"vault hole thought beyond young winter common federal measure hobby gold better salmon fetch exhibit follow strong genius large group galaxy doll assist tip",
"032698de13d4b56f9e5f884daa14eaa1978d599fc4cdcb092c36f15e7498172d64"
);
const utxos = SilentPayment.detectOurUtxos(tx, "vault hole thought beyond young winter common federal measure hobby gold better salmon fetch exhibit follow strong genius large group galaxy doll assist tip", '032698de13d4b56f9e5f884daa14eaa1978d599fc4cdcb092c36f15e7498172d64');
assert.deepStrictEqual(utxos, [
{
txid: "511e007f9c96b6d713a72b730506198f61dd96046edee72f0dc636bfe1f3a9cf",
txid: '511e007f9c96b6d713a72b730506198f61dd96046edee72f0dc636bfe1f3a9cf',
vout: 0,
wif: "L4PKRVk1Peaar5WuH5LiKfkTygWtFfGrFeH2g2t3YVVqiwpJjMoF", // thats bc1payhxedzyjtu8w7ven7au9925pmhc5gl59m77ht9vqq0l5xq8fsgqtwg8vf
utxoType: "p2tr",
},
wif: 'L4PKRVk1Peaar5WuH5LiKfkTygWtFfGrFeH2g2t3YVVqiwpJjMoF', // thats bc1payhxedzyjtu8w7ven7au9925pmhc5gl59m77ht9vqq0l5xq8fsgqtwg8vf
utxoType: 'p2tr'
}
]);
});
@ -364,17 +344,17 @@ it("can detect incoming payment in transaction using tweak", async () => {
"02000000000101e79e2690d05d3589257a5d1094de7f46bb1cfae3fc3fb3b644b790d4337931c5000000000001000000013226000000000000225120e92e6cb44492f87779999fbbc295540eef8a23f42efdebacac001ffa18074c100140692f4e81047496cd755c4a24b54ae36e74f7e303a265b1a9a643774d5699a6723cc66e9cdd395d2e487f7881a74bbb5740241498e70ede269583f862a3d47b4600000000"
);
const tweak = "032698de13d4b56f9e5f884daa14eaa1978d599fc4cdcb092c36f15e7498172d64";
const tweak = '032698de13d4b56f9e5f884daa14eaa1978d599fc4cdcb092c36f15e7498172d64';
const bscan = "8ec7ee5936f993b57dcc4e182eea413136e2a897b76328ae3ca19eca7804b45d";
const Bspend = "02a9a4b5ff061e3c07c3a4979cba003995376601bc4e45160cc4adf1227fd3c9f6";
const utxos = SilentPayment.detectOurUtxosUsingTweakbscanBspend(tx, tweak, bscan, Bspend);
assert.deepStrictEqual(utxos, [
{
txid: "511e007f9c96b6d713a72b730506198f61dd96046edee72f0dc636bfe1f3a9cf",
txid: '511e007f9c96b6d713a72b730506198f61dd96046edee72f0dc636bfe1f3a9cf',
vout: 0,
utxoType: "p2tr",
},
utxoType: 'p2tr'
}
]);
});
@ -384,19 +364,16 @@ it("can detect incoming payment in transaction using seed 2", async () => {
"02000000000101e79e2690d05d3589257a5d1094de7f46bb1cfae3fc3fb3b644b790d4337931c501000000000000008002102700000000000022512040fb1745d1c5f6d3f2b8825b83f6d90e74d6f278b0fe6d17e8173751e5bcaa4ab6ec0e00000000001600143adbcced77635b09bfe108295a8e39a73d1494b402483045022100d3f7a5edf1e592aae46499073eee7f93f63b1bda22bb83557918b42148128558022036a1dde48756f6d09dbf2ae884424d20985c6d8b05b5555eafd91d4de0b2238d0121033d484bbc02f16f0c5ada1fa14d8812e09e73cc8cf01ed9be3e78bda2322b778900000000"
);
const utxos = SilentPayment.detectOurUtxos(
tx,
"vault hole thought beyond young winter common federal measure hobby gold better salmon fetch exhibit follow strong genius large group galaxy doll assist tip",
"03363f3e1db6a545fc3a98ce6c55d7bdc288009109442d539c09ebc7a7cb515aa1"
);
const utxos = SilentPayment.detectOurUtxos(tx, "vault hole thought beyond young winter common federal measure hobby gold better salmon fetch exhibit follow strong genius large group galaxy doll assist tip", '03363f3e1db6a545fc3a98ce6c55d7bdc288009109442d539c09ebc7a7cb515aa1');
assert.deepStrictEqual(utxos, [
{
txid: "c0deeef514bc1bcb959e51a414db1dc107ef299d9b140d1a6d7f4efe5f3f50f9",
utxoType: "p2tr",
vout: 0,
wif: "L1qJxwybxM8ntGs5XAt4yXp37o7PWYfvGwgxnJkR329YMaRCjxv1",
},
]);
{
"txid": "c0deeef514bc1bcb959e51a414db1dc107ef299d9b140d1a6d7f4efe5f3f50f9",
"utxoType": "p2tr",
"vout": 0,
"wif": "L1qJxwybxM8ntGs5XAt4yXp37o7PWYfvGwgxnJkR329YMaRCjxv1",
},
]
);
});
it("can detect incoming payment in transaction using tweak 2", async () => {
@ -405,89 +382,16 @@ it("can detect incoming payment in transaction using tweak 2", async () => {
"02000000000101e79e2690d05d3589257a5d1094de7f46bb1cfae3fc3fb3b644b790d4337931c501000000000000008002102700000000000022512040fb1745d1c5f6d3f2b8825b83f6d90e74d6f278b0fe6d17e8173751e5bcaa4ab6ec0e00000000001600143adbcced77635b09bfe108295a8e39a73d1494b402483045022100d3f7a5edf1e592aae46499073eee7f93f63b1bda22bb83557918b42148128558022036a1dde48756f6d09dbf2ae884424d20985c6d8b05b5555eafd91d4de0b2238d0121033d484bbc02f16f0c5ada1fa14d8812e09e73cc8cf01ed9be3e78bda2322b778900000000"
);
const tweak = "03363f3e1db6a545fc3a98ce6c55d7bdc288009109442d539c09ebc7a7cb515aa1";
const tweak = '03363f3e1db6a545fc3a98ce6c55d7bdc288009109442d539c09ebc7a7cb515aa1';
const bscan = "8ec7ee5936f993b57dcc4e182eea413136e2a897b76328ae3ca19eca7804b45d";
const Bspend = "02a9a4b5ff061e3c07c3a4979cba003995376601bc4e45160cc4adf1227fd3c9f6";
const utxos = SilentPayment.detectOurUtxosUsingTweakbscanBspend(tx, tweak, bscan, Bspend);
assert.deepStrictEqual(utxos, [
{
txid: "c0deeef514bc1bcb959e51a414db1dc107ef299d9b140d1a6d7f4efe5f3f50f9",
utxoType: "p2tr",
vout: 0,
},
"txid": "c0deeef514bc1bcb959e51a414db1dc107ef299d9b140d1a6d7f4efe5f3f50f9",
"utxoType": "p2tr",
"vout": 0
}
]);
});
it("can detect incoming payment in tx output (having output script only) using tweak", async () => {
// txid 511e007f9c96b6d713a72b730506198f61dd96046edee72f0dc636bfe1f3a9cf
let tx = Transaction.fromHex(
"02000000000101e79e2690d05d3589257a5d1094de7f46bb1cfae3fc3fb3b644b790d4337931c5000000000001000000013226000000000000225120e92e6cb44492f87779999fbbc295540eef8a23f42efdebacac001ffa18074c100140692f4e81047496cd755c4a24b54ae36e74f7e303a265b1a9a643774d5699a6723cc66e9cdd395d2e487f7881a74bbb5740241498e70ede269583f862a3d47b4600000000"
);
const outputScriptHex = uint8ArrayToHex(tx.outs[0].script);
const tweak = "032698de13d4b56f9e5f884daa14eaa1978d599fc4cdcb092c36f15e7498172d64";
const bscan = "8ec7ee5936f993b57dcc4e182eea413136e2a897b76328ae3ca19eca7804b45d";
const Bspend = "02a9a4b5ff061e3c07c3a4979cba003995376601bc4e45160cc4adf1227fd3c9f6";
const isOurs = SilentPayment.isOurUtxoUsingTweakbscanBspendAndOutputScript(outputScriptHex, tweak, bscan, Bspend);
assert.strictEqual(isOurs, true);
});
it("can detect incoming payment in tx output (having output script only) using tweak 2", async () => {
// txid c0deeef514bc1bcb959e51a414db1dc107ef299d9b140d1a6d7f4efe5f3f50f9
let tx = Transaction.fromHex(
"02000000000101e79e2690d05d3589257a5d1094de7f46bb1cfae3fc3fb3b644b790d4337931c501000000000000008002102700000000000022512040fb1745d1c5f6d3f2b8825b83f6d90e74d6f278b0fe6d17e8173751e5bcaa4ab6ec0e00000000001600143adbcced77635b09bfe108295a8e39a73d1494b402483045022100d3f7a5edf1e592aae46499073eee7f93f63b1bda22bb83557918b42148128558022036a1dde48756f6d09dbf2ae884424d20985c6d8b05b5555eafd91d4de0b2238d0121033d484bbc02f16f0c5ada1fa14d8812e09e73cc8cf01ed9be3e78bda2322b778900000000"
);
const outputScriptHex = uint8ArrayToHex(tx.outs[0].script);
const outputScriptHexWrong = uint8ArrayToHex(tx.outs[1].script); // not SP output, most likely change
const tweak = "03363f3e1db6a545fc3a98ce6c55d7bdc288009109442d539c09ebc7a7cb515aa1";
const bscan = "8ec7ee5936f993b57dcc4e182eea413136e2a897b76328ae3ca19eca7804b45d";
const Bspend = "02a9a4b5ff061e3c07c3a4979cba003995376601bc4e45160cc4adf1227fd3c9f6";
const isOurs = SilentPayment.isOurUtxoUsingTweakbscanBspendAndOutputScript(outputScriptHex, tweak, bscan, Bspend);
assert.strictEqual(isOurs, true);
const isOurs2 = SilentPayment.isOurUtxoUsingTweakbscanBspendAndOutputScript(outputScriptHexWrong, tweak, bscan, Bspend);
assert.strictEqual(isOurs2, false);
});
it("can detect incoming payment in tx output (having output script only) using tweak 3, plus time measure", async () => {
// txid ba7597f306e32836ba0dae64f760b2cb3ec6e5b5681ca93af878e49342016c10 height 933626
const outputScriptHex = "51203fd5ab8ef219b411bd410e457766ce11057a502e07537e60b44fd7d90836e0d8";
const tweak = "02670bbd884161533aefd5248fbe8143e5084dba0a82229094a90377a75fd5cd15";
const bscan = "8ec7ee5936f993b57dcc4e182eea413136e2a897b76328ae3ca19eca7804b45d";
const Bspend = "02a9a4b5ff061e3c07c3a4979cba003995376601bc4e45160cc4adf1227fd3c9f6";
const start = Date.now();
for (let c = 0; c < 1000; c++) {
const isOurs = SilentPayment.isOurUtxoUsingTweakbscanBspendAndOutputScript(outputScriptHex, tweak, bscan, Bspend);
assert.strictEqual(isOurs, true);
}
const end = Date.now();
console.log("1000 tweak mults took", (end - start) / 1000, "sec");
});
it("can detect incoming payment in tx output (having output script only) using tweak 3 v2, plus time measure", async () => {
// txid ba7597f306e32836ba0dae64f760b2cb3ec6e5b5681ca93af878e49342016c10 height 933626
const outputScript = hexToUint8Array("51203fd5ab8ef219b411bd410e457766ce11057a502e07537e60b44fd7d90836e0d8");
const tweak = hexToUint8Array("02670bbd884161533aefd5248fbe8143e5084dba0a82229094a90377a75fd5cd15");
const bscan = hexToUint8Array("8ec7ee5936f993b57dcc4e182eea413136e2a897b76328ae3ca19eca7804b45d");
const Bspend = hexToUint8Array("02a9a4b5ff061e3c07c3a4979cba003995376601bc4e45160cc4adf1227fd3c9f6");
const start = Date.now();
let isOurs;
for (let c = 0; c < 1000; c++) {
isOurs = SilentPayment.isOurUtxoUsingTweakbscanBspendAndOutputScriptUint8array(outputScript, tweak, bscan, Bspend);
}
assert.strictEqual(isOurs, true);
const end = Date.now();
console.log("1000 tweak mults took", (end - start) / 1000, "sec");
});

View File

@ -1,5 +1,6 @@
import { UTXOType } from "../src";
import * as crypto from "crypto";
import { sha256 } from "@noble/hashes/sha2";
import { ripemd160 } from "@noble/hashes/ripemd160";
import { areUint8ArraysEqual, hexToUint8Array, readUInt16, readUInt32 } from "../src/uint8array-extras";
// The following utilities are provided to determine the UTXOType of a transaction input.
@ -14,61 +15,61 @@ import { areUint8ArraysEqual, hexToUint8Array, readUInt16, readUInt32 } from "..
// light client wallet is getting the 33 bytes of input public data from a full node that has
// already done the transaction parsing and determined which inputs are eligible and which are not.
class BufferReader {
private b: Uint8Array;
private offset: number;
private b: Uint8Array;
private offset: number;
constructor(b: Uint8Array) {
this.b = b;
this.offset = 0;
}
readUInt64LE() {
const a = readUInt32(this.b, this.offset, true);
let b = readUInt32(this.b, this.offset + 4, true);
b *= 0x100000000;
return b + a;
}
readCompactSize(): number {
if (this.b.length === 0) {
return 0; // end of stream
constructor(b: Uint8Array) {
this.b = b;
this.offset = 0;
}
let nit: number;
const firstByte = this.b[this.offset];
this.offset += 1;
if (firstByte === 0xfd) {
nit = readUInt16(this.b, this.offset, true);
this.offset += 2;
} else if (firstByte === 0xfe) {
nit = readUInt32(this.b, this.offset, true);
this.offset += 4;
} else if (firstByte === 0xff) {
nit = this.readUInt64LE();
this.offset += 8;
} else {
nit = firstByte;
}
return nit;
}
readElement(): Uint8Array {
const nit = this.readCompactSize();
return this.b.slice(this.offset, this.offset + nit);
}
public readVector(): Uint8Array[] {
const nit = this.readCompactSize();
const r: Uint8Array[] = [];
for (let i = 0; i < nit; i++) {
const t = this.readElement();
r.push(t);
this.offset += t.length;
readUInt64LE() {
const a = readUInt32(this.b, this.offset, true);
let b = readUInt32(this.b, this.offset + 4, true);
b *= 0x100000000;
return b + a;
}
readCompactSize(): number {
if (this.b.length === 0) {
return 0; // end of stream
}
let nit: number;
const firstByte = this.b[this.offset];
this.offset += 1;
if (firstByte === 0xfd) {
nit = readUInt16(this.b, this.offset, true);
this.offset += 2;
} else if (firstByte === 0xfe) {
nit = readUInt32(this.b, this.offset, true);
this.offset += 4;
} else if (firstByte === 0xff) {
nit = this.readUInt64LE();
this.offset += 8;
} else {
nit = firstByte;
}
return nit;
}
readElement(): Uint8Array {
const nit = this.readCompactSize();
return this.b.slice(this.offset, this.offset + nit);
}
public readVector(): Uint8Array[] {
const nit = this.readCompactSize();
const r: Uint8Array[] = [];
for (let i = 0; i < nit; i++) {
const t = this.readElement();
r.push(t);
this.offset += t.length;
}
return r;
}
return r;
}
}
const NUMS_H = hexToUint8Array("50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0");
const NUMS_H = hexToUint8Array('50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0');
export type Vin = {
txid: string;
@ -84,105 +85,104 @@ export type Vin = {
};
function hash160(s: Uint8Array): Uint8Array {
const sha256Digest = new Uint8Array(crypto.createHash("sha256").update(s).digest());
const ripemd160Digest = crypto.createHash("ripemd160").update(sha256Digest).digest();
return new Uint8Array(ripemd160Digest);
return ripemd160(sha256(s));
}
function isP2tr(spk: Uint8Array): boolean {
if (spk.length !== 34) {
return false;
}
// OP_1 OP_PUSHBYTES_32 <32 bytes>
return spk[0] === 0x51 && spk[1] === 0x20;
if (spk.length !== 34) {
return false;
}
// OP_1 OP_PUSHBYTES_32 <32 bytes>
return spk[0] === 0x51 && spk[1] === 0x20;
}
function isP2wpkh(spk: Uint8Array): boolean {
if (spk.length !== 22) {
return false;
}
// OP_0 OP_PUSHBYTES_20 <20 bytes>
return spk[0] === 0x00 && spk[1] === 0x14;
if (spk.length !== 22) {
return false;
}
// OP_0 OP_PUSHBYTES_20 <20 bytes>
return spk[0] === 0x00 && spk[1] === 0x14;
}
function isP2sh(spk: Uint8Array): boolean {
if (spk.length !== 23) {
return false;
}
// OP_HASH160 OP_PUSHBYTES_20 <20 bytes> OP_EQUAL
return spk[0] === 0xa9 && spk[1] === 0x14 && spk[spk.length - 1] === 0x87;
if (spk.length !== 23) {
return false;
}
// OP_HASH160 OP_PUSHBYTES_20 <20 bytes> OP_EQUAL
return spk[0] === 0xA9 && spk[1] === 0x14 && spk[spk.length - 1] === 0x87;
}
function isP2pkh(spk: Uint8Array): boolean {
if (spk.length !== 25) {
return false;
}
// OP_DUP OP_HASH160 OP_PUSHBYTES_20 <20 bytes> OP_EQUALVERIFY OP_CHECKSIG
return spk[0] === 0x76 && spk[1] === 0xa9 && spk[2] === 0x14 && spk[spk.length - 2] === 0x88 && spk[spk.length - 1] === 0xac;
if (spk.length !== 25) {
return false;
}
// OP_DUP OP_HASH160 OP_PUSHBYTES_20 <20 bytes> OP_EQUALVERIFY OP_CHECKSIG
return spk[0] === 0x76 && spk[1] === 0xA9 && spk[2] === 0x14 && spk[spk.length - 2] === 0x88 && spk[spk.length - 1] === 0xAC;
}
export function getUTXOType(vin: Vin): UTXOType {
const spk = hexToUint8Array(vin.prevout.scriptPubKey.hex);
if (isP2pkh(spk)) {
// skip the first 3 op_codes and grab the 20 byte hash
// from the scriptPubKey
const spkHash = spk.slice(3, 3 + 20);
const scriptSig = hexToUint8Array(vin.scriptSig);
for (let i = scriptSig.length; i > 0; i--) {
if (i - 33 >= 0) {
// starting from the back, we move over the scriptSig with a 33 byte
// window (to match a compressed pubkey). we hash this and check if it matches
// the 20 byte has from the scriptPubKey. for standard scriptSigs, this will match
// right away because the pubkey is the last item in the scriptSig.
// if its a non-standard (malleated) scriptSig, we will still find the pubkey if its
// a compressed pubkey.
//
// note: this is an incredibly inefficient implementation, for demonstration purposes only.
const pubkeyBytes = scriptSig.slice(i - 33, i);
const pubkeyHash = hash160(pubkeyBytes);
if (areUint8ArraysEqual(pubkeyHash, spkHash)) {
return "p2pkh";
const spk = hexToUint8Array(vin.prevout.scriptPubKey.hex);
if (isP2pkh(spk)) {
// skip the first 3 op_codes and grab the 20 byte hash
// from the scriptPubKey
const spkHash = spk.slice(3, 3 + 20);
const scriptSig = hexToUint8Array(vin.scriptSig);
for (let i = scriptSig.length; i > 0; i--) {
if (i - 33 >= 0) {
// starting from the back, we move over the scriptSig with a 33 byte
// window (to match a compressed pubkey). we hash this and check if it matches
// the 20 byte has from the scriptPubKey. for standard scriptSigs, this will match
// right away because the pubkey is the last item in the scriptSig.
// if its a non-standard (malleated) scriptSig, we will still find the pubkey if its
// a compressed pubkey.
//
// note: this is an incredibly inefficient implementation, for demonstration purposes only.
const pubkeyBytes = scriptSig.slice(i - 33, i);
const pubkeyHash = hash160(pubkeyBytes);
if (areUint8ArraysEqual(pubkeyHash,spkHash)) {
return 'p2pkh';
}
}
}
}
}
}
if (isP2sh(spk)) {
const redeemScript = hexToUint8Array(vin.scriptSig).slice(1);
if (isP2wpkh(redeemScript)) {
const br = new BufferReader(hexToUint8Array(vin.txinwitness));
const witnessStack = br.readVector();
if (witnessStack[1].length === 33) {
return "p2wpkh";
}
}
}
if (isP2wpkh(spk)) {
const br = new BufferReader(hexToUint8Array(vin.txinwitness));
const witnessStack = br.readVector();
if (witnessStack[1].length === 33) {
return "p2wpkh";
}
}
if (isP2tr(spk)) {
const br = new BufferReader(hexToUint8Array(vin.txinwitness));
const witnessStack = br.readVector();
if (witnessStack.length >= 1) {
if (witnessStack.length > 1 && witnessStack[witnessStack.length - 1][0] === 0x50) {
// Last item is annex
witnessStack.pop();
}
if (witnessStack.length > 1) {
// Script-path spend
const controlBlock = witnessStack[witnessStack.length - 1];
// control block is <control byte> <32 byte internal key> and 0 or more <32 byte hash>
const internalKey = controlBlock.slice(1, 33);
if (areUint8ArraysEqual(internalKey, NUMS_H)) {
// Skip if NUMS_H
return "non-eligible";
if (isP2sh(spk)) {
const redeemScript = hexToUint8Array(vin.scriptSig).slice(1);
if (isP2wpkh(redeemScript)) {
const br = new BufferReader(hexToUint8Array(vin.txinwitness));
const witnessStack = br.readVector();
if (witnessStack[1].length === 33) {
return 'p2wpkh';
}
}
}
return "p2tr";
}
}
return "non-eligible";
if (isP2wpkh(spk)) {
const br = new BufferReader(hexToUint8Array(vin.txinwitness));
const witnessStack = br.readVector();
if (witnessStack[1].length === 33) {
return 'p2wpkh';
}
}
if (isP2tr(spk)) {
const br = new BufferReader(hexToUint8Array(vin.txinwitness));
const witnessStack = br.readVector();
if (witnessStack.length >= 1) {
if (witnessStack.length > 1 && witnessStack[witnessStack.length - 1][0] === 0x50) {
// Last item is annex
witnessStack.pop();
}
if (witnessStack.length > 1) {
// Script-path spend
const controlBlock = witnessStack[witnessStack.length - 1];
// control block is <control byte> <32 byte internal key> and 0 or more <32 byte hash>
const internalKey = controlBlock.slice(1, 33);
if (areUint8ArraysEqual(internalKey,NUMS_H)) {
// Skip if NUMS_H
return 'non-eligible';
}
}
return 'p2tr';
}
}
return 'non-eligible';
}