|
| 1 | +// Copyright © 2025–2026 Dankest, LLC |
| 2 | +// Based on XChain Platform by Dankest, LLC – https://dankest.llc |
| 3 | +// |
| 4 | +// SPDX-License-Identifier: AGPL-3.0-or-later |
| 5 | + |
| 6 | +'use strict' |
| 7 | + |
| 8 | +const assert = require('assert') |
| 9 | +const bitcoin = require('bitcoinjs-lib') |
| 10 | +const ecc = require('tiny-secp256k1') |
| 11 | +const XChainDecoder = require('../../../src/XChainDecoder') |
| 12 | +const XChainBlockDecoder = require('../../../src/chain/XChainBlockDecoder') |
| 13 | +const CryptoNetworks = require('../../../src/chain/crypto_networks') |
| 14 | +const NEW_VECTORS = require('../../fixtures/decoder_parse_vectors.json') |
| 15 | +const WIRE_VECTORS = require('../../fixtures/wire_parse_vectors.json') |
| 16 | + |
| 17 | +bitcoin.initEccLib(ecc) |
| 18 | + |
| 19 | +const P2PKH = Buffer.from('76a914' + '11'.repeat(20) + '88ac', 'hex') |
| 20 | +const P2SH = Buffer.from('a914' + '22'.repeat(20) + '87', 'hex') |
| 21 | +const P2WPKH = Buffer.from('0014' + '33'.repeat(20), 'hex') |
| 22 | +const P2WSH = Buffer.from('0020' + '44'.repeat(32), 'hex') |
| 23 | +const P2TR = Buffer.from('5120' + '22'.repeat(32), 'hex') |
| 24 | + |
| 25 | +describe('transaction wire parsing conformance', function () { |
| 26 | + it('matches node txids for legacy, segwit v0, taproot v1, LTC MWEB and DOGE coinbase forms', function () { |
| 27 | + const forms = [ |
| 28 | + ['bitcoin-mainnet', NEW_VECTORS.bitcoinGenesisCoinbase], |
| 29 | + ['bitcoin-mainnet', WIRE_VECTORS.ltcPegin], |
| 30 | + ['bitcoin-mainnet', NEW_VECTORS.taprootWitnessSpend], |
| 31 | + ['litecoin-mainnet', WIRE_VECTORS.ltcHogEx3195200], |
| 32 | + ['dogecoin-mainnet', { hex: WIRE_VECTORS.doge4500000.txs[0], txid: WIRE_VECTORS.doge4500000.txids[0] }], |
| 33 | + ] |
| 34 | + |
| 35 | + for (const [network, vector] of forms) { |
| 36 | + assert.strictEqual(new XChainBlockDecoder(network).transactionFromHex(vector.hex).getId(), vector.txid) |
| 37 | + } |
| 38 | + }) |
| 39 | + |
| 40 | + it('rejects truncated, non-hex and trailing bytes with a typed error', function () { |
| 41 | + const decoder = new XChainBlockDecoder('bitcoin-mainnet') |
| 42 | + const valid = NEW_VECTORS.taprootWitnessSpend.hex |
| 43 | + const malformed = [ |
| 44 | + '', |
| 45 | + '0', |
| 46 | + 'zz', |
| 47 | + valid.slice(0, -2), |
| 48 | + valid + '00', |
| 49 | + ] |
| 50 | + |
| 51 | + for (const hex of malformed) { |
| 52 | + assert.throws( |
| 53 | + () => decoder.transactionFromHex(hex), |
| 54 | + err => err instanceof XChainBlockDecoder.TransactionParseError && err.code === 'ERR_TRANSACTION_PARSE' |
| 55 | + ) |
| 56 | + } |
| 57 | + }) |
| 58 | + |
| 59 | + it('rejects an MWEB CompactSize above 2^53 before using it as a length', function () { |
| 60 | + // version 2, MWEB flag 0x08, then input-count CompactSize 2^53. |
| 61 | + const unsafeInputCount = '020000000008ff0000000000002000' |
| 62 | + assert.throws( |
| 63 | + () => new XChainBlockDecoder('litecoin-mainnet').transactionFromHex(unsafeInputCount), |
| 64 | + err => err instanceof XChainBlockDecoder.TransactionParseError && |
| 65 | + /exceeds Number\.MAX_SAFE_INTEGER/.test(err.message) |
| 66 | + ) |
| 67 | + }) |
| 68 | +}) |
| 69 | + |
| 70 | +describe('transaction wire parsing conformance', function () { |
| 71 | + it('bounds-checks every truncation of a fixed MWEB transaction', function () { |
| 72 | + const decoder = new XChainBlockDecoder('litecoin-mainnet') |
| 73 | + const valid = WIRE_VECTORS.ltcHogEx3195200.hex |
| 74 | + for (let end = 0; end < valid.length; end += 2) { |
| 75 | + assert.throws( |
| 76 | + () => decoder.transactionFromHex(valid.slice(0, end)), |
| 77 | + err => err instanceof XChainBlockDecoder.TransactionParseError |
| 78 | + ) |
| 79 | + } |
| 80 | + assert.throws( |
| 81 | + () => decoder.transactionFromHex(valid + '00'), |
| 82 | + err => err instanceof XChainBlockDecoder.TransactionParseError && /trailing byte/.test(err.message) |
| 83 | + ) |
| 84 | + }) |
| 85 | + |
| 86 | + it('types a truncated transaction inside a block for the block-loop caller', function () { |
| 87 | + const block = Buffer.concat([ |
| 88 | + Buffer.from(WIRE_VECTORS.doge4500000.header, 'hex'), |
| 89 | + Buffer.from([WIRE_VECTORS.doge4500000.txs.length]), |
| 90 | + ...WIRE_VECTORS.doge4500000.txs.map(hex => Buffer.from(hex, 'hex')), |
| 91 | + ]) |
| 92 | + assert.throws( |
| 93 | + () => new XChainBlockDecoder('dogecoin-mainnet').blockFromBuffer(block.subarray(0, -1)), |
| 94 | + err => err instanceof XChainBlockDecoder.TransactionParseError && err.code === 'ERR_TRANSACTION_PARSE' |
| 95 | + ) |
| 96 | + }) |
| 97 | +}) |
| 98 | + |
| 99 | +// The addresses each network derives for P2PKH, P2SH, P2WPKH, P2WSH and P2TR, in |
| 100 | +// that order; DOGE has no witness outputs, so it lists only the first two. |
| 101 | +const EXPECTED_ADDRESSES = { |
| 102 | + 'bitcoin-mainnet': [ |
| 103 | + '12ZEw5Hcv1hTb6YUQJ69y1V7uhcoDz92PH', |
| 104 | + '34oVnh4gNviJGMnNvgquMeLAxvXJuaRVMZ', |
| 105 | + 'bc1qxvenxvenxvenxvenxvenxvenxvenxven2ymjt8', |
| 106 | + 'bc1qg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zqd8sxw4', |
| 107 | + 'bc1pyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3qfpjpqe', |
| 108 | + ], |
| 109 | + 'bitcoin-testnet': [ |
| 110 | + 'mh5CE8Nbj38iND267s4XnvhSmhDW7yWc6Q', |
| 111 | + '2MvMhrRzhzPDeU9QvbpTmybKSBGjUmC6TTu', |
| 112 | + 'tb1qxvenxvenxvenxvenxvenxvenxvenxvenqzqps5', |
| 113 | + 'tb1qg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zq60xf56', |
| 114 | + 'tb1pyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3q7fyw6k', |
| 115 | + ], |
| 116 | + 'bitcoin-regtest': [ |
| 117 | + 'mh5CE8Nbj38iND267s4XnvhSmhDW7yWc6Q', |
| 118 | + '2MvMhrRzhzPDeU9QvbpTmybKSBGjUmC6TTu', |
| 119 | + 'bcrt1qxvenxvenxvenxvenxvenxvenxvenxvenztev8a', |
| 120 | + 'bcrt1qg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zqhkv0pq', |
| 121 | + 'bcrt1pyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3qnswg0v', |
| 122 | + ], |
| 123 | + 'litecoin-mainnet': [ |
| 124 | + 'LLnCCHbSzfwWquEdaS5TF2Yt7uz5Qb1SZ1', |
| 125 | + 'MB1e6aUeL3Zj4s4H2ZqFBHaaHd7kvvzTco', |
| 126 | + 'ltc1qxvenxvenxvenxvenxvenxvenxvenxvenwcpknh', |
| 127 | + 'ltc1qg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zqwr7k5s', |
| 128 | + 'ltc1pyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3q29u36u', |
| 129 | + ], |
| 130 | + 'litecoin-testnet': [ |
| 131 | + 'mh5CE8Nbj38iND267s4XnvhSmhDW7yWc6Q', |
| 132 | + '2MvMhrRzhzPDeU9QvbpTmybKSBGjUmC6TTu', |
| 133 | + 'tltc1qxvenxvenxvenxvenxvenxvenxvenxvene2zlqa', |
| 134 | + 'tltc1qg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zq9v6gt9', |
| 135 | + 'tltc1pyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3qp2c09f', |
| 136 | + ], |
| 137 | + 'litecoin-regtest': [ |
| 138 | + 'mh5CE8Nbj38iND267s4XnvhSmhDW7yWc6Q', |
| 139 | + '2MvMhrRzhzPDeU9QvbpTmybKSBGjUmC6TTu', |
| 140 | + 'rltc1qxvenxvenxvenxvenxvenxvenxvenxvenuxr9sr', |
| 141 | + 'rltc1qg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zql4xswd', |
| 142 | + 'rltc1pyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3qmnyhqp', |
| 143 | + ], |
| 144 | + 'dogecoin-mainnet': [ |
| 145 | + 'D6hLULEGDRbk86j58t5iWmeinqM6acA16V', |
| 146 | + '9uYkXY8aSzbCAj9rLpWKbmxYfVuM33MqfZ', |
| 147 | + ], |
| 148 | + 'dogecoin-testnet': [ |
| 149 | + 'nVkQCLyB9Q4U15JGAhjAmBF22hjPYUwj3w', |
| 150 | + '2MvMhrRzhzPDeU9QvbpTmybKSBGjUmC6TTu', |
| 151 | + ], |
| 152 | + 'dogecoin-regtest': [ |
| 153 | + 'mh5CE8Nbj38iND267s4XnvhSmhDW7yWc6Q', |
| 154 | + '2MvMhrRzhzPDeU9QvbpTmybKSBGjUmC6TTu', |
| 155 | + ], |
| 156 | +} |
| 157 | + |
| 158 | +async function assertNetworkAddresses(network, addresses) { |
| 159 | + const decoder = new XChainDecoder( |
| 160 | + network, null, null, null, null, null, |
| 161 | + '127.0.0.1', 0, null, null, false |
| 162 | + ) |
| 163 | + const tx = new bitcoin.Transaction() |
| 164 | + tx.addInput(Buffer.alloc(32, 1), 0) |
| 165 | + const scripts = network.startsWith('dogecoin') |
| 166 | + ? [P2PKH, P2SH] |
| 167 | + : [P2PKH, P2SH, P2WPKH, P2WSH, P2TR] |
| 168 | + for (const script of scripts) tx.addOutput(script, 1) |
| 169 | + |
| 170 | + const parsed = await decoder.parseTransaction(tx, new Set()) |
| 171 | + assert.deepStrictEqual(parsed.paymentOutputs.map(output => output.destinationAddress), addresses) |
| 172 | + |
| 173 | + if (!network.startsWith('dogecoin')) { |
| 174 | + assert.strictEqual(bitcoin.address.fromOutputScript(P2WPKH, CryptoNetworks.getBitcoinJsNetwork(network)), addresses[2]) |
| 175 | + assert.strictEqual(bitcoin.address.fromOutputScript(P2TR, CryptoNetworks.getBitcoinJsNetwork(network)), addresses[4]) |
| 176 | + } else { |
| 177 | + assert.throws(() => bitcoin.address.fromOutputScript(P2WPKH, CryptoNetworks.getBitcoinJsNetwork(network))) |
| 178 | + assert.throws(() => bitcoin.address.fromOutputScript(P2TR, CryptoNetworks.getBitcoinJsNetwork(network))) |
| 179 | + } |
| 180 | +} |
| 181 | + |
| 182 | +describe('output address derivation conformance', function () { |
| 183 | + for (const [network, addresses] of Object.entries(EXPECTED_ADDRESSES)) { |
| 184 | + it(network + ' derives the coin prefixes and witness checksum variant', async function () { |
| 185 | + await assertNetworkAddresses(network, addresses) |
| 186 | + }) |
| 187 | + } |
| 188 | +}) |
0 commit comments