Skip to content

Commit

Permalink
Merge branch 'develop' into refactor/wallets
Browse files Browse the repository at this point in the history
  • Loading branch information
spkjp committed Mar 8, 2019
2 parents 97dd7f6 + 62aeb20 commit e99f1aa
Show file tree
Hide file tree
Showing 30 changed files with 571 additions and 248 deletions.
140 changes: 139 additions & 1 deletion __tests__/integration/core-blockchain/blockchain.test.ts
@@ -1,12 +1,22 @@
/* tslint:disable:max-line-length */
import { crypto, models, slots } from "@arkecosystem/crypto";
import {
Bignum,
crypto,
HashAlgorithms,
ITransactionData,
models,
slots,
sortTransactions,
transactionBuilder,
} from "@arkecosystem/crypto";
import { asValue } from "awilix";
import delay from "delay";
import { Blockchain } from "../../../packages/core-blockchain/src/blockchain";
import { defaults } from "../../../packages/core-blockchain/src/defaults";
import "../../utils";
import { blocks101to155 } from "../../utils/fixtures/testnet/blocks101to155";
import { blocks2to100 } from "../../utils/fixtures/testnet/blocks2to100";
import { delegates } from "../../utils/fixtures/testnet/delegates";
import { setUp, tearDown } from "./__support__/setup";

const { Block, Wallet } = models;
Expand Down Expand Up @@ -121,6 +131,134 @@ describe("Blockchain", () => {
});
});

describe("rollback", () => {
beforeEach(async () => {
await __resetToHeight1();
await __addBlocks(155);
});

const getNextForger = async () => {
const lastBlock = blockchain.state.getLastBlock();
const activeDelegates = await blockchain.database.getActiveDelegates(lastBlock.data.height);
const nextSlot = slots.getSlotNumber(lastBlock.data.timestamp) + 1;
return activeDelegates[nextSlot % activeDelegates.length];
};

const createBlock = (generatorKeys: any, transactions: ITransactionData[]) => {
const transactionData = {
amount: Bignum.ZERO,
fee: Bignum.ZERO,
ids: [],
};

const sortedTransactions = sortTransactions(transactions);
sortedTransactions.forEach(transaction => {
transactionData.amount = transactionData.amount.plus(transaction.amount);
transactionData.fee = transactionData.fee.plus(transaction.fee);
transactionData.ids.push(Buffer.from(transaction.id, "hex"));
});

const lastBlock = blockchain.state.getLastBlock();
const data = {
timestamp: slots.getSlotTime(slots.getSlotNumber(lastBlock.data.timestamp) + 1),
version: 0,
previousBlock: lastBlock.data.id,
previousBlockHex: lastBlock.data.idHex,
height: lastBlock.data.height + 1,
numberOfTransactions: sortedTransactions.length,
totalAmount: transactionData.amount,
totalFee: transactionData.fee,
reward: Bignum.ZERO,
payloadLength: 32 * sortedTransactions.length,
payloadHash: HashAlgorithms.sha256(transactionData.ids).toString("hex"),
transactions: sortedTransactions,
};

return Block.create(data, crypto.getKeys(generatorKeys.secret));
};

it("should restore vote balances after a rollback", async () => {
const mockCallback = jest.fn(() => true);

// Create key pair for new voter
const keyPair = crypto.getKeys("secret");
const recipient = crypto.getAddress(keyPair.publicKey);

let nextForger = await getNextForger();
const initialVoteBalance = nextForger.voteBalance;

// First send funds to new voter wallet
const forgerKeys = delegates.find(wallet => wallet.publicKey === nextForger.publicKey);
const transfer = transactionBuilder
.transfer()
.recipientId(recipient)
.amount(125)
.sign(forgerKeys.passphrase)
.getStruct();

const transferBlock = createBlock(forgerKeys, [transfer]);
await blockchain.processBlock(transferBlock, mockCallback);

const wallet = blockchain.database.walletManager.findByPublicKey(keyPair.publicKey);
const walletForger = blockchain.database.walletManager.findByPublicKey(forgerKeys.publicKey);

// New wallet received funds and vote balance of delegate has been reduced by the same amount,
// since it forged it's own transaction the fees for the transaction have been recovered.
expect(wallet.balance).toEqual(new Bignum(transfer.amount));
expect(walletForger.voteBalance).toEqual(new Bignum(initialVoteBalance).minus(transfer.amount));

// Now vote with newly created wallet for previous forger.
const vote = transactionBuilder
.vote()
.fee(1)
.votesAsset([`+${forgerKeys.publicKey}`])
.sign("secret")
.getStruct();

nextForger = await getNextForger();
let nextForgerWallet = delegates.find(wallet => wallet.publicKey === nextForger.publicKey);

const voteBlock = createBlock(nextForgerWallet, [vote]);
await blockchain.processBlock(voteBlock, mockCallback);

// Wallet paid a fee of 1 and the vote has been placed.
expect(wallet.balance).toEqual(new Bignum(124));
expect(wallet.vote).toEqual(forgerKeys.publicKey);

// Vote balance of delegate now equals initial vote balance minus 1 for the vote fee
// since it was forged by a different delegate.
expect(walletForger.voteBalance).toEqual(new Bignum(initialVoteBalance).minus(vote.fee));

// Now unvote again
const unvote = transactionBuilder
.vote()
.fee(1)
.votesAsset([`-${forgerKeys.publicKey}`])
.sign("secret")
.getStruct();

nextForger = await getNextForger();
nextForgerWallet = delegates.find(wallet => wallet.publicKey === nextForger.publicKey);

const unvoteBlock = createBlock(nextForgerWallet, [unvote]);
await blockchain.processBlock(unvoteBlock, mockCallback);

// Wallet paid a fee of 1 and no longer voted a delegate
expect(wallet.balance).toEqual(new Bignum(123));
expect(wallet.vote).toBeNull();

// Vote balance of delegate now equals initial vote balance minus the amount sent to the voter wallet.
expect(walletForger.voteBalance).toEqual(new Bignum(initialVoteBalance).minus(transfer.amount));

// Now rewind 3 blocks back to the initial state
await blockchain.removeBlocks(3);

// Wallet is now a cold wallet and the initial vote balance has been restored.
expect(wallet.balance).toEqual(Bignum.ZERO);
expect(walletForger.voteBalance).toEqual(new Bignum(initialVoteBalance));
});
});

