factom-vote
Version:
JS implementation of the Factom voting specification
321 lines (269 loc) • 16.3 kB
JavaScript
const assert = require('chai').assert,
sinon = require('sinon'),
crypto = require('crypto'),
{ FactomCli, Entry } = require('factom'),
{ keyToSecretIdentityKey, getPublicIdentityKey, walletdIdentityPublicKeysResolver } = require('../src/factom-identity'),
{ parseVote, parseVoteChain, parseVoteChainEntry, parseEligibleVotersChain } = require('../src/read-vote/parse-vote'),
{ generateVoteChain, generateEligibleVotersChain, generateAppendEligibleVotersEntry } = require('../src/write-vote/create-vote-struct'),
{ generateVoteCommitEntry, generateVoteRevealEntry } = require('../src/write-vote/participate-vote-struct');
describe('Parse vote', function () {
it('Should parse eligible voters chain', async function () {
const initiator = {
id: crypto.randomBytes(32), publicKey: '310df171d50ad46f0f5c115520b0fcde4522801de4732589df14b42d5f980818',
secretKey: 'd1011e7b33b3bb18c184730530a6b1977ce3ed3c3b66677a276bf326116a885b'
};
const initiatorPublicIdentityKey = getPublicIdentityKey(keyToSecretIdentityKey(initiator.secretKey));
const initialVoters = [{ voterId: 'ID_1', weight: 22 }, { voterId: 'ID_2' }];
const appendVoters = [{ voterId: 'ID_1', weight: 0 }, { voterId: 'ID_3' }];
const firstEntry = Entry.builder((await generateEligibleVotersChain(initialVoters, initiator)).firstEntry)
.blockContext({ directoryBlockHeight: 1 })
.build();
const appendEntry = Entry.builder(await generateAppendEligibleVotersEntry(appendVoters, '7b11a72cd69d3083e4d20137bb569423923a55696017b36f46222e9f83964679', initiator))
.blockContext({ directoryBlockHeight: 2 })
.build();
const invalidEntry = Entry.builder()
.chainId('7b11a72cd69d3083e4d20137bb569423923a55696017b36f46222e9f83964679')
.content('random', 'utf8')
.build();
const cli = new FactomCli();
const mock = sinon.mock(cli);
mock.expects('getAllEntriesOfChain')
.once()
.withArgs('7b11a72cd69d3083e4d20137bb569423923a55696017b36f46222e9f83964679')
.returns(Promise.resolve([firstEntry, invalidEntry, appendEntry]));
mock.expects('getHeights').never();
expectIdentityKeysAtHeightCall(mock, initiator, 1, initiatorPublicIdentityKey);
expectIdentityKeysAtHeightCall(mock, initiator, 2, initiatorPublicIdentityKey);
const publicKeysResolver = walletdIdentityPublicKeysResolver.bind(null, cli);
const { eligibleVotersRegitrations, parseErrors } = await parseEligibleVotersChain(cli, publicKeysResolver, '7b11a72cd69d3083e4d20137bb569423923a55696017b36f46222e9f83964679');
mock.verify();
assert.lengthOf(eligibleVotersRegitrations, 2);
assert.lengthOf(parseErrors, 1);
const registration1 = eligibleVotersRegitrations[0];
assert.equal(registration1.type, 'initial-eligible-voters');
assert.equal(registration1.identity, initiator.id.toString('hex'));
assert.equal(registration1.blockContext.directoryBlockHeight, 1);
assert.deepEqual(registration1.data, initialVoters);
const registration2 = eligibleVotersRegitrations[1];
assert.equal(registration2.type, 'append-eligible-voters');
assert.deepEqual(registration2.data, appendVoters);
assert.equal(registration2.blockContext.directoryBlockHeight, 2);
});
it('Should skip replayed entries', async function () {
const initiator = {
id: crypto.randomBytes(32), publicKey: '310df171d50ad46f0f5c115520b0fcde4522801de4732589df14b42d5f980818',
secretKey: 'd1011e7b33b3bb18c184730530a6b1977ce3ed3c3b66677a276bf326116a885b'
}; const initiatorPublicIdentityKey = getPublicIdentityKey(keyToSecretIdentityKey(initiator.secretKey));
const initialVoters = [{ voterId: 'ID_1', weight: 22 }, { voterId: 'ID_2' }];
const appendVoters = [{ voterId: 'ID_1', weight: 0 }, { voterId: 'ID_3' }];
const firstEntry = Entry.builder((await generateEligibleVotersChain(initialVoters, initiator)).firstEntry)
.blockContext({ directoryBlockHeight: 1 })
.build();
const appendEntry = Entry.builder(await generateAppendEligibleVotersEntry(appendVoters, '7b11a72cd69d3083e4d20137bb569423923a55696017b36f46222e9f83964679', initiator))
.blockContext({ directoryBlockHeight: 2 })
.build();
const duplicateEntry = Entry.builder(appendEntry)
.blockContext({ directoryBlockHeight: 3 })
.build();
const cli = new FactomCli();
const mock = sinon.mock(cli);
mock.expects('getAllEntriesOfChain')
.once()
.withArgs('7b11a72cd69d3083e4d20137bb569423923a55696017b36f46222e9f83964679')
.returns(Promise.resolve([firstEntry, appendEntry, duplicateEntry]));
mock.expects('getHeights').never();
expectIdentityKeysAtHeightCall(mock, initiator, 1, initiatorPublicIdentityKey);
expectIdentityKeysAtHeightCall(mock, initiator, 2, initiatorPublicIdentityKey);
const publicKeysResolver = walletdIdentityPublicKeysResolver.bind(null, cli);
const { eligibleVotersRegitrations, parseErrors } = await parseEligibleVotersChain(cli, publicKeysResolver, '7b11a72cd69d3083e4d20137bb569423923a55696017b36f46222e9f83964679');
mock.verify();
assert.lengthOf(eligibleVotersRegitrations, 2);
assert.lengthOf(parseErrors, 1);
assert.equal(eligibleVotersRegitrations[1].blockContext.directoryBlockHeight, 2);
assert.equal(parseErrors[0].error, 'Replayed entry');
});
it('Should parse vote definition entry', async function () {
const initiator = {
id: crypto.randomBytes(32), publicKey: '310df171d50ad46f0f5c115520b0fcde4522801de4732589df14b42d5f980818',
secretKey: 'd1011e7b33b3bb18c184730530a6b1977ce3ed3c3b66677a276bf326116a885b'
}; const initiatorPublicIdentityKey = getPublicIdentityKey(keyToSecretIdentityKey(initiator.secretKey));
const vote = require('./data/vote-definition');
const entry = Entry.builder((await generateVoteChain(vote, initiator)).firstEntry)
.blockContext({ directoryBlockHeight: 11 })
.build();
const cli = new FactomCli();
const mock = sinon.mock(cli);
mock.expects('getHeights').never();
expectIdentityKeysAtHeightCall(mock, initiator, 11, initiatorPublicIdentityKey);
const publicKeysResolver = walletdIdentityPublicKeysResolver.bind(null, cli);
const parsed = await parseVoteChainEntry(publicKeysResolver, entry);
mock.verify();
assert.equal(parsed.type, 'definition');
assert.equal(parsed.identity, initiator.id.toString('hex'));
assert.deepEqual(parsed.data, vote);
assert.equal(parsed.blockContext.directoryBlockHeight, 11);
});
it('Should parse vote commit entry', async function () {
const voter = {
id: crypto.randomBytes(32), publicKey: '310df171d50ad46f0f5c115520b0fcde4522801de4732589df14b42d5f980818',
secretKey: 'd1011e7b33b3bb18c184730530a6b1977ce3ed3c3b66677a276bf326116a885b'
}; const voterPublicIdentityKey = getPublicIdentityKey(keyToSecretIdentityKey(voter.secretKey));
const vote = require('./data/vote.json');
const entry = Entry.builder(await generateVoteCommitEntry('7b11a72cd69d3083e4d20137bb569423923a55696017b36f46222e9f83964679', vote, voter))
.blockContext({ directoryBlockHeight: 2 })
.build();
const cli = new FactomCli();
const mock = sinon.mock(cli);
mock.expects('getHeights').never();
expectIdentityKeysAtHeightCall(mock, voter, 2, voterPublicIdentityKey);
const publicKeysResolver = walletdIdentityPublicKeysResolver.bind(null, cli);
const parsed = await parseVoteChainEntry(publicKeysResolver, entry);
assert.equal(parsed.type, 'commit');
assert.equal(parsed.identity, voter.id.toString('hex'));
assert.equal(parsed.data, JSON.parse(entry.content.toString()).commitment);
assert.equal(parsed.blockContext.directoryBlockHeight, 2);
});
it('Should parse vote reveal entry', async function () {
const voterId = crypto.randomBytes(32);
const vote = require('./data/vote.json');
const entry = Entry.builder(generateVoteRevealEntry('7b11a72cd69d3083e4d20137bb569423923a55696017b36f46222e9f83964679', vote, voterId))
.blockContext({ directoryBlockHeight: 7 })
.build();
const parsed = parseVoteChainEntry(null, entry);
assert.equal(parsed.type, 'reveal');
assert.equal(parsed.identity, voterId.toString('hex'));
assert.deepEqual(parsed.data, JSON.parse(entry.content.toString()));
assert.equal(parsed.blockContext.directoryBlockHeight, 7);
});
it('Should parse vote chain', async function () {
const initiator = {
id: crypto.randomBytes(32),
publicKey: '310df171d50ad46f0f5c115520b0fcde4522801de4732589df14b42d5f980818',
secretKey: 'd1011e7b33b3bb18c184730530a6b1977ce3ed3c3b66677a276bf326116a885b'
};
const initiatorPublicIdentityKey = getPublicIdentityKey(keyToSecretIdentityKey(initiator.secretKey));
const voteDef = require('./data/vote-definition');
const voteChainFirstEntry = Entry.builder((await generateVoteChain(voteDef, initiator)).firstEntry)
.blockContext({ directoryBlockHeight: 1 })
.build();
const voter = {
id: crypto.randomBytes(32),
publicKey: 'bea0ba0061046926a18a0811ad7a90c356a9ec4a96ed087abb364dba12dd5525',
secretKey: '8514257f1c373550199aa981ff07abd3ecd29a091ed1930eccebd161d54e9642'
};
const voterPublicIdentityKey = getPublicIdentityKey(keyToSecretIdentityKey(voter.secretKey));
const vote = require('./data/vote.json');
const commitEntry = Entry.builder(await generateVoteCommitEntry('7b11a72cd69d3083e4d20137bb569423923a55696017b36f46222e9f83964679', vote, voter))
.blockContext({ directoryBlockHeight: 2 })
.build();
const revealEntry = Entry.builder(generateVoteRevealEntry('7b11a72cd69d3083e4d20137bb569423923a55696017b36f46222e9f83964679', vote, voter.id))
.blockContext({ directoryBlockHeight: 7 })
.build();
const invalidEntry = Entry.builder()
.chainId('7b11a72cd69d3083e4d20137bb569423923a55696017b36f46222e9f83964679')
.content('Hello', 'utf8')
.build();
const cli = new FactomCli();
const mock = sinon.mock(cli);
mock.expects('rewindChainWhile')
.once()
.withArgs('7b11a72cd69d3083e4d20137bb569423923a55696017b36f46222e9f83964679')
.onFirstCall()
.callsFake(async function (chainId, condition, f) {
await f(revealEntry);
await f(invalidEntry);
await f(commitEntry);
await f(voteChainFirstEntry);
});
mock.expects('getHeights').never();
expectIdentityKeysAtHeightCall(mock, voter, 2, voterPublicIdentityKey);
expectIdentityKeysAtHeightCall(mock, initiator, 1, initiatorPublicIdentityKey);
const publicKeysResolver = walletdIdentityPublicKeysResolver.bind(null, cli);
const { definition,
commits,
reveals,
parseErrors } = await parseVoteChain(cli, publicKeysResolver, '7b11a72cd69d3083e4d20137bb569423923a55696017b36f46222e9f83964679');
mock.verify();
// Definition
assert.equal(definition.type, 'definition');
assert.equal(definition.identity, initiator.id.toString('hex'));
assert.deepEqual(definition.data, voteDef);
assert.equal(definition.blockContext.directoryBlockHeight, 1);
// Commit
assert.lengthOf(commits, 1);
assert.equal(commits[0].type, 'commit');
assert.equal(commits[0].identity, voter.id.toString('hex'));
assert.equal(commits[0].data, JSON.parse(commitEntry.content.toString()).commitment);
assert.equal(commits[0].blockContext.directoryBlockHeight, 2);
// Reveal
assert.lengthOf(reveals, 1);
assert.equal(reveals[0].type, 'reveal');
assert.equal(reveals[0].identity, voter.id.toString('hex'));
assert.deepEqual(reveals[0].data, JSON.parse(revealEntry.content.toString()));
assert.equal(reveals[0].blockContext.directoryBlockHeight, 7);
// Parse errors
assert.lengthOf(parseErrors, 1);
});
it('Should parse vote', async function () {
const initiator = {
id: crypto.randomBytes(32),
publicKey: '310df171d50ad46f0f5c115520b0fcde4522801de4732589df14b42d5f980818',
secretKey: 'd1011e7b33b3bb18c184730530a6b1977ce3ed3c3b66677a276bf326116a885b'
};
const initiatorPublicIdentityKey = getPublicIdentityKey(keyToSecretIdentityKey(initiator.secretKey));
const initialVoters = [{ voterId: 'ID_1', weight: 22 }, { voterId: 'ID_2' }];
const initialVotersEntry = Entry.builder((await generateEligibleVotersChain(initialVoters, initiator)).firstEntry)
.blockContext({ directoryBlockHeight: 1 })
.build();
const voteDef = require('./data/vote-definition');
const voteChainFirstEntry = Entry.builder((await generateVoteChain(voteDef, initiator)).firstEntry)
.blockContext({ directoryBlockHeight: 1 })
.build();
const voter = {
id: crypto.randomBytes(32),
publicKey: 'bea0ba0061046926a18a0811ad7a90c356a9ec4a96ed087abb364dba12dd5525',
secretKey: '8514257f1c373550199aa981ff07abd3ecd29a091ed1930eccebd161d54e9642'
};
const voterPublicIdentityKey = getPublicIdentityKey(keyToSecretIdentityKey(voter.secretKey));
const vote = require('./data/vote.json');
const commitEntry = Entry.builder(await generateVoteCommitEntry('7b11a72cd69d3083e4d20137bb569423923a55696017b36f46222e9f83964679', vote, voter))
.blockContext({ directoryBlockHeight: 2 })
.build();
const revealEntry = Entry.builder(generateVoteRevealEntry('7b11a72cd69d3083e4d20137bb569423923a55696017b36f46222e9f83964679', vote, voter.id))
.blockContext({ directoryBlockHeight: 7 })
.build();
const cli = new FactomCli();
const mock = sinon.mock(cli);
mock.expects('rewindChainWhile')
.once()
.withArgs('7b11a72cd69d3083e4d20137bb569423923a55696017b36f46222e9f83964679')
.onFirstCall()
.callsFake(async function (chainId, condition, f) {
await f(revealEntry);
await f(commitEntry);
await f(voteChainFirstEntry);
});
mock.expects('getAllEntriesOfChain')
.once()
.returns(Promise.resolve([initialVotersEntry]));
mock.expects('getHeights').never();
expectIdentityKeysAtHeightCall(mock, voter, 2, voterPublicIdentityKey);
expectIdentityKeysAtHeightCall(mock, initiator, 1, initiatorPublicIdentityKey, 2);
const publicKeysResolver = walletdIdentityPublicKeysResolver.bind(null, cli);
const { definition, commits, reveals, eligibleVotersRegitrations } = await parseVote(cli, publicKeysResolver, '7b11a72cd69d3083e4d20137bb569423923a55696017b36f46222e9f83964679');
mock.verify();
assert.deepEqual(definition.data, voteDef);
assert.lengthOf(commits, 1);
assert.lengthOf(reveals, 1);
assert.lengthOf(eligibleVotersRegitrations, 1);
});
function expectIdentityKeysAtHeightCall(mock, identity, height, key, exactly) {
mock.expects('walletdApi')
.exactly(exactly || 1)
.withArgs('active-identity-keys', {
chainid: identity.id.toString('hex'),
height: height
})
.returns(Promise.resolve({ keys: [key] }));
}
});