diff --git a/README.md b/README.md index f6b4b64..5db473e 100644 --- a/README.md +++ b/README.md @@ -100,8 +100,8 @@ The definitions are complete and up to date with version 2.2.0. The definitions The below examples are implemented as integration tests, they should be very easy to understand. Otherwise, pull requests are appreciated. - [Generate a random address](https://github.com/bitcoinjs/bitcoinjs-lib/blob/d853806/test/integration/basic.js#L9) -- [Generate a address from a SHA256 hash](https://github.com/bitcoinjs/bitcoinjs-lib/blob/d853806/test/integration/basic.js#L20) -- [Generate a address and WIF for Litecoin](https://github.com/bitcoinjs/bitcoinjs-lib/blob/d853806/test/integration/basic.js#L30) +- [Generate an address from a SHA256 hash](https://github.com/bitcoinjs/bitcoinjs-lib/blob/d853806/test/integration/basic.js#L20) +- [Generate an address and WIF for Litecoin](https://github.com/bitcoinjs/bitcoinjs-lib/blob/d853806/test/integration/basic.js#L30) - [Import an address via WIF](https://github.com/bitcoinjs/bitcoinjs-lib/blob/d853806/test/integration/basic.js#L43) - [Create a Transaction](https://github.com/bitcoinjs/bitcoinjs-lib/blob/d853806/test/integration/basic.js#L50) - [Create an OP RETURN transaction](https://github.com/bitcoinjs/bitcoinjs-lib/blob/d853806/test/integration/advanced.js#L24) diff --git a/package.json b/package.json index 963488e..61ccdcc 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "pushdata-bitcoin": "^1.0.1", "randombytes": "^2.0.1", "safe-buffer": "^5.0.1", - "typeforce": "^1.8.7", + "typeforce": "^1.11.3", "varuint-bitcoin": "^1.0.4", "wif": "^2.0.1" }, @@ -72,6 +72,7 @@ "bs58": "^4.0.0", "cb-http-client": "^0.2.0", "coinselect": "^3.1.1", + "dhttp": "^2.3.5", "minimaldata": "^1.0.2", "mocha": "^3.1.0", "nyc": "^10.2.0", diff --git a/test/integration/_mainnet.js b/test/integration/_mainnet.js new file mode 100644 index 0000000..0741ce8 --- /dev/null +++ b/test/integration/_mainnet.js @@ -0,0 +1,3 @@ +var Blockchain = require('cb-http-client') +var BLOCKTRAIL_API_KEY = process.env.BLOCKTRAIL_API_KEY || 'c0bd8155c66e3fb148bb1664adc1e4dacd872548' +module.exports = new Blockchain('https://api.blocktrail.com/cb/v0.2.1/BTC', { api_key: BLOCKTRAIL_API_KEY }) diff --git a/test/integration/_blockchain.js b/test/integration/_testnet.js similarity index 57% rename from test/integration/_blockchain.js rename to test/integration/_testnet.js index 198b745..8a81e59 100644 --- a/test/integration/_blockchain.js +++ b/test/integration/_testnet.js @@ -1,13 +1,12 @@ +var async = require('async') var bitcoin = require('../../') var Blockchain = require('cb-http-client') -var BLOCKTRAIL_API_KEY = process.env.BLOCKTRAIL_API_KEY || 'c0bd8155c66e3fb148bb1664adc1e4dacd872548' var coinSelect = require('coinselect') var typeforce = require('typeforce') var types = require('../../src/types') -var mainnet = new Blockchain('https://api.blocktrail.com/cb/v0.2.1/BTC', { api_key: BLOCKTRAIL_API_KEY }) -var testnet = new Blockchain('https://api.blocktrail.com/cb/v0.2.1/tBTC', { api_key: BLOCKTRAIL_API_KEY }) - +var BLOCKTRAIL_API_KEY = process.env.BLOCKTRAIL_API_KEY || 'c0bd8155c66e3fb148bb1664adc1e4dacd872548' +var blockchain = new Blockchain('https://api.blocktrail.com/cb/v0.2.1/tBTC', { api_key: BLOCKTRAIL_API_KEY }) var kpNetwork = bitcoin.networks.testnet var keyPair = bitcoin.ECPair.fromWIF('cQqjeq2rxqwnqwMewJhkNtJDixtX8ctA4bYoWHdxY4xRPVvAEjmk', kpNetwork) var kpAddress = keyPair.getAddress() @@ -37,11 +36,11 @@ function fundAddress (unspents, outputs, callback) { var tx = txb.build() var txId = tx.getId() - testnet.transactions.propagate(tx.toHex(), function (err) { + blockchain.transactions.propagate(tx.toHex(), function (err) { if (err) return callback(err) // FIXME: @blocktrail can be very slow, give it time - setTimeout(() => { + setTimeout(function () { callback(null, outputs.map(function (_, i) { return { txId: txId, vout: i } })) @@ -49,8 +48,8 @@ function fundAddress (unspents, outputs, callback) { }) } -testnet.faucetMany = function faucetMany (outputs, callback) { - testnet.addresses.unspents(kpAddress, function (err, unspents) { +blockchain.faucetMany = function faucetMany (outputs, callback) { + blockchain.addresses.unspents(kpAddress, function (err, unspents) { if (err) return callback(err) typeforce([{ @@ -63,15 +62,28 @@ testnet.faucetMany = function faucetMany (outputs, callback) { }) } -testnet.faucet = function faucet (address, value, callback) { - testnet.faucetMany([{ address: address, value: value }], function (err, unspents) { +blockchain.faucet = function faucet (address, value, callback) { + blockchain.faucetMany([{ address: address, value: value }], function (err, unspents) { callback(err, unspents && unspents[0]) }) } -testnet.RETURN = kpAddress +// verify TX was accepted +blockchain.verify = function (address, txId, value, done) { + async.retry(5, function (callback) { + setTimeout(function () { + // check that the above transaction included the intended address + blockchain.addresses.unspents(blockchain.RETURN_ADDRESS, function (err, unspents) { + if (err) return callback(err) + if (!unspents.some(function (x) { + return x.txId === txId && x.value === value + })) return callback(new Error('Could not find unspent')) -module.exports = { - m: mainnet, - t: testnet + callback() + }) + }, 600) + }, done) } + +blockchain.RETURN_ADDRESS = kpAddress +module.exports = blockchain diff --git a/test/integration/addresses.js b/test/integration/addresses.js new file mode 100644 index 0000000..ad8ad97 --- /dev/null +++ b/test/integration/addresses.js @@ -0,0 +1,92 @@ +/* global describe, it */ + +var assert = require('assert') +var bigi = require('bigi') +var bitcoin = require('../../') +var dhttp = require('dhttp/200') + +// deterministic RNG for testing only +function rng () { return Buffer.from('zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz') } + +describe('bitcoinjs-lib (addresses)', function () { + it('can generate a random address', function () { + var keyPair = bitcoin.ECPair.makeRandom({ rng: rng }) + var address = keyPair.getAddress() + + assert.strictEqual(address, '1F5VhMHukdnUES9kfXqzPzMeF1GPHKiF64') + }) + + it('can generate an address from a SHA256 hash', function () { + var hash = bitcoin.crypto.sha256('correct horse battery staple') + var d = bigi.fromBuffer(hash) + + var keyPair = new bitcoin.ECPair(d) + var address = keyPair.getAddress() + + assert.strictEqual(address, '1C7zdTfnkzmr13HfA2vNm5SJYRK6nEKyq8') + }) + + it('can import an address via WIF', function () { + var keyPair = bitcoin.ECPair.fromWIF('Kxr9tQED9H44gCmp6HAdmemAzU3n84H3dGkuWTKvE23JgHMW8gct') + var address = keyPair.getAddress() + + assert.strictEqual(address, '19AAjaTUbRjQCMuVczepkoPswiZRhjtg31') + }) + + it('can generate a 2-of-3 multisig P2SH address', function () { + var pubKeys = [ + '026477115981fe981a6918a6297d9803c4dc04f328f22041bedff886bbc2962e01', + '02c96db2302d19b43d4c69368babace7854cc84eb9e061cde51cfa77ca4a22b8b9', + '03c6103b3b83e4a24a0e33a4df246ef11772f9992663db0c35759a5e2ebf68d8e9' + ].map(function (hex) { + return Buffer.from(hex, 'hex') + }) + + var redeemScript = bitcoin.script.multisig.output.encode(2, pubKeys) // 2 of 3 + var scriptPubKey = bitcoin.script.scriptHash.output.encode(bitcoin.crypto.hash160(redeemScript)) + var address = bitcoin.address.fromOutputScript(scriptPubKey) + + assert.strictEqual(address, '36NUkt6FWUi3LAWBqWRdDmdTWbt91Yvfu7') + }) + + it('can support the retrieval of transactions for an address (3rd party blockchain)', function (done) { + var keyPair = bitcoin.ECPair.makeRandom() + var address = keyPair.getAddress() + + dhttp({ + method: 'POST', + url: 'https://api.ei8ht.com.au/3/addrtxs', + body: { + addrs: [address], + height: 0 + } + }, function (err, transactions) { + if (err) return done(err) + + // random private keys [probably] have no transactions + assert.strictEqual(Object.keys(transactions).length, 0) + done() + }) + }) + + // other networks + it('can generate a Testnet address', function () { + var testnet = bitcoin.networks.testnet + var keyPair = bitcoin.ECPair.makeRandom({ network: testnet, rng: rng }) + var wif = keyPair.toWIF() + var address = keyPair.getAddress() + + assert.strictEqual(address, 'mubSzQNtZfDj1YdNP6pNDuZy6zs6GDn61L') + assert.strictEqual(wif, 'cRgnQe9MUu1JznntrLaoQpB476M8PURvXVQB5R2eqms5tXnzNsrr') + }) + + it('can generate a Litecoin address', function () { + var litecoin = bitcoin.networks.litecoin + var keyPair = bitcoin.ECPair.makeRandom({ network: litecoin, rng: rng }) + var wif = keyPair.toWIF() + var address = keyPair.getAddress() + + assert.strictEqual(address, 'LZJSxZbjqJ2XVEquqfqHg1RQTDdfST5PTn') + assert.strictEqual(wif, 'T7A4PUSgTDHecBxW1ZiYFrDNRih2o7M8Gf9xpoCgudPF9gDiNvuS') + }) +}) diff --git a/test/integration/advanced.js b/test/integration/advanced.js deleted file mode 100644 index b5cb4df..0000000 --- a/test/integration/advanced.js +++ /dev/null @@ -1,30 +0,0 @@ -/* global describe, it */ - -var bitcoin = require('../../') -var blockchain = require('./_blockchain') - -describe('bitcoinjs-lib (advanced)', function () { - it('can create an OP_RETURN transaction', function (done) { - this.timeout(30000) - - var network = bitcoin.networks.testnet - var keyPair = bitcoin.ECPair.makeRandom({ network: network }) - var address = keyPair.getAddress() - - blockchain.t.faucet(address, 5e4, function (err, unspent) { - if (err) return done(err) - - var tx = new bitcoin.TransactionBuilder(network) - var data = Buffer.from('bitcoinjs-lib') - var dataScript = bitcoin.script.nullData.output.encode(data) - - tx.addInput(unspent.txId, unspent.vout) - tx.addOutput(dataScript, 1000) - tx.addOutput(blockchain.t.RETURN, 4e4) - tx.sign(0, keyPair) - var txRaw = tx.build() - - blockchain.t.transactions.propagate(txRaw.toHex(), done) - }) - }) -}) diff --git a/test/integration/basic.js b/test/integration/basic.js deleted file mode 100644 index 2374f73..0000000 --- a/test/integration/basic.js +++ /dev/null @@ -1,94 +0,0 @@ -/* global describe, it */ - -var assert = require('assert') -var bigi = require('bigi') -var bitcoin = require('../../') -var blockchain = require('./_blockchain') - -describe('bitcoinjs-lib (basic)', function () { - it('can generate a random bitcoin address', function () { - // for testing only - function rng () { return Buffer.from('zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz') } - - // generate random keyPair - var keyPair = bitcoin.ECPair.makeRandom({ rng: rng }) - var address = keyPair.getAddress() - - assert.strictEqual(address, '1F5VhMHukdnUES9kfXqzPzMeF1GPHKiF64') - }) - - it('can generate an address from a SHA256 hash', function () { - var hash = bitcoin.crypto.sha256('correct horse battery staple') - var d = bigi.fromBuffer(hash) - - var keyPair = new bitcoin.ECPair(d) - var address = keyPair.getAddress() - - assert.strictEqual(address, '1C7zdTfnkzmr13HfA2vNm5SJYRK6nEKyq8') - }) - - it('can generate a random keypair for alternative networks', function () { - // for testing only - function rng () { return Buffer.from('zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz') } - - var litecoin = bitcoin.networks.litecoin - - var keyPair = bitcoin.ECPair.makeRandom({ network: litecoin, rng: rng }) - var wif = keyPair.toWIF() - var address = keyPair.getAddress() - - assert.strictEqual(address, 'LZJSxZbjqJ2XVEquqfqHg1RQTDdfST5PTn') - assert.strictEqual(wif, 'T7A4PUSgTDHecBxW1ZiYFrDNRih2o7M8Gf9xpoCgudPF9gDiNvuS') - }) - - it('can import an address via WIF', function () { - var keyPair = bitcoin.ECPair.fromWIF('Kxr9tQED9H44gCmp6HAdmemAzU3n84H3dGkuWTKvE23JgHMW8gct') - var address = keyPair.getAddress() - - assert.strictEqual(address, '19AAjaTUbRjQCMuVczepkoPswiZRhjtg31') - }) - - it('can create a Transaction', function () { - var keyPair = bitcoin.ECPair.fromWIF('L1uyy5qTuGrVXrmrsvHWHgVzW9kKdrp27wBC7Vs6nZDTF2BRUVwy') - var tx = new bitcoin.TransactionBuilder() - - tx.addInput('aa94ab02c182214f090e99a0d57021caffd0f195a81c24602b1028b130b63e31', 0) - tx.addOutput('1Gokm82v6DmtwKEB8AiVhm82hyFSsEvBDK', 15000) - tx.sign(0, keyPair) - - assert.strictEqual(tx.build().toHex(), '0100000001313eb630b128102b60241ca895f1d0ffca2170d5a0990e094f2182c102ab94aa000000006b483045022100aefbcf847900b01dd3e3debe054d3b6d03d715d50aea8525f5ea3396f168a1fb022013d181d05b15b90111808b22ef4f9ebe701caf2ab48db269691fdf4e9048f4f60121029f50f51d63b345039a290c94bffd3180c99ed659ff6ea6b1242bca47eb93b59fffffffff01983a0000000000001976a914ad618cf4333b3b248f9744e8e81db2964d0ae39788ac00000000') - }) - - it('can create a [complex] Transaction', function (done) { - this.timeout(30000) - - var testnet = bitcoin.networks.testnet - var alice = bitcoin.ECPair.makeRandom({ network: testnet }) - var bob = bitcoin.ECPair.makeRandom({ network: testnet }) - var alicesAddress = alice.getAddress() - var bobsAddress = bob.getAddress() - - blockchain.t.faucetMany([ - { - address: alicesAddress, - value: 4e4 - }, - { - address: bobsAddress, - value: 2e4 - } - ], function (err, unspents) { - if (err) return done(err) - - var tx = new bitcoin.TransactionBuilder(testnet) - tx.addInput(unspents[0].txId, unspents[0].vout) - tx.addInput(unspents[1].txId, unspents[1].vout) - tx.addOutput(blockchain.t.RETURN, 3e4) - tx.addOutput('mvGVHWi6gbkBZZPaqBVRcxvKVPYd9r3fp7', 1e4) - tx.sign(0, alice) - tx.sign(1, bob) - - blockchain.t.transactions.propagate(tx.build().toHex(), done) - }) - }) -}) diff --git a/test/integration/bip32.js b/test/integration/bip32.js index c697343..7499242 100644 --- a/test/integration/bip32.js +++ b/test/integration/bip32.js @@ -1,13 +1,8 @@ /* global describe, it */ var assert = require('assert') -var bigi = require('bigi') var bip39 = require('bip39') var bitcoin = require('../../') -var crypto = require('crypto') - -var ecurve = require('ecurve') -var secp256k1 = ecurve.getCurveByName('secp256k1') describe('bitcoinjs-lib (BIP32)', function () { it('can import a BIP32 testnet xpriv and export to WIF', function () { @@ -66,53 +61,6 @@ describe('bitcoinjs-lib (BIP32)', function () { assert.equal(address, '2Mww8dCYPUpKHofjgcXcBCEGmniw9CoaiD2') }) - it('can recover a BIP32 parent private key from the parent public key, and a derived, non-hardened child private key', function () { - function recoverParent (master, child) { - assert(!master.keyPair.d, 'You already have the parent private key') - assert(child.keyPair.d, 'Missing child private key') - - var curve = secp256k1 - var QP = master.keyPair.Q - var serQP = master.keyPair.getPublicKeyBuffer() - - var d1 = child.keyPair.d - var d2 - var data = Buffer.alloc(37) - serQP.copy(data, 0) - - // search index space until we find it - for (var i = 0; i < bitcoin.HDNode.HIGHEST_BIT; ++i) { - data.writeUInt32BE(i, 33) - - // calculate I - var I = crypto.createHmac('sha512', master.chainCode).update(data).digest() - var IL = I.slice(0, 32) - var pIL = bigi.fromBuffer(IL) - - // See hdnode.js:273 to understand - d2 = d1.subtract(pIL).mod(curve.n) - - var Qp = new bitcoin.ECPair(d2).Q - if (Qp.equals(QP)) break - } - - var node = new bitcoin.HDNode(new bitcoin.ECPair(d2), master.chainCode, master.network) - node.depth = master.depth - node.index = master.index - node.masterFingerprint = master.masterFingerprint - return node - } - - var seed = crypto.randomBytes(32) - var master = bitcoin.HDNode.fromSeedBuffer(seed) - var child = master.derive(6) // m/6 - - // now for the recovery - var neuteredMaster = master.neutered() - var recovered = recoverParent(neuteredMaster, child) - assert.strictEqual(recovered.toBase58(), master.toBase58()) - }) - it('can use BIP39 to generate BIP32 wallet address', function () { // var mnemonic = bip39.generateMnemonic() var mnemonic = 'praise you muffin lion enable neck grocery crumble super myself license ghost' diff --git a/test/integration/cltv.js b/test/integration/cltv.js index b3bb4c0..8372d67 100644 --- a/test/integration/cltv.js +++ b/test/integration/cltv.js @@ -2,15 +2,16 @@ var assert = require('assert') var bitcoin = require('../../') -var blockchain = require('./_blockchain') +var testnetUtils = require('./_testnet') -var network = bitcoin.networks.testnet -var alice = bitcoin.ECPair.fromWIF('cScfkGjbzzoeewVWmU2hYPUHeVGJRDdFt7WhmrVVGkxpmPP8BHWe', network) -var bob = bitcoin.ECPair.fromWIF('cMkopUXKWsEzAjfa1zApksGRwjVpJRB3831qM9W4gKZsLwjHXA9x', network) +var testnet = bitcoin.networks.testnet +var alice = bitcoin.ECPair.fromWIF('cScfkGjbzzoeewVWmU2hYPUHeVGJRDdFt7WhmrVVGkxpmPP8BHWe', testnet) +var bob = bitcoin.ECPair.fromWIF('cMkopUXKWsEzAjfa1zApksGRwjVpJRB3831qM9W4gKZsLwjHXA9x', testnet) -describe('bitcoinjs-lib (CLTV)', function () { +describe('bitcoinjs-lib (transactions w/ CLTV)', function () { var hashType = bitcoin.Transaction.SIGHASH_ALL + // IF MTP > utcSeconds, aQ can redeem, ELSE bQ, aQ joint redeem function cltvCheckSigOutput (aQ, bQ, utcSeconds) { return bitcoin.script.compile([ bitcoin.opcodes.OP_IF, @@ -33,23 +34,23 @@ describe('bitcoinjs-lib (CLTV)', function () { } // expiry past, {Alice's signature} OP_TRUE - it('where Alice can redeem after the expiry is past', function (done) { + it('can create (and broadcast via 3PBP) a Transaction where Alice can redeem the output after the expiry', function (done) { this.timeout(30000) // three hours ago var timeUtc = utcNow() - (3600 * 3) var redeemScript = cltvCheckSigOutput(alice, bob, timeUtc) var scriptPubKey = bitcoin.script.scriptHash.output.encode(bitcoin.crypto.hash160(redeemScript)) - var address = bitcoin.address.fromOutputScript(scriptPubKey, network) + var address = bitcoin.address.fromOutputScript(scriptPubKey, testnet) // fund the P2SH(CLTV) address - blockchain.t.faucet(address, 2e4, function (err, unspent) { + testnetUtils.faucet(address, 2e4, function (err, unspent) { if (err) return done(err) - var tx = new bitcoin.TransactionBuilder(network) + var tx = new bitcoin.TransactionBuilder(testnet) tx.setLockTime(timeUtc) tx.addInput(unspent.txId, 0, 0xfffffffe) - tx.addOutput(blockchain.t.RETURN, 1e4) + tx.addOutput(testnetUtils.RETURN_ADDRESS, 1e4) var txRaw = tx.buildIncomplete() var signatureHash = txRaw.hashForSignature(0, redeemScript, hashType) @@ -62,27 +63,28 @@ describe('bitcoinjs-lib (CLTV)', function () { txRaw.setInputScript(0, redeemScriptSig) - blockchain.t.transactions.propagate(txRaw.toHex(), done) + testnetUtils.transactions.propagate(txRaw.toHex(), done) }) }) // expiry ignored, {Bob's signature} {Alice's signature} OP_FALSE - it('where Alice and Bob can redeem at any time', function (done) { + it('can create (and broadcast via 3PBP) a Transaction where Alice and Bob can redeem the output at any time', function (done) { this.timeout(30000) // two hours ago var timeUtc = utcNow() - (3600 * 2) var redeemScript = cltvCheckSigOutput(alice, bob, timeUtc) var scriptPubKey = bitcoin.script.scriptHash.output.encode(bitcoin.crypto.hash160(redeemScript)) - var address = bitcoin.address.fromOutputScript(scriptPubKey, network) + var address = bitcoin.address.fromOutputScript(scriptPubKey, testnet) // fund the P2SH(CLTV) address - blockchain.t.faucet(address, 2e4, function (err, unspent) { + testnetUtils.faucet(address, 2e4, function (err, unspent) { if (err) return done(err) - var tx = new bitcoin.TransactionBuilder(network) + var tx = new bitcoin.TransactionBuilder(testnet) + tx.setLockTime(timeUtc) tx.addInput(unspent.txId, 0, 0xfffffffe) - tx.addOutput(blockchain.t.RETURN, 1e4) + tx.addOutput(testnetUtils.RETURN_ADDRESS, 1e4) var txRaw = tx.buildIncomplete() var signatureHash = txRaw.hashForSignature(0, redeemScript, hashType) @@ -94,28 +96,28 @@ describe('bitcoinjs-lib (CLTV)', function () { txRaw.setInputScript(0, redeemScriptSig) - blockchain.t.transactions.propagate(txRaw.toHex(), done) + testnetUtils.transactions.propagate(txRaw.toHex(), done) }) }) // expiry in the future, {Alice's signature} OP_TRUE - it('fails when still time-locked', function (done) { + it('can create (but fail to broadcast via 3PBP) a Transaction where Alice attempts to redeem before the expiry', function (done) { this.timeout(30000) // two hours from now var timeUtc = utcNow() + (3600 * 2) var redeemScript = cltvCheckSigOutput(alice, bob, timeUtc) var scriptPubKey = bitcoin.script.scriptHash.output.encode(bitcoin.crypto.hash160(redeemScript)) - var address = bitcoin.address.fromOutputScript(scriptPubKey, network) + var address = bitcoin.address.fromOutputScript(scriptPubKey, testnet) // fund the P2SH(CLTV) address - blockchain.t.faucet(address, 2e4, function (err, unspent) { + testnetUtils.faucet(address, 2e4, function (err, unspent) { if (err) return done(err) - var tx = new bitcoin.TransactionBuilder(network) + var tx = new bitcoin.TransactionBuilder(testnet) tx.setLockTime(timeUtc) tx.addInput(unspent.txId, 0, 0xfffffffe) - tx.addOutput(blockchain.t.RETURN, 1e4) + tx.addOutput(testnetUtils.RETURN_ADDRESS, 1e4) var txRaw = tx.buildIncomplete() var signatureHash = txRaw.hashForSignature(0, redeemScript, hashType) @@ -128,7 +130,7 @@ describe('bitcoinjs-lib (CLTV)', function () { txRaw.setInputScript(0, redeemScriptSig) - blockchain.t.transactions.propagate(txRaw.toHex(), function (err) { + testnetUtils.transactions.propagate(txRaw.toHex(), function (err) { assert.throws(function () { if (err) throw err }, /Error: 64: non-final/) diff --git a/test/integration/crypto.js b/test/integration/crypto.js index 6b5a076..16dec80 100644 --- a/test/integration/crypto.js +++ b/test/integration/crypto.js @@ -4,7 +4,8 @@ var assert = require('assert') var async = require('async') var bigi = require('bigi') var bitcoin = require('../../') -var blockchain = require('./_blockchain') +var mainnet = require('./_mainnet') +var crypto = require('crypto') var ecurve = require('ecurve') var secp256k1 = ecurve.getCurveByName('secp256k1') @@ -27,7 +28,7 @@ describe('bitcoinjs-lib (crypto)', function () { var txIds = inputs.map(function (x) { return x.txId }) // first retrieve the relevant transactions - blockchain.m.transactions.get(txIds, function (err, results) { + mainnet.transactions.get(txIds, function (err, results) { assert.ifError(err) var transactions = {} @@ -49,7 +50,7 @@ describe('bitcoinjs-lib (crypto)', function () { var prevVout = transaction.ins[input.vout].index tasks.push(function (callback) { - blockchain.m.transactions.get(prevOutTxId, function (err, result) { + mainnet.transactions.get(prevOutTxId, function (err, result) { if (err) return callback(err) var prevOut = bitcoin.Transaction.fromHex(result.txHex) @@ -110,4 +111,51 @@ describe('bitcoinjs-lib (crypto)', function () { }) }) }) + + it('can recover a BIP32 parent private key from the parent public key, and a derived, non-hardened child private key', function () { + function recoverParent (master, child) { + assert(!master.keyPair.d, 'You already have the parent private key') + assert(child.keyPair.d, 'Missing child private key') + + var curve = secp256k1 + var QP = master.keyPair.Q + var serQP = master.keyPair.getPublicKeyBuffer() + + var d1 = child.keyPair.d + var d2 + var data = Buffer.alloc(37) + serQP.copy(data, 0) + + // search index space until we find it + for (var i = 0; i < bitcoin.HDNode.HIGHEST_BIT; ++i) { + data.writeUInt32BE(i, 33) + + // calculate I + var I = crypto.createHmac('sha512', master.chainCode).update(data).digest() + var IL = I.slice(0, 32) + var pIL = bigi.fromBuffer(IL) + + // See hdnode.js:273 to understand + d2 = d1.subtract(pIL).mod(curve.n) + + var Qp = new bitcoin.ECPair(d2).Q + if (Qp.equals(QP)) break + } + + var node = new bitcoin.HDNode(new bitcoin.ECPair(d2), master.chainCode, master.network) + node.depth = master.depth + node.index = master.index + node.masterFingerprint = master.masterFingerprint + return node + } + + var seed = crypto.randomBytes(32) + var master = bitcoin.HDNode.fromSeedBuffer(seed) + var child = master.derive(6) // m/6 + + // now for the recovery + var neuteredMaster = master.neutered() + var recovered = recoverParent(neuteredMaster, child) + assert.strictEqual(recovered.toBase58(), master.toBase58()) + }) }) diff --git a/test/integration/multisig.js b/test/integration/multisig.js deleted file mode 100644 index a8dc5d3..0000000 --- a/test/integration/multisig.js +++ /dev/null @@ -1,76 +0,0 @@ -/* global describe, it */ - -var async = require('async') -var assert = require('assert') -var bitcoin = require('../../') -var blockchain = require('./_blockchain') - -describe('bitcoinjs-lib (multisig)', function () { - it('can create a 2-of-3 multisig P2SH address', function () { - var pubKeys = [ - '026477115981fe981a6918a6297d9803c4dc04f328f22041bedff886bbc2962e01', - '02c96db2302d19b43d4c69368babace7854cc84eb9e061cde51cfa77ca4a22b8b9', - '03c6103b3b83e4a24a0e33a4df246ef11772f9992663db0c35759a5e2ebf68d8e9' - ].map(function (hex) { - return Buffer.from(hex, 'hex') - }) - - var redeemScript = bitcoin.script.multisig.output.encode(2, pubKeys) // 2 of 3 - var scriptPubKey = bitcoin.script.scriptHash.output.encode(bitcoin.crypto.hash160(redeemScript)) - var address = bitcoin.address.fromOutputScript(scriptPubKey) - - assert.strictEqual(address, '36NUkt6FWUi3LAWBqWRdDmdTWbt91Yvfu7') - }) - - it('can spend from a 2-of-4 multsig P2SH address', function (done) { - this.timeout(30000) - - var keyPairs = [ - '91avARGdfge8E4tZfYLoxeJ5sGBdNJQH4kvjJoQFacbgwmaKkrx', - '91avARGdfge8E4tZfYLoxeJ5sGBdNJQH4kvjJoQFacbgww7vXtT', - '91avARGdfge8E4tZfYLoxeJ5sGBdNJQH4kvjJoQFacbgx3cTMqe', - '91avARGdfge8E4tZfYLoxeJ5sGBdNJQH4kvjJoQFacbgx9rcrL7' - ].map(function (wif) { return bitcoin.ECPair.fromWIF(wif, bitcoin.networks.testnet) }) - var pubKeys = keyPairs.map(function (x) { return x.getPublicKeyBuffer() }) - - var redeemScript = bitcoin.script.multisig.output.encode(2, pubKeys) // 2 of 4 - var scriptPubKey = bitcoin.script.scriptHash.output.encode(bitcoin.crypto.hash160(redeemScript)) - var address = bitcoin.address.fromOutputScript(scriptPubKey, bitcoin.networks.testnet) - - // attempt to send funds to the source address - blockchain.t.faucet(address, 2e4, function (err, unspent) { - if (err) return done(err) - - var txb = new bitcoin.TransactionBuilder(bitcoin.networks.testnet) - txb.addInput(unspent.txId, unspent.vout) - txb.addOutput(blockchain.t.RETURN, 1e4) - - // sign with 1st and 3rd key - txb.sign(0, keyPairs[0], redeemScript) - txb.sign(0, keyPairs[2], redeemScript) - - // broadcast our transaction - var tx = txb.build() - var txId = tx.getId() - - blockchain.t.transactions.propagate(tx.toHex(), function (err) { - if (err) return done(err) - - // wait for TX to be accepted - async.retry(5, function (callback) { - setTimeout(function () { - // check that the above transaction included the intended address - blockchain.t.addresses.unspents(blockchain.t.RETURN, function (err, unspents) { - if (err) return callback(err) - if (!unspents.some(function (x) { - return x.txId === txId && x.value === 1e4 - })) return callback(new Error('Could not find unspent after broadcast')) - - callback() - }) - }, 600) - }, done) - }) - }) - }) -}) diff --git a/test/integration/transactions.js b/test/integration/transactions.js new file mode 100644 index 0000000..8832c7f --- /dev/null +++ b/test/integration/transactions.js @@ -0,0 +1,157 @@ +/* global describe, it */ + +var assert = require('assert') +var bitcoin = require('../../') +var dhttp = require('dhttp/200') +var testnet = bitcoin.networks.testnet +var testnetUtils = require('./_testnet') + +function rng () { + return Buffer.from('YT8dAtK4d16A3P1z+TpwB2jJ4aFH3g9M1EioIBkLEV4=', 'base64') +} + +describe('bitcoinjs-lib (transactions)', function () { + it('can create a 1-to-1 Transaction', function () { + var alice = bitcoin.ECPair.fromWIF('L1uyy5qTuGrVXrmrsvHWHgVzW9kKdrp27wBC7Vs6nZDTF2BRUVwy') + var tx = new bitcoin.TransactionBuilder() + + tx.addInput('61d520ccb74288c96bc1a2b20ea1c0d5a704776dd0164a396efec3ea7040349d', 0) // Alice's previous transaction output, has 15000 satoshis + tx.addOutput('1cMh228HTCiwS8ZsaakH8A8wze1JR5ZsP', 12000) + // (in)15000 - (out)12000 = (fee)3000, this is the miner fee + + tx.sign(0, alice) + + // build, prepare for broadcast to the Bitcoin network, see "can broadcast a Transaction" below + assert.strictEqual(tx.build().toHex(), '01000000019d344070eac3fe6e394a16d06d7704a7d5c0a10eb2a2c16bc98842b7cc20d561000000006b48304502210088828c0bdfcdca68d8ae0caeb6ec62cd3fd5f9b2191848edae33feb533df35d302202e0beadd35e17e7f83a733f5277028a9b453d525553e3f5d2d7a7aa8010a81d60121029f50f51d63b345039a290c94bffd3180c99ed659ff6ea6b1242bca47eb93b59fffffffff01e02e0000000000001976a91406afd46bcdfd22ef94ac122aa11f241244a37ecc88ac00000000') + }) + + it('can create a 2-to-2 Transaction', function () { + var alice = bitcoin.ECPair.fromWIF('L1Knwj9W3qK3qMKdTvmg3VfzUs3ij2LETTFhxza9LfD5dngnoLG1') + var bob = bitcoin.ECPair.fromWIF('KwcN2pT3wnRAurhy7qMczzbkpY5nXMW2ubh696UBc1bcwctTx26z') + + var tx = new bitcoin.TransactionBuilder() + tx.addInput('b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c', 6) // Alice's previous transaction output, has 200000 satoshis + tx.addInput('7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730', 0) // Bob's previous transaction output, has 300000 satoshis + tx.addOutput('1CUNEBjYrCn2y1SdiUMohaKUi4wpP326Lb', 180000) + tx.addOutput('1JtK9CQw1syfWj1WtFMWomrYdV3W2tWBF9', 170000) + // (in)(200000 + 300000) - (out)(180000 + 150000) = (fee)170000, this is the miner fee + + tx.sign(1, bob) // Bob signs his input, which was the second input (1th) + tx.sign(0, alice) // Alice signs her input, which was the first input (0th) + + // build, prepare for broadcast to the Bitcoin network, see "can broadcast a Transaction" below + assert.strictEqual(tx.build().toHex(), '01000000024c94e48a870b85f41228d33cf25213dfcc8dd796e7211ed6b1f9a014809dbbb5060000006a473044022041450c258ce7cac7da97316bf2ea1ce66d88967c4df94f3e91f4c2a30f5d08cb02203674d516e6bb2b0afd084c3551614bd9cec3c2945231245e891b145f2d6951f0012103e05ce435e462ec503143305feb6c00e06a3ad52fbf939e85c65f3a765bb7baacffffffff3077d9de049574c3af9bc9c09a7c9db80f2d94caaf63988c9166249b955e867d000000006b483045022100aeb5f1332c79c446d3f906e4499b2e678500580a3f90329edf1ba502eec9402e022072c8b863f8c8d6c26f4c691ac9a6610aa4200edc697306648ee844cfbc089d7a012103df7940ee7cddd2f97763f67e1fb13488da3fbdd7f9c68ec5ef0864074745a289ffffffff0220bf0200000000001976a9147dd65592d0ab2fe0d0257d571abf032cd9db93dc88ac10980200000000001976a914c42e7ef92fdb603af844d064faad95db9bcdfd3d88ac00000000') + }) + + it('can create (and broadcast via 3PBP) a typical Transaction', function (done) { + this.timeout(30000) + + var alice1 = bitcoin.ECPair.makeRandom({ network: testnet }) + var alice2 = bitcoin.ECPair.makeRandom({ network: testnet }) + var aliceChange = bitcoin.ECPair.makeRandom({ rng: rng, network: testnet }) + + // "simulate" on testnet that Alice has 2 unspent outputs + testnetUtils.faucetMany([ + { + address: alice1.getAddress(), + value: 4e4 + }, + { + address: alice2.getAddress(), + value: 2e4 + } + ], function (err, unspents) { + if (err) return done(err) + + var tx = new bitcoin.TransactionBuilder(testnet) + tx.addInput(unspents[0].txId, unspents[0].vout) // alice1 unspent + tx.addInput(unspents[1].txId, unspents[1].vout) // alice2 unspent + tx.addOutput('mvGVHWi6gbkBZZPaqBVRcxvKVPYd9r3fp7', 1e4) // the actual "spend" + tx.addOutput(aliceChange.getAddress(), 3e4) // Alice's change + // (in)(4e4 + 2e4) - (out)(1e4 + 3e4) = (fee)2e4 = 20000, this is the miner fee + + // Alice signs each input with the respective private keys + tx.sign(0, alice1) + tx.sign(1, alice2) + + // build and broadcast to the Bitcoin Testnet network + dhttp({ + method: 'POST', + url: 'https://api.ei8ht.com.au:9443/3/pushtx', +// url: 'http://tbtc.blockr.io/api/v1/tx/push', + body: tx.build().toHex() + }, done) + // to build and broadcast to the actual Bitcoin network, see https://github.com/bitcoinjs/bitcoinjs-lib/issues/839 + }) + }) + + it('can create (and broadcast via 3PBP) a Transaction with an OP_RETURN output', function (done) { + this.timeout(30000) + + var keyPair = bitcoin.ECPair.makeRandom({ network: testnet }) + var address = keyPair.getAddress() + + testnetUtils.faucet(address, 5e4, function (err, unspent) { + if (err) return done(err) + + var tx = new bitcoin.TransactionBuilder(testnet) + var data = Buffer.from('bitcoinjs-lib', 'utf8') + var dataScript = bitcoin.script.nullData.output.encode(data) + + tx.addInput(unspent.txId, unspent.vout) + tx.addOutput(dataScript, 1000) + tx.addOutput(testnetUtils.RETURN_ADDRESS, 4e4) + tx.sign(0, keyPair) + + // build and broadcast to the Bitcoin Testnet network + dhttp({ + method: 'POST', + url: 'https://api.ei8ht.com.au:9443/3/pushtx', + body: tx.build().toHex() + }, done) + }) + }) + + it('can create (and broadcast via 3PBP) a Transaction with a 2-of-4 multisig P2SH input', function (done) { + this.timeout(30000) + + var keyPairs = [ + '91avARGdfge8E4tZfYLoxeJ5sGBdNJQH4kvjJoQFacbgwmaKkrx', + '91avARGdfge8E4tZfYLoxeJ5sGBdNJQH4kvjJoQFacbgww7vXtT', + '91avARGdfge8E4tZfYLoxeJ5sGBdNJQH4kvjJoQFacbgx3cTMqe', + '91avARGdfge8E4tZfYLoxeJ5sGBdNJQH4kvjJoQFacbgx9rcrL7' + ].map(function (wif) { return bitcoin.ECPair.fromWIF(wif, testnet) }) + var pubKeys = keyPairs.map(function (x) { return x.getPublicKeyBuffer() }) + + var redeemScript = bitcoin.script.multisig.output.encode(2, pubKeys) // 2 of 4 + var scriptPubKey = bitcoin.script.scriptHash.output.encode(bitcoin.crypto.hash160(redeemScript)) + var address = bitcoin.address.fromOutputScript(scriptPubKey, testnet) + + // attempt to send funds to the source address + testnetUtils.faucet(address, 2e4, function (err, unspent) { + if (err) return done(err) + + var txb = new bitcoin.TransactionBuilder(testnet) + txb.addInput(unspent.txId, unspent.vout) + txb.addOutput(testnetUtils.RETURN_ADDRESS, 1e4) + + // sign with 1st and 3rd key + txb.sign(0, keyPairs[0], redeemScript) + txb.sign(0, keyPairs[2], redeemScript) + + // broadcast our transaction + var tx = txb.build() + var txId = tx.getId() + + dhttp({ + method: 'POST', + url: 'https://api.ei8ht.com.au:9443/3/pushtx', + body: tx.toHex() + }, function (err) { + if (err) return done(err) + + testnetUtils.verify(address, txId, 1e4, done) + }) + }) + }) +})