From 889bc8fc7ac24351c2210a853ccd7de56be3283e Mon Sep 17 00:00:00 2001 From: Gal Buki Date: Sun, 26 Jun 2022 19:42:44 +0300 Subject: [PATCH] sign p2pkh inputs using an array of private keys --- classes/transaction.js | 45 +++++++++++++++++++++++++++++++++++++ test/classes/transaction.js | 34 ++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/classes/transaction.js b/classes/transaction.js index 71972cd..8e00da9 100644 --- a/classes/transaction.js +++ b/classes/transaction.js @@ -1,5 +1,6 @@ const generateTxSignature = require('../functions/generate-tx-signature') const createP2PKHLockScript = require('../functions/create-p2pkh-lock-script') +const encodeAddress = require('../functions/encode-address') const encodeHex = require('../functions/encode-hex') const decodeHex = require('../functions/decode-hex') const extractP2PKHLockScriptPubkeyhash = require('../functions/extract-p2pkh-lock-script-pubkeyhash') @@ -164,6 +165,8 @@ class Transaction { } sign (privateKey) { + if (Array.isArray(privateKey)) return this.#signMany(privateKey) + if (Object.isFrozen(this)) throw new Error('transaction finalized') if (typeof privateKey === 'string') { privateKey = PrivateKey.fromString(privateKey) } @@ -197,6 +200,48 @@ class Transaction { return this } + #signMany (privateKeys) { + if (Object.isFrozen(this)) throw new Error('transaction finalized') + + const keys = {} + for (let privateKey of privateKeys) { + if (typeof privateKey === 'string') { privateKey = PrivateKey.fromString(privateKey) } + if (!(privateKey instanceof PrivateKey)) throw new Error(`not a private key: ${privateKey}`) + + keys[privateKey.toAddress()] = privateKey + } + + for (let vin = 0; vin < this.inputs.length; vin++) { + const input = this.inputs[vin] + const output = input.output + + if (input.script.length) continue + if (!output) continue + + const outputScript = output.script + const outputSatoshis = output.satoshis + + if (!isP2PKHLockScript(output.script)) continue + + const inputAddress = encodeAddress(extractP2PKHLockScriptPubkeyhash(output.script)) + if (keys[inputAddress] === undefined) continue + + const privateKey = keys[inputAddress] + + const txsignature = generateTxSignature(this, vin, outputScript, outputSatoshis, + privateKey.number, privateKey.toPublicKey().point) + + const writer = new BufferWriter() + writePushData(writer, txsignature) + writePushData(writer, privateKey.toPublicKey().toBuffer()) + const script = writer.toBuffer() + + input.script = Script.fromBuffer(script) + } + + return this + } + verify () { const parents = this.inputs.map(input => input.output) const minFeePerKb = require('../index').feePerKb diff --git a/test/classes/transaction.js b/test/classes/transaction.js index 6cc9b85..8ba10a1 100644 --- a/test/classes/transaction.js +++ b/test/classes/transaction.js @@ -470,13 +470,16 @@ describe('Transaction', () => { const privateKey = PrivateKey.fromRandom() const tx1 = new Transaction().to(privateKey.toAddress(), 1000) const tx2 = new Transaction().from(tx1.outputs[0]).to(privateKey.toAddress(), 2000).sign(privateKey) + const tx3 = new Transaction().from(tx1.outputs[0]).to(privateKey.toAddress(), 2000).sign([privateKey]) expect(tx2.inputs[0].script.length > 0).to.equal(true) + expect(tx3.inputs[0].script.length > 0).to.equal(true) nimble.functions.verifyScript(tx2.inputs[0].script, tx1.outputs[0].script, tx2, 0, tx1.outputs[0].satoshis) }) it('supports string private key', () => { const privateKey = PrivateKey.fromRandom() new Transaction().sign(privateKey.toString()) // eslint-disable-line + new Transaction().sign([privateKey.toString()]) // eslint-disable-line }) it('does not sign different addresses', () => { @@ -485,8 +488,11 @@ describe('Transaction', () => { const tx0 = new Transaction().to(privateKey1.toAddress(), 1000) const tx1 = new Transaction().to(privateKey2.toAddress(), 1000) const tx2 = new Transaction().from(tx0.outputs[0]).from(tx1.outputs[0]).to(privateKey2.toAddress(), 2000).sign(privateKey2) + const tx3 = new Transaction().from(tx0.outputs[0]).from(tx1.outputs[0]).to(privateKey2.toAddress(), 2000).sign([privateKey2]) expect(tx2.inputs[0].script.length === 0).to.equal(true) expect(tx2.inputs[1].script.length > 0).to.equal(true) + expect(tx3.inputs[0].script.length === 0).to.equal(true) + expect(tx3.inputs[1].script.length > 0).to.equal(true) }) it('does not sign non-p2pkh', () => { @@ -494,7 +500,9 @@ describe('Transaction', () => { const script = Array.from([...nimble.functions.createP2PKHLockScript(privateKey.toAddress().pubkeyhash), 1]) const utxo = { txid: new Transaction().hash, vout: 0, script, satoshis: 1000 } const tx = new Transaction().from(utxo).sign(privateKey) + const tx2 = new Transaction().from(utxo).sign([privateKey]) expect(tx.inputs[0].script.length).to.equal(0) + expect(tx2.inputs[0].script.length).to.equal(0) }) it('does not sign without previous outputs', () => { @@ -502,7 +510,9 @@ describe('Transaction', () => { const tx1 = new Transaction().to(privateKey.toAddress(), 1000) const input = { txid: tx1.hash, vout: 0, script: [], sequence: 0 } const tx2 = new Transaction().input(input).to(privateKey.toAddress(), 2000).sign(privateKey) + const tx3 = new Transaction().input(input).to(privateKey.toAddress(), 2000).sign([privateKey]) expect(tx2.inputs[0].script.length).to.equal(0) + expect(tx3.inputs[0].script.length).to.equal(0) }) it('does not sign if already have sign data', () => { @@ -511,12 +521,17 @@ describe('Transaction', () => { const tx2 = new Transaction().from(tx1.outputs[0]).to(privateKey.toAddress(), 2000) tx2.inputs[0].script = [0x01] tx2.sign(privateKey) + const tx3 = new Transaction().from(tx1.outputs[0]).to(privateKey.toAddress(), 2000) + tx3.inputs[0].script = [0x01] + tx3.sign([privateKey]) expect(tx2.inputs[0].script).to.deep.equal([0x01]) + expect(tx3.inputs[0].script).to.deep.equal([0x01]) }) it('returns self for chaining', () => { const tx = new Transaction() expect(tx.sign(PrivateKey.fromRandom())).to.equal(tx) + expect(tx.sign([PrivateKey.fromRandom()])).to.equal(tx) }) it('throws if private key not provided', () => { @@ -524,6 +539,24 @@ describe('Transaction', () => { expect(() => new Transaction().sign({})).to.throw('not a private key: [object Object]') expect(() => new Transaction().sign(123)).to.throw('not a private key: 123') expect(() => new Transaction().sign('abc')).to.throw('bad checksum') + + expect(() => new Transaction().sign([new nimble.Script()])).to.throw('not a private key') + expect(() => new Transaction().sign([123])).to.throw('not a private key: 123') + expect(() => new Transaction().sign(['abc'])).to.throw('bad checksum') + }) + + it('sign many', () => { + const privateKey1 = PrivateKey.fromRandom() + const privateKey2 = PrivateKey.fromRandom() + const privateKey3 = PrivateKey.fromRandom() + const tx0 = new Transaction().to(privateKey1.toAddress(), 1000) + const tx1 = new Transaction().to(privateKey2.toAddress(), 1000) + const tx2 = new Transaction().from(tx0.outputs[0]).from(tx1.outputs[0]).to(privateKey3.toAddress(), 2000).sign([privateKey1, privateKey2]) + const tx3 = new Transaction().from(tx0.outputs[0]).from(tx1.outputs[0]).to(privateKey3.toAddress(), 2000).sign([privateKey2, privateKey1]) // the order of the private keys should not matter + expect(tx2.inputs[0].script.length > 0).to.equal(true) + expect(tx2.inputs[1].script.length > 0).to.equal(true) + expect(tx3.inputs[0].script.length > 0).to.equal(true) + expect(tx3.inputs[1].script.length > 0).to.equal(true) }) }) @@ -567,6 +600,7 @@ describe('Transaction', () => { expect(() => tx.output({ script: [], satoshis: 0 })).to.throw(err) expect(() => tx.change(address)).to.throw(err) expect(() => tx.sign(privateKey)).to.throw(err) + expect(() => tx.sign([privateKey])).to.throw(err) tx.n = 1 expect('n' in tx).to.equal(false)