@ledgerhq/bitcoin_signer
Advanced tools
Comparing version 0.1.1 to 0.2.0
{ | ||
"name": "@ledgerhq/bitcoin_signer", | ||
"version": "0.1.1", | ||
"version": "0.2.0", | ||
"description": "[Internal development use] INSECURE CLI BTC transaction signer for integration tests", | ||
@@ -5,0 +5,0 @@ "repository": "https://github.com/LedgerHQ/bitcoin_signer", |
@@ -1,2 +0,2 @@ | ||
import { signTxWithKeyPairs, generateKeychain, toWDBroadcastPayload } from "../lib/sign"; | ||
import { SignedTransaction, signTxWithKeyPairs, signTxWithKeyPairsAndTransactionBuilder, generateKeychain, toWDBroadcastPayload } from "../lib/sign"; | ||
import { seedFromMnemonic } from "../lib/seeds"; | ||
@@ -16,3 +16,3 @@ import { Transaction } from "bitcoinjs-lib"; | ||
test("signTxWithKeyPairs signs testnet transaction from seed with derivation", () => { | ||
test("signTxWithKeyPairsAndTransactionBuilder signs testnet transaction from seed with derivation", () => { | ||
const seed = seedFromMnemonic(TEST_SEED, TEST_NETWORK); | ||
@@ -25,7 +25,7 @@ | ||
const signedTx = signTxWithKeyPairs(parsed, keyChain, TEST_NETWORK); | ||
const signedTx = signTxWithKeyPairsAndTransactionBuilder(parsed, keyChain, TEST_NETWORK); | ||
expect(signedTx.signed_tx.toHex().length - parsed.toHex().length).toBe(162); | ||
}); | ||
test("signTxWithKeyPairs signs testnet transaction from seed with derivation in list", () => { | ||
test("signTxWithKeyPairsAndTransactionBuilder signs testnet transaction from seed with derivation in list", () => { | ||
const seed = seedFromMnemonic(TEST_SEED, TEST_NETWORK); | ||
@@ -38,3 +38,3 @@ | ||
const signedTx = signTxWithKeyPairs(parsed, keyChain, TEST_NETWORK); | ||
const signedTx = signTxWithKeyPairsAndTransactionBuilder(parsed, keyChain, TEST_NETWORK); | ||
expect(signedTx.signed_tx.toHex().length - parsed.toHex().length).toBe(162); | ||
@@ -45,3 +45,3 @@ expect(signedTx.pubkey_paths.length).toBe(1); | ||
test("signTxWithKeyPairs signs testnet transaction from seed with derivation not first in list", () => { | ||
test("signTxWithKeyPairsAndTransactionBuilder signs testnet transaction from seed with derivation not first in list", () => { | ||
const seed = seedFromMnemonic(TEST_SEED, TEST_NETWORK); | ||
@@ -54,3 +54,3 @@ | ||
const signedTx = signTxWithKeyPairs(parsed, keyChain, TEST_NETWORK); | ||
const signedTx = signTxWithKeyPairsAndTransactionBuilder(parsed, keyChain, TEST_NETWORK); | ||
expect(signedTx.signed_tx.toHex().length - parsed.toHex().length).toBe(162); | ||
@@ -61,3 +61,3 @@ expect(signedTx.pubkey_paths.length).toBe(1); | ||
test("signTxWithKeyPairs fails to sign testnet transaction from seed with wrong derivation", () => { | ||
test("signTxWithKeyPairsAndTransactionBuilder fails to sign testnet transaction from seed with wrong derivation", () => { | ||
const seed = seedFromMnemonic(TEST_SEED, TEST_NETWORK); | ||
@@ -70,3 +70,3 @@ | ||
expect(() => signTxWithKeyPairs(parsed, keyChain, TEST_NETWORK)).toThrow(); | ||
expect(() => signTxWithKeyPairsAndTransactionBuilder(parsed, keyChain, TEST_NETWORK)).toThrow(); | ||
}); | ||
@@ -82,3 +82,3 @@ | ||
expect(() => signTxWithKeyPairs(parsed, keyChain, TEST_NETWORK)).toThrow(); | ||
expect(() => signTxWithKeyPairsAndTransactionBuilder(parsed, keyChain, TEST_NETWORK)).toThrow(); | ||
}); | ||
@@ -94,3 +94,3 @@ | ||
const signedTx = signTxWithKeyPairs(parsed, keyChain, TEST_NETWORK); | ||
const signedTx = signTxWithKeyPairsAndTransactionBuilder(parsed, keyChain, TEST_NETWORK); | ||
const actualPayload = toWDBroadcastPayload(signedTx); | ||
@@ -100,5 +100,111 @@ const expectedPayload = { | ||
signatures: ["304402201fe1ee3f6bffdb01ff756beb42d1973c79bec5f42e35258b78d770751db7c15b02206f96be67d74e0d503afde5ecc56407b3b09bc4de7a03715bc2191cb29add4108"], | ||
pubkeys: ["m/49'/1'/0'/0/0"] | ||
pubkeys: ["03b556e3484fdd0e33bcddbb3775958ff23d28b18190f1e37986f7b62747a7ff2c"] | ||
} | ||
expect(actualPayload).toEqual(expectedPayload); | ||
}); | ||
const ANOTHER_TEST_SEED = "proof client label tragic dilemma base exclude dawn eight make economy arch" | ||
const TEST_LEGACY_TX = | ||
"01000000011F482468DA36F8FE6E362EC477CB499040670691DFFDE6AE024BDC327A9521BF000000" + | ||
"001976A914AD5C03E7556D9ADBF0F97F750D9744070DB384E288ACFFFFFFFF0200E1F50500000000" + | ||
"1976A914A8D8164837AC86AD2A2DC6C0A288D60153F790A188AC2C081024010000001976A914897A" + | ||
"D9E309DE8B24E20A60017F6A3731C5149F5788AC65000000" | ||
const TEST_SEGWIT_TX = | ||
"0100000000010188B8D6D2E2106432D9E1D1525F339D1CF3C09E0CF7C0A722428EF2D87908D48900" + | ||
"000000160014C8771AAC8641DA24220E8B5A2AC1FA56C9AA0FC7FFFFFFFF0200E1F5050000000019" + | ||
"76A914A08BB1CDBFDD3EBD4FEF98359984CF294CE6233E88AC4C0B102401000000160014A072A72A" + | ||
"C84FD4924781A5084918C42437EB92C80100A4000000" | ||
const TEST_LEGACY_TX_CACHE = [ | ||
"020000000001010000000000000000000000000000000000000000000000000000000000000000FF" + | ||
"FFFFFF03510101FFFFFFFF0200F2052A010000001976A914AD5C03E7556D9ADBF0F97F750D974407" + | ||
"0DB384E288AC0000000000000000266A24AA21A9EDE2F61C3F71D1DEFD3FA999DFA36953755C6906" + | ||
"89799962B48BEBD836974E8CF9012000000000000000000000000000000000000000000000000000" + | ||
"0000000000000000000000" | ||
] | ||
const TEST_SEGWIT_TX_CACHE = [ | ||
"020000000001010000000000000000000000000000000000000000000000000000000000000000FF" + | ||
"FFFFFF0401680101FFFFFFFF0200F2052A01000000160014C8771AAC8641DA24220E8B5A2AC1FA56" + | ||
"C9AA0FC70000000000000000266A24AA21A9EDE2F61C3F71D1DEFD3FA999DFA36953755C69068979" + | ||
"9962B48BEBD836974E8CF90120000000000000000000000000000000000000000000000000000000" + | ||
"000000000000000000" | ||
] | ||
function testSignTxFromSeed(tx: string, mnemonic_seed: string, cache: string[], derivations: string[], expected_len_diff: number) : SignedTransaction { | ||
const seed = seedFromMnemonic(mnemonic_seed, bitcoin.networks.regtest); | ||
const parsed = Transaction.fromHex(tx); | ||
const keyChain = generateKeychain(seed, derivations, bitcoin.networks.regtest); | ||
const signedTx = signTxWithKeyPairs(parsed, cache, keyChain); | ||
expect(signedTx.signed_tx.toHex().length - parsed.toHex().length).toBe(expected_len_diff); | ||
return signedTx | ||
} | ||
test("signTxWithKeyPairs signs legacy transaction from seed with derivation", () => { | ||
testSignTxFromSeed(TEST_LEGACY_TX, ANOTHER_TEST_SEED, TEST_LEGACY_TX_CACHE, ["m/44'/1'/0'/0/0"], 164); | ||
}); | ||
test("signTxWithKeyPairs signs segwit transaction from seed with derivation", () => { | ||
testSignTxFromSeed(TEST_SEGWIT_TX, ANOTHER_TEST_SEED, TEST_SEGWIT_TX_CACHE, ["m/84'/1'/0'/0/0"], 166); | ||
}); | ||
function testSignTxFromSeedWithDerivationList(tx: string, mnemonic_seed: string, cache: string[], derivations: string[], expected_path: string, expected_len_diff: number) : void { | ||
const seed = seedFromMnemonic(mnemonic_seed, bitcoin.networks.regtest); | ||
const parsed = Transaction.fromHex(tx); | ||
const keyChain = generateKeychain(seed, derivations, bitcoin.networks.regtest); | ||
const signedTx = signTxWithKeyPairs(parsed, cache, keyChain); | ||
expect(signedTx.signed_tx.toHex().length - parsed.toHex().length).toBe(expected_len_diff); | ||
expect(signedTx.pubkey_paths.length).toBe(1); | ||
expect(signedTx.pubkey_paths[0]).toBe(expected_path); | ||
} | ||
test("signTxWithKeyPairs signs legacy regtest transaction from seed with derivation in list", () => { | ||
testSignTxFromSeedWithDerivationList(TEST_LEGACY_TX, ANOTHER_TEST_SEED, TEST_LEGACY_TX_CACHE, ["m/44'/1'/0'/0/0", "m/44'/1'/0'/1/1"], "m/44'/1'/0'/0/0", 164); | ||
}); | ||
test("signTxWithKeyPairs signs segwit regtest transaction from seed with derivation in list", () => { | ||
testSignTxFromSeedWithDerivationList(TEST_SEGWIT_TX, ANOTHER_TEST_SEED, TEST_SEGWIT_TX_CACHE, ["m/84'/1'/0'/0/0", "m/84'/1'/0'/1/1"], "m/84'/1'/0'/0/0", 166); | ||
}); | ||
test("signTxWithKeyPairs signs legacy regtest transaction from seed with derivation not first in list", () => { | ||
testSignTxFromSeedWithDerivationList(TEST_LEGACY_TX, ANOTHER_TEST_SEED, TEST_LEGACY_TX_CACHE, ["m/44/1/0/0/0", "m/44'/1'/0'/0/0", "m/44'/1'/1'/0/0"], "m/44'/1'/0'/0/0", 164); | ||
}); | ||
test("signTxWithKeyPairs signs segwit regtest transaction from seed with derivation not first in list", () => { | ||
testSignTxFromSeedWithDerivationList(TEST_SEGWIT_TX, ANOTHER_TEST_SEED, TEST_SEGWIT_TX_CACHE, ["m/84/1/0/0/0", "m/84'/1'/0'/0/0", "m/84'/1'/1'/0/0"], "m/84'/1'/0'/0/0", 166); | ||
}); | ||
test("signTxWithKeyPairs fails to sign legacy regtest transaction from seed with wrong derivation", () => { | ||
expect(() => testSignTxFromSeed(TEST_LEGACY_TX, ANOTHER_TEST_SEED, TEST_LEGACY_TX_CACHE, ["m/44'/1'/0'/0/1"], 164)).toThrow(); | ||
}); | ||
test("signTxWithKeyPairs fails to sign segwit regtest transaction from seed with wrong derivation", () => { | ||
expect(() => testSignTxFromSeed(TEST_SEGWIT_TX, ANOTHER_TEST_SEED, TEST_SEGWIT_TX_CACHE, ["m/84'/1'/0'/0/1"], 166)).toThrow(); | ||
}); | ||
test("signTxWithKeyPairs hardened derivation matters with legacy tx", () => { | ||
expect(() => testSignTxFromSeed(TEST_LEGACY_TX, ANOTHER_TEST_SEED, TEST_LEGACY_TX_CACHE, ["m/44/1/0/0/1"], 164)).toThrow(); | ||
}); | ||
test("signTxWithKeyPairs hardened derivation matters with segwit tx", () => { | ||
expect(() => testSignTxFromSeed(TEST_SEGWIT_TX, ANOTHER_TEST_SEED, TEST_SEGWIT_TX_CACHE, ["m/84/1/0/0/1"], 166)).toThrow(); | ||
}); | ||
test("signTxWithKeyPairs: toWDBroadcastPayload transforms a transaction correctly", () => { | ||
const signedTx = testSignTxFromSeed(TEST_LEGACY_TX, ANOTHER_TEST_SEED, TEST_LEGACY_TX_CACHE, ["m/44'/1'/0'/0/0"], 164); | ||
const actualPayload = toWDBroadcastPayload(signedTx); | ||
const expectedPayload = { | ||
raw_transaction: "01000000011f482468da36f8fe6e362ec477cb499040670691dffde6ae024bdc327a9521bf" + | ||
"000000001976a914ad5c03e7556d9adbf0f97f750d9744070db384e288acffffffff0200e1" + | ||
"f505000000001976a914a8d8164837ac86ad2a2dc6c0a288d60153f790a188ac2c08102401" + | ||
"0000001976a914897ad9e309de8b24e20a60017f6a3731c5149f5788ac65000000", | ||
signatures: ["3045022100a0850959a65799bda24f1d490fd8e46f0a017546dfe0c933331fef313478223d0220651b6c0488837133a70ec4099475d9b22dc384535d9bfba82c531c394c3e2836"], | ||
pubkeys: ["029069d4f870f439053e2a3b15a93c263677b62c91f1339a58007f549e0da0b34d"] | ||
} | ||
expect(actualPayload).toEqual(expectedPayload); | ||
}); | ||
import type { Arguments, CommandBuilder } from 'yargs'; | ||
import { seedFromMnemonic } from '../lib/seeds'; | ||
import { signTxWithKeyPairs, generateKeychain, toWDBroadcastPayload } from '../lib/sign'; | ||
import { signTxWithKeyPairsAndTransactionBuilder, generateKeychain, toWDBroadcastPayload } from '../lib/sign'; | ||
import { Transaction } from 'bitcoinjs-lib'; | ||
@@ -45,3 +45,3 @@ import * as fs from 'fs'; | ||
network); | ||
const signedTx = signTxWithKeyPairs(parsed, keyChain, network); | ||
const signedTx = signTxWithKeyPairsAndTransactionBuilder(parsed, keyChain, network); | ||
log.info("Signed the transaction", signedTx); | ||
@@ -48,0 +48,0 @@ log.info("WD format", toWDBroadcastPayload(signedTx)); |
@@ -25,8 +25,10 @@ import { BIP32Interface } from 'bip32'; | ||
pubkey_paths: string[]; | ||
pubkeys: string[]; | ||
} | ||
export function signTxWithKeyPairs(tx: Transaction, keyChain: Keychain, network?: Network): SignedTransaction { | ||
export function signTxWithKeyPairsAndTransactionBuilder(tx: Transaction, keyChain: Keychain, network?: Network): SignedTransaction { | ||
const txb = bitcoin.TransactionBuilder.fromTransaction(tx, network || DEFAULT_NETWORK); | ||
const pubkey_paths = Array<string>(); | ||
const pubkeys = Array<string>(); | ||
@@ -40,5 +42,7 @@ // This assumes all inputs are spending utxos sent to the same Dogecoin P2PKH address (starts with D) | ||
pubkey_paths.push(keyChain[j].path) | ||
pubkeys.push(keyChain[j].keypair.publicKey.toString('hex')) | ||
break; | ||
} catch (_e) { | ||
if (j == keyChainSize - 1) { | ||
console.error(_e) | ||
if (keyChainSize == 1) { | ||
@@ -57,6 +61,99 @@ throw `The single key in the keychain cannot sign this tx's input ${i}`; | ||
signed_tx, | ||
pubkey_paths | ||
pubkey_paths, | ||
pubkeys | ||
}; | ||
} | ||
function addInputsToPsbt(psbt: bitcoin.Psbt, tx: Transaction, prevTxCache: string[]) { | ||
for (let i = 0; i < tx.ins.length; i++) { | ||
const prevTx = Transaction.fromHex(prevTxCache[i]); | ||
const utxoScript = prevTx.outs[tx.ins[i].index].script | ||
const firstByteOfUtxoScript = utxoScript.readUInt8(0); | ||
if (firstByteOfUtxoScript >= 0 && firstByteOfUtxoScript <= 16) { | ||
// We are spending segwit UTXO | ||
psbt.addInput({ | ||
hash: tx.ins[i].hash, | ||
index: tx.ins[i].index, | ||
witnessUtxo: { | ||
script: utxoScript, | ||
value: prevTx.outs[tx.ins[i].index].value | ||
} | ||
}); | ||
} else { | ||
// We are spending something else | ||
psbt.addInput({ | ||
hash: tx.ins[i].hash, | ||
index: tx.ins[i].index, | ||
nonWitnessUtxo: Buffer.from(prevTxCache[i], 'hex') | ||
}); | ||
} | ||
} | ||
} | ||
function addOutputsToPsbt(psbt: bitcoin.Psbt, tx: Transaction) { | ||
for (let i = 0; i < tx.outs.length; i++) { | ||
psbt.addOutput({ | ||
script: tx.outs[i].script, | ||
value: tx.outs[i].value, | ||
}); | ||
} | ||
} | ||
function craftTxWithPsbt(tx: Transaction, prevTxCache: string[]): bitcoin.Psbt { | ||
const psbt = new bitcoin.Psbt(); | ||
psbt.setVersion(tx.version); | ||
psbt.setLocktime(tx.locktime) | ||
addInputsToPsbt(psbt, tx, prevTxCache) | ||
addOutputsToPsbt(psbt, tx) | ||
return psbt | ||
} | ||
function signTxWithPsbt(psbt: bitcoin.Psbt, tx: Transaction, keyChain: Keychain): {pubkey_paths: string[], pubkeys: string[]} { | ||
const pubkey_paths: string[] = [] | ||
const pubkeys: string[] = [] | ||
// This assumes all inputs are spending utxos sent to the same address | ||
for (let i = 0; i < tx.ins.length; i++) { | ||
const keyChainSize = keyChain.length; | ||
for (let j = 0; j < keyChainSize; j++) { | ||
try { | ||
psbt.signInput(i, keyChain[j].keypair); | ||
pubkey_paths.push(keyChain[j].path) | ||
pubkeys.push(keyChain[j].keypair.publicKey.toString('hex')) | ||
break; | ||
} catch (_e) { | ||
if (j == keyChainSize - 1) { | ||
console.error(_e) | ||
if (keyChainSize == 1) { | ||
throw `The single key in the keychain cannot sign this tx's input ${i}`; | ||
} | ||
throw `All ${keyChainSize} key(s) in the keychain have been tested and none can sign this tx's input ${i}`; | ||
} | ||
} | ||
} | ||
} | ||
return { pubkey_paths: pubkey_paths, pubkeys: pubkeys } | ||
} | ||
export function signTxWithKeyPairs(tx: Transaction, prevTxCache: string[], keyChain: Keychain): SignedTransaction { | ||
const psbt = craftTxWithPsbt(tx, prevTxCache) | ||
const { pubkey_paths: pubkey_paths, pubkeys: pubkeys } = signTxWithPsbt(psbt, tx, keyChain) | ||
if (!psbt.validateSignaturesOfAllInputs()) { | ||
console.error("psbt.validateSignaturesOfAllInputs() returned false"); | ||
} | ||
psbt.finalizeAllInputs(); | ||
const signed_tx = psbt.extractTransaction(); | ||
return { | ||
unsigned_raw: tx.toHex(), | ||
signed_tx, | ||
pubkey_paths, | ||
pubkeys | ||
}; | ||
} | ||
export function toWDBroadcastPayload(tx: SignedTransaction): { | ||
@@ -68,9 +165,13 @@ raw_transaction: string, | ||
const s_tx = tx.signed_tx; | ||
const signatures = Array<string>(); | ||
const pubkeys = tx.pubkey_paths; | ||
for (const input of s_tx.ins) { | ||
const signatures = Array<string>(); | ||
const pubkeys = tx.pubkeys; | ||
for (const input of s_tx.ins) { | ||
if (input.script.length > 0) { | ||
const sig_size = input.script.readUInt8(0); | ||
const der_sig = input.script.subarray(1, sig_size).toString('hex'); | ||
signatures.push(der_sig); | ||
} else if (input.witness.length > 0) { | ||
signatures.push(input.witness[0].subarray(0, input.witness[0].length-1).toString('hex')); | ||
} | ||
} | ||
@@ -77,0 +178,0 @@ return { |
65011
1110