From 45187a32d06349546ed91e4d813c7bc8eb385433 Mon Sep 17 00:00:00 2001 From: junderw Date: Fri, 12 Nov 2021 08:33:18 +0900 Subject: [PATCH] Add taggedHash, sigHash v1 Co-authored-by: Brandon Black Co-authored-by: Otto Allmendinger Co-authored-by: Tyler Levine Co-authored-by: Daniel McNally --- package-lock.json | 6 +- package.json | 2 +- src/bufferutils.d.ts | 2 + src/bufferutils.js | 9 ++ src/crypto.d.ts | 4 + src/crypto.js | 24 ++++- src/index.d.ts | 1 + src/transaction.d.ts | 4 + src/transaction.js | 146 ++++++++++++++++++++++++++++- test/bufferutils.spec.ts | 17 ++++ test/crypto.spec.ts | 15 ++- test/fixtures/crypto.json | 77 +++++++++------- test/fixtures/transaction.json | 81 ++++++++++++++++ test/transaction.spec.ts | 21 +++++ ts_src/bufferutils.ts | 11 +++ ts_src/crypto.ts | 24 +++++ ts_src/index.ts | 1 + ts_src/transaction.ts | 163 ++++++++++++++++++++++++++++++++- 18 files changed, 559 insertions(+), 49 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4d00add..f07cf3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -416,9 +416,9 @@ "dev": true }, "@types/node": { - "version": "16.11.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.1.tgz", - "integrity": "sha512-PYGcJHL9mwl1Ek3PLiYgyEKtwTMmkMw4vbiyz/ps3pfdRYLVv+SN7qHVAImrjdAXxgluDEw6Ph4lyv+m9UpRmA==", + "version": "16.11.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.7.tgz", + "integrity": "sha512-QB5D2sqfSjCmTuWcBWyJ+/44bcjO7VbjSbOE0ucoVbAsSNQc4Lt6QkgkVXkTDwkL4z/beecZNDvVX15D4P8Jbw==", "dev": true }, "@types/proxyquire": { diff --git a/package.json b/package.json index 9d15766..0eb793e 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "@types/bs58check": "^2.1.0", "@types/create-hash": "^1.2.2", "@types/mocha": "^5.2.7", - "@types/node": "^16.11.1", + "@types/node": "^16.11.7", "@types/proxyquire": "^1.3.28", "@types/randombytes": "^2.0.0", "@types/wif": "^2.0.2", diff --git a/src/bufferutils.d.ts b/src/bufferutils.d.ts index 40f89b3..b1d8966 100644 --- a/src/bufferutils.d.ts +++ b/src/bufferutils.d.ts @@ -11,6 +11,7 @@ export declare function cloneBuffer(buffer: Buffer): Buffer; export declare class BufferWriter { buffer: Buffer; offset: number; + static withCapacity(size: number): BufferWriter; constructor(buffer: Buffer, offset?: number); writeUInt8(i: number): void; writeInt32(i: number): void; @@ -20,6 +21,7 @@ export declare class BufferWriter { writeSlice(slice: Buffer): void; writeVarSlice(slice: Buffer): void; writeVector(vector: Buffer[]): void; + end(): Buffer; } /** * Helper class for reading of bitcoin data types from a buffer. diff --git a/src/bufferutils.js b/src/bufferutils.js index fcab0b7..83a013b 100644 --- a/src/bufferutils.js +++ b/src/bufferutils.js @@ -58,6 +58,9 @@ class BufferWriter { this.offset = offset; typeforce(types.tuple(types.Buffer, types.UInt32), [buffer, offset]); } + static withCapacity(size) { + return new BufferWriter(Buffer.alloc(size)); + } writeUInt8(i) { this.offset = this.buffer.writeUInt8(i, this.offset); } @@ -88,6 +91,12 @@ class BufferWriter { this.writeVarInt(vector.length); vector.forEach(buf => this.writeVarSlice(buf)); } + end() { + if (this.buffer.length === this.offset) { + return this.buffer; + } + throw new Error(`buffer size ${this.buffer.length}, offset ${this.offset}`); + } } exports.BufferWriter = BufferWriter; /** diff --git a/src/crypto.d.ts b/src/crypto.d.ts index 1743681..ec088f3 100644 --- a/src/crypto.d.ts +++ b/src/crypto.d.ts @@ -4,3 +4,7 @@ export declare function sha1(buffer: Buffer): Buffer; export declare function sha256(buffer: Buffer): Buffer; export declare function hash160(buffer: Buffer): Buffer; export declare function hash256(buffer: Buffer): Buffer; +declare const TAGS: readonly ["BIP0340/challenge", "BIP0340/aux", "BIP0340/nonce", "TapLeaf", "TapBranch", "TapSighash", "TapTweak", "KeyAgg list", "KeyAgg coefficient"]; +export declare type TaggedHashPrefix = typeof TAGS[number]; +export declare function taggedHash(prefix: TaggedHashPrefix, data: Buffer): Buffer; +export {}; diff --git a/src/crypto.js b/src/crypto.js index f53b041..3c308da 100644 --- a/src/crypto.js +++ b/src/crypto.js @@ -1,6 +1,6 @@ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); -exports.hash256 = exports.hash160 = exports.sha256 = exports.sha1 = exports.ripemd160 = void 0; +exports.taggedHash = exports.hash256 = exports.hash160 = exports.sha256 = exports.sha1 = exports.ripemd160 = void 0; const createHash = require('create-hash'); function ripemd160(buffer) { try { @@ -34,3 +34,25 @@ function hash256(buffer) { return sha256(sha256(buffer)); } exports.hash256 = hash256; +const TAGS = [ + 'BIP0340/challenge', + 'BIP0340/aux', + 'BIP0340/nonce', + 'TapLeaf', + 'TapBranch', + 'TapSighash', + 'TapTweak', + 'KeyAgg list', + 'KeyAgg coefficient', +]; +/** An object mapping tags to their tagged hash prefix of [SHA256(tag) | SHA256(tag)] */ +const TAGGED_HASH_PREFIXES = Object.fromEntries( + TAGS.map(tag => { + const tagHash = sha256(Buffer.from(tag)); + return [tag, Buffer.concat([tagHash, tagHash])]; + }), +); +function taggedHash(prefix, data) { + return sha256(Buffer.concat([TAGGED_HASH_PREFIXES[prefix], data])); +} +exports.taggedHash = taggedHash; diff --git a/src/index.d.ts b/src/index.d.ts index 5b339ca..b93c2aa 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -5,6 +5,7 @@ import * as payments from './payments'; import * as script from './script'; export { address, crypto, networks, payments, script }; export { Block } from './block'; +export { TaggedHashPrefix } from './crypto'; export { Psbt, PsbtTxInput, PsbtTxOutput, Signer, SignerAsync, HDSigner, HDSignerAsync, } from './psbt'; export { OPS as opcodes } from './ops'; export { Transaction } from './transaction'; diff --git a/src/transaction.d.ts b/src/transaction.d.ts index c4de954..613706b 100644 --- a/src/transaction.d.ts +++ b/src/transaction.d.ts @@ -12,10 +12,13 @@ export interface Input { } export declare class Transaction { static readonly DEFAULT_SEQUENCE = 4294967295; + static readonly SIGHASH_DEFAULT = 0; static readonly SIGHASH_ALL = 1; static readonly SIGHASH_NONE = 2; static readonly SIGHASH_SINGLE = 3; static readonly SIGHASH_ANYONECANPAY = 128; + static readonly SIGHASH_OUTPUT_MASK = 3; + static readonly SIGHASH_INPUT_MASK = 128; static readonly ADVANCED_TRANSACTION_MARKER = 0; static readonly ADVANCED_TRANSACTION_FLAG = 1; static fromBuffer(buffer: Buffer, _NO_STRICT?: boolean): Transaction; @@ -42,6 +45,7 @@ export declare class Transaction { * This hash can then be used to sign the provided transaction input. */ hashForSignature(inIndex: number, prevOutScript: Buffer, hashType: number): Buffer; + hashForWitnessV1(inIndex: number, prevOutScripts: Buffer[], values: number[], hashType: number, leafHash?: Buffer, annex?: Buffer): Buffer; hashForWitnessV0(inIndex: number, prevOutScript: Buffer, value: number, hashType: number): Buffer; getHash(forWitness?: boolean): Buffer; getId(): string; diff --git a/src/transaction.js b/src/transaction.js index 5a29569..6f1382c 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -20,7 +20,7 @@ function vectorSize(someVector) { }, 0) ); } -const EMPTY_SCRIPT = Buffer.allocUnsafe(0); +const EMPTY_BUFFER = Buffer.allocUnsafe(0); const EMPTY_WITNESS = []; const ZERO = Buffer.from( '0000000000000000000000000000000000000000000000000000000000000000', @@ -32,7 +32,7 @@ const ONE = Buffer.from( ); const VALUE_UINT64_MAX = Buffer.from('ffffffffffffffff', 'hex'); const BLANK_OUTPUT = { - script: EMPTY_SCRIPT, + script: EMPTY_BUFFER, valueBuffer: VALUE_UINT64_MAX, }; function isOutput(out) { @@ -124,7 +124,7 @@ class Transaction { this.ins.push({ hash, index, - script: scriptSig || EMPTY_SCRIPT, + script: scriptSig || EMPTY_BUFFER, sequence: sequence, witness: EMPTY_WITNESS, }) - 1 @@ -247,7 +247,7 @@ class Transaction { } else { // "blank" others input scripts txTmp.ins.forEach(input => { - input.script = EMPTY_SCRIPT; + input.script = EMPTY_BUFFER; }); txTmp.ins[inIndex].script = ourScript; } @@ -257,6 +257,141 @@ class Transaction { txTmp.__toBuffer(buffer, 0, false); return bcrypto.hash256(buffer); } + hashForWitnessV1(inIndex, prevOutScripts, values, hashType, leafHash, annex) { + // https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#common-signature-message + typeforce( + types.tuple( + types.UInt32, + typeforce.arrayOf(types.Buffer), + typeforce.arrayOf(types.Satoshi), + types.UInt32, + ), + arguments, + ); + if ( + values.length !== this.ins.length || + prevOutScripts.length !== this.ins.length + ) { + throw new Error('Must supply prevout script and value for all inputs'); + } + const outputType = + hashType === Transaction.SIGHASH_DEFAULT + ? Transaction.SIGHASH_ALL + : hashType & Transaction.SIGHASH_OUTPUT_MASK; + const inputType = hashType & Transaction.SIGHASH_INPUT_MASK; + const isAnyoneCanPay = inputType === Transaction.SIGHASH_ANYONECANPAY; + const isNone = outputType === Transaction.SIGHASH_NONE; + const isSingle = outputType === Transaction.SIGHASH_SINGLE; + let hashPrevouts = EMPTY_BUFFER; + let hashAmounts = EMPTY_BUFFER; + let hashScriptPubKeys = EMPTY_BUFFER; + let hashSequences = EMPTY_BUFFER; + let hashOutputs = EMPTY_BUFFER; + if (!isAnyoneCanPay) { + let bufferWriter = bufferutils_1.BufferWriter.withCapacity( + 36 * this.ins.length, + ); + this.ins.forEach(txIn => { + bufferWriter.writeSlice(txIn.hash); + bufferWriter.writeUInt32(txIn.index); + }); + hashPrevouts = bcrypto.sha256(bufferWriter.end()); + bufferWriter = bufferutils_1.BufferWriter.withCapacity( + 8 * this.ins.length, + ); + values.forEach(value => bufferWriter.writeUInt64(value)); + hashAmounts = bcrypto.sha256(bufferWriter.end()); + bufferWriter = bufferutils_1.BufferWriter.withCapacity( + prevOutScripts.map(varSliceSize).reduce((a, b) => a + b), + ); + prevOutScripts.forEach(prevOutScript => + bufferWriter.writeVarSlice(prevOutScript), + ); + hashScriptPubKeys = bcrypto.sha256(bufferWriter.end()); + bufferWriter = bufferutils_1.BufferWriter.withCapacity( + 4 * this.ins.length, + ); + this.ins.forEach(txIn => bufferWriter.writeUInt32(txIn.sequence)); + hashSequences = bcrypto.sha256(bufferWriter.end()); + } + if (!(isNone || isSingle)) { + const txOutsSize = this.outs + .map(output => 8 + varSliceSize(output.script)) + .reduce((a, b) => a + b); + const bufferWriter = bufferutils_1.BufferWriter.withCapacity(txOutsSize); + this.outs.forEach(out => { + bufferWriter.writeUInt64(out.value); + bufferWriter.writeVarSlice(out.script); + }); + hashOutputs = bcrypto.sha256(bufferWriter.end()); + } else if (isSingle && inIndex < this.outs.length) { + const output = this.outs[inIndex]; + const bufferWriter = bufferutils_1.BufferWriter.withCapacity( + 8 + varSliceSize(output.script), + ); + bufferWriter.writeUInt64(output.value); + bufferWriter.writeVarSlice(output.script); + hashOutputs = bcrypto.sha256(bufferWriter.end()); + } + const spendType = (leafHash ? 2 : 0) + (annex ? 1 : 0); + // Length calculation from: + // https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#cite_note-14 + // With extension from: + // https://github.com/bitcoin/bips/blob/master/bip-0342.mediawiki#signature-validation + const sigMsgSize = + 174 - + (isAnyoneCanPay ? 49 : 0) - + (isNone ? 32 : 0) + + (annex ? 32 : 0) + + (leafHash ? 37 : 0); + const sigMsgWriter = bufferutils_1.BufferWriter.withCapacity(sigMsgSize); + sigMsgWriter.writeUInt8(hashType); + // Transaction + sigMsgWriter.writeInt32(this.version); + sigMsgWriter.writeUInt32(this.locktime); + sigMsgWriter.writeSlice(hashPrevouts); + sigMsgWriter.writeSlice(hashAmounts); + sigMsgWriter.writeSlice(hashScriptPubKeys); + sigMsgWriter.writeSlice(hashSequences); + if (!(isNone || isSingle)) { + sigMsgWriter.writeSlice(hashOutputs); + } + // Input + sigMsgWriter.writeUInt8(spendType); + if (isAnyoneCanPay) { + const input = this.ins[inIndex]; + sigMsgWriter.writeSlice(input.hash); + sigMsgWriter.writeUInt32(input.index); + sigMsgWriter.writeUInt64(values[inIndex]); + sigMsgWriter.writeVarSlice(prevOutScripts[inIndex]); + sigMsgWriter.writeUInt32(input.sequence); + } else { + sigMsgWriter.writeUInt32(inIndex); + } + if (annex) { + const bufferWriter = bufferutils_1.BufferWriter.withCapacity( + varSliceSize(annex), + ); + bufferWriter.writeVarSlice(annex); + sigMsgWriter.writeSlice(bcrypto.sha256(bufferWriter.end())); + } + // Output + if (isSingle) { + sigMsgWriter.writeSlice(hashOutputs); + } + // BIP342 extension + if (leafHash) { + sigMsgWriter.writeSlice(leafHash); + sigMsgWriter.writeUInt8(0); + sigMsgWriter.writeUInt32(0xffffffff); + } + // Extra zero byte because: + // https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#cite_note-19 + return bcrypto.taggedHash( + 'TapSighash', + Buffer.concat([Buffer.of(0x00), sigMsgWriter.end()]), + ); + } hashForWitnessV0(inIndex, prevOutScript, value, hashType) { typeforce( types.tuple(types.UInt32, types.Buffer, types.Satoshi, types.UInt32), @@ -396,9 +531,12 @@ class Transaction { } exports.Transaction = Transaction; Transaction.DEFAULT_SEQUENCE = 0xffffffff; +Transaction.SIGHASH_DEFAULT = 0x00; Transaction.SIGHASH_ALL = 0x01; Transaction.SIGHASH_NONE = 0x02; Transaction.SIGHASH_SINGLE = 0x03; Transaction.SIGHASH_ANYONECANPAY = 0x80; +Transaction.SIGHASH_OUTPUT_MASK = 0x03; +Transaction.SIGHASH_INPUT_MASK = 0x80; Transaction.ADVANCED_TRANSACTION_MARKER = 0x00; Transaction.ADVANCED_TRANSACTION_FLAG = 0x01; diff --git a/test/bufferutils.spec.ts b/test/bufferutils.spec.ts index 213b156..0f1f1a9 100644 --- a/test/bufferutils.spec.ts +++ b/test/bufferutils.spec.ts @@ -66,6 +66,13 @@ describe('bufferutils', () => { ); } + it('withCapacity', () => { + const expectedBuffer = Buffer.from('04030201', 'hex'); + const withCapacity = BufferWriter.withCapacity(4); + withCapacity.writeInt32(0x01020304); + testBuffer(withCapacity, expectedBuffer); + }); + it('writeUint8', () => { const values = [0, 1, 254, 255]; const expectedBuffer = Buffer.from([0, 1, 0xfe, 0xff]); @@ -277,6 +284,16 @@ describe('bufferutils', () => { }); testBuffer(bufferWriter, expectedBuffer); }); + + it('end', () => { + const expected = Buffer.from('0403020108070605', 'hex'); + const bufferWriter = BufferWriter.withCapacity(8); + bufferWriter.writeUInt32(0x01020304); + bufferWriter.writeUInt32(0x05060708); + const result = bufferWriter.end(); + testBuffer(bufferWriter, result); + testBuffer(bufferWriter, expected); + }); }); describe('BufferReader', () => { diff --git a/test/crypto.spec.ts b/test/crypto.spec.ts index 89ffabb..0482ec9 100644 --- a/test/crypto.spec.ts +++ b/test/crypto.spec.ts @@ -1,12 +1,12 @@ import * as assert from 'assert'; import { describe, it } from 'mocha'; -import { crypto as bcrypto } from '..'; +import { crypto as bcrypto, TaggedHashPrefix } from '..'; import * as fixtures from './fixtures/crypto.json'; describe('crypto', () => { ['hash160', 'hash256', 'ripemd160', 'sha1', 'sha256'].forEach(algorithm => { describe(algorithm, () => { - fixtures.forEach(f => { + fixtures.hashes.forEach(f => { const fn = (bcrypto as any)[algorithm]; const expected = (f as any)[algorithm]; @@ -19,4 +19,15 @@ describe('crypto', () => { }); }); }); + + describe('taggedHash', () => { + fixtures.taggedHash.forEach(f => { + const bytes = Buffer.from(f.hex, 'hex'); + const expected = Buffer.from(f.result, 'hex'); + it(`returns ${f.result} for taggedHash "${f.tag}" of ${f.hex}`, () => { + const actual = bcrypto.taggedHash(f.tag as TaggedHashPrefix, bytes); + assert.strictEqual(actual.toString('hex'), expected.toString('hex')); + }); + }); + }); }); diff --git a/test/fixtures/crypto.json b/test/fixtures/crypto.json index 102b50a..1d1976b 100644 --- a/test/fixtures/crypto.json +++ b/test/fixtures/crypto.json @@ -1,34 +1,43 @@ -[ - { - "hex": "0000000000000001", - "hash160": "cdb00698f02afd929ffabea308340fa99ac2afa8", - "hash256": "3ae5c198d17634e79059c2cd735491553d22c4e09d1d9fea3ecf214565df2284", - "ripemd160": "8d1a05d1bc08870968eb8a81ad4393fd3aac6633", - "sha1": "cb473678976f425d6ec1339838f11011007ad27d", - "sha256": "cd2662154e6d76b2b2b92e70c0cac3ccf534f9b74eb5b89819ec509083d00a50" - }, - { - "hex": "0101010101010101", - "hash160": "abaf1119f83e384210fe8e222eac76e2f0da39dc", - "hash256": "728338d99f356175c4945ef5cccfa61b7b56143cbbf426ddd0e0fc7cfe8c3c23", - "ripemd160": "5825701b4b9767fd35063b286dca3582853e0630", - "sha1": "c0357a32ed1f6a03be92dd094476f7f1a2e214ec", - "sha256": "04abc8821a06e5a30937967d11ad10221cb5ac3b5273e434f1284ee87129a061" - }, - { - "hex": "ffffffffffffffff", - "hash160": "f86221f5a1fca059a865c0b7d374dfa9d5f3aeb4", - "hash256": "752adad0a7b9ceca853768aebb6965eca126a62965f698a0c1bc43d83db632ad", - "ripemd160": "cb760221600ed34337ca3ab70016b5f58c838120", - "sha1": "be673e8a56eaa9d8c1d35064866701c11ef8e089", - "sha256": "12a3ae445661ce5dee78d0650d33362dec29c4f82af05e7e57fb595bbbacf0ca" - }, - { - "hex": "4c6f72656d20697073756d20646f6c6f722073697420616d65742c20636f6e73656374657475722061646970697363696e6720656c69742e20446f6e65632061742066617563696275732073617069656e2c2076656c20666163696c6973697320617263752e20536564207574206d61737361206e6962682e205574206d6f6c6c69732070756c76696e6172206d617373612e20557420756c6c616d636f7270657220646f6c6f7220656e696d2c20696e206d6f6c657374696520656e696d20636f6e64696d656e74756d2061632e20416c697175616d206572617420766f6c75747061742e204e756c6c6120736f64616c657320617420647569206e656320", - "hash160": "9763e6b367c363bd6b88a7b361c98e6beee243a5", - "hash256": "033588797115feb3545052670cac2a46584ab3cb460de63756ee0275e66b5799", - "ripemd160": "cad8593dcdef12ee334c97bab9787f07b3f3a1a5", - "sha1": "10d96fb43aca84e342206887bbeed3065d4e4344", - "sha256": "a7fb8276035057ed6479c5f2305a96da100ac43f0ac10f277e5ab8c5457429da" - } -] +{ + "hashes": [ + { + "hex": "0000000000000001", + "hash160": "cdb00698f02afd929ffabea308340fa99ac2afa8", + "hash256": "3ae5c198d17634e79059c2cd735491553d22c4e09d1d9fea3ecf214565df2284", + "ripemd160": "8d1a05d1bc08870968eb8a81ad4393fd3aac6633", + "sha1": "cb473678976f425d6ec1339838f11011007ad27d", + "sha256": "cd2662154e6d76b2b2b92e70c0cac3ccf534f9b74eb5b89819ec509083d00a50" + }, + { + "hex": "0101010101010101", + "hash160": "abaf1119f83e384210fe8e222eac76e2f0da39dc", + "hash256": "728338d99f356175c4945ef5cccfa61b7b56143cbbf426ddd0e0fc7cfe8c3c23", + "ripemd160": "5825701b4b9767fd35063b286dca3582853e0630", + "sha1": "c0357a32ed1f6a03be92dd094476f7f1a2e214ec", + "sha256": "04abc8821a06e5a30937967d11ad10221cb5ac3b5273e434f1284ee87129a061" + }, + { + "hex": "ffffffffffffffff", + "hash160": "f86221f5a1fca059a865c0b7d374dfa9d5f3aeb4", + "hash256": "752adad0a7b9ceca853768aebb6965eca126a62965f698a0c1bc43d83db632ad", + "ripemd160": "cb760221600ed34337ca3ab70016b5f58c838120", + "sha1": "be673e8a56eaa9d8c1d35064866701c11ef8e089", + "sha256": "12a3ae445661ce5dee78d0650d33362dec29c4f82af05e7e57fb595bbbacf0ca" + }, + { + "hex": "4c6f72656d20697073756d20646f6c6f722073697420616d65742c20636f6e73656374657475722061646970697363696e6720656c69742e20446f6e65632061742066617563696275732073617069656e2c2076656c20666163696c6973697320617263752e20536564207574206d61737361206e6962682e205574206d6f6c6c69732070756c76696e6172206d617373612e20557420756c6c616d636f7270657220646f6c6f7220656e696d2c20696e206d6f6c657374696520656e696d20636f6e64696d656e74756d2061632e20416c697175616d206572617420766f6c75747061742e204e756c6c6120736f64616c657320617420647569206e656320", + "hash160": "9763e6b367c363bd6b88a7b361c98e6beee243a5", + "hash256": "033588797115feb3545052670cac2a46584ab3cb460de63756ee0275e66b5799", + "ripemd160": "cad8593dcdef12ee334c97bab9787f07b3f3a1a5", + "sha1": "10d96fb43aca84e342206887bbeed3065d4e4344", + "sha256": "a7fb8276035057ed6479c5f2305a96da100ac43f0ac10f277e5ab8c5457429da" + } + ], + "taggedHash": [ + { + "tag": "TapTweak", + "hex": "0101010101010101", + "result": "71ae15bad52efcecf4c9f672bfbded68a4adb8258f1b95f0d06aefdb5ebd14e9" + } + ] +} \ No newline at end of file diff --git a/test/fixtures/transaction.json b/test/fixtures/transaction.json index 6bf6090..979549a 100644 --- a/test/fixtures/transaction.json +++ b/test/fixtures/transaction.json @@ -808,6 +808,87 @@ "value": 987654321 } ], + "taprootSigning": [ + { + "description": "P2TR key path", + "utxos": [ + { + "scriptHex": "512053a1f6e454df1aa2776a2814a721372d6258050de330b3c6d10ee8f4e0dda343", + "value": 420000000 + }, + { + "scriptHex": "5120147c9c57132f6e7ecddba9800bb0c4449251c92a1e60371ee77557b6620f3ea3", + "value": 462000000 + }, + { + "scriptHex": "76a914751e76e8199196d454941c45d1b3a323f1433bd688ac", + "value": 294000000 + }, + { + "scriptHex": "5120e4d810fd50586274face62b8a807eb9719cef49c04177cc6b76a9a4251d5450e", + "value": 504000000 + }, + { + "scriptHex": "512091b64d5324723a985170e4dc5a0f84c041804f2cd12660fa5dec09fc21783605", + "value": 630000000 + }, + { + "scriptHex": "00147dd65592d0ab2fe0d0257d571abf032cd9db93dc", + "value": 378000000 + }, + { + "scriptHex": "512075169f4001aa68f15bbed28b218df1d0a62cbbcf1188c6665110c293c907b831", + "value": 672000000 + }, + { + "scriptHex": "51200f63ca2c7639b9bb4be0465cc0aa3ee78a0761ba5f5f7d6ff8eab340f09da561", + "value": 546000000 + }, + { + "scriptHex": "5120053690babeabbb7850c32eead0acf8df990ced79f7a31e358fabf2658b4bc587", + "value": 588000000 + } + ], + "txHex": "02000000097de20cbff686da83a54981d2b9bab3586f4ca7e48f57f5b55963115f3b334e9c010000000000000000d7b7cab57b1393ace2d064f4d4a2cb8af6def61273e127517d44759b6dafdd990000000000fffffffff8e1f583384333689228c5d28eac13366be082dc57441760d957275419a418420000000000fffffffff0689180aa63b30cb162a73c6d2a38b7eeda2a83ece74310fda0843ad604853b0100000000feffffff0c638ca38362001f5e128a01ae2b379288eb22cfaf903652b2ec1c88588f487a0000000000feffffff956149bdc66faa968eb2be2d2faa29718acbfe3941215893a2a3446d32acd05000000000000000000081efa267f1f0e46e054ecec01773de7c844721e010c2db5d5864a6a6b53e013a010000000000000000a690669c3c4a62507d93609810c6de3f99d1a6e311fe39dd23683d695c07bdee0000000000ffffffff727ab5f877438496f8613ca84002ff38e8292f7bd11f0a9b9b83ebd16779669e0100000000ffffffff0200ca9a3b000000001976a91406afd46bcdfd22ef94ac122aa11f241244a37ecc88ac807840cb0000000020ac9a87f5594be208f8532db38cff670c450ed2fea8fcdefcc9a663f78bab962b0065cd1d", + "cases": [ + { + "vin": 0, + "typeHex": "03", + "hash": "7e584883b084ace0469c6962a9a7d2a9060e1f3c218ab40d32c77651482122bc" + }, + { + "vin": 1, + "typeHex": "83", + "hash": "325a644af47e8a5a2591cda0ab0723978537318f10e6a63d4eed783b96a71a4d" + }, + { + "vin": 3, + "typeHex": "01", + "hash": "6ffd256e108685b41831385f57eebf2fca041bc6b5e607ea11b3e03d4cf9d9ba" + }, + { + "vin": 4, + "typeHex": "00", + "hash": "9f90136737540ccc18707e1fd398ad222a1a7e4dd65cbfd22dbe4660191efa58" + }, + { + "vin": 6, + "typeHex": "02", + "hash": "835c9ab6084ed9a8ae9b7cda21e0aa797aca3b76a54bd1e3c7db093f6c57e23f" + }, + { + "vin": 7, + "typeHex": "82", + "hash": "df1cca638283c667084b8ffe6bf6e116cc5a53cf7ae1202c5fee45a9085f1ba5" + }, + { + "vin": 8, + "typeHex": "81", + "hash": "30319859ca79ea1b7a9782e9daebc46e4ca4ca2bc04c9c53b2ec87fa83a526bd" + } + ] + } + ], "invalid": { "addInput": [ { diff --git a/test/transaction.spec.ts b/test/transaction.spec.ts index 6744545..13d64d1 100644 --- a/test/transaction.spec.ts +++ b/test/transaction.spec.ts @@ -328,6 +328,27 @@ describe('Transaction', () => { }); }); + describe('taprootSigning', () => { + fixtures.taprootSigning.forEach(f => { + const tx = Transaction.fromHex(f.txHex); + const prevOutScripts = f.utxos.map(({ scriptHex }) => + Buffer.from(scriptHex, 'hex'), + ); + const values = f.utxos.map(({ value }) => value); + + f.cases.forEach(c => { + let hash: Buffer; + + it(`should hash to ${c.hash} for ${f.description}:${c.vin}`, () => { + const hashType = Buffer.from(c.typeHex, 'hex').readUInt8(0); + + hash = tx.hashForWitnessV1(c.vin, prevOutScripts, values, hashType); + assert.strictEqual(hash.toString('hex'), c.hash); + }); + }); + }); + }); + describe('setWitness', () => { it('only accepts a a witness stack (Array of Buffers)', () => { assert.throws(() => { diff --git a/ts_src/bufferutils.ts b/ts_src/bufferutils.ts index 43171c4..901d72a 100644 --- a/ts_src/bufferutils.ts +++ b/ts_src/bufferutils.ts @@ -58,6 +58,10 @@ export function cloneBuffer(buffer: Buffer): Buffer { * Helper class for serialization of bitcoin data types into a pre-allocated buffer. */ export class BufferWriter { + static withCapacity(size: number): BufferWriter { + return new BufferWriter(Buffer.alloc(size)); + } + constructor(public buffer: Buffer, public offset: number = 0) { typeforce(types.tuple(types.Buffer, types.UInt32), [buffer, offset]); } @@ -99,6 +103,13 @@ export class BufferWriter { this.writeVarInt(vector.length); vector.forEach((buf: Buffer) => this.writeVarSlice(buf)); } + + end(): Buffer { + if (this.buffer.length === this.offset) { + return this.buffer; + } + throw new Error(`buffer size ${this.buffer.length}, offset ${this.offset}`); + } } /** diff --git a/ts_src/crypto.ts b/ts_src/crypto.ts index 7f69c40..b7c355a 100644 --- a/ts_src/crypto.ts +++ b/ts_src/crypto.ts @@ -31,3 +31,27 @@ export function hash160(buffer: Buffer): Buffer { export function hash256(buffer: Buffer): Buffer { return sha256(sha256(buffer)); } + +const TAGS = [ + 'BIP0340/challenge', + 'BIP0340/aux', + 'BIP0340/nonce', + 'TapLeaf', + 'TapBranch', + 'TapSighash', + 'TapTweak', + 'KeyAgg list', + 'KeyAgg coefficient', +] as const; +export type TaggedHashPrefix = typeof TAGS[number]; +/** An object mapping tags to their tagged hash prefix of [SHA256(tag) | SHA256(tag)] */ +const TAGGED_HASH_PREFIXES = Object.fromEntries( + TAGS.map(tag => { + const tagHash = sha256(Buffer.from(tag)); + return [tag, Buffer.concat([tagHash, tagHash])]; + }), +) as { [k in TaggedHashPrefix]: Buffer }; + +export function taggedHash(prefix: TaggedHashPrefix, data: Buffer): Buffer { + return sha256(Buffer.concat([TAGGED_HASH_PREFIXES[prefix], data])); +} diff --git a/ts_src/index.ts b/ts_src/index.ts index 4cc82bf..d8b8619 100644 --- a/ts_src/index.ts +++ b/ts_src/index.ts @@ -7,6 +7,7 @@ import * as script from './script'; export { address, crypto, networks, payments, script }; export { Block } from './block'; +export { TaggedHashPrefix } from './crypto'; export { Psbt, PsbtTxInput, diff --git a/ts_src/transaction.ts b/ts_src/transaction.ts index c5dde9a..416f20e 100644 --- a/ts_src/transaction.ts +++ b/ts_src/transaction.ts @@ -27,7 +27,7 @@ function vectorSize(someVector: Buffer[]): number { ); } -const EMPTY_SCRIPT: Buffer = Buffer.allocUnsafe(0); +const EMPTY_BUFFER: Buffer = Buffer.allocUnsafe(0); const EMPTY_WITNESS: Buffer[] = []; const ZERO: Buffer = Buffer.from( '0000000000000000000000000000000000000000000000000000000000000000', @@ -39,7 +39,7 @@ const ONE: Buffer = Buffer.from( ); const VALUE_UINT64_MAX: Buffer = Buffer.from('ffffffffffffffff', 'hex'); const BLANK_OUTPUT = { - script: EMPTY_SCRIPT, + script: EMPTY_BUFFER, valueBuffer: VALUE_UINT64_MAX, }; @@ -62,10 +62,13 @@ export interface Input { export class Transaction { static readonly DEFAULT_SEQUENCE = 0xffffffff; + static readonly SIGHASH_DEFAULT = 0x00; static readonly SIGHASH_ALL = 0x01; static readonly SIGHASH_NONE = 0x02; static readonly SIGHASH_SINGLE = 0x03; static readonly SIGHASH_ANYONECANPAY = 0x80; + static readonly SIGHASH_OUTPUT_MASK = 0x03; + static readonly SIGHASH_INPUT_MASK = 0x80; static readonly ADVANCED_TRANSACTION_MARKER = 0x00; static readonly ADVANCED_TRANSACTION_FLAG = 0x01; @@ -174,7 +177,7 @@ export class Transaction { this.ins.push({ hash, index, - script: scriptSig || EMPTY_SCRIPT, + script: scriptSig || EMPTY_BUFFER, sequence: sequence as number, witness: EMPTY_WITNESS, }) - 1 @@ -326,7 +329,7 @@ export class Transaction { } else { // "blank" others input scripts txTmp.ins.forEach(input => { - input.script = EMPTY_SCRIPT; + input.script = EMPTY_BUFFER; }); txTmp.ins[inIndex].script = ourScript; } @@ -339,6 +342,158 @@ export class Transaction { return bcrypto.hash256(buffer); } + hashForWitnessV1( + inIndex: number, + prevOutScripts: Buffer[], + values: number[], + hashType: number, + leafHash?: Buffer, + annex?: Buffer, + ): Buffer { + // https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#common-signature-message + typeforce( + types.tuple( + types.UInt32, + typeforce.arrayOf(types.Buffer), + typeforce.arrayOf(types.Satoshi), + types.UInt32, + ), + arguments, + ); + + if ( + values.length !== this.ins.length || + prevOutScripts.length !== this.ins.length + ) { + throw new Error('Must supply prevout script and value for all inputs'); + } + + const outputType = + hashType === Transaction.SIGHASH_DEFAULT + ? Transaction.SIGHASH_ALL + : hashType & Transaction.SIGHASH_OUTPUT_MASK; + + const inputType = hashType & Transaction.SIGHASH_INPUT_MASK; + + const isAnyoneCanPay = inputType === Transaction.SIGHASH_ANYONECANPAY; + const isNone = outputType === Transaction.SIGHASH_NONE; + const isSingle = outputType === Transaction.SIGHASH_SINGLE; + + let hashPrevouts = EMPTY_BUFFER; + let hashAmounts = EMPTY_BUFFER; + let hashScriptPubKeys = EMPTY_BUFFER; + let hashSequences = EMPTY_BUFFER; + let hashOutputs = EMPTY_BUFFER; + + if (!isAnyoneCanPay) { + let bufferWriter = BufferWriter.withCapacity(36 * this.ins.length); + this.ins.forEach(txIn => { + bufferWriter.writeSlice(txIn.hash); + bufferWriter.writeUInt32(txIn.index); + }); + hashPrevouts = bcrypto.sha256(bufferWriter.end()); + + bufferWriter = BufferWriter.withCapacity(8 * this.ins.length); + values.forEach(value => bufferWriter.writeUInt64(value)); + hashAmounts = bcrypto.sha256(bufferWriter.end()); + + bufferWriter = BufferWriter.withCapacity( + prevOutScripts.map(varSliceSize).reduce((a, b) => a + b), + ); + prevOutScripts.forEach(prevOutScript => + bufferWriter.writeVarSlice(prevOutScript), + ); + hashScriptPubKeys = bcrypto.sha256(bufferWriter.end()); + + bufferWriter = BufferWriter.withCapacity(4 * this.ins.length); + this.ins.forEach(txIn => bufferWriter.writeUInt32(txIn.sequence)); + hashSequences = bcrypto.sha256(bufferWriter.end()); + } + + if (!(isNone || isSingle)) { + const txOutsSize = this.outs + .map(output => 8 + varSliceSize(output.script)) + .reduce((a, b) => a + b); + const bufferWriter = BufferWriter.withCapacity(txOutsSize); + + this.outs.forEach(out => { + bufferWriter.writeUInt64(out.value); + bufferWriter.writeVarSlice(out.script); + }); + + hashOutputs = bcrypto.sha256(bufferWriter.end()); + } else if (isSingle && inIndex < this.outs.length) { + const output = this.outs[inIndex]; + + const bufferWriter = BufferWriter.withCapacity( + 8 + varSliceSize(output.script), + ); + bufferWriter.writeUInt64(output.value); + bufferWriter.writeVarSlice(output.script); + hashOutputs = bcrypto.sha256(bufferWriter.end()); + } + + const spendType = (leafHash ? 2 : 0) + (annex ? 1 : 0); + + // Length calculation from: + // https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#cite_note-14 + // With extension from: + // https://github.com/bitcoin/bips/blob/master/bip-0342.mediawiki#signature-validation + const sigMsgSize = + 174 - + (isAnyoneCanPay ? 49 : 0) - + (isNone ? 32 : 0) + + (annex ? 32 : 0) + + (leafHash ? 37 : 0); + const sigMsgWriter = BufferWriter.withCapacity(sigMsgSize); + + sigMsgWriter.writeUInt8(hashType); + // Transaction + sigMsgWriter.writeInt32(this.version); + sigMsgWriter.writeUInt32(this.locktime); + sigMsgWriter.writeSlice(hashPrevouts); + sigMsgWriter.writeSlice(hashAmounts); + sigMsgWriter.writeSlice(hashScriptPubKeys); + sigMsgWriter.writeSlice(hashSequences); + if (!(isNone || isSingle)) { + sigMsgWriter.writeSlice(hashOutputs); + } + // Input + sigMsgWriter.writeUInt8(spendType); + if (isAnyoneCanPay) { + const input = this.ins[inIndex]; + sigMsgWriter.writeSlice(input.hash); + sigMsgWriter.writeUInt32(input.index); + sigMsgWriter.writeUInt64(values[inIndex]); + sigMsgWriter.writeVarSlice(prevOutScripts[inIndex]); + sigMsgWriter.writeUInt32(input.sequence); + } else { + sigMsgWriter.writeUInt32(inIndex); + } + if (annex) { + const bufferWriter = BufferWriter.withCapacity(varSliceSize(annex)); + bufferWriter.writeVarSlice(annex); + sigMsgWriter.writeSlice(bcrypto.sha256(bufferWriter.end())); + } + // Output + if (isSingle) { + sigMsgWriter.writeSlice(hashOutputs); + } + // BIP342 extension + if (leafHash) { + sigMsgWriter.writeSlice(leafHash); + sigMsgWriter.writeUInt8(0); + sigMsgWriter.writeUInt32(0xffffffff); + } + + // Extra zero byte because: + // https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#cite_note-19 + return bcrypto.taggedHash( + 'TapSighash', + Buffer.concat([Buffer.of(0x00), sigMsgWriter.end()]), + ); + } + hashForWitnessV0( inIndex: number, prevOutScript: Buffer,