diff --git a/test/config-example.js b/test/config-example.js index 1322ea9e97..f54874ac69 100644 --- a/test/config-example.js +++ b/test/config-example.js @@ -72,8 +72,9 @@ exports.servers = { 'admin = 127.0.0.1', 'protocol = ws'), - 'node_db': lines('type=memory', - 'path=integration') + 'node_db': lines('type=memory', 'path=integration'), + + features: lines('MultiSign') }, 'uniport_tests' : { diff --git a/test/multi-sign-test.js b/test/multi-sign-test.js new file mode 100644 index 0000000000..9d4b04c493 --- /dev/null +++ b/test/multi-sign-test.js @@ -0,0 +1,378 @@ +let assert = require('assert'); +let _ = require('lodash'); +let async = require('async'); +let testutils = require('./testutils'); +let config = testutils.init_config(); +let accounts = require('./testconfig').accounts; +let Amount = require('ripple-lib').Amount; +let Transaction = require('ripple-lib').Transaction; + +suite('MultiSign', function() { + let $ = {}; + let opts = {}; + + setup(function(done) { + testutils.build_setup(opts).call($, done); + }); + + setup(function(done) { + $.remote.local_signing = false; + testutils.create_accounts( + $.remote, + 'root', + Amount.from_human('1000 XRP'), + ['alice', 'bob', 'carol'], + done); + }); + + teardown(function(done) { + testutils.build_teardown().call($, done); + }); + + function getAliceSequence(callback) { + return $.remote.account('alice')._transactionManager._nextSequence; + } + + test('remote signing', function(done) { + async.series([ + function(callback) { + let tx = $.remote.createTransaction('SignerListSet', { + account: accounts.alice.account, + signers: [ + {account: accounts.bob.account, weight: 1}, + {account: accounts.carol.account, weight: 2 } + ], + signerQuorum: 3 + }); + + testutils.submit_transaction(tx, callback); + }, + + function(callback) { + let tx = $.remote.createTransaction('AccountSet', {account: accounts.alice.account}); + tx.setFee(100); + tx.setSequence(getAliceSequence()); + tx.setLastLedgerSequenceOffset(5); + + let mTx = Transaction.from_json(tx.getMultiSigningJson()); + + [accounts.bob, accounts.carol].forEach(account => { + let signer = mTx.multiSign( account.account, account.secret); + assert(signer.Account); + assert(signer.SigningPubKey); + assert(signer.TxnSignature); + + tx.addMultiSigner(signer); + }); + + tx.once('submitted', function(res) { + assert.strictEqual(res.engine_result, 'tesSUCCESS'); + assert.deepEqual(res.tx_json.Signers, tx.getMultiSigners()); + callback(); + }); + + tx.submit(); + } + ], done); + }); + + test('local signing', function(done) { + $.remote.local_signing = true; + + async.series([ + function(callback) { + let tx = $.remote.createTransaction('SignerListSet', { + account: accounts.alice.account, + signers: [ + {account: accounts.bob.account, weight: 1}, + {account: accounts.carol.account, weight: 2 } + ], + signerQuorum: 3 + }); + + testutils.submit_transaction(tx, callback); + }, + + function(callback) { + let tx = $.remote.createTransaction('AccountSet', {account: accounts.alice.account}); + tx.setFee(100); + tx.setSequence(getAliceSequence()); + tx.setLastLedgerSequenceOffset(5); + + let mTx = Transaction.from_json(tx.getMultiSigningJson()); + + [accounts.bob, accounts.carol].forEach(account => { + let signer = mTx.multiSign( account.account, account.secret); + assert(signer.Account); + assert(signer.SigningPubKey); + assert(signer.TxnSignature); + + tx.addMultiSigner(signer); + }); + + tx.once('submitted', function(res) { + assert.strictEqual(res.engine_result, 'tesSUCCESS'); + assert.deepEqual(res.tx_json.Signers, tx.getMultiSigners()); + callback(); + }); + + tx.submit(); + } + ], done); + }); + + test('No multi-signers specified for account', function(done) { + async.series([ + function(callback) { + let tx = $.remote.createTransaction('AccountSet', {account: accounts.alice.account}); + tx.setFee(100); + tx.setSequence(getAliceSequence()); + tx.setLastLedgerSequenceOffset(5); + + let mTx = Transaction.from_json(tx.getMultiSigningJson()); + + let signer = mTx.multiSign(accounts.bob.account, accounts.bob.secret); + assert(signer.Account); + assert(signer.SigningPubKey); + assert(signer.TxnSignature); + + tx.addMultiSigner(signer); + + tx.once('submitted', function(res) { + assert.strictEqual(res.engine_result, 'tefNOT_MULTI_SIGNING'); + callback(); + }); + + tx.submit(); + } + ], done); + }); + + test('Attempt to use unspecified signer', function(done) { + async.series([ + function(callback) { + let tx = $.remote.createTransaction('SignerListSet', { + account: accounts.alice.account, + signers: [ + {account: accounts.bob.account, weight: 1}, + ], + signerQuorum: 1 + }); + + testutils.submit_transaction(tx, callback); + }, + + function(callback) { + let tx = $.remote.createTransaction('AccountSet', {account: accounts.alice.account}); + tx.setFee(100); + tx.setSequence(getAliceSequence()); + tx.setLastLedgerSequenceOffset(5); + + let mTx = Transaction.from_json(tx.getMultiSigningJson()); + + let signer = mTx.multiSign(accounts.carol.account, accounts.carol.secret); + assert(signer.Account); + assert(signer.SigningPubKey); + assert(signer.TxnSignature); + + tx.addMultiSigner(signer); + + tx.once('submitted', function(res) { + assert.strictEqual(res.engine_result, 'tefBAD_SIGNATURE'); + callback(); + }); + + tx.submit(); + } + ], done); + }); + + test('Unmet quorum', function(done) { + async.series([ + function(callback) { + let tx = $.remote.createTransaction('SignerListSet', { + account: accounts.alice.account, + signers: [ + {account: accounts.bob.account, weight: 1}, + {account: accounts.carol.account, weight: 1} + ], + signerQuorum: 2 + }); + + testutils.submit_transaction(tx, callback); + }, + + function(callback) { + let tx = $.remote.createTransaction('AccountSet', {account: accounts.alice.account}); + tx.setFee(100); + tx.setSequence(getAliceSequence()); + tx.setLastLedgerSequenceOffset(5); + + let mTx = Transaction.from_json(tx.getMultiSigningJson()); + + let signer = mTx.multiSign(accounts.bob.account, accounts.bob.secret); + assert(signer.Account); + assert(signer.SigningPubKey); + assert(signer.TxnSignature); + + tx.addMultiSigner(signer); + + tx.once('submitted', function(res) { + assert.strictEqual(res.engine_result, 'tefBAD_QUORUM'); + callback(); + }); + + tx.submit(); + } + ], done); + }); + + test('Unreachable quorum', function(done) { + async.series([ + function(callback) { + let tx = $.remote.createTransaction('SignerListSet', { + account: accounts.alice.account, + signers: [ + {account: accounts.bob.account, weight: 1}, + ], + signerQuorum: 2 + }); + + tx.once('submitted', function(res) { + assert.strictEqual(res.engine_result, 'temBAD_QUORUM'); + callback(); + }); + tx.submit(); + }, + ], done); + }); + + test('Invalid signature -- modified tx_json', function(done) { + async.series([ + function(callback) { + let tx = $.remote.createTransaction('SignerListSet', { + account: accounts.alice.account, + signers: [ + {account: accounts.bob.account, weight: 1}, + ], + signerQuorum: 1 + }); + + testutils.submit_transaction(tx, callback); + }, + + function(callback) { + let tx = $.remote.createTransaction('AccountSet', {account: accounts.alice.account}); + tx.setFee(100); + tx.setSequence(getAliceSequence()); + tx.setLastLedgerSequenceOffset(5); + + let mTx = Transaction.from_json(tx.getMultiSigningJson()); + // Tamper with transaction data prior to multi-signing + mTx.setSequence(getAliceSequence() + 1); + + let signer = mTx.multiSign(accounts.bob.account, accounts.bob.secret); + assert(signer.Account); + assert(signer.SigningPubKey); + assert(signer.TxnSignature); + + tx.addMultiSigner(signer); + + tx.once('submitted', function(res) { + assert(res.error) + assert.strictEqual(res.remote.error_message, 'Invalid signature.') + callback(); + }); + + tx.submit(); + } + ], done); + }); + + test('Invalid signature -- malformed signer', function(done) { + async.series([ + function(callback) { + let tx = $.remote.createTransaction('SignerListSet', { + account: accounts.alice.account, + signers: [ + {account: accounts.bob.account, weight: 1}, + ], + signerQuorum: 1 + }); + + testutils.submit_transaction(tx, callback); + }, + + function(callback) { + let tx = $.remote.createTransaction('AccountSet', {account: accounts.alice.account}); + tx.setFee(100); + tx.setSequence(getAliceSequence()); + tx.setLastLedgerSequenceOffset(5); + + let mTx = Transaction.from_json(tx.getMultiSigningJson()); + + let signer = mTx.multiSign(accounts.bob.account, accounts.bob.secret); + // Tamper with signer after multi-signing + signer.Account = accounts.carol.account; + + assert(signer.Account); + assert(signer.SigningPubKey); + assert(signer.TxnSignature); + + tx.addMultiSigner(signer); + + tx.once('submitted', function(res) { + assert(res.error) + assert.strictEqual(res.remote.error_message, 'Invalid signature.') + callback(); + }); + + tx.submit(); + } + ], done); + }); + + test('Invalid signature -- SigningPubKey non-empty', function(done) { + async.series([ + function(callback) { + let tx = $.remote.createTransaction('SignerListSet', { + account: accounts.alice.account, + signers: [ + {account: accounts.bob.account, weight: 1}, + ], + signerQuorum: 1 + }); + + testutils.submit_transaction(tx, callback); + }, + + function(callback) { + let tx = $.remote.createTransaction('AccountSet', {account: accounts.alice.account}); + tx.setFee(100); + tx.setSequence(getAliceSequence()); + tx.setLastLedgerSequenceOffset(5); + + let mTx = Transaction.from_json(tx.getMultiSigningJson()); + + let signer = mTx.multiSign(accounts.bob.account, accounts.bob.secret); + assert(signer.Account); + assert(signer.SigningPubKey); + assert(signer.TxnSignature); + + tx.addMultiSigner(signer); + + tx.once('presubmit', function(res) { + // SigningPubKey must be empty + tx.setSigningPubKey(tx.getSigningPubKey()); + }); + tx.once('submitted', function(res) { + assert(res.error); + assert.strictEqual(res.remote.error, 'invalidParams'); + callback(); + }); + + tx.submit(); + } + ], done); + }); +});