ldpos-chain
Version:
LDPoS chain module
674 lines (616 loc) • 19.9 kB
JavaScript
const crypto = require('crypto');
const ID_BYTE_SIZE = 20;
const DEFAULT_MAX_VOTES_PER_ACCOUNT = 5;
class DAL {
constructor() {
this.clearAllData();
}
async clearAllData() {
this.accounts = {};
this.delegates = {};
this.ballots = {};
this.blocks = [];
this.transactions = {};
this.multisigMembers = {};
this.store = {};
}
async init(options) {
let { genesis, maxVotesPerAccount } = options;
let { accounts } = genesis;
let multisigWalletList = genesis.multisigWallets || [];
this.maxVotesPerAccount = maxVotesPerAccount == null ? DEFAULT_MAX_VOTES_PER_ACCOUNT : maxVotesPerAccount;
await Promise.all(
accounts.map(async (accountInfo) => {
let { votes, ...accountWithoutVotes } = accountInfo;
let account = {
...accountWithoutVotes,
type: accountWithoutVotes.type || 'sig',
updateHeight: 0
};
await this.upsertAccount(account);
if (account.forgingPublicKey) {
await this.upsertDelegate({
address: account.address,
voteWeight: '0',
forgingRewards: '0'
});
}
})
);
for (let accountInfo of accounts) {
let { votes } = accountInfo;
for (let delegateAddress of votes) {
await this.vote({
id: crypto.randomBytes(ID_BYTE_SIZE).toString('hex'),
voterAddress: accountInfo.address,
delegateAddress
});
let delegate = await this.getDelegate(delegateAddress);
let updatedVoteWeight = BigInt(delegate.voteWeight) + BigInt(accountInfo.balance);
await this.upsertDelegate({
address: delegateAddress,
voteWeight: updatedVoteWeight.toString(),
forgingRewards: '0'
});
}
}
await Promise.all(
multisigWalletList.map(async (multisigWallet) => {
await this.registerMultisigWallet(
multisigWallet.address,
multisigWallet.members,
multisigWallet.requiredSignatureCount
);
})
);
}
async destroy() {
this.accounts = {};
this.delegates = {};
this.ballots = {};
this.blocks = [];
this.transactions = {};
this.multisigMembers = {};
this.store = {};
}
async saveItem(key, value) {
this.store[key] = value;
}
async loadItem(key) {
return this.store[key];
}
async upsertAccount(account) {
let existingAccount = this.accounts[account.address];
this.accounts[account.address] = {
...existingAccount,
...account
};
}
async hasAccount(walletAddress) {
return !!this.accounts[walletAddress];
}
async getAccount(walletAddress) {
let account = this.accounts[walletAddress];
if (!account) {
let error = new Error(`Account ${walletAddress} did not exist`);
error.name = 'AccountDidNotExistError';
error.type = 'InvalidActionError';
throw error;
}
return {...account};
}
async getAccountsByBalance(offset, limit, order) {
return this.sortByProperty(Object.values(this.accounts), 'balance', order, BigInt)
.slice(offset, offset + limit);
}
async getAccountVotes(voterAddress) {
let voterAccount = this.accounts[voterAddress];
if (!voterAccount) {
let error = new Error(`Voter ${voterAddress} did not exist`);
error.name = 'VoterAccountDidNotExistError';
error.type = 'InvalidActionError';
throw error;
}
let voteSet = new Set();
let voteList = Object.values(this.ballots).filter(
currentBallot => (
currentBallot.active &&
currentBallot.type === 'vote' &&
currentBallot.voterAddress === voterAddress
)
);
for (let vote of voteList) {
voteSet.add(vote.delegateAddress);
}
return [...voteSet];
}
async hasVoteForDelegate(voterAddress, delegateAddress) {
return Object.values(this.ballots).some(
ballot => (
ballot.active &&
ballot.type === 'vote' &&
ballot.voterAddress === voterAddress &&
ballot.delegateAddress === delegateAddress
)
);
}
async vote(ballot) {
if (this.ballots[ballot.id]) {
this.ballots[ballot.id] = {...ballot, type: 'vote', active: true};
return;
}
let { voterAddress, delegateAddress } = ballot;
let hasDelegate = await this.hasDelegate(delegateAddress);
if (!hasDelegate) {
let error = new Error(
`Delegate ${delegateAddress} did not exist to vote for`
);
error.name = 'DelegateDidNotExistError'
error.type = 'InvalidActionError';
throw error;
}
let votes = await this.getAccountVotes(voterAddress);
let voteSet = new Set(votes);
if (voteSet.size >= this.maxVotesPerAccount) {
let error = new Error(
`Voter account ${
voterAddress
} has already voted for ${
voteSet.size
} delegates so it cannot vote for any more`
);
error.name = 'VoterExceededVoteCountError';
error.type = 'InvalidActionError';
throw error;
}
let hasExistingVote = await this.hasVoteForDelegate(voterAddress, delegateAddress);
if (hasExistingVote) {
let error = new Error(
`Voter ${voterAddress} has already voted for delegate ${delegateAddress}`
);
error.name = 'VoterAlreadyVotedForDelegateError';
error.type = 'InvalidActionError';
throw error;
}
let existingUnvoteBallots = Object.values(this.ballots).filter(
currentBallot => (
currentBallot.active &&
currentBallot.type === 'unvote' &&
currentBallot.voterAddress === voterAddress &&
currentBallot.delegateAddress === delegateAddress
)
);
for (let unvoteBallot of existingUnvoteBallots) {
unvoteBallot.active = false;
}
this.ballots[ballot.id] = {...ballot, type: 'vote', active: true};
}
async unvote(ballot) {
if (this.ballots[ballot.id]) {
this.ballots[ballot.id] = {...ballot, type: 'unvote', active: true};
return;
}
let { voterAddress, delegateAddress } = ballot;
let hasDelegate = await this.hasDelegate(delegateAddress);
if (!hasDelegate) {
let error = new Error(
`Delegate ${delegateAddress} did not exist to unvote`
);
error.name = 'DelegateDidNotExistError';
error.type = 'InvalidActionError';
throw error;
}
let existingVoteBallots = Object.values(this.ballots).filter(
currentBallot => (
currentBallot.active &&
currentBallot.type === 'vote' &&
currentBallot.voterAddress === voterAddress &&
currentBallot.delegateAddress === delegateAddress
)
);
let existingUnvoteBallot = Object.values(this.ballots).find(
currentBallot => (
currentBallot.active &&
currentBallot.type === 'unvote' &&
currentBallot.voterAddress === voterAddress &&
currentBallot.delegateAddress === delegateAddress
)
);
if (!existingVoteBallots.length || existingUnvoteBallot) {
let error = new Error(
`Voter ${voterAddress} could not unvote delegate ${delegateAddress} because it was not voting for it`
);
error.name = 'VoterNotVotingForDelegateError';
error.type = 'InvalidActionError';
throw error;
}
for (let voteBallot of existingVoteBallots) {
voteBallot.active = false;
}
this.ballots[ballot.id] = {...ballot, type: 'unvote', active: true};
}
async registerMultisigWallet(multisigAddress, memberAddresses, requiredSignatureCount) {
let multisigAccount = this.accounts[multisigAddress];
if (!multisigAccount) {
let error = new Error(
`Account ${multisigAddress} did not exist for multisig wallet registration`
);
error.name = 'MultisigAccountDidNotExistError';
error.type = 'InvalidActionError';
throw error;
}
for (let memberAddress of memberAddresses) {
let memberAccount = this.accounts[memberAddress];
if (!memberAccount) {
let error = new Error(
`Account ${memberAddress} did not exist for multisig member registration`
);
error.name = 'MemberAccountDidNotExistError';
error.type = 'InvalidActionError';
throw error;
}
if (!memberAccount.multisigPublicKey) {
let error = new Error(
`Account ${memberAddress} was not registered for multisig so it cannot be a member of a multisig wallet`
);
error.name = 'MemberAccountWasNotRegisteredError';
error.type = 'InvalidActionError';
throw error;
}
if (memberAccount.type === 'multisig') {
let error = new Error(
`Account ${
memberAddress
} was a multisig wallet so it could not be registered as a member of another multisig wallet`
);
error.name = 'MemberAccountWasMultisigAccountError';
error.type = 'InvalidActionError';
throw error;
}
}
multisigAccount.type = 'multisig';
multisigAccount.requiredSignatureCount = requiredSignatureCount;
this.multisigMembers[multisigAddress] = new Set(memberAddresses);
}
async getMultisigWalletMembers(multisigAddress) {
let memberAddresses = this.multisigMembers[multisigAddress];
if (!memberAddresses) {
let error = new Error(
`Address ${multisigAddress} is not registered as a multisig wallet`
);
error.name = 'MultisigAccountDidNotExistError';
error.type = 'InvalidActionError';
throw error;
}
return [...memberAddresses];
}
async getBlocksFromHeight(height, limit) {
let blocks = await this.getSignedBlocksFromHeight(height, limit)
return blocks.map(block => this.simplifyBlock(block));
}
async getSignedBlocksFromHeight(height, limit) {
if (height < 1) {
height = 1;
}
let startIndex = height - 1;
return this.blocks
.slice(startIndex, startIndex + limit)
.map((block) => {
return {...block};
});
}
async getLastBlockAtTimestamp(timestamp) {
let blockList = [...this.blocks];
blockList.sort((blockA, blockB) => {
if (blockA.timestamp > blockB.timestamp) {
return -1;
}
if (blockA.timestamp < blockB.timestamp) {
return 1;
}
return 0;
});
let block = blockList.find(block => block.timestamp <= timestamp);
if (!block) {
let error = new Error(
`No block existed with timestamp less than or equal to ${timestamp}`
);
error.name = 'BlockDidNotExistError';
error.type = 'InvalidActionError';
throw error;
}
return this.simplifyBlock(block);
}
async getBlocksBetweenHeights(fromHeight, toHeight, limit) {
let selectedBlocks = [];
for (let block of this.blocks) {
if (block.height > fromHeight && block.height <= toHeight) {
selectedBlocks.push(block);
if (selectedBlocks.length >= limit) {
break;
}
}
}
return selectedBlocks.map(block => this.simplifyBlock(block));
}
async getBlockAtHeight(height) {
let block = await this.getSignedBlockAtHeight(height);
return this.simplifyBlock(block);
}
async getSignedBlockAtHeight(height) {
let block = this.blocks[height - 1];
if (!block) {
let error = new Error(
`No block existed at height ${height}`
);
error.name = 'BlockDidNotExistError';
error.type = 'InvalidActionError';
throw error;
}
return {...block};
}
async hasBlock(id) {
return this.blocks.some(currentBlock => currentBlock.id === id);
}
async getSignedBlock(id) {
let block = this.blocks.find(currentBlock => currentBlock.id === id);
if (!block) {
let error = new Error(
`No block existed with ID ${id}`
);
error.name = 'BlockDidNotExistError';
error.type = 'InvalidActionError';
throw error;
}
return {...block};
}
async getBlock(id) {
let block = this.getSignedBlock(id);
return this.simplifyBlock(block);
}
async getBlocksByTimestamp(offset, limit, order) {
return this.sortByProperty(this.blocks, 'timestamp', order)
.slice(offset, offset + limit)
.map(block => this.simplifyBlock(block));
}
async upsertBlock(block, synched) {
let { transactions } = block;
this.blocks[block.height - 1] = { ...block };
let len = transactions.length;
for (let i = 0; i < len; i++) {
let txn = transactions[i];
this.transactions[txn.id] = {
...txn,
blockId: block.id,
indexInBlock: i
};
}
}
async getMaxBlockHeight() {
return this.blocks.length;
}
async hasTransaction(transactionId) {
let transaction = this.transactions[transactionId];
return !!transaction;
}
async getTransaction(transactionId) {
let transaction = this.transactions[transactionId];
if (!transaction) {
let error = new Error(`Transaction ${transactionId} did not exist`);
error.name = 'TransactionDidNotExistError';
error.type = 'InvalidActionError';
throw error;
}
return {...transaction};
}
async getTransactionsByTimestamp(offset, limit, order) {
return this.sortByProperty(Object.values(this.transactions), 'timestamp', order)
.slice(offset, offset + limit)
.map((transaction) => {
return {...transaction};
});
}
async getTransactionsFromBlock(blockId, offset, limit) {
if (offset == null) {
offset = 0;
}
let transactionList = Object.values(this.transactions);
let blockTxns = transactionList.filter(
transaction => transaction.blockId === blockId && transaction.indexInBlock >= offset
);
if (limit == null) {
return blockTxns;
}
return blockTxns
.slice(0, limit)
.map((transaction) => {
return {...transaction};
});
}
async getAccountTransactions(walletAddress, fromTimestamp, offset, limit, order) {
let transactionList = this.sortByProperty(Object.values(this.transactions), 'timestamp', order);
let accountTransactions = [];
for (let transaction of transactionList) {
if (
(
transaction.recipientAddress === walletAddress ||
transaction.senderAddress === walletAddress
) &&
(
fromTimestamp == null ||
(
order === 'desc' ? transaction.timestamp <= fromTimestamp : transaction.timestamp >= fromTimestamp
)
)
) {
accountTransactions.push(transaction);
}
}
return accountTransactions.map((transaction) => {
return {...transaction};
}).slice(offset, offset + limit);
}
async getInboundTransactions(walletAddress, fromTimestamp, offset, limit, order) {
let transactionList = this.sortByProperty(Object.values(this.transactions), 'timestamp', order);
let inboundTransactions = [];
for (let transaction of transactionList) {
if (
transaction.recipientAddress === walletAddress &&
(
fromTimestamp == null ||
(
order === 'desc' ? transaction.timestamp <= fromTimestamp : transaction.timestamp >= fromTimestamp
)
)
) {
inboundTransactions.push(transaction);
}
}
return inboundTransactions.map((transaction) => {
return {...transaction};
}).slice(offset, offset + limit);
}
async getOutboundTransactions(walletAddress, fromTimestamp, offset, limit, order) {
let transactionList = this.sortByProperty(Object.values(this.transactions), 'timestamp', order);
let outboundTransactions = [];
for (let transaction of transactionList) {
if (
transaction.senderAddress === walletAddress &&
(
fromTimestamp == null ||
(
order === 'desc' ? transaction.timestamp <= fromTimestamp : transaction.timestamp >= fromTimestamp
)
)
) {
outboundTransactions.push(transaction);
}
}
return outboundTransactions.map((transaction) => {
return {...transaction};
}).slice(offset, offset + limit);
}
async getInboundTransactionsFromBlock(walletAddress, blockId) {
let transactionList = Object.values(this.transactions);
return transactionList
.filter(
transaction => transaction.blockId === blockId && transaction.recipientAddress === walletAddress
)
.map((transaction) => {
return {...transaction};
});
}
async getOutboundTransactionsFromBlock(walletAddress, blockId) {
let transactionList = Object.values(this.transactions);
return transactionList
.filter(
transaction => transaction.blockId === blockId &&
transaction.senderAddress === walletAddress
)
.map((transaction) => {
return {...transaction};
});
}
async upsertDelegate(delegate) {
let existingDelegate = this.delegates[delegate.address];
this.delegates[delegate.address] = {
...existingDelegate,
...delegate
};
}
async hasDelegate(walletAddress) {
return !!this.delegates[walletAddress];
}
async getDelegate(walletAddress) {
let delegate = this.delegates[walletAddress];
if (!delegate) {
let error = new Error(`Delegate ${walletAddress} did not exist`);
error.name = 'DelegateDidNotExistError';
error.type = 'InvalidActionError';
throw error;
}
return {...delegate};
}
async getDelegateVoters(walletAddress, offset, limit, order) {
let delegate = this.delegates[walletAddress];
if (!delegate) {
let error = new Error(`Delegate ${walletAddress} did not exist`);
error.name = 'DelegateDidNotExistError';
error.type = 'InvalidActionError';
throw error;
}
let voterSet = new Set();
let voteList = Object.values(this.ballots).filter(
currentBallot => (
currentBallot.active &&
currentBallot.type === 'vote' &&
currentBallot.delegateAddress === walletAddress
)
);
for (let vote of voteList) {
voterSet.add(vote.voterAddress);
}
return [...voterSet];
}
async getDelegatesByVoteWeight(offset, limit, order) {
return this.sortByProperty(
Object.values(this.delegates),
'voteWeight',
order,
BigInt,
'address',
'asc'
)
.slice(offset, offset + limit)
.map((delegate) => {
return {...delegate};
});
}
sortByProperty(list, property, order, typeCastFunction, secondaryProperty, secondaryOrder, secondaryTypeCastFunction) {
let sortFactor = order === 'asc' ? 1 : -1;
let secondarySortFactor = secondaryOrder === 'asc' ? 1 : -1;
return list.sort((a, b) => {
let itemA;
let itemB;
if (typeCastFunction) {
itemA = typeCastFunction(a[property]);
itemB = typeCastFunction(b[property]);
} else {
itemA = a[property];
itemB = b[property];
}
if (itemA > itemB) {
return sortFactor;
}
if (itemA < itemB) {
return -sortFactor;
}
if (secondaryProperty != null) {
if (secondaryTypeCastFunction) {
itemA = secondaryTypeCastFunction(a[secondaryProperty]);
itemB = secondaryTypeCastFunction(b[secondaryProperty]);
} else {
itemA = a[secondaryProperty];
itemB = b[secondaryProperty];
}
if (itemA > itemB) {
return secondarySortFactor;
}
if (itemA < itemB) {
return -secondarySortFactor;
}
}
return 0;
});
}
simplifyBlock(signedBlock) {
let {
transactions,
forgerSignature,
signatures,
...simpleBlock
} = signedBlock;
return simpleBlock;
}
}
module.exports = DAL;