|
| 1 | +'use strict'; |
| 2 | +Object.defineProperty(exports, '__esModule', { value: true }); |
| 3 | +const bcrypto = require('../crypto'); |
| 4 | +const networks_1 = require('../networks'); |
| 5 | +const bscript = require('../script'); |
| 6 | +const lazy = require('./lazy'); |
| 7 | +const typef = require('typeforce'); |
| 8 | +const OPS = bscript.OPS; |
| 9 | +const ecc = require('tiny-secp256k1'); |
| 10 | +const { bech32, bech32m } = require('bech32'); |
| 11 | +const EMPTY_BUFFER = Buffer.alloc(0); |
| 12 | +function stacksEqual(a, b) { |
| 13 | + if (a.length !== b.length) return false; |
| 14 | + return a.every((x, i) => { |
| 15 | + return x.equals(b[i]); |
| 16 | + }); |
| 17 | +} |
| 18 | +function chunkHasUncompressedPubkey(chunk) { |
| 19 | + if ( |
| 20 | + Buffer.isBuffer(chunk) && |
| 21 | + chunk.length === 65 && |
| 22 | + chunk[0] === 0x04 && |
| 23 | + ecc.isPoint(chunk) |
| 24 | + ) { |
| 25 | + return true; |
| 26 | + } else { |
| 27 | + return false; |
| 28 | + } |
| 29 | +} |
| 30 | +// input: <> |
| 31 | +// witness: [redeemScriptSig ...] {redeemScript} |
| 32 | +// output: OP_1 {publicKey} |
| 33 | +function p2tr(a, opts) { |
| 34 | + if (!a.address && !a.hash && !a.output && !a.redeem && !a.witness) |
| 35 | + throw new TypeError('Not enough data'); |
| 36 | + opts = Object.assign({ validate: true }, opts || {}); |
| 37 | + typef( |
| 38 | + { |
| 39 | + network: typef.maybe(typef.Object), |
| 40 | + address: typef.maybe(typef.String), |
| 41 | + hash: typef.maybe(typef.BufferN(32)), |
| 42 | + output: typef.maybe(typef.BufferN(34)), |
| 43 | + redeem: typef.maybe({ |
| 44 | + input: typef.maybe(typef.Buffer), |
| 45 | + network: typef.maybe(typef.Object), |
| 46 | + output: typef.maybe(typef.Buffer), |
| 47 | + witness: typef.maybe(typef.arrayOf(typef.Buffer)), |
| 48 | + }), |
| 49 | + input: typef.maybe(typef.BufferN(0)), |
| 50 | + witness: typef.maybe(typef.arrayOf(typef.Buffer)), |
| 51 | + }, |
| 52 | + a, |
| 53 | + ); |
| 54 | + const _address = lazy.value(() => { |
| 55 | + const result = bech32m.decode(a.address); |
| 56 | + const version = result.words.shift(); |
| 57 | + const data = bech32m.fromWords(result.words); |
| 58 | + return { |
| 59 | + version, |
| 60 | + prefix: result.prefix, |
| 61 | + data: Buffer.from(data), |
| 62 | + }; |
| 63 | + }); |
| 64 | + const _rchunks = lazy.value(() => { |
| 65 | + return bscript.decompile(a.redeem.input); |
| 66 | + }); |
| 67 | + let network = a.network; |
| 68 | + if (!network) { |
| 69 | + network = (a.redeem && a.redeem.network) || networks_1.bitcoin; |
| 70 | + } |
| 71 | + const o = { network }; |
| 72 | + lazy.prop(o, 'address', () => { |
| 73 | + if (!o.hash) return; |
| 74 | + const words = bech32.toWords(o.hash); |
| 75 | + words.unshift(0x00); |
| 76 | + return bech32.encode(network.bech32, words); |
| 77 | + }); |
| 78 | + lazy.prop(o, 'hash', () => { |
| 79 | + if (a.output) return a.output.slice(2); |
| 80 | + if (a.address) return _address().data; |
| 81 | + if (o.redeem && o.redeem.output) return bcrypto.sha256(o.redeem.output); |
| 82 | + }); |
| 83 | + lazy.prop(o, 'output', () => { |
| 84 | + if (!o.hash) return; |
| 85 | + return bscript.compile([OPS.OP_1, o.hash]); |
| 86 | + }); |
| 87 | + lazy.prop(o, 'redeem', () => { |
| 88 | + if (!a.witness) return; |
| 89 | + return { |
| 90 | + output: a.witness[a.witness.length - 1], |
| 91 | + input: EMPTY_BUFFER, |
| 92 | + witness: a.witness.slice(0, -1), |
| 93 | + }; |
| 94 | + }); |
| 95 | + lazy.prop(o, 'input', () => { |
| 96 | + if (!o.witness) return; |
| 97 | + return EMPTY_BUFFER; |
| 98 | + }); |
| 99 | + lazy.prop(o, 'witness', () => { |
| 100 | + // transform redeem input to witness stack? |
| 101 | + if ( |
| 102 | + a.redeem && |
| 103 | + a.redeem.input && |
| 104 | + a.redeem.input.length > 0 && |
| 105 | + a.redeem.output && |
| 106 | + a.redeem.output.length > 0 |
| 107 | + ) { |
| 108 | + const stack = bscript.toStack(_rchunks()); |
| 109 | + // assign, and blank the existing input |
| 110 | + o.redeem = Object.assign({ witness: stack }, a.redeem); |
| 111 | + o.redeem.input = EMPTY_BUFFER; |
| 112 | + return [].concat(stack, a.redeem.output); |
| 113 | + } |
| 114 | + if (!a.redeem) return; |
| 115 | + if (!a.redeem.output) return; |
| 116 | + if (!a.redeem.witness) return; |
| 117 | + return [].concat(a.redeem.witness, a.redeem.output); |
| 118 | + }); |
| 119 | + lazy.prop(o, 'name', () => { |
| 120 | + const nameParts = ['p2wsh']; |
| 121 | + if (o.redeem !== undefined && o.redeem.name !== undefined) |
| 122 | + nameParts.push(o.redeem.name); |
| 123 | + return nameParts.join('-'); |
| 124 | + }); |
| 125 | + // extended validation |
| 126 | + if (opts.validate) { |
| 127 | + let hash = Buffer.from([]); |
| 128 | + if (a.address) { |
| 129 | + if (_address().prefix !== network.bech32) |
| 130 | + throw new TypeError('Invalid prefix or Network mismatch'); |
| 131 | + if (_address().version !== 0x00) |
| 132 | + throw new TypeError('Invalid address version'); |
| 133 | + if (_address().data.length !== 32) |
| 134 | + throw new TypeError('Invalid address data'); |
| 135 | + hash = _address().data; |
| 136 | + } |
| 137 | + if (a.hash) { |
| 138 | + if (hash.length > 0 && !hash.equals(a.hash)) |
| 139 | + throw new TypeError('Hash mismatch'); |
| 140 | + else hash = a.hash; |
| 141 | + } |
| 142 | + if (a.output) { |
| 143 | + if ( |
| 144 | + a.output.length !== 34 || |
| 145 | + a.output[0] !== OPS.OP_0 || |
| 146 | + a.output[1] !== 0x20 |
| 147 | + ) |
| 148 | + throw new TypeError('Output is invalid'); |
| 149 | + const hash2 = a.output.slice(2); |
| 150 | + if (hash.length > 0 && !hash.equals(hash2)) |
| 151 | + throw new TypeError('Hash mismatch'); |
| 152 | + else hash = hash2; |
| 153 | + } |
| 154 | + if (a.redeem) { |
| 155 | + if (a.redeem.network && a.redeem.network !== network) |
| 156 | + throw new TypeError('Network mismatch'); |
| 157 | + // is there two redeem sources? |
| 158 | + if ( |
| 159 | + a.redeem.input && |
| 160 | + a.redeem.input.length > 0 && |
| 161 | + a.redeem.witness && |
| 162 | + a.redeem.witness.length > 0 |
| 163 | + ) |
| 164 | + throw new TypeError('Ambiguous witness source'); |
| 165 | + // is the redeem output non-empty? |
| 166 | + if (a.redeem.output) { |
| 167 | + if (bscript.decompile(a.redeem.output).length === 0) |
| 168 | + throw new TypeError('Redeem.output is invalid'); |
| 169 | + // match hash against other sources |
| 170 | + const hash2 = bcrypto.sha256(a.redeem.output); |
| 171 | + if (hash.length > 0 && !hash.equals(hash2)) |
| 172 | + throw new TypeError('Hash mismatch'); |
| 173 | + else hash = hash2; |
| 174 | + } |
| 175 | + if (a.redeem.input && !bscript.isPushOnly(_rchunks())) |
| 176 | + throw new TypeError('Non push-only scriptSig'); |
| 177 | + if ( |
| 178 | + a.witness && |
| 179 | + a.redeem.witness && |
| 180 | + !stacksEqual(a.witness, a.redeem.witness) |
| 181 | + ) |
| 182 | + throw new TypeError('Witness and redeem.witness mismatch'); |
| 183 | + if ( |
| 184 | + (a.redeem.input && _rchunks().some(chunkHasUncompressedPubkey)) || |
| 185 | + (a.redeem.output && |
| 186 | + (bscript.decompile(a.redeem.output) || []).some( |
| 187 | + chunkHasUncompressedPubkey, |
| 188 | + )) |
| 189 | + ) { |
| 190 | + throw new TypeError( |
| 191 | + 'redeem.input or redeem.output contains uncompressed pubkey', |
| 192 | + ); |
| 193 | + } |
| 194 | + } |
| 195 | + if (a.witness && a.witness.length > 0) { |
| 196 | + const wScript = a.witness[a.witness.length - 1]; |
| 197 | + if (a.redeem && a.redeem.output && !a.redeem.output.equals(wScript)) |
| 198 | + throw new TypeError('Witness and redeem.output mismatch'); |
| 199 | + if ( |
| 200 | + a.witness.some(chunkHasUncompressedPubkey) || |
| 201 | + (bscript.decompile(wScript) || []).some(chunkHasUncompressedPubkey) |
| 202 | + ) |
| 203 | + throw new TypeError('Witness contains uncompressed pubkey'); |
| 204 | + } |
| 205 | + } |
| 206 | + return Object.assign(o, a); |
| 207 | +} |
| 208 | +exports.p2tr = p2tr; |
0 commit comments