describe("getUnconfirmedTransactions", () => {
it("should get unconfirmed transactions", async () => {
const transactionsWithoutType2 = genesisBlock.transactions.filter(tx => tx.type !== 2);
Expand Down
12 changes: 3 additions & 9 deletions __tests__/integration/core-transaction-pool/guard.test.ts
Expand Up @@ -269,9 +269,7 @@ describe("Transaction Guard", () => {

const errorExpected = [
{
message: `["[PoolWalletManager] Can't apply transaction id:${transaction[0].id} from sender:${
newWallet.address
}","Insufficient balance in the wallet."]`,
message: `["Insufficient balance in the wallet."]`,
type: "ERR_APPLY",
},
];
Expand All @@ -297,9 +295,7 @@ describe("Transaction Guard", () => {

expect(result.errors[transactions[1].id]).toEqual([
{
message: `["[PoolWalletManager] Can't apply transaction id:${transactions[1].id} from sender:${
delegates[0].address
}","Insufficient balance in the wallet."]`,
message: `["Insufficient balance in the wallet."]`,
type: "ERR_APPLY",
},
]);
Expand Down Expand Up @@ -374,9 +370,7 @@ describe("Transaction Guard", () => {
expect(Object.keys(result.errors).length).toBe(1);
expect(result.errors[lastTransaction[0].id]).toEqual([
{
message: `["[PoolWalletManager] Can't apply transaction id:${
lastTransaction[0].id
} from sender:${sender.address}","Insufficient balance in the wallet."]`,
message: `["Insufficient balance in the wallet."]`,
type: "ERR_APPLY",
},
]);
Expand Down
Expand Up @@ -33,17 +33,19 @@ describe("canApply", () => {
const errors = [];

expect(poolWalletManager.canApply(delegateReg, errors)).toBeFalse();
expect(errors).toEqual([`Can't apply transaction ${delegateReg.id}: delegate name already taken.`]);
expect(errors).toEqual([
`Failed to apply transaction, because the username '${
delegateReg.data.asset.delegate.username
}' is already registered.`,
]);
});

it("should add an error when voting for a delegate that doesn't exist", () => {
const vote = generateVote("unitnet", wallets[11].passphrase, wallets[12].keys.publicKey, 1)[0];
const errors = [];

expect(poolWalletManager.canApply(vote, errors)).toBeFalse();
expect(errors).toEqual([
`Can't apply transaction ${vote.id}: delegate +${wallets[12].keys.publicKey} does not exist.`,
]);
expect(errors).toEqual([`Failed to apply transaction, because only delegates can be voted.`]);
});
});

Expand Down Expand Up @@ -137,11 +139,7 @@ describe("applyPoolTransactionToSender", () => {
expect(t.from).toBe(delegate);
} else {
expect(t.from).toBe(walletsGen[0]);
expect(JSON.stringify(errors)).toEqual(
`["[PoolWalletManager] Can't apply transaction id:${transfer.id} from sender:${
t.from.address
}","Insufficient balance in the wallet."]`,
);
expect(errors).toEqual(["Insufficient balance in the wallet."]);
}

(container.resolvePlugin<Database.IDatabaseService>("database").walletManager as any).forgetByPublicKey(
Expand Down
86 changes: 86 additions & 0 deletions __tests__/unit/core-database/wallet-manager.test.ts
Expand Up @@ -265,6 +265,92 @@ describe("Wallet Manager", () => {
expect(sender.balance).toEqual(transaction.data.amount);
expect(recipient.balance).toEqual(Bignum.ZERO);
});

it("should revert vote transaction and correctly update vote balances", async () => {
const delegateKeys = crypto.getKeys("delegate");
const voterKeys = crypto.getKeys("secret");

const delegate = walletManager.findByPublicKey(delegateKeys.publicKey);
delegate.username = "unittest";
delegate.balance = new Bignum(100_000_000);
delegate.vote = delegate.publicKey;
delegate.voteBalance = new Bignum(delegate.balance);
walletManager.reindex(delegate);

const voter = walletManager.findByPublicKey(voterKeys.publicKey);
voter.balance = new Bignum(100_000);

const voteTransaction = transactionBuilder
.vote()
.votesAsset([`+${delegateKeys.publicKey}`])
.fee(125)
.sign("secret")
.build();

expect(delegate.balance).toEqual(new Bignum(100_000_000));
expect(delegate.voteBalance).toEqual(new Bignum(100_000_000));
expect(voter.balance).toEqual(new Bignum(100_000));

walletManager.applyTransaction(voteTransaction);

expect(voter.balance).toEqual(new Bignum(100_000).minus(voteTransaction.data.fee));
expect(delegate.voteBalance).toEqual(new Bignum(100_000_000).plus(voter.balance));

walletManager.revertTransaction(voteTransaction);

expect(voter.balance).toEqual(new Bignum(100_000));
expect(delegate.voteBalance).toEqual(new Bignum(100_000_000));
});

it("should revert unvote transaction and correctly update vote balances", async () => {
const delegateKeys = crypto.getKeys("delegate");
const voterKeys = crypto.getKeys("secret");

const delegate = walletManager.findByPublicKey(delegateKeys.publicKey);
delegate.username = "unittest";
delegate.balance = new Bignum(100_000_000);
delegate.vote = delegate.publicKey;
delegate.voteBalance = new Bignum(delegate.balance);
walletManager.reindex(delegate);

const voter = walletManager.findByPublicKey(voterKeys.publicKey);
voter.balance = new Bignum(100_000);

const voteTransaction = transactionBuilder
.vote()
.votesAsset([`+${delegateKeys.publicKey}`])
.fee(125)
.sign("secret")
.build();

expect(delegate.balance).toEqual(new Bignum(100_000_000));
expect(delegate.voteBalance).toEqual(new Bignum(100_000_000));
expect(voter.balance).toEqual(new Bignum(100_000));

walletManager.applyTransaction(voteTransaction);

expect(voter.balance).toEqual(new Bignum(100_000).minus(voteTransaction.data.fee));
expect(delegate.voteBalance).toEqual(new Bignum(100_000_000).plus(voter.balance));

const unvoteTransaction = transactionBuilder
.vote()
.votesAsset([`-${delegateKeys.publicKey}`])
.fee(125)
.sign("secret")
.build();

walletManager.applyTransaction(unvoteTransaction);

expect(voter.balance).toEqual(
new Bignum(100_000).minus(voteTransaction.data.fee).minus(unvoteTransaction.data.fee),
);
expect(delegate.voteBalance).toEqual(new Bignum(100_000_000));

walletManager.revertTransaction(unvoteTransaction);

expect(voter.balance).toEqual(new Bignum(100_000).minus(voteTransaction.data.fee));
expect(delegate.voteBalance).toEqual(new Bignum(100_000_000).plus(voter.balance));
});
});

describe("findByAddress", () => {
Expand Down
8 changes: 2 additions & 6 deletions __tests__/unit/core-transaction-pool/connection.test.ts
Expand Up @@ -7,9 +7,9 @@ import { dato } from "@faustbrian/dato";
import delay from "delay";
import cloneDeep from "lodash.clonedeep";
import randomSeed from "random-seed";
import { TransactionPool } from "../../../packages/core-transaction-pool/src";
import { generators } from "../../utils";
import { block2, delegates } from "../../utils/fixtures/unitnet";
import { TransactionPool } from "../../../packages/core-transaction-pool/src";
import { transactions as mockData } from "./__fixtures__/transactions";
import { setUpFull, tearDownFull } from "./__support__/setup";

Expand Down Expand Up @@ -177,11 +177,7 @@ describe("Connection", () => {
];

const { added, notAdded } = connection.addTransactions(transactions);
expect(notAdded[0].message).toEqual(
`["[PoolWalletManager] Can't apply transaction id:${
mockData.dummy3.id
} from sender:AHkZLLjUdjjjJzNe1zCXqHh27bUhzg8GZw","Insufficient balance in the wallet."]`,
);
expect(notAdded[0].message).toEqual('["Insufficient balance in the wallet."]');
expect(connection.getPoolSize()).toBe(5);
});
});
Expand Down

0 comments on commit e99f1aa

Please sign in to comment.