From 318bfe9bdb83edf75d99f7385dcd174f870c9640 Mon Sep 17 00:00:00 2001 From: cmd Date: Tue, 14 Mar 2023 18:22:02 -0500 Subject: [PATCH] update --- README.md | 250 +++++++++++++++++++++++++++++++------- src/index.ts | 20 +-- src/lib/script/decode.ts | 57 ++------- src/lib/script/words.ts | 67 +++++++++- src/lib/sig/taproot.ts | 42 +++---- src/lib/tap/proof.ts | 85 +++++++++++++ src/lib/tap/script.ts | 168 ++----------------------- src/lib/tap/tweak.ts | 76 ++++++++++++ src/lib/tap/types.ts | 2 +- src/lib/tx/decode.ts | 12 +- src/lib/tx/encode.ts | 43 +++++-- src/schema/types.ts | 6 +- test/scratch.ts | 20 +-- test/src/tap/sig.test.ts | 6 +- test/src/tap/tree.test.ts | 12 +- test/src/tap/unit.test.ts | 8 +- yarn.lock | 21 +++- 17 files changed, 573 insertions(+), 322 deletions(-) create mode 100644 src/lib/tap/proof.ts create mode 100644 src/lib/tap/tweak.ts diff --git a/README.md b/README.md index b7f6ba0..4ad100d 100644 --- a/README.md +++ b/README.md @@ -11,77 +11,245 @@ import * as BTON from '@cmdcode/tapscript' ``` ## How to Use + +The BTON library provides a number of tools for working with bitcoin script. + +### Script Tool. + +This tool helps with parsing / serializing scripts. + ```ts BTON.script = { + // Encode a JSON formatted script into hex. encode : (script : ScriptData, varint = true) => string, + // Decode a hex formatted script into JSON. decode : (script : string) => ScriptData } +``` + +### Signature Tool. + +This tool helps with signatures and validation. -BTON.sig = { - segwit: { - hash : // Calculate the signature hash. - } - taproot: { - hash : // Calulate the signature hash. - sign : // Sign a transaction. - verify : // Verify a signed transaction. - tweakPubkey : // Tweak a public key. - tweakPrvkey : // Tweak a private key. - } +```ts +BTON.sig.taproot = { + // Calculate the signature hash for a transaction. + hash : ( + txdata : TxData | string | Uint8Array, + index : number, + config : HashConfig = {} + ) => Promise, + // Sign a transaction using your *tweaked* private key. + sign : ( + prvkey : string | Uint8Array, + txdata : TxData | string | Uint8Array, + index : number, + config : HashConfig = {} + ) => Promise, + // Verify a transaction using the included tapkey (or specify a pubkey). + verify : ( + txdata : TxData | string | Uint8Array, + index : number, + config : HashConfig = {}, + shouldThrow = false + ) => Promise +} + +interface HashConfig { + extension ?: Bytes // Include a tapleaf hash with your signature hash. + pubkey ?: Bytes // Verify using this pubkey instead of the tapkey. + sigflag ?: number // Set the signature type flag. + separator_pos ?: number // If using OP_CODESEPARATOR, specify the latest opcode position. + extflag ?: number // Set the extention version flag (future use). + key_version ?: number // Set the key version flag (future use). } +``` + +### Tap Tool + +This tool helps with creating a tree of scripts / data, plus creating the proofs to validate / spend them. +```ts BTON.tap = { - // Returns a 'hashtag' used for padding. - getTag - // Returns a 'tapleaf' used for building a tree. - getLeaf - // Returns a 'branch' which combines two leaves. - getBranch - // Returns the merkle root of a tree. - getRoot - // Returns a 'taptweak' which is used to tweak the internal key. - getTweak - // Returns the merkle-proof needed for validating a tapleaf. - getPath - // Checks if a merkle-proof is valid for a given tapleaf. - checkPath + // Returns a 'hashtag' used for padding. Mainly for internal use. + getTag : (tag : string) => Promise, + + // Returns a 'tapleaf' used for building a tree. + getLeaf : ( + data : string | Uint8Array, + version ?: number + ) => Promise, + + // Returns a 'branch' which combines two leaves (or branches). + getBranch : ( + leafA : string, + leafB : string + ) => Promise, + + // Returns the root hash of a tree. + getRoot : ( + leaves : TapTree + ) => Promise, + + // Returns the 'control block' path needed for validating a tapleaf. + getPath : ( + pubkey : string | Uint8Array, + target : string, + taptree : TapTree = [ target ], + version ?: number + parity ?: 0 | 1 + ) : Promise, + + // Checks if a path is valid for a given tapleaf. + checkPath : ( + tapkey : string | Uint8Array, + cblock : string | Uint8Array, + target : string + ) => Promise, + // Encodes a public key into a taproot address. - encodeAddress + encodeAddress : ( + tapkey : string | Uint8Array, + prefix ?: string + ) => string, + // Decodes a taproot address into a public key. - decodeAddress + decodeAddress : ( + address : string + ) => Uint8Array +} +``` + +### Tweak Tool + +This tool helps with tweaking public / secret (private) keys. + +```ts +BTON.tweak = { + // Return a tweaked private key using the provided TapTree. + getSeckey : ( + seckey : string | Uint8Array, + leaves : TapTree = [] + ) => Promise, + + // Return a tweaked public key using the provided TapTree. + getPubkey : ( + pubkey : string | Uint8Array, + leaves : TapTree = [] +) : Promise, + + // Return a 'taptweak' which is used for key tweaking. + getTweak : ( + pubkey : string | Uint8Array, + tweak : string | Uint8Array + ) => Promise, + + // Return a tweaked secret key using the provided tweak. + tweakSeckey : ( + prvkey : string | Uint8Array, + tweak : string | Uint8Array + ) => Uint8Array, + + // Return a tweaked public key using the provided tweak. + tweakPubkey : ( + pubkey : string | Uint8Array, + tweak : string | Uint8Array + ) => Uint8Array } +// A tree is an array of leaves, formatted as strings. +// These arrays can also be nested in multiple layers. +type TapTree = Array + +type TapKey = [ + tapkey : string, // The tweaked public key. + parity : number // 0 or 1 depending on whether the key was even / odd. +] + +``` + +### Tx Tool. + +This tool helps with parsing / serializing transaction data. + +```ts BTON.tx = { // Serializes a JSON transaction into a hex-encoded string. - encode : (txObject, options) => 'hex encoded string', + encode : ( + txdata : TxData, // The transaction JSON. + omitWitness ?: boolean // If you wish to omit the witness data. + ) => string, + // Parses a hex-encoded transaction into a JSON object. - decode : (scriptArr, options) => 'hex encoded string' + decode : ( + bytes : string | Uint8Array + ) => TxData +} + +interface TxData { + version : number // The transaction verion. + input : InputData[] // Ann array of transaction inputs. + output : OutputData[] // An array of transaction outputs. + locktime : LockData // The locktime of the transaction. +} + +interface InputData { + txid : string // The txid of the UTXO being spent. + vout : number // The output index of the UTXO being spent. + prevout ?: OutputData // The output data of the UTXO being spent. + scriptSig ?: ScriptData // The ScriptSig field (mostly deprecated). + sequence ?: SequenceData // The sequence field for the input. + witness ?: WitnessData // An array of witness data for the input. } + +interface OutputData { + value : number | bigint // The satoshi value of the output. + scriptPubKey ?: ScriptData // The locking script data. + address ?: string // (optional) provide a locking script +} // that is encoded as an address. + +type SequenceData = string | number +type ScriptData = Bytes | WordArray +type WitnessData = ScriptData[] +type LockData = number +type WordArray = Word[] +type Word = string | number | Uint8Array +type Bytes = string | Uint8Array ``` ## Example Transaction Object + ```ts -interface TxData { - version : number - input : [ +const txdata = { + version : 2 + input: [ { - txid : string - vout : number - scriptSig : string | string[] - sequence : number - prevout : { value : number | bigint, scriptPubKey : string | string[] } - witness : Array + txid : '1351f611fa0ae6124d0f55c625ae5c929ca09ae93f9e88656a4a82d160d99052', + vout : 0, + prevout : { + value: 10000, + scriptPubkey: '512005a18fccd1909f3317e4dd7f11257e4428884902b1c7466c34e7f490e0e627da' + + }, + sequence : 0xfffffffd, + witness : [] } ], output : [ - { value: number | bigint, scriptPubkey: string | string[] } + { + value: 9000, + address: 'bcrt1pqksclnx3jz0nx9lym4l3zft7gs5gsjgzk8r5vmp5ul6fpc8xyldqaxu8ys' + } ], - locktime: number + locktime: 0 } ``` +## Bugs / Issues +If you run into any bugs or have any questions, please submit an issue ticket. + ## Contribution -Feel free to fork and make contributions. Suggestions are also welcome! +Feel free to fork and make contributions. Suggestions are welcome! ## License Use this library however you want! diff --git a/src/index.ts b/src/index.ts index bc6710f..ec1750e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,8 +3,11 @@ import { decodeTx } from './lib/tx/decode.js' import { encodeScript } from './lib/script/encode.js' import { decodeScript } from './lib/script/decode.js' import { segwitHash } from './lib/sig/segwit.js' + import * as SIG from './lib/sig/taproot.js' import * as TAP from './lib/tap/script.js' +import * as TWK from './lib/tap/tweak.js' +import * as CHK from './lib/tap/proof.js' export * from './schema/types.js' @@ -29,17 +32,20 @@ export const Tap = { getLeaf : TAP.getTapLeaf, getBranch : TAP.getTapBranch, getRoot : TAP.getTapRoot, - getTweak : TAP.getTapTweak, - getPubkey : TAP.getTapPubkey, - getSeckey : TAP.getTapSeckey, - getPath : TAP.getTapPath, - checkPath : TAP.checkTapPath, - tweakSeckey : TAP.tweakPrvkey, - tweakPubkey : TAP.tweakPubkey, + getPath : CHK.getTapPath, + checkPath : CHK.checkTapPath, encodeAddress : TAP.encodeTapAddress, decodeAddress : TAP.decodeTapAddress } +export const Tweak = { + getPubkey : TWK.getTapPubkey, + getSeckey : TWK.getTapSeckey, + getTweak : TWK.getTapTweak, + tweakSeckey : TWK.tweakPrvkey, + tweakPubkey : TWK.tweakPubkey +} + export const Tx = { encode : encodeTx, decode : decodeTx diff --git a/src/lib/script/decode.ts b/src/lib/script/decode.ts index 6bd1bf0..c49aac2 100644 --- a/src/lib/script/decode.ts +++ b/src/lib/script/decode.ts @@ -1,5 +1,10 @@ import { Buff, Stream } from '@cmdcode/buff-utils' -import { getOpLabel } from './words.js' + +import { + getOpLabel, + getWordType, + isValidWord +} from './words.js' import { ScriptData, WordArray } from '../../schema/types.js' @@ -21,6 +26,11 @@ export function normalizeData ( return Buff.normalize(script) } +export function decodeAddress (address : string) : string { + // if (address.length === 20) + return address +} + export function decodeWords ( words : Uint8Array, fmt = 'asm' @@ -74,48 +84,3 @@ export function decodeWords ( } return stack } - -export function getWordType (word : number) : string { - switch (true) { - case (word === 0): - return 'opcode' - case (word >= 1 && word <= 75): - return 'varint' - case (word === 76): - return 'pushdata1' - case (word === 77): - return 'pushdata2' - case (word === 78): - return 'pushdata4' - case (word <= 185): - return 'opcode' - default: - throw new Error(`Invalid word range: ${word}`) - } -} - -export function isValidWord (word : number) : boolean { - /** Check if the provided value - * is a valid script opcode. - * */ - const MIN_RANGE = 75 - const MAX_RANGE = 186 - - const DISABLED_OPCODES = [ - 126, 127, 128, 129, 131, 132, 133, 134, - 141, 142, 149, 150, 151, 152, 153 - ] - - switch (true) { - case (typeof (word) !== 'number'): - return false - case (word === 0): - return true - case (DISABLED_OPCODES.includes(word)): - return false - case (MIN_RANGE < word && word < MAX_RANGE): - return true - default: - return false - } -} diff --git a/src/lib/script/words.ts b/src/lib/script/words.ts index 2afa8dd..c9d617b 100644 --- a/src/lib/script/words.ts +++ b/src/lib/script/words.ts @@ -4,6 +4,7 @@ export const OPCODE_MAP = { OP_PUSHDATA2 : 77, OP_PUSHDATA4 : 78, OP_1NEGATE : 79, + OP_SUCCESS80 : 80, OP_1 : 81, OP_2 : 82, OP_3 : 83, @@ -21,6 +22,7 @@ export const OPCODE_MAP = { OP_15 : 95, OP_16 : 96, OP_NOP : 97, + OP_SUCCESS98 : 98, OP_IF : 99, OP_NOTIF : 100, OP_ELSE : 103, @@ -46,17 +48,34 @@ export const OPCODE_MAP = { OP_ROT : 123, OP_SWAP : 124, OP_TUCK : 125, + OP_SUCCESS126 : 126, + OP_SUCCESS127 : 127, + OP_SUCCESS128 : 128, + OP_SUCCESS129 : 129, OP_SIZE : 130, + OP_SUCCESS131 : 131, + OP_SUCCESS132 : 132, + OP_SUCCESS133 : 133, + OP_SUCCESS134 : 134, OP_EQUAL : 135, OP_EQUALVERIFY : 136, + OP_SUCCESS137 : 137, + OP_SUCCESS138 : 138, OP_1ADD : 139, OP_1SUB : 140, + OP_SUCCESS141 : 141, + OP_SUCCESS142 : 142, OP_NEGATE : 143, OP_ABS : 144, OP_NOT : 145, OP_0NOTEQUAL : 146, OP_ADD : 147, OP_SUB : 148, + OP_SUCCESS149 : 149, + OP_SUCCESS150 : 150, + OP_SUCCESS151 : 151, + OP_SUCCESS152 : 152, + OP_SUCCESS153 : 153, OP_BOOLAND : 154, OP_BOOLOR : 155, OP_NUMEQUAL : 156, @@ -88,10 +107,14 @@ export const OPCODE_MAP = { OP_NOP7 : 182, OP_NOP8 : 183, OP_NOP9 : 184, - OP_NOP10 : 185 + OP_NOP10 : 185, + OP_CHECKSIGADD : 186 } export function getOpLabel (num : number) : string { + if (num > 186 && num < 255) { + return 'OP_SUCCESS' + String(num) + } for (const [ k, v ] of Object.entries(OPCODE_MAP)) { if (v === num) return k } @@ -104,3 +127,45 @@ export function getOpCode (string : string) : number { } throw new Error('OPCODE not found:' + string) } + +export function getWordType (word : number) : string { + switch (true) { + case (word === 0): + return 'opcode' + case (word >= 1 && word <= 75): + return 'varint' + case (word === 76): + return 'pushdata1' + case (word === 77): + return 'pushdata2' + case (word === 78): + return 'pushdata4' + case (word <= 254): + return 'opcode' + default: + throw new Error(`Invalid word range: ${word}`) + } +} + +export function isValidWord (word : number) : boolean { + /** Check if the provided value + * is a valid script opcode. + * */ + const MIN_RANGE = 75 + const MAX_RANGE = 254 + + const DISABLED_OPCODES : number[] = [] + + switch (true) { + case (typeof (word) !== 'number'): + return false + case (word === 0): + return true + case (DISABLED_OPCODES.includes(word)): + return false + case (MIN_RANGE < word && word < MAX_RANGE): + return true + default: + return false + } +} diff --git a/src/lib/sig/taproot.ts b/src/lib/sig/taproot.ts index 9ff90ce..0b6b3d7 100644 --- a/src/lib/sig/taproot.ts +++ b/src/lib/sig/taproot.ts @@ -1,17 +1,13 @@ import { Buff, Stream } from '@cmdcode/buff-utils' +import { Hash, Noble } from '@cmdcode/crypto-utils' import * as ENC from '../tx/encode.js' import { encodeScript } from '../script/encode.js' import { normalizeData } from '../script/decode.js' import { safeThrow } from '../utils.js' +import { checkTapPath } from '../tap/proof.js' -import { - getTapTag, - getTapLeaf, - checkTapPath -} from '../tap/script.js' - -import { Hash, Noble, Point } from '@cmdcode/crypto-utils' import { decodeTx, normalizeTx } from '../tx/decode.js' +import { getTapTag, getTapLeaf } from '../tap/script.js' import { TxData, @@ -22,26 +18,16 @@ import { } from '../../schema/types.js' interface HashConfig { - extention ?: Bytes - sigflag ?: number - extflag ?: number - key_version ?: number - separator_pos ?: number + extension ?: Bytes // Include a tapleaf hash with your signature hash. + pubkey ?: Bytes // Verify using this pubkey instead of the tapkey. + sigflag ?: number // Set the signature type flag. + separator_pos ?: number // If using OP_CODESEPARATOR, specify the latest opcode position. + extflag ?: number // Set the extention version flag (future use). + key_version ?: number // Set the key version flag (future use). } const VALID_HASH_TYPES = [ 0x00, 0x01, 0x02, 0x03, 0x81, 0x82, 0x83 ] -export function getTweakFromPub ( - internal : string | Uint8Array, - tweaked : string | Uint8Array -) : Uint8Array { - return Point - .fromX(tweaked) - .negate() - .add(Point.fromX(internal)) - .rawX -} - export async function taprootSign ( prvkey : string | Uint8Array, txdata : TxData | string | Uint8Array, @@ -102,7 +88,7 @@ export async function taprootVerify ( const script = encodeScript(witness.pop()) const version = cblock[0] & 0xfe target = await getTapLeaf(script, version) - config.extention = target + config.extension = target } const hash = await taprootHash(tx, index, config) @@ -135,7 +121,7 @@ export async function taprootHash ( } // Unpack configuration. const { - extention, + extension, sigflag = 0x00, extflag = 0x00, key_version = 0x00, @@ -167,7 +153,7 @@ export async function taprootHash ( const isAnyPay = (sigflag & 0x80) === 0x80 const annex = await getAnnexData(witness) const annexBit = (annex !== undefined) ? 1 : 0 - const extendBit = (extention !== undefined) ? 1 : 0 + const extendBit = (extension !== undefined) ? 1 : 0 const spendType = ((extflag + extendBit) * 2) + annexBit // Begin building our digest. @@ -219,9 +205,9 @@ export async function taprootHash ( digest.push(await hashOutput(output[index])) } - if (extention !== undefined) { + if (extension !== undefined) { digest.push( - Buff.normalize(extention), + Buff.normalize(extension), Buff.num(key_version), Buff.num(separator_pos, 4) ) diff --git a/src/lib/tap/proof.ts b/src/lib/tap/proof.ts new file mode 100644 index 0000000..c0fbced --- /dev/null +++ b/src/lib/tap/proof.ts @@ -0,0 +1,85 @@ +import { Buff, Stream } from '@cmdcode/buff-utils' +import { merkleize, getTapBranch } from './script.js' +import { getTapTweak, tweakPubkey } from './tweak.js' +import { TapTree } from './types.js' + +const DEFAULT_VERSION = 0xc0 + +export async function getTapPath ( + pubkey : string | Uint8Array, + target : string, + taptree : TapTree = [ target ], + version = DEFAULT_VERSION, + parity = 0 +) : Promise { + // Merkelize the leaves into a root hash (with proof). + const p = Buff.normalize(pubkey) + const [ root, _t, path ] = await merkleize(taptree, target) + + // Create the control block with pubkey. + const ctrl = Buff.num(version + getParityBit(parity)) + const block = [ ctrl, Buff.normalize(pubkey) ] + + if (taptree.length > 1) { + // If there is more than one path, add to block. + path.forEach(e => block.push(Buff.hex(e))) + } + + const cblock = Buff.join(block) + const tweak = await getTapTweak(p, Buff.hex(root)) + const tapkey = tweakPubkey(p, tweak).slice(1) + + if (!await checkTapPath(tapkey, cblock, target)) { + if (parity === 0) { + return getTapPath(pubkey, target, taptree, version, 1) + } + throw new Error('Path checking failed! Unable to generate path.') + } + + return cblock.hex +} + +export async function checkTapPath ( + tapkey : string | Uint8Array, + cblock : string | Uint8Array, + target : string +) : Promise { + const buffer = new Stream(Buff.normalize(cblock)) + const [ _v, y ] = decodeCByte(buffer.read(1).num) + const intkey = buffer.read(32) + const pubkey = Buff.of(y, ...Buff.normalize(tapkey)) + + const path = [] + + let branch = target + + while (buffer.size >= 32) { + path.push(buffer.read(32).hex) + } + + if (buffer.size !== 0) { + throw new Error('Invalid control block size!') + } + + for (const p of path) { + branch = await getTapBranch(branch, p) + } + + const t = await getTapTweak(intkey, Buff.hex(branch)) + const k = tweakPubkey(intkey, t) + + return (Buff.raw(k).hex === Buff.raw(pubkey).hex) +} + +export function getParityBit (parity : number | string = 0x02) : number { + if (parity === 0 || parity === 1) return parity + if (parity === 0x02 || parity === '02') return 0 + if (parity === 0x03 || parity === '03') return 1 + throw new Error('Invalid parity bit:' + String(parity)) +} + +export function decodeCByte ( + byte : number +) : number[] { + return (byte % 2 === 0) ? [ byte, 0x02 ] : [ byte - 1, 0x03 ] +} diff --git a/src/lib/tap/script.ts b/src/lib/tap/script.ts index 1683f2e..001c096 100644 --- a/src/lib/tap/script.ts +++ b/src/lib/tap/script.ts @@ -1,79 +1,10 @@ -import { Buff, Stream } from '@cmdcode/buff-utils' -import { Hash, Field, Point, Noble } from '@cmdcode/crypto-utils' - -import { TapTree, TapRoot, TapKey } from './types.js' +import { Buff } from '@cmdcode/buff-utils' +import { Hash } from '@cmdcode/crypto-utils' +import { TapTree, TapProof } from './types.js' const DEFAULT_VERSION = 0xc0 const ec = new TextEncoder() -export function tweakPrvkey ( - prvkey : string | Uint8Array, - tweak : string | Uint8Array -) : Uint8Array { - let sec = new Field(prvkey) - if (sec.point.hasOddY) { - sec = sec.negate() - } - return sec.add(tweak) -} - -export function tweakPubkey ( - pubkey : string | Uint8Array, - tweak : string | Uint8Array -) : Uint8Array { - const P = Point.fromX(pubkey) - const Q = P.add(tweak) - return Q.rawX -} - -async function getTapKey ( - intkey : string | Uint8Array, - leaves : TapTree = [], - isPrivate = false -) : Promise { - const k = Buff.normalize(intkey) - // Get the merkle root data. - const r = (leaves.length > 0) - ? await getTapRoot(leaves) - : new Uint8Array() - // Get the pubkey for the tweak. - const P = (isPrivate) - ? Noble.getPublicKey(k, true).slice(1) - : k - // Calculate the tweak. - const t = await getTapTweak(P, r) - // Return the tweaked key based on type. - if (isPrivate) { - // Return tweaked private key. - return [ Buff.raw(tweakPrvkey(k, t)).hex, 0 ] - } else { - // Return tweaked public key. - const p = Buff.raw(tweakPubkey(k, t)) - return [ p.slice(1).hex, p.slice(0, 1).num ] - } -} - -export async function getTapPubkey ( - pubkey : string | Uint8Array, - leaves : TapTree = [] -) : Promise { - return getTapKey(pubkey, leaves) -} - -export async function getTapSeckey ( - seckey : string | Uint8Array, - leaves : TapTree = [] -) : Promise { - return getTapKey(seckey, leaves, true).then(ret => ret[0]) -} - -export async function getTapRoot ( - leaves : TapTree -) : Promise { - // Merkelize the leaves into a root hash. - return merkleize(leaves).then(r => Buff.hex(r[0])) -} - export async function getTapTag (tag : string) : Promise { const htag = await Hash.sha256(ec.encode(tag)) return Uint8Array.of(...htag, ...htag) @@ -106,81 +37,11 @@ export async function getTapBranch ( )).then(e => Buff.raw(e).hex) } -export async function getTapTweak ( - pubkey : string | Uint8Array, - tweak : string | Uint8Array +export async function getTapRoot ( + leaves : TapTree ) : Promise { - return Hash.sha256(Uint8Array.of( - ...await getTapTag('TapTweak'), - ...Buff.normalize(pubkey), - ...Buff.normalize(tweak) - )) -} - -export async function getTapPath ( - pubkey : string | Uint8Array, - target : string, - taptree : TapTree = [ target ], - version = DEFAULT_VERSION, - parity = 0 -) : Promise { - // Merkelize the leaves into a root hash (with proof). - const p = Buff.normalize(pubkey) - const [ root, _t, path ] = await merkleize(taptree, target) - - // Create the control block with pubkey. - const ctrl = Buff.num(version + getParityBit(parity)) - const block = [ ctrl, Buff.normalize(pubkey) ] - - if (taptree.length > 1) { - // If there is more than one path, add to block. - path.forEach(e => block.push(Buff.hex(e))) - } - - const cblock = Buff.join(block) - const tweak = await getTapTweak(p, Buff.hex(root)) - const tapkey = tweakPubkey(p, tweak).slice(1) - - if (!await checkTapPath(tapkey, cblock, target)) { - if (parity === 0) { - return getTapPath(pubkey, target, taptree, version, 1) - } - throw new Error('Path checking failed! Unable to generate path.') - } - - return cblock.hex -} - -export async function checkTapPath ( - tapkey : string | Uint8Array, - cblock : string | Uint8Array, - target : string -) : Promise { - const buffer = new Stream(Buff.normalize(cblock)) - const [ _v, y ] = decodeCByte(buffer.read(1).num) - const intkey = buffer.read(32) - const pubkey = Buff.of(y, ...Buff.normalize(tapkey)) - - const path = [] - - let branch = target - - while (buffer.size >= 32) { - path.push(buffer.read(32).hex) - } - - if (buffer.size !== 0) { - throw new Error('Invalid control block size!') - } - - for (const p of path) { - branch = await getTapBranch(branch, p) - } - - const t = await getTapTweak(intkey, Buff.hex(branch)) - const k = tweakPubkey(intkey, t) - - return (Buff.raw(k).hex === Buff.raw(pubkey).hex) + // Merkelize the leaves into a root hash. + return merkleize(leaves).then(r => Buff.hex(r[0])) } export function decodeTapAddress ( @@ -204,7 +65,7 @@ export async function merkleize ( taptree : TapTree, target : string | null = null, path : string[] = [] -) : Promise { +) : Promise { const leaves : string[] = [] const tree : string[] = [] @@ -268,16 +129,3 @@ export async function merkleize ( export function getVersion (version = 0xc0) : number { return version & 0xfe } - -export function getParityBit (parity : number | string = 0x02) : number { - if (parity === 0 || parity === 1) return parity - if (parity === 0x02 || parity === '02') return 0 - if (parity === 0x03 || parity === '03') return 1 - throw new Error('Invalid parity bit:' + String(parity)) -} - -export function decodeCByte ( - byte : number -) : number[] { - return (byte % 2 === 0) ? [ byte, 0x02 ] : [ byte - 1, 0x03 ] -} diff --git a/src/lib/tap/tweak.ts b/src/lib/tap/tweak.ts new file mode 100644 index 0000000..55421c4 --- /dev/null +++ b/src/lib/tap/tweak.ts @@ -0,0 +1,76 @@ +import { Buff } from '@cmdcode/buff-utils' +import { Field, Hash, Point, Noble } from '@cmdcode/crypto-utils' +import { getTapTag, getTapRoot } from './script.js' +import { TapTree, TapKey } from './types.js' + +export function tweakPrvkey ( + prvkey : string | Uint8Array, + tweak : string | Uint8Array +) : Uint8Array { + let sec = new Field(prvkey) + if (sec.point.hasOddY) { + sec = sec.negate() + } + return sec.add(tweak) +} + +export function tweakPubkey ( + pubkey : string | Uint8Array, + tweak : string | Uint8Array +) : Uint8Array { + const P = Point.fromX(pubkey) + const Q = P.add(tweak) + return Q.rawX +} + +export async function getTapTweak ( + pubkey : string | Uint8Array, + tweak : string | Uint8Array +) : Promise { + return Hash.sha256(Uint8Array.of( + ...await getTapTag('TapTweak'), + ...Buff.normalize(pubkey), + ...Buff.normalize(tweak) + )) +} + +async function getTapKey ( + intkey : string | Uint8Array, + leaves : TapTree = [], + isPrivate = false +) : Promise { + const k = Buff.normalize(intkey) + // Get the merkle root data. + const r = (leaves.length > 0) + ? await getTapRoot(leaves) + : new Uint8Array() + // Get the pubkey for the tweak. + const P = (isPrivate) + ? Noble.getPublicKey(k, true).slice(1) + : k + // Calculate the tweak. + const t = await getTapTweak(P, r) + // Return the tweaked key based on type. + if (isPrivate) { + // Return tweaked private key. + return [ Buff.raw(tweakPrvkey(k, t)).hex, 0 ] + } else { + // Return tweaked public key. + const p = Buff.raw(tweakPubkey(k, t)) + return [ p.slice(1).hex, p.slice(0, 1).num ] + } +} + +export async function getTapPubkey ( + pubkey : string | Uint8Array, + leaves : TapTree = [] +) : Promise { + return getTapKey(pubkey, leaves) +} + +export async function getTapSeckey ( + seckey : string | Uint8Array, + leaves : TapTree = [] +) : Promise { + return getTapKey(seckey, leaves, true).then(ret => ret[0]) +} diff --git a/src/lib/tap/types.ts b/src/lib/tap/types.ts index 375f202..b00f726 100644 --- a/src/lib/tap/types.ts +++ b/src/lib/tap/types.ts @@ -5,7 +5,7 @@ export type TapKey = [ export type TapTree = Array -export type TapRoot = [ +export type TapProof = [ root : string, target : string | null, path : string[] diff --git a/src/lib/tx/decode.ts b/src/lib/tx/decode.ts index 2148355..5e0aabc 100644 --- a/src/lib/tx/decode.ts +++ b/src/lib/tx/decode.ts @@ -69,7 +69,7 @@ function readInput (stream : Stream) : InputData { return { txid : stream.read(32).reverse().toHex(), vout : stream.read(4).reverse().toNum(), - scriptSig : readData(stream, true), + scriptSig : readScript(stream, true), sequence : stream.read(4).reverse().toHex() } } @@ -86,7 +86,7 @@ function readOutputs (stream : Stream) : OutputData[] { function readOutput (stream : Stream) : OutputData { return { value : stream.read(8).reverse().big, - scriptPubKey : readData(stream, true) + scriptPubKey : readScript(stream, true) } } @@ -113,6 +113,14 @@ function readData ( : Buff.num(0).toHex() } +function readScript ( + stream : Stream, + hasVarint ?: boolean +) : string | string[] { + const data = readData(stream, hasVarint) + return (data !== '00') ? data : [] +} + function readLocktime (stream : Stream) : number { return stream.read(4).reverse().toNum() } diff --git a/src/lib/tx/encode.ts b/src/lib/tx/encode.ts index 351fe83..55ec607 100644 --- a/src/lib/tx/encode.ts +++ b/src/lib/tx/encode.ts @@ -1,5 +1,4 @@ import { Buff } from '@cmdcode/buff-utils' - import { encodeScript } from '../script/encode.js' import { @@ -7,9 +6,12 @@ import { InputData, OutputData, SequenceData, - WitnessData + WitnessData, + ScriptData } from '../../schema/types.js' +const BECH32_PREFIX = [ 'bc', 'tb', 'bcrt' ] + export function encodeTx ( txdata : TxData, omitWitness ?: boolean @@ -41,10 +43,6 @@ export function encodeTx ( return Buff.join(raw).hex } -// export function encodeBaseTx(obj : Transaction) : string { -// return encodeTx(obj, { omitWitness: true, omitMeta: true }) -// } - function checkForWitness (vin : InputData[]) : boolean { /** Check if any witness data is present. */ for (const txin of vin) { @@ -107,8 +105,9 @@ function encodeOutputs (arr : OutputData[]) : Uint8Array { for (const vout of arr) { const { address, value, scriptPubKey } = vout raw.push(encodeValue(value)) - if (address !== undefined) { - raw.push(Buff.bech32(address).raw) + if (typeof address === 'string') { + const script = encodeAddress(address) + raw.push(encodeScript(script)) } else { raw.push(encodeScript(scriptPubKey)) } @@ -132,3 +131,31 @@ function encodeWitness ( export function encodeLocktime (num : number) : Uint8Array { return Buff.num(num, 4).reverse() } + +export function encodeAddress ( + address : string +) : ScriptData { + if (address.startsWith('1')) { + const b58 = Buff.b58check(address) + return [ 'OP_DUP', 'OP_HASH160', b58, 'OP_EQUALVERIFY', 'OP_CHECKSIG' ] + } + if (address.startsWith('3')) { + const b58 = Buff.b58check(address) + return [ 'OP_HASH160', b58, 'OP_EQUAL' ] + } + if (address.includes('1')) { + const [ prefix, rest ] = address.split('1') + if (!BECH32_PREFIX.includes(prefix)) { + throw new Error('Invalid bech32 prefix!') + } + if (rest.startsWith('p')) { + const raw = Buff.bech32m(address).prefixSize() + return Buff.of(0x51, ...raw) + } + if (rest.startsWith('q')) { + const raw = Buff.bech32(address, 0).prefixSize() + return Buff.of(0x00, ...raw) + } + } + throw new Error('Unable to parse address!') +} diff --git a/src/schema/types.ts b/src/schema/types.ts index 12dd2ed..0627a9f 100644 --- a/src/schema/types.ts +++ b/src/schema/types.ts @@ -15,9 +15,9 @@ export interface InputData { } export interface OutputData { - address ?: string - value : number | bigint - scriptPubKey : ScriptData + address ?: string + value : number | bigint + scriptPubKey ?: ScriptData } export type SequenceData = string | number diff --git a/test/scratch.ts b/test/scratch.ts index d13e3e5..3468a44 100644 --- a/test/scratch.ts +++ b/test/scratch.ts @@ -2,7 +2,7 @@ import fs from 'fs/promises' import path from 'path' import { Buff } from '@cmdcode/buff-utils' import { KeyPair } from '@cmdcode/crypto-utils' -import { Script, Sig, Tap, Tx } from '../src/index.js' +import { Script, Sig, Tap, Tweak, Tx } from '../src/index.js' const ec = new TextEncoder() const fpath = path.join(process.cwd(), '/test') @@ -14,7 +14,7 @@ const mimetype = ec.encode('image/png') const script = [ pubkey, 'OP_CHECKSIG' ] // [ 'OP_0', 'OP_IF', ec.encode('ord'), '01', mimetype, 'OP_0', data, 'OP_ENDIF' ] const leaf = await Tap.getLeaf(Script.encode(script)) -const [ tapkey ] = await Tap.getPubkey(pubkey, [ leaf ]) +const [ tapkey ] = await Tweak.getPubkey(pubkey, [ leaf ]) const cblock = await Tap.getPath(pubkey, leaf) console.log('leaf:', leaf) @@ -29,20 +29,22 @@ const redeemtx = { prevout: { value: 100000, scriptPubKey: '5120' + tapkey }, witness: [] }], - output:[{ + output: [{ value: 90000, - scriptPubKey: "001439144dbc3c59b3b9b9a6a8275bd4c550129484e8" + address: 'bcrt1pqksclnx3jz0nx9lym4l3zft7gs5gsjgzk8r5vmp5ul6fpc8xyldqaxu8ys' }], locktime: 0 } -const sec = await Tap.getSeckey(seckey.raw, [ leaf ]) -const sig = await Sig.taproot.sign(seckey.raw, redeemtx, 0, { extention: leaf }) +// const sec = await Tap.getSeckey(seckey.raw, [ leaf ]) +// const sig = await Sig.taproot.sign(seckey.raw, redeemtx, 0, { extension: leaf }) -redeemtx.input[0].witness = [ sig, script, cblock ] +// redeemtx.input[0].witness = [ sig, script, cblock ] -console.dir(redeemtx, { depth: null }) +// console.dir(redeemtx, { depth: null }) // await Sig.taproot.verify(redeemtx, 0, true) -console.log('Txdata:', Tx.encode(redeemtx)) +const txhex = Tx.encode(redeemtx) +console.log('Txdata:', txhex) +console.log('Txdata:', Tx.decode(txhex)) diff --git a/test/src/tap/sig.test.ts b/test/src/tap/sig.test.ts index 2ca675e..c6f8f83 100644 --- a/test/src/tap/sig.test.ts +++ b/test/src/tap/sig.test.ts @@ -5,7 +5,7 @@ import { Noble } from '@cmdcode/crypto-utils' import { decodeTx } from '../../../src/lib/tx/decode.js' import * as SIG from '../../../src/lib/sig/taproot.js' import test_vectors from './sig.vectors.json' assert { type: 'json' } -import { Tap } from '../../../src/index.js' +import { Tweak } from '../../../src/index.js' const verify = Noble.schnorr.verify @@ -39,10 +39,10 @@ export async function test_signatures(t : Test) : Promise { const { txinIndex, hashType, internalPrivkey, merkleRoot } = given const { sigHash, tweak, internalPubkey, tweakedPrivkey } = intermediary // Test our ability to create the tweak. - const taptweak = await Tap.getTweak(internalPubkey, merkleRoot ?? new Uint8Array()) + const taptweak = await Tweak.getTweak(internalPubkey, merkleRoot ?? new Uint8Array()) t.equal(Buff.raw(taptweak).hex, tweak, 'The tap tweak should match.') // Test our ability to tweak the private key. - const tweakedPrv = Tap.tweakSeckey(internalPrivkey, tweak) + const tweakedPrv = Tweak.tweakSeckey(internalPrivkey, tweak) t.equal(Buff.raw(tweakedPrv).hex, tweakedPrivkey, 'The tweaked prvkey should match.') // Test our ability to calculate the signature hash. const actual_hash = await SIG.taprootHash(tx, txinIndex, { sigflag: hashType }) diff --git a/test/src/tap/tree.test.ts b/test/src/tap/tree.test.ts index 44db3ff..1bb6485 100644 --- a/test/src/tap/tree.test.ts +++ b/test/src/tap/tree.test.ts @@ -1,8 +1,10 @@ import { Test } from 'tape' import { Buff } from '@cmdcode/buff-utils' +import * as CHK from '../../../src/lib/tap/proof.js' import * as TAP from '../../../src/lib/tap/script.js' +import * as TWK from '../../../src/lib/tap/tweak.js' -import tree_vectors from './tree.vectors.json' assert { type: 'json' } +import tree_vectors from './tree.vectors.json' assert { type: 'json' } import { encodeScript } from '../../../src/lib/script/encode.js' interface Vector { @@ -39,7 +41,7 @@ export async function tweak_test(t : Test) : Promise { if (scripts.length === 0) { t.test('Testing empty key tweak.', async t => { t.plan(1) - const [ tapkey ] = await TAP.getTapPubkey(internalPubkey) + const [ tapkey ] = await TWK.getTapPubkey(internalPubkey) t.equal(tapkey, tweakedPubkey, 'Tweaked pubs should match.') }) } else { @@ -47,9 +49,9 @@ export async function tweak_test(t : Test) : Promise { t.plan(3) const root = await TAP.getTapRoot(leafHashes) t.equal(Buff.raw(root).hex, merkleRoot, 'Root hash should match.') - const taptweak = await TAP.getTapTweak(internalPubkey, merkleRoot as string) + const taptweak = await TWK.getTapTweak(internalPubkey, merkleRoot as string) t.equal(Buff.raw(taptweak).hex, tweak, 'Tweak hash should match.') - const [ tapkey ] = (await TAP.getTapPubkey(internalPubkey, leafHashes)) + const [ tapkey ] = (await TWK.getTapPubkey(internalPubkey, leafHashes)) t.equal(tapkey, tweakedPubkey, 'Tweaked pubs should match.') }) @@ -67,7 +69,7 @@ export async function tweak_test(t : Test) : Promise { t.equal(tapleaf, leaves[i], 'Leaf hash should match.') const target = leaves[i] - const block = await TAP.getTapPath(internalPubkey, target, leafHashes, version, parity) + const block = await CHK.getTapPath(internalPubkey, target, leafHashes, version, parity) t.equal(block, cblocks[i], 'Control blocks should be equal.') }) } diff --git a/test/src/tap/unit.test.ts b/test/src/tap/unit.test.ts index 440f692..29d115b 100644 --- a/test/src/tap/unit.test.ts +++ b/test/src/tap/unit.test.ts @@ -1,8 +1,8 @@ import { Test } from 'tape' import { Buff } from '@cmdcode/buff-utils' -import { Tap, Script } from '../../../src/index.js' -import test_vectors from './unit.vectors.json' assert { type: 'json' } -import { encodeScript } from '../../../src/lib/script/encode.js' +import { Tap, Tweak, Script } from '../../../src/index.js' +import test_vectors from './unit.vectors.json' assert { type: 'json' } +import { encodeScript } from '../../../src/lib/script/encode.js' export async function unit_tests(t : Test) : Promise { t.test('Testing tapleaf creation:', async t => { @@ -26,7 +26,7 @@ export async function unit_tests(t : Test) : Promise { const vectors = test_vectors.taproot t.plan(vectors.length) for (const [ pub, root, ans ] of vectors) { - const key = await Tap.getTweak(Buff.hex(pub), Buff.hex(root)) + const key = await Tweak.getTweak(Buff.hex(pub), Buff.hex(root)) t.equal(Buff.raw(key).hex, ans, 'Taptweak should match') } }), diff --git a/yarn.lock b/yarn.lock index 29d63ce..3f3ff8e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -194,10 +194,10 @@ resolved "https://registry.yarnpkg.com/@cmdcode/buff-utils/-/buff-utils-1.3.4.tgz#dcce811000cc2c66c9f7f2bdb2ff44edfc8d0cbc" integrity sha512-VaT4L5j+AVYKy1LB/yu6L6q1jDjtSi7zq/zNJiBFULXdpasWd8q+r750pkNEmQ/159tgOhCYvd9AUMldMCrnWw== -"@cmdcode/buff-utils@^1.4.4": - version "1.4.4" - resolved "https://registry.yarnpkg.com/@cmdcode/buff-utils/-/buff-utils-1.4.4.tgz#f6907988b1a567b71ca47b8067f8afed08748501" - integrity sha512-ULynzoRqmPSesgRbL82wcEHOoLUmf3pYr6cZf1FY744+4dMQhvnJff46fah3ukjhq9DgYONb/+Ed30ZG3YEW4g== +"@cmdcode/buff-utils@^1.5.2": + version "1.5.2" + resolved "https://registry.yarnpkg.com/@cmdcode/buff-utils/-/buff-utils-1.5.2.tgz#1180d369f36753b40142bb238d53a2d08fd2b615" + integrity sha512-VP5y7cebpHDlJhHPmrZC8GimZi18a1aqppE5diGTnx79mH3X+7QtnHwkKPoylZ4Zsv1XPu2nc/OHqcNwM3YKgg== "@cmdcode/crypto-utils@^1.5.11": version "1.5.11" @@ -207,6 +207,14 @@ "@cmdcode/buff-utils" "^1.3.4" "@cmdcode/secp256k1" "^1.0.6" +"@cmdcode/crypto-utils@^1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@cmdcode/crypto-utils/-/crypto-utils-1.6.0.tgz#c9dc85edda1dcea2dc8cd9e05fdb23288d0fdedd" + integrity sha512-0Lodd3Xd8Mix06dtMRs7NqvWvMfc6sW/k7CoLTMENDa0OsFhQoT0UsBrVjjRJ+BK6c93/LRQ860y54aFsosIQA== + dependencies: + "@cmdcode/buff-utils" "^1.5.2" + "@cmdcode/secp256k1" "^1.0.7" + "@cmdcode/keylink@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@cmdcode/keylink/-/keylink-1.2.0.tgz#f997aab25382d3104e5e3a8749aefedaa646813f" @@ -220,6 +228,11 @@ resolved "https://registry.yarnpkg.com/@cmdcode/secp256k1/-/secp256k1-1.0.6.tgz#6d7f0abba2b23aa00022ef1f76c23dc956d6c9cb" integrity sha512-uEo4AUcKB+jAEqjEIpoAfaovMFrldUweoWTQgcbvJYX4wjc7FilUmS8stCqQFzkKKmcWkTxsmiTXXbYXCvHZuw== +"@cmdcode/secp256k1@^1.0.7": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@cmdcode/secp256k1/-/secp256k1-1.0.7.tgz#5cfa3a6e8cacb67ce7b69d07c7538df3aadabab5" + integrity sha512-Os5BgDCqxYVw8e6w5oZ1PaNQBP6j1H/GceM+yIaz7XbGmM8KHWJhMEAy3tWjR/JHjWDJeXIs+a5T6+0OwAipTg== + "@cspotcode/source-map-support@^0.8.0": version "0.8.1" resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1"