From b1272a12005a83520473236879f104eb4153413d Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Thu, 17 Aug 2017 14:12:43 +1000 Subject: [PATCH] add Bech32 support to toOutputScript/fromOutputScript --- src/address.js | 65 ++++++++++--------- src/networks.js | 2 + test/address.js | 49 ++++++++------- test/fixtures/address.json | 126 ++++++++++++++++++++----------------- 4 files changed, 129 insertions(+), 113 deletions(-) diff --git a/src/address.js b/src/address.js index 5bc49ba..508b6ba 100644 --- a/src/address.js +++ b/src/address.js @@ -8,35 +8,26 @@ var types = require('./types') function fromBase58Check (address) { var payload = bs58check.decode(address) + + // TODO: 4.0.0, move to "toOutputScript" if (payload.length < 21) throw new TypeError(address + ' is too short') if (payload.length > 21) throw new TypeError(address + ' is too long') var version = payload.readUInt8(0) var hash = payload.slice(1) - return { hash: hash, version: version } + return { version: version, hash: hash } } -function fromBech32 (address, expectedPrefix) { +function fromBech32 (address) { var result = bech32.decode(address) - var prefix = result.prefix - var words = result.words - if (expectedPrefix !== undefined) { - if (prefix !== expectedPrefix) throw new Error('Expected ' + expectedPrefix + ', got ' + prefix) - } - - var version = words[0] - if (version > 16) throw new Error('Invalid version (' + version + ')') - var program = bech32.fromWords(words.slice(1)) + var data = bech32.fromWords(result.words.slice(1)) - if (version === 0) { - if (program.length !== 20 && program.length !== 32) throw new Error('Unknown program') - } else { - if (program.length < 2) throw new Error('Program too short') - if (program.length > 40) throw new Error('Program too long') + return { + version: result.words[0], + prefix: result.prefix, + data: Buffer.from(data) } - - return { version, prefix, program: Buffer.from(program) } } function toBase58Check (hash, version) { @@ -49,16 +40,8 @@ function toBase58Check (hash, version) { return bs58check.encode(payload) } -function toBech32 (prefix, version, program) { - if (version > 16) throw new Error('Invalid version (' + version + ')') - if (version === 0) { - if (program.length !== 20 && program.length !== 32) throw new Error('Unknown program') - } else { - if (program.length < 2) throw new Error('Program too short') - if (program.length > 40) throw new Error('Program too long') - } - - var words = bech32.toWords(program) +function toBech32 (data, version, prefix) { + var words = bech32.toWords(data) words.unshift(version) return bech32.encode(prefix, words) @@ -69,6 +52,8 @@ function fromOutputScript (outputScript, network) { if (bscript.pubKeyHash.output.check(outputScript)) return toBase58Check(bscript.compile(outputScript).slice(3, 23), network.pubKeyHash) if (bscript.scriptHash.output.check(outputScript)) return toBase58Check(bscript.compile(outputScript).slice(2, 22), network.scriptHash) + if (bscript.witnessPubKeyHash.output.check(outputScript)) return toBech32(bscript.compile(outputScript).slice(2, 22), 0, network.bech32) + if (bscript.witnessScriptHash.output.check(outputScript)) return toBech32(bscript.compile(outputScript).slice(2, 34), 0, network.bech32) throw new Error(bscript.toASM(outputScript) + ' has no matching Address') } @@ -76,9 +61,27 @@ function fromOutputScript (outputScript, network) { function toOutputScript (address, network) { network = network || networks.bitcoin - var decode = fromBase58Check(address) - if (decode.version === network.pubKeyHash) return bscript.pubKeyHash.output.encode(decode.hash) - if (decode.version === network.scriptHash) return bscript.scriptHash.output.encode(decode.hash) + var decode + try { + decode = fromBase58Check(address) + } catch (e) {} + + if (decode) { + if (decode.version === network.pubKeyHash) return bscript.pubKeyHash.output.encode(decode.hash) + if (decode.version === network.scriptHash) return bscript.scriptHash.output.encode(decode.hash) + } else { + try { + decode = fromBech32(address) + } catch (e) {} + + if (decode) { + if (decode.prefix !== network.bech32) throw new Error(address + ' has an invalid prefix') + if (decode.version === 0) { + if (decode.data.length === 20) return bscript.witnessPubKeyHash.output.encode(decode.data) + if (decode.data.length === 32) return bscript.witnessScriptHash.output.encode(decode.data) + } + } + } throw new Error(address + ' has no matching Script') } diff --git a/src/networks.js b/src/networks.js index 7515b09..43cd5fc 100644 --- a/src/networks.js +++ b/src/networks.js @@ -4,6 +4,7 @@ module.exports = { bitcoin: { messagePrefix: '\x18Bitcoin Signed Message:\n', + bech32: 'bc', bip32: { public: 0x0488b21e, private: 0x0488ade4 @@ -14,6 +15,7 @@ module.exports = { }, testnet: { messagePrefix: '\x18Bitcoin Signed Message:\n', + bech32: 'tb', bip32: { public: 0x043587cf, private: 0x04358394 diff --git a/test/address.js b/test/address.js index 13bea6a..78376ee 100644 --- a/test/address.js +++ b/test/address.js @@ -9,6 +9,8 @@ var fixtures = require('./fixtures/address.json') describe('address', function () { describe('fromBase58Check', function () { fixtures.standard.forEach(function (f) { + if (!f.base58check) return + it('decodes ' + f.base58check, function () { var decode = baddress.fromBase58Check(f.base58check) @@ -27,22 +29,22 @@ describe('address', function () { }) describe('fromBech32', function () { - fixtures.bech32.forEach((f) => { - it('encodes ' + f.address, function () { - var actual = baddress.fromBech32(f.address) + fixtures.standard.forEach((f) => { + if (!f.bech32) return + + it('decodes ' + f.bech32, function () { + var actual = baddress.fromBech32(f.bech32) - assert.strictEqual(actual.prefix, f.prefix) - assert.strictEqual(actual.program.toString('hex'), f.program) assert.strictEqual(actual.version, f.version) + assert.strictEqual(actual.prefix, networks[f.network].bech32) + assert.strictEqual(actual.data.toString('hex'), f.data) }) }) fixtures.invalid.bech32.forEach((f, i) => { - if (f.address === undefined) return - - it('decode fails for ' + f.address + '(' + f.exception + ')', function () { + it('decode fails for ' + f.bech32 + '(' + f.exception + ')', function () { assert.throws(function () { - baddress.fromBech32(f.address, f.prefix) + baddress.fromBech32(f.address) }, new RegExp(f.exception)) }) }) @@ -50,11 +52,11 @@ describe('address', function () { describe('fromOutputScript', function () { fixtures.standard.forEach(function (f) { - it('parses ' + f.script.slice(0, 30) + '... (' + f.network + ')', function () { + it('encodes ' + f.script.slice(0, 30) + '... (' + f.network + ')', function () { var script = bscript.fromASM(f.script) var address = baddress.fromOutputScript(script, networks[f.network]) - assert.strictEqual(address, f.base58check) + assert.strictEqual(address, f.base58check || f.bech32.toLowerCase()) }) }) @@ -71,7 +73,9 @@ describe('address', function () { describe('toBase58Check', function () { fixtures.standard.forEach(function (f) { - it('formats ' + f.hash + ' (' + f.network + ')', function () { + if (!f.base58check) return + + it('encodes ' + f.hash + ' (' + f.network + ')', function () { var address = baddress.toBase58Check(Buffer.from(f.hash, 'hex'), f.version) assert.strictEqual(address, f.base58check) @@ -81,21 +85,20 @@ describe('address', function () { describe('toBech32', function () { fixtures.bech32.forEach((f, i) => { - // unlike the reference impl., we don't support mixed/uppercase - var string = f.address.toLowerCase() - var program = Buffer.from(f.program, 'hex') + if (!f.bech32) return + var data = Buffer.from(f.data, 'hex') - it('encode ' + string, function () { - assert.deepEqual(baddress.toBech32(f.prefix, f.version, program), string) + it('encode ' + f.address, function () { + assert.deepEqual(baddress.toBech32(data, f.version, f.prefix), f.address) }) }) fixtures.invalid.bech32.forEach((f, i) => { - if (!f.prefix || f.version === undefined || f.program === undefined) return + if (!f.prefix || f.version === undefined || f.data === undefined) return it('encode fails (' + f.exception, function () { assert.throws(function () { - baddress.toBech32(f.prefix, f.version, Buffer.from(f.program, 'hex')) + baddress.toBech32(Buffer.from(f.data, 'hex'), f.version, f.prefix) }, new RegExp(f.exception)) }) }) @@ -103,10 +106,8 @@ describe('address', function () { describe('toOutputScript', function () { fixtures.standard.forEach(function (f) { - var network = networks[f.network] - - it('exports ' + f.script.slice(0, 30) + '... (' + f.network + ')', function () { - var script = baddress.toOutputScript(f.base58check, network) + it('decodes ' + f.script.slice(0, 30) + '... (' + f.network + ')', function () { + var script = baddress.toOutputScript(f.base58check || f.bech32, networks[f.network]) assert.strictEqual(bscript.toASM(script), f.script) }) @@ -115,7 +116,7 @@ describe('address', function () { fixtures.invalid.toOutputScript.forEach(function (f) { it('throws when ' + f.exception, function () { assert.throws(function () { - baddress.toOutputScript(f.address) + baddress.toOutputScript(f.address, f.network) }, new RegExp(f.address + ' ' + f.exception)) }) }) diff --git a/test/fixtures/address.json b/test/fixtures/address.json index 3aba4b2..69e3448 100644 --- a/test/fixtures/address.json +++ b/test/fixtures/address.json @@ -41,44 +41,48 @@ "hash": "cd7b44d0b03f2d026d1e586d7ae18903b0d385f6", "base58check": "2NByiBUaEXrhmqAsg7BbLpcQSAQs1EDwt5w", "script": "OP_HASH160 cd7b44d0b03f2d026d1e586d7ae18903b0d385f6 OP_EQUAL" - } - ], - "bech32": [ - { - "address": "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4", - "prefix": "bc", - "program": "751e76e8199196d454941c45d1b3a323f1433bd6", - "version": 0 }, { - "address": "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", - "prefix": "tb", - "program": "1863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262", - "version": 0 + "network": "bitcoin", + "version": 0, + "bech32": "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4", + "data": "751e76e8199196d454941c45d1b3a323f1433bd6", + "script": "OP_0 751e76e8199196d454941c45d1b3a323f1433bd6" + }, { - "address": "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx", - "prefix": "bc", - "program": "751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6", - "version": 1 + "network": "testnet", + "version": 0, + "bech32": "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", + "data": "1863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262", + "script": "OP_0 1863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262" }, { - "address": "BC1SW50QA3JX3S", + "network": "testnet", + "version": 0, + "bech32": "tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy", + "data": "000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433", + "script": "OP_0 000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433" + } + ], + "bech32": [ + { + "address": "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx", + "version": 1, "prefix": "bc", - "program": "751e", - "version": 16 + "data": "751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6" }, { "address": "bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj", + "version": 2, "prefix": "bc", - "program": "751e76e8199196d454941c45d1b3a323", - "version": 2 + "data": "751e76e8199196d454941c45d1b3a323" }, { - "address": "tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy", - "prefix": "tb", - "program": "000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433", - "version": 0 + "address": "BC1SW50QA3JX3S", + "version": 16, + "prefix": "bc", + "data": "751e" } ], "invalid": { @@ -87,39 +91,6 @@ "address": "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5", "exception": "Invalid checksum" }, - { - "address": "BC13W508D6QEJXTDG4Y5R3ZARVARY0C5XW7KN40WF2", - "prefix": "bc", - "version": 17, - "program": "751e76e8199196d454941c45d1b3a323f1433bd6", - "exception": "Invalid version \\(17\\)" - }, - { - "address": "BC1SW50QA3JX3S", - "prefix": "foo", - "exception": "Expected foo, got bc" - }, - { - "address": "bc1rw5uspcuh", - "prefix": "bc", - "version": 1, - "program": "75", - "exception": "Program too short" - }, - { - "address": "bc10w508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kw5rljs90", - "prefix": "bc", - "version": 1, - "program": "751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd675", - "exception": "Program too long" - }, - { - "address": "BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P", - "prefix": "bc", - "version": 0, - "program": "1d1e76e8199196d454941c45d1b3a323", - "exception": "Unknown program" - }, { "address": "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sL5k7", "exception": "Mixed-case string" @@ -155,12 +126,51 @@ { "exception": "has no matching Address", "script": "OP_RETURN 06deadbeef03f895a2ad89fb6d696497af486cb7c644a27aa568c7a18dd06113401115185474" + }, + { + "exception": "has no matching Address", + "script": "OP_0 75" + }, + { + "exception": "has no matching Address", + "script": "OP_0 751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd675" } ], "toOutputScript": [ { "exception": "has no matching Script", "address": "24kPZCmVgzfkpGdXExy56234MRHrsqQxNWE" + }, + { + "exception": "has an invalid prefix", + "address": "BC1SW50QA3JX3S", + "network": { + "bech32": "foo" + } + }, + { + "exception": "has no matching Script", + "address": "bc1rw5uspcuh" + }, + { + "exception": "has no matching Script", + "address": "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx" + }, + { + "exception": "has no matching Script", + "address": "bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj" + }, + { + "exception": "has no matching Script", + "address": "BC1SW50QA3JX3S" + }, + { + "exception": "has no matching Script", + "address": "bc10w508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kw5rljs90" + }, + { + "exception": "has no matching Script", + "address": "BC13W508D6QEJXTDG4Y5R3ZARVARY0C5XW7KN40WF2" } ] }