add Bech32 support to toOutputScript/fromOutputScript

This commit is contained in:
Daniel Cousens 2017-08-17 14:12:43 +10:00 committed by Daniel Cousens
parent d1052e4996
commit b1272a1200
4 changed files with 131 additions and 115 deletions

View file

@ -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 data = bech32.fromWords(result.words.slice(1))
return {
version: result.words[0],
prefix: result.prefix,
data: Buffer.from(data)
}
var version = words[0]
if (version > 16) throw new Error('Invalid version (' + version + ')')
var program = bech32.fromWords(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, 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')
}

View file

@ -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

View file

@ -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))
})
})

View file

@ -41,44 +41,48 @@
"hash": "cd7b44d0b03f2d026d1e586d7ae18903b0d385f6",
"base58check": "2NByiBUaEXrhmqAsg7BbLpcQSAQs1EDwt5w",
"script": "OP_HASH160 cd7b44d0b03f2d026d1e586d7ae18903b0d385f6 OP_EQUAL"
},
{
"network": "bitcoin",
"version": 0,
"bech32": "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4",
"data": "751e76e8199196d454941c45d1b3a323f1433bd6",
"script": "OP_0 751e76e8199196d454941c45d1b3a323f1433bd6"
},
{
"network": "testnet",
"version": 0,
"bech32": "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7",
"data": "1863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262",
"script": "OP_0 1863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262"
},
{
"network": "testnet",
"version": 0,
"bech32": "tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy",
"data": "000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433",
"script": "OP_0 000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433"
}
],
"bech32": [
{
"address": "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4",
"prefix": "bc",
"program": "751e76e8199196d454941c45d1b3a323f1433bd6",
"version": 0
},
{
"address": "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7",
"prefix": "tb",
"program": "1863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262",
"version": 0
},
{
"address": "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx",
"version": 1,
"prefix": "bc",
"program": "751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6",
"version": 1
},
{
"address": "BC1SW50QA3JX3S",
"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"
}
]
}