ldpos-chain
Version:
LDPoS chain module
1,541 lines (1,309 loc) • 55 kB
JavaScript
const assert = require('assert');
const Channel = require('./utils/channel');
const NetworkModule = require('./utils/network');
const AppModule = require('./utils/app');
const MockLDPoSChainModule = require('./utils/chain');
const { sha256 } = require('./utils/hash');
const wait = require('./utils/wait');
const { createClient } = require('ldpos-client');
const { LDPoSPgDAL } = require('ldpos-pg-dal');
const LDPoSChainModule = require('../index');
const NETWORK_SYMBOL = 'ldpos';
const useDal = process.env.USE_DAL;
const dalLibPath = useDal ? useDal : './test/utils/dal';
describe('Functional tests', async () => {
let chainModule;
let dal;
let adapter;
let keyIndex;
let keyManager;
let channel;
let options;
let bootstrapEventTriggered;
let clientForger;
let chainChangeEvents;
let walletAPassphrase;
let clientA;
let clientB;
beforeEach(async () => {
chainModule = new LDPoSChainModule({
config: {
components: {
dal: {
libPath: dalLibPath
}
}
},
logger: {
info: () => {},
// info: (...args) => console.info.apply(console, args),
debug: () => {},
// debug: (...args) => console.debug.apply(console, args),
warn: (...args) => console.warn.apply(console, args),
error: (...args) => console.error.apply(console, args)
}
});
dal = chainModule.dal;
adapter = {
getNetworkSymbol: async () => {
return chainModule.actions.getNetworkSymbol.handler();
},
getAccount: async (walletAddress) => {
return chainModule.actions.getAccount.handler({ params: { walletAddress } });
},
postTransaction: async (transaction) => {
return chainModule.actions.postTransaction.handler({ params: { transaction } });
}
};
keyIndex = 0;
keyManager = {
saveKeyIndex: async () => {},
loadKeyIndex: async () => {
return 0;
},
incrementKeyIndex: async () => {
return keyIndex++;
},
deleteKeyIndex: async () => {},
};
channel = new Channel({
modules: {
app: new AppModule(),
network: new NetworkModule({
modules: {
ldpos_chain: new MockLDPoSChainModule()
}
})
}
});
bootstrapEventTriggered = false;
channel.subscribe(`${chainModule.alias}:bootstrap`, async () => {
bootstrapEventTriggered = true;
});
chainChangeEvents = [];
channel.subscribe(`${chainModule.alias}:chainChanges`, async (event) => {
chainChangeEvents.push(event);
});
});
afterEach(async () => {
await chainModule.unload();
});
describe('block forging', async () => {
describe('with a single registered delegate', async () => {
beforeEach(async () => {
await chainModule.clearAllData();
// Forger address: ldpos313ac2d3d1d081901be0c5ce074d1e81a8a0bf5f
options = {
genesisPath: './test/utils/genesis-functional.json',
forgingCredentials: [
{
forgingPassphrase: 'clerk aware give dog reopen peasant duty cheese tobacco trouble gold angle'
}
],
minTransactionsPerBlock: 0, // Enable forging empty blocks.
forgingInterval: 10000,
forgingBlockBroadcastDelay: 500,
forgingSignatureBroadcastDelay: 2000,
propagationRandomness: 100,
propagationTimeout: 3000
};
await chainModule.load(channel, options);
clientForger = createClient({
adapter,
keyManager,
networkSymbol: NETWORK_SYMBOL
});
await clientForger.connect({
passphrase: options.forgingCredentials[0].forgingPassphrase
});
// Address: ldpos5f0bc55450657f7fcb188e90122f7e4cee894199
walletAPassphrase = 'birth select quiz process bid raccoon memory village snow cable agent bean';
clientA = createClient({
adapter,
keyManager,
networkSymbol: NETWORK_SYMBOL
});
await clientA.connect({
passphrase: walletAPassphrase
});
});
describe('without any transactions', async () => {
it('should forge correct number of valid blocks based on forging interval', async () => {
await wait(25000);
let newBlocks = chainChangeEvents.map(event => event.data.block);
let blockList = await chainModule.actions.getBlocksFromHeight.handler({
params: {
height: 1,
limit: 100
}
});
// Can be 3 blocks if the node launches near the end of the first timeslot.
let blockCount = newBlocks.length;
assert.equal(blockCount === 2 || blockCount === 3, true);
assert.equal(newBlocks[0].previousBlockId, null);
for (let i = 1; i < blockCount; i++) {
let previousBlock = newBlocks[i - 1];
let block = newBlocks[i];
assert.equal(block.previousBlockId, previousBlock.id);
}
for (let i = 0; i < blockCount; i++) {
let block = newBlocks[i];
assert.equal(block.height, i + 1);
assert.equal(block.timestamp % 10000, 0);
assert.equal(block.forgerAddress, 'ldpos313ac2d3d1d081901be0c5ce074d1e81a8a0bf5f');
assert.equal(typeof block.forgingPublicKey, 'string');
assert.equal(typeof block.id, 'string');
assert.equal(block.numberOfTransactions, 0);
}
});
});
describe('with transactions', async () => {
beforeEach(async () => {
for (let i = 0; i < 10; i++) {
await wait(600);
// Recipient passphrase: genius shoulder into daring armor proof cycle bench patrol paper grant picture
let preparedTxn = await clientA.prepareTransaction({
type: 'transfer',
recipientAddress: 'ldpos3a7bb5751c811b76bf13edce4105b5b330a71054',
amount: `${i + 1}00000000`,
fee: `${i + 1}0000000`,
timestamp: 100000,
message: ''
});
await chainModule.actions.postTransaction.handler({
params: {
transaction: preparedTxn
}
});
}
await wait(25000);
});
it('should forge valid blocks which contain the correct number of transactions', async () => {
let newBlocks = chainChangeEvents.map(event => event.data.block);
let blockList = await chainModule.actions.getBlocksFromHeight.handler({
params: {
height: 1,
limit: 100
}
});
let totalTxnCount = 0;
for (let block of blockList) {
totalTxnCount += block.numberOfTransactions;
}
assert.equal(totalTxnCount, 10);
});
});
});
});
describe('block processing', async () => {
let multisigClient;
beforeEach(async () => {
await chainModule.clearAllData();
options = {
genesisPath: './test/utils/genesis-functional.json',
forgingCredentials: [
{
forgingPassphrase: 'clerk aware give dog reopen peasant duty cheese tobacco trouble gold angle'
}
],
minTransactionsPerBlock: 0, // Enable forging empty blocks.
forgingInterval: 10000,
forgingBlockBroadcastDelay: 500,
forgingSignatureBroadcastDelay: 2000,
propagationRandomness: 100,
propagationTimeout: 3000
};
await chainModule.load(channel, options);
await wait(2000);
clientForger = createClient({
adapter,
keyManager,
networkSymbol: NETWORK_SYMBOL
});
await clientForger.connect({
passphrase: options.forgingCredentials[0].forgingPassphrase
});
// Address: ldpos5f0bc55450657f7fcb188e90122f7e4cee894199
walletAPassphrase = 'birth select quiz process bid raccoon memory village snow cable agent bean';
clientA = createClient({
adapter,
keyManager,
networkSymbol: NETWORK_SYMBOL
});
await clientA.connect({
passphrase: walletAPassphrase
});
});
describe('when processing blocks which contain valid sig transfer transactions', async () => {
beforeEach(async () => {
for (let i = 0; i < 10; i++) {
await wait(600);
// Recipient passphrase: genius shoulder into daring armor proof cycle bench patrol paper grant picture
let preparedTxn = await clientA.prepareTransaction({
type: 'transfer',
recipientAddress: 'ldpos3a7bb5751c811b76bf13edce4105b5b330a71054',
amount: `${i + 1}00000000`,
fee: `${i + 1}0000000`,
timestamp: 100000,
message: ''
});
await chainModule.actions.postTransaction.handler({
params: {
transaction: preparedTxn
}
});
}
await wait(18000);
});
it('should process all valid transactions within blocks and correctly update account balances', async () => {
let newBlocks = chainChangeEvents.map(event => event.data.block);
let blockList = await chainModule.actions.getBlocksFromHeight.handler({
params: {
height: 1,
limit: 100
}
});
let totalTxnCount = 0;
let txnList = [];
for (let block of blockList) {
totalTxnCount += block.numberOfTransactions;
let blockTxns = await chainModule.actions.getTransactionsFromBlock.handler({
params: {
blockId: block.id,
offset: 0
}
});
for (let txn of blockTxns) {
txnList.push(txn);
}
}
assert.equal(totalTxnCount, 10);
assert.equal(txnList.length, 10);
let [senderAccount, recipientAccount] = await Promise.all([
chainModule.actions.getAccount.handler({
params: {
walletAddress: clientA.walletAddress
}
}),
chainModule.actions.getAccount.handler({
params: {
walletAddress: 'ldpos3a7bb5751c811b76bf13edce4105b5b330a71054'
}
})
]);
let initialAmount = 1000;
let expectedSentAmount = 55;
let expectedFees = 5.5;
let unitSize = 100000000;
assert.equal(senderAccount.balance, String((initialAmount - expectedSentAmount - expectedFees) * unitSize));
assert.equal(recipientAccount.balance, String(expectedSentAmount * unitSize));
});
});
describe('when processing a block multiple times which contains sig transactions due to database connection failure', async () => {
let realUpsertBlockMethod;
beforeEach(async () => {
let hasFailed = false;
realUpsertBlockMethod = chainModule.dal.upsertBlock;
chainModule.dal.upsertBlock = function (...args) {
let block = args[0];
// Fail the first time only. This should force the block at height 2 to be re-processed.
if (block.height === 2 && !hasFailed) {
hasFailed = true;
throw new Error('Failed to upsert block because of simulated database connection issue');
}
return realUpsertBlockMethod.apply(this, args);
};
for (let i = 0; i < 10; i++) {
await wait(600);
// Recipient passphrase: genius shoulder into daring armor proof cycle bench patrol paper grant picture
let preparedTxn = await clientA.prepareTransaction({
type: 'transfer',
recipientAddress: 'ldpos3a7bb5751c811b76bf13edce4105b5b330a71054',
amount: `${i + 1}00000000`,
fee: `${i + 1}0000000`,
timestamp: 100000,
message: ''
});
await chainModule.actions.postTransaction.handler({
params: {
transaction: preparedTxn
}
});
}
await wait(18000);
});
afterEach(async () => {
chainModule.dal.upsertBlock = realUpsertBlockMethod;
});
it('should update the state in an idempotent way', async () => {
let newBlocks = chainChangeEvents.map(event => event.data.block);
let blockList = await chainModule.actions.getBlocksFromHeight.handler({
params: {
height: 1,
limit: 100
}
});
let totalTxnCount = 0;
let txnList = [];
assert.equal(blockList.length >= 2, true);
assert.equal(blockList[0].height, 1);
assert.equal(blockList[1].height, 2);
for (let block of blockList) {
totalTxnCount += block.numberOfTransactions;
let blockTxns = await chainModule.actions.getTransactionsFromBlock.handler({
params: {
blockId: block.id,
offset: 0
}
});
for (let txn of blockTxns) {
txnList.push(txn);
}
}
assert.equal(totalTxnCount, 10);
assert.equal(txnList.length, 10);
let [senderAccount, recipientAccount] = await Promise.all([
chainModule.actions.getAccount.handler({
params: {
walletAddress: clientA.walletAddress
}
}),
chainModule.actions.getAccount.handler({
params: {
walletAddress: 'ldpos3a7bb5751c811b76bf13edce4105b5b330a71054'
}
})
]);
let initialAmount = 1000;
let expectedSentAmount = 55;
let expectedFees = 5.5;
let unitSize = 100000000;
assert.equal(senderAccount.balance, String((initialAmount - expectedSentAmount - expectedFees) * unitSize));
assert.equal(recipientAccount.balance, String(expectedSentAmount * unitSize));
});
});
describe('when processing blocks which contain valid multisig transfer transactions', async () => {
beforeEach(async () => {
// Address: ldpos6da4367efe495a14a8cc38064c61879cea884e60
multisigClient = createClient({
adapter,
keyManager,
networkSymbol: NETWORK_SYMBOL
});
await multisigClient.connect({
passphrase: 'guitar sight absurd copper right amount habit boat trigger bundle high pudding'
});
// Address: ldposaed82ed3f324e738306301ca4d6b955580d4bc24
clientB = createClient({
adapter,
keyManager,
networkSymbol: NETWORK_SYMBOL
});
await clientB.connect({
passphrase: 'trip timber saddle fine shock orbit lamp nominee subject pledge random wedding'
});
for (let i = 0; i < 5; i++) {
await wait(1200);
// Recipient passphrase: genius shoulder into daring armor proof cycle bench patrol paper grant picture
let preparedTxn = multisigClient.prepareMultisigTransaction({
type: 'transfer',
recipientAddress: 'ldpos3a7bb5751c811b76bf13edce4105b5b330a71054',
amount: `${i + 1}00000000`,
fee: `${i + 1}1000000`,
timestamp: 100000,
message: ''
});
let memberASignature = await clientA.signMultisigTransaction(preparedTxn);
let memberBSignature = await clientB.signMultisigTransaction(preparedTxn);
multisigClient.attachMultisigTransactionSignature(preparedTxn, memberASignature);
multisigClient.attachMultisigTransactionSignature(preparedTxn, memberBSignature);
await chainModule.actions.postTransaction.handler({
params: {
transaction: preparedTxn
}
});
}
await wait(18000);
});
it('should process all valid transactions within blocks and correctly update account balances', async () => {
let newBlocks = chainChangeEvents.map(event => event.data.block);
let blockList = await chainModule.actions.getBlocksFromHeight.handler({
params: {
height: 1,
limit: 100
}
});
let totalTxnCount = 0;
let txnList = [];
for (let block of blockList) {
totalTxnCount += block.numberOfTransactions;
let blockTxns = await chainModule.actions.getTransactionsFromBlock.handler({
params: {
blockId: block.id,
offset: 0
}
});
for (let txn of blockTxns) {
txnList.push(txn);
}
}
assert.equal(totalTxnCount, 5);
assert.equal(txnList.length, 5);
let [senderAccount, recipientAccount] = await Promise.all([
chainModule.actions.getAccount.handler({
params: {
walletAddress: multisigClient.walletAddress
}
}),
chainModule.actions.getAccount.handler({
params: {
walletAddress: 'ldpos3a7bb5751c811b76bf13edce4105b5b330a71054'
}
})
]);
let initialAmount = 300;
let expectedSentAmount = 15;
let expectedFees = 1.55;
let unitSize = 100000000;
assert.equal(senderAccount.balance, String((initialAmount - expectedSentAmount - expectedFees) * unitSize));
assert.equal(recipientAccount.balance, String(expectedSentAmount * unitSize));
});
});
describe('when processing a block multiple times which contains multisig transactions due to database connection failure', async () => {
let realUpsertBlockMethod;
beforeEach(async () => {
let hasFailed = false;
realUpsertBlockMethod = chainModule.dal.upsertBlock;
chainModule.dal.upsertBlock = function (...args) {
let block = args[0];
// Fail the first time only. This should force the block at height 2 to be re-processed.
if (block.height === 2 && !hasFailed) {
hasFailed = true;
throw new Error('Failed to upsert block because of simulated database connection issue');
}
return realUpsertBlockMethod.apply(this, args);
};
multisigClient = createClient({
adapter,
keyManager,
networkSymbol: NETWORK_SYMBOL
});
await multisigClient.connect({
passphrase: 'guitar sight absurd copper right amount habit boat trigger bundle high pudding'
});
clientB = createClient({
adapter,
keyManager,
networkSymbol: NETWORK_SYMBOL
});
await clientB.connect({
passphrase: 'trip timber saddle fine shock orbit lamp nominee subject pledge random wedding'
});
for (let i = 0; i < 5; i++) {
await wait(1200);
// Recipient passphrase: genius shoulder into daring armor proof cycle bench patrol paper grant picture
let preparedTxn = multisigClient.prepareMultisigTransaction({
type: 'transfer',
recipientAddress: 'ldpos3a7bb5751c811b76bf13edce4105b5b330a71054',
amount: `${i + 1}00000000`,
fee: `${i + 1}1000000`,
timestamp: 100000,
message: ''
});
let memberASignature = await clientA.signMultisigTransaction(preparedTxn);
let memberBSignature = await clientB.signMultisigTransaction(preparedTxn);
multisigClient.attachMultisigTransactionSignature(preparedTxn, memberASignature);
multisigClient.attachMultisigTransactionSignature(preparedTxn, memberBSignature);
await chainModule.actions.postTransaction.handler({
params: {
transaction: preparedTxn
}
});
}
await wait(18000);
});
afterEach(async () => {
chainModule.dal.upsertBlock = realUpsertBlockMethod;
});
it('should update the state in an idempotent way', async () => {
let newBlocks = chainChangeEvents.map(event => event.data.block);
let blockList = await chainModule.actions.getBlocksFromHeight.handler({
params: {
height: 1,
limit: 100
}
});
let totalTxnCount = 0;
let txnList = [];
assert.equal(blockList.length >= 2, true);
assert.equal(blockList[0].height, 1);
assert.equal(blockList[1].height, 2);
for (let block of blockList) {
totalTxnCount += block.numberOfTransactions;
let blockTxns = await chainModule.actions.getTransactionsFromBlock.handler({
params: {
blockId: block.id,
offset: 0
}
});
for (let txn of blockTxns) {
txnList.push(txn);
}
}
assert.equal(totalTxnCount, 5);
assert.equal(txnList.length, 5);
let [senderAccount, recipientAccount] = await Promise.all([
chainModule.actions.getAccount.handler({
params: {
walletAddress: multisigClient.walletAddress
}
}),
chainModule.actions.getAccount.handler({
params: {
walletAddress: 'ldpos3a7bb5751c811b76bf13edce4105b5b330a71054'
}
})
]);
let initialAmount = 300;
let expectedSentAmount = 15;
let expectedFees = 1.55;
let unitSize = 100000000;
assert.equal(senderAccount.balance, String((initialAmount - expectedSentAmount - expectedFees) * unitSize));
assert.equal(recipientAccount.balance, String(expectedSentAmount * unitSize));
});
});
});
describe('transfer transaction', async () => {
beforeEach(async () => {
await chainModule.clearAllData();
options = {
genesisPath: './test/utils/genesis-functional.json',
forgingCredentials: [
{
forgingPassphrase: 'clerk aware give dog reopen peasant duty cheese tobacco trouble gold angle'
}
],
minTransactionsPerBlock: 0, // Enable forging empty blocks.
forgingInterval: 10000,
forgingBlockBroadcastDelay: 500,
forgingSignatureBroadcastDelay: 2000,
propagationRandomness: 100,
propagationTimeout: 3000
};
await chainModule.load(channel, options);
clientForger = createClient({
adapter,
keyManager,
networkSymbol: NETWORK_SYMBOL
});
await clientForger.connect({
passphrase: options.forgingCredentials[0].forgingPassphrase
});
// Address: ldpos5f0bc55450657f7fcb188e90122f7e4cee894199
walletAPassphrase = 'birth select quiz process bid raccoon memory village snow cable agent bean';
clientA = createClient({
adapter,
keyManager,
networkSymbol: NETWORK_SYMBOL
});
await clientA.connect({
passphrase: walletAPassphrase
});
});
describe('valid transfers', async () => {
let firstRecipientClient;
beforeEach(async () => {
let preparedTxn = await clientA.prepareTransaction({
type: 'transfer',
recipientAddress: 'ldpos3a7bb5751c811b76bf13edce4105b5b330a71054',
amount: '10000000000',
fee: '10000000',
timestamp: 100000,
message: ''
});
await chainModule.actions.postTransaction.handler({
params: {
transaction: preparedTxn
}
});
await wait(18000);
firstRecipientClient = createClient({
adapter,
keyManager,
networkSymbol: NETWORK_SYMBOL
});
// Address: ldpos3a7bb5751c811b76bf13edce4105b5b330a71054
await firstRecipientClient.connect({
passphrase: 'genius shoulder into daring armor proof cycle bench patrol paper grant picture'
});
// Recipient passphrase: sniff there advice door hand eyebrow story eyebrow brief window mushroom legend
let firstRecipientPreparedTxn = await firstRecipientClient.prepareTransaction({
type: 'transfer',
recipientAddress: 'ldposfa181bd957848641ece236b5b94eff4ec0af8ebd',
amount: '500000000',
fee: '10000000',
timestamp: 100000,
message: ''
});
await chainModule.actions.postTransaction.handler({
params: {
transaction: firstRecipientPreparedTxn
}
});
await wait(18000);
});
it('should update account balances', async () => {
let [initialSenderAccount, firstRecipientAccount, secondRecipientAccount] = await Promise.all([
chainModule.actions.getAccount.handler({
params: {
walletAddress: clientA.walletAddress
}
}),
chainModule.actions.getAccount.handler({
params: {
walletAddress: firstRecipientClient.walletAddress
}
}),
chainModule.actions.getAccount.handler({
params: {
walletAddress: 'ldposfa181bd957848641ece236b5b94eff4ec0af8ebd'
}
})
]);
assert.notEqual(initialSenderAccount, null);
assert.equal(initialSenderAccount.balance, '89990000000');
assert.notEqual(firstRecipientAccount, null);
assert.equal(firstRecipientAccount.balance, '9490000000');
assert.notEqual(firstRecipientAccount.sigPublicKey, firstRecipientAccount.nextSigPublicKey);
assert.notEqual(secondRecipientAccount, null);
assert.equal(secondRecipientAccount.balance, '500000000');
});
});
describe('invalid transfer', async () => {
let caughtError;
beforeEach(async () => {
caughtError = null;
// Recipient passphrase: genius shoulder into daring armor proof cycle bench patrol paper grant picture
let preparedTxn = await clientA.prepareTransaction({
type: 'transfer',
recipientAddress: 'ldpos3a7bb5751c811b76bf13edce4105b5b330a71054',
amount: '100000000000',
fee: '10000000',
timestamp: 100000,
message: ''
});
try {
await chainModule.actions.postTransaction.handler({
params: {
transaction: preparedTxn
}
});
} catch (error) {
caughtError = error;
}
});
it('should throw an error', async () => {
assert.notEqual(caughtError, null);
});
});
});
describe('vote transaction', async () => {
let caughtError;
beforeEach(async () => {
await chainModule.clearAllData();
options = {
genesisPath: './test/utils/genesis-functional.json',
forgingCredentials: [
{
forgingPassphrase: 'clerk aware give dog reopen peasant duty cheese tobacco trouble gold angle'
}
],
minTransactionsPerBlock: 0, // Enable forging empty blocks.
forgingInterval: 15000,
forgingBlockBroadcastDelay: 1000,
forgingSignatureBroadcastDelay: 3000,
propagationRandomness: 100,
propagationTimeout: 5000,
maxVotesPerAccount: 2
};
await chainModule.load(channel, options);
clientForger = createClient({
adapter,
keyManager,
networkSymbol: NETWORK_SYMBOL
});
await clientForger.connect({
passphrase: options.forgingCredentials[0].forgingPassphrase
});
// Address: ldpos5f0bc55450657f7fcb188e90122f7e4cee894199
walletAPassphrase = 'birth select quiz process bid raccoon memory village snow cable agent bean';
clientA = createClient({
adapter,
keyManager,
networkSymbol: NETWORK_SYMBOL
});
await clientA.connect({
passphrase: walletAPassphrase
});
});
describe('valid vote', async () => {
beforeEach(async () => {
let preparedTxn = await clientA.prepareTransaction({
type: 'vote',
delegateAddress: 'ldpos5f0bc55450657f7fcb188e90122f7e4cee894199',
fee: '20000000',
timestamp: 100000,
message: ''
});
await chainModule.actions.postTransaction.handler({
params: {
transaction: preparedTxn
}
});
await wait(25000);
});
it('should update the top delegate list', async () => {
let activeDelegatesAfterList = await chainModule.actions.getForgingDelegates.handler();
assert.equal(Array.isArray(activeDelegatesAfterList), true);
assert.equal(activeDelegatesAfterList.length, 2);
assert.equal(activeDelegatesAfterList[1].address, 'ldpos5f0bc55450657f7fcb188e90122f7e4cee894199');
assert.equal(activeDelegatesAfterList[1].voteWeight, '99980000000');
});
it('should update vote and voter lists', async () => {
let votes = await chainModule.actions.getAccountVotes.handler({
params: {
walletAddress: clientA.walletAddress
}
});
assert.equal(Array.isArray(votes), true);
assert.equal(votes.length, 2);
assert.equal(votes[1], 'ldpos5f0bc55450657f7fcb188e90122f7e4cee894199');
let voters = await chainModule.actions.getDelegateVoters.handler({
params: {
walletAddress: 'ldpos5f0bc55450657f7fcb188e90122f7e4cee894199',
offset: 0,
limit: 10,
order: 'asc'
}
});
assert.equal(Array.isArray(voters), true);
assert.equal(voters.length, 1);
assert.equal(voters[0], clientA.walletAddress);
});
});
describe('invalid vote; already voted for delegate', async () => {
beforeEach(async () => {
caughtError = null;
let preparedTxn = await clientA.prepareTransaction({
type: 'vote',
delegateAddress: 'ldpos313ac2d3d1d081901be0c5ce074d1e81a8a0bf5f',
fee: '20000000',
timestamp: 100000,
message: ''
});
try {
await chainModule.actions.postTransaction.handler({
params: {
transaction: preparedTxn
}
});
} catch (error) {
caughtError = error;
}
await wait(25000);
});
it('should not send back an error, it should be a no-op', async () => {
let activeDelegatesAfterList = await chainModule.actions.getForgingDelegates.handler();
assert.equal(caughtError, null);
assert.equal(activeDelegatesAfterList.length, 1);
});
});
describe('invalid vote; exceeded maximum number of votes per account', async () => {
beforeEach(async () => {
caughtError = null;
let preparedTxn = await clientA.prepareTransaction({
type: 'vote',
delegateAddress: 'ldpos5f0bc55450657f7fcb188e90122f7e4cee894199',
fee: '20000000',
timestamp: 100000,
message: ''
});
await chainModule.actions.postTransaction.handler({
params: {
transaction: preparedTxn
}
});
let secondPreparedTxn = await clientA.prepareTransaction({
type: 'vote',
delegateAddress: 'ldposaed82ed3f324e738306301ca4d6b955580d4bc24',
fee: '20000000',
timestamp: 100000,
message: ''
});
try {
await chainModule.actions.postTransaction.handler({
params: {
transaction: secondPreparedTxn
}
});
} catch (error) {
caughtError = error;
}
await wait(25000);
});
it('should not exceed the maximum number of votes', async () => {
let activeDelegatesAfterList = await chainModule.actions.getForgingDelegates.handler();
// A vote transaction may be accepted even if it turns out of be a no-op.
assert.equal(caughtError, null);
assert.equal(Array.isArray(activeDelegatesAfterList), true);
assert.equal(activeDelegatesAfterList.length, 2);
});
});
});
describe('unvote transaction', async () => {
beforeEach(async () => {
await chainModule.clearAllData();
options = {
genesisPath: './test/utils/genesis-functional.json',
forgingCredentials: [
{
forgingPassphrase: 'clerk aware give dog reopen peasant duty cheese tobacco trouble gold angle'
}
],
minTransactionsPerBlock: 0, // Enable forging empty blocks.
forgingInterval: 10000,
forgingBlockBroadcastDelay: 500,
forgingSignatureBroadcastDelay: 2000,
propagationRandomness: 100,
propagationTimeout: 3000
};
await chainModule.load(channel, options);
clientForger = createClient({
adapter,
keyManager,
networkSymbol: NETWORK_SYMBOL
});
await clientForger.connect({
passphrase: options.forgingCredentials[0].forgingPassphrase
});
// Address: ldpos5f0bc55450657f7fcb188e90122f7e4cee894199
walletAPassphrase = 'birth select quiz process bid raccoon memory village snow cable agent bean';
clientA = createClient({
adapter,
keyManager,
networkSymbol: NETWORK_SYMBOL
});
await clientA.connect({
passphrase: walletAPassphrase
});
});
describe('valid unvote', async () => {
let activeDelegatesBeforeList;
beforeEach(async () => {
activeDelegatesBeforeList = await chainModule.actions.getForgingDelegates.handler();
let preparedTxn = await clientA.prepareTransaction({
type: 'unvote',
delegateAddress: 'ldpos313ac2d3d1d081901be0c5ce074d1e81a8a0bf5f',
fee: '20000000',
timestamp: 100000,
message: ''
});
await chainModule.actions.postTransaction.handler({
params: {
transaction: preparedTxn
}
});
await wait(18000);
});
it('should update the top delegate list', async () => {
let account = await chainModule.actions.getAccount.handler({
params: {
walletAddress: clientA.walletAddress
}
});
let activeDelegatesAfterList = await chainModule.actions.getForgingDelegates.handler();
assert.equal(Array.isArray(activeDelegatesAfterList), true);
assert.equal(activeDelegatesAfterList.length, 1);
let expectedVoteWeight = BigInt(activeDelegatesBeforeList[0].voteWeight) - BigInt(account.balance);
assert.equal(activeDelegatesAfterList[0].voteWeight, expectedVoteWeight.toString());
});
});
describe('invalid unvote; unvoting an address which the voter is not voting for', async () => {
let caughtError;
beforeEach(async () => {
activeDelegatesBeforeList = await chainModule.actions.getForgingDelegates.handler();
let preparedTxn = await clientA.prepareTransaction({
type: 'unvote',
delegateAddress: 'ldposaed82ed3f324e738306301ca4d6b955580d4bc24',
fee: '20000000',
timestamp: 100000,
message: ''
});
try {
await chainModule.actions.postTransaction.handler({
params: {
transaction: preparedTxn
}
});
} catch (error) {
caughtError = error;
}
await wait(18000);
});
it('should not send back an error, it should be a no-op', async () => {
let activeDelegatesAfterList = await chainModule.actions.getForgingDelegates.handler();
assert.equal(caughtError, null);
assert.equal(activeDelegatesAfterList.length, 1);
});
});
});
describe('registerMultisigWallet transaction', async () => {
beforeEach(async () => {
await chainModule.clearAllData();
options = {
genesisPath: './test/utils/genesis-functional.json',
forgingCredentials: [
{
forgingPassphrase: 'clerk aware give dog reopen peasant duty cheese tobacco trouble gold angle'
}
],
minTransactionsPerBlock: 0, // Enable forging empty blocks.
forgingInterval: 10000,
forgingBlockBroadcastDelay: 1000,
forgingSignatureBroadcastDelay: 2000,
propagationRandomness: 100,
propagationTimeout: 3000
};
await chainModule.load(channel, options);
clientForger = createClient({
adapter,
keyManager,
networkSymbol: NETWORK_SYMBOL
});
await clientForger.connect({
passphrase: options.forgingCredentials[0].forgingPassphrase
});
// Address: ldpos5f0bc55450657f7fcb188e90122f7e4cee894199
clientA = createClient({
adapter,
keyManager,
networkSymbol: NETWORK_SYMBOL
});
await clientA.connect({
passphrase: 'birth select quiz process bid raccoon memory village snow cable agent bean'
});
// Address: ldposaed82ed3f324e738306301ca4d6b955580d4bc24
clientB = createClient({
adapter,
keyManager,
networkSymbol: NETWORK_SYMBOL
});
await clientB.connect({
passphrase: 'trip timber saddle fine shock orbit lamp nominee subject pledge random wedding'
});
});
describe('valid registerMultisigWallet', async () => {
let caughtError;
beforeEach(async () => {
let preparedTxn = await clientA.prepareTransaction({
type: 'registerMultisigWallet',
requiredSignatureCount: 2,
memberAddresses: [
'ldpos313ac2d3d1d081901be0c5ce074d1e81a8a0bf5f',
'ldposaed82ed3f324e738306301ca4d6b955580d4bc24'
],
fee: '250000000',
timestamp: 100000,
message: ''
});
await chainModule.actions.postTransaction.handler({
params: {
transaction: preparedTxn
}
});
await wait(18000);
// Recipient passphrase: genius shoulder into daring armor proof cycle bench patrol paper grant picture
let preparedTransferTxn = await clientA.prepareTransaction({
type: 'transfer',
recipientAddress: 'ldpos3a7bb5751c811b76bf13edce4105b5b330a71054',
amount: '12300000000',
fee: '11000000',
timestamp: 100000,
message: ''
});
try {
await chainModule.actions.postTransaction.handler({
params: {
transaction: preparedTransferTxn
}
});
} catch (error) {
caughtError = error;
}
await wait(18000);
});
it('should convert sig account into multisig wallet', async () => {
let account = await chainModule.actions.getAccount.handler({
params: {
walletAddress: clientA.walletAddress
}
});
assert.equal(account.type, 'multisig');
assert.equal(account.balance, '99750000000');
assert.notEqual(caughtError, null);
});
});
describe('multiple valid registerMultisigWallet', async () => {
let accountBefore;
let accountAfter;
beforeEach(async () => {
let preparedTxn = await clientA.prepareTransaction({
type: 'registerMultisigWallet',
requiredSignatureCount: 2,
memberAddresses: [
'ldpos313ac2d3d1d081901be0c5ce074d1e81a8a0bf5f',
'ldposaed82ed3f324e738306301ca4d6b955580d4bc24'
],
fee: '250000000',
timestamp: 100000,
message: ''
});
await chainModule.actions.postTransaction.handler({
params: {
transaction: preparedTxn
}
});
await wait(22000);
accountBefore = await chainModule.actions.getAccount.handler({
params: {
walletAddress: clientA.walletAddress
}
});
let preparedTxnB = await clientA.prepareMultisigTransaction({
type: 'registerMultisigWallet',
requiredSignatureCount: 1,
memberAddresses: [
'ldpos313ac2d3d1d081901be0c5ce074d1e81a8a0bf5f'
],
fee: '251000000',
timestamp: 100000,
message: ''
});
let signatureA = await clientForger.signMultisigTransaction(preparedTxnB);
let signatureB = await clientB.signMultisigTransaction(preparedTxnB);
clientA.attachMultisigTransactionSignature(preparedTxnB, signatureA);
clientA.attachMultisigTransactionSignature(preparedTxnB, signatureB);
await chainModule.actions.postTransaction.handler({
params: {
transaction: preparedTxnB
}
});
await wait(22000);
accountAfter = await chainModule.actions.getAccount.handler({
params: {
walletAddress: clientA.walletAddress
}
});
});
it('should support re-registering an existing multisig wallet with a different set of member addresses', async () => {
assert.equal(accountBefore.requiredSignatureCount, 2);
assert.equal(accountAfter.requiredSignatureCount, 1);
assert.equal(accountAfter.balance, '99499000000');
});
});
describe('invalid registerMultisigWallet', async () => {
let caughtError;
beforeEach(async () => {
let preparedTxn = await clientA.prepareTransaction({
type: 'registerMultisigWallet',
requiredSignatureCount: 3,
memberAddresses: [
'ldpos313ac2d3d1d081901be0c5ce074d1e81a8a0bf5f',
'ldposaed82ed3f324e738306301ca4d6b955580d4bc24'
],
fee: '50000000',
timestamp: 100000,
message: ''
});
try {
await chainModule.actions.postTransaction.handler({
params: {
transaction: preparedTxn
}
});
} catch (error) {
caughtError = error;
}
await wait(18000);
});
it('should send back an error', async () => {
assert.notEqual(caughtError, null);
});
});
});
describe('registerSigDetails transaction', async () => {
let caughtError;
beforeEach(async () => {
await chainModule.clearAllData();
caughtError = null;
options = {
genesisPath: './test/utils/genesis-functional.json',
forgingCredentials: [
{
forgingPassphrase: 'clerk aware give dog reopen peasant duty cheese tobacco trouble gold angle'
}
],
minTransactionsPerBlock: 0, // Enable forging empty blocks.
forgingInterval: 10000,
forgingBlockBroadcastDelay: 500,
forgingSignatureBroadcastDelay: 2000,
propagationRandomness: 100,
propagationTimeout: 3000
};
await chainModule.load(channel, options);
clientForger = createClient({
adapter,
keyManager,
networkSymbol: NETWORK_SYMBOL
});
await clientForger.connect({
passphrase: options.forgingCredentials[0].forgingPassphrase
});
// Address: ldpos5f0bc55450657f7fcb188e90122f7e4cee894199
clientA = createClient({
adapter,
keyManager,
networkSymbol: NETWORK_SYMBOL
});
await clientA.connect({
passphrase: 'birth select quiz process bid raccoon memory village snow cable agent bean'
});
});
describe('valid registerSigDetails', async () => {
beforeEach(async () => {
let preparedTxn = await clientA.prepareTransaction({
type: 'registerSigDetails',
newSigPublicKey: clientForger.sigPublicKey,
newNextSigPublicKey: clientForger.nextSigPublicKey,
newNextSigKeyIndex: clientForger.sigKeyIndex,
fee: '500000000',
timestamp: 100000,
message: ''
});
await chainModule.actions.postTransaction.handler({
params: {
transaction: preparedTxn
}
});
await wait(18000);
// Should allow control of an account to be transferred to a different user.
clientForger.walletAddress = 'ldpos5f0bc55450657f7fcb188e90122f7e4cee894199';
// Recipient passphrase: genius shoulder into daring armor proof cycle bench patrol paper grant picture
let preparedTxnB = await clientForger.prepareTransaction({
type: 'transfer',
recipientAddress: 'ldpos3a7bb5751c811b76bf13edce4105b5b330a71054',
amount: '2000000000',
fee: '10000000',
timestamp: 100000,
message: ''
});
try {
await chainModule.actions.postTransaction.handler({
params: {
transaction: preparedTxnB
}
});
} catch (error) {
caughtError = error;
}
await wait(18000);
});
it('should add all the necessary keys on the account', async () => {
let account = await chainModule.actions.getAccount.handler({
params: {
walletAddress: clientA.walletAddress
}
});
assert.equal(caughtError, null);
assert.equal(account.balance, '97490000000');
});
});
describe('invalid registerSigDetails', async () => {
beforeEach(async () => {
let preparedTxn = await clientA.prepareTransaction({
type: 'registerSigDetails',
newSigPublicKey: clientForger.sigPublicKey,
newNextSigPublicKey: clientForger.nextSigPublicKey,
newNextSigKeyIndex: -1,
fee: '500000000',
timestamp: 100000,
message: ''
});
try {
await chainModule.actions.postTransaction.handler({
params: {
transaction: preparedTxn
}
});
} catch (error) {
caughtError = error;
}
await wait(18000);
});
it('should send back an error', async () => {
assert.notEqual(caughtError, null);
});
});
});
describe('registerMultisigDetails transaction', async () => {
let caughtError;
beforeEach(async () => {
await chainModule.clearAllData();
caughtError = null;
options = {
genesisPath: './test/utils/genesis-functional.json',
forgingCredentials: [
{
forgingPassphrase: 'clerk aware give dog reopen peasant duty cheese tobacco trouble gold angle'
}
],
minTransactionsPerBlock: 0, // Enable forging empty blocks.
forgingInterval: 10000,
forgingBlockBroadcastDelay: 500,
forgingSignatureBroadcastDelay: 2000,
propagationRandomness: 100,
propagationTimeout: 3000
};
await chainModule.load(channel, options);
clientForger = createClient({
adapter,
keyManager,
networkSymbol: NETWORK_SYMBOL
});
await clientForger.connect({
passphrase: options.forgingCredentials[0].forgingPassphrase
});
// Address: ldpos5f0bc55450657f7fcb188e90122f7e4cee894199
clientA = createClient({
adapter,
keyManager,
networkSymbol: NETWORK_SYMBOL
});
await clientA.connect({
passphrase: 'birth select quiz process bid raccoon memory village snow cable agent bean'
});
});
describe('valid registerMultisigDetails', async () => {
beforeEach(async () => {
let preparedTxn = await clientA.prepareTransaction({
type: 'registerMultisigDetails',
newMultisigPublicKey: clientForger.multisigPublicKey,
newNextMultisigPublicKey: clientForger.nextMultisigPublicKey,
newNextMultisigKeyIndex: clientForger.multisigKeyIndex,
fee: '500000000',
timestamp: 100000,
message: ''
});
await chainModule.actions.postTransaction.handler({
params: {
transaction: prepa