Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af6609bcbf |
56
README.md
56
README.md
@ -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
|
||||
|
||||
|
||||
22
package.json
22
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
99
src/index.ts
99
src/index.ts
@ -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
28
src/noble_ecc.ts
Normal 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);
|
||||
},
|
||||
};
|
||||
@ -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");
|
||||
});
|
||||
|
||||
264
tests/utils.ts
264
tests/utils.ts
@ -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';
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user