Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add slot bitmap #267

Merged
merged 7 commits into from
Jan 2, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 22 additions & 10 deletions contracts/PoaOperator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@ contract PoaOperator is Adminable {
_setSlot(_slotId, _signerAddr, _tenderAddr);
}

function setTakenSlotBitmap(uint256 _before, uint256 _pos, bool isActive) internal pure returns (uint256) {
if (isActive) {
return _before | (0x01 << (255 - _pos));
} else {
return _before & (uint256(-2) << (255 - _pos));
}
}

function _setSlot(uint256 _slotId, address _signerAddr, bytes32 _tenderAddr) internal {
require(_slotId < epochLength, "out of range slotId");
Slot storage slot = slots[_slotId];
Expand All @@ -110,6 +118,7 @@ contract PoaOperator is Adminable {
slot.tendermint = _tenderAddr;
slot.activationEpoch = 0;
slot.eventCounter++;
takenSlots = setTakenSlotBitmap(takenSlots, _slotId, true);
emit ValidatorJoin(
slot.signer,
_slotId,
Expand All @@ -123,6 +132,7 @@ contract PoaOperator is Adminable {
if (_signerAddr == address(0) && _tenderAddr == 0) {
slot.activationEpoch = uint32(lastCompleteEpoch + 3);
slot.eventCounter++;
takenSlots = setTakenSlotBitmap(takenSlots, _slotId, false);
emit ValidatorLogout(
slot.signer,
_slotId,
Expand Down Expand Up @@ -155,6 +165,7 @@ contract PoaOperator is Adminable {
slot.newTendermint = 0x0;
slot.eventCounter++;
if (slot.signer != address(0)) {
takenSlots = setTakenSlotBitmap(takenSlots, _slotId, true);
emit ValidatorJoin(
slot.signer,
_slotId,
Expand Down Expand Up @@ -183,7 +194,7 @@ contract PoaOperator is Adminable {
bytes32 _blocksRoot,
bytes32 _casBitmap
) public {
require(countSigs(uint256(_casBitmap), epochLength) == neededSigs(epochLength), "incorrect number of sigs");
require(_checkSigs(uint256(_casBitmap), takenSlots, epochLength), "incorrect number of sigs");
_submitPeriod(_slotId, _prevHash, _blocksRoot, _casBitmap); // solium-disable-line arg-overflow
}

Expand Down Expand Up @@ -317,6 +328,7 @@ contract PoaOperator is Adminable {
uint256 public minimumPulse; // max amount of periods one can go without a heartbeat
uint16 public heartbeatColor;
mapping(address => BeatChallenge) public beatChallenges;
uint256 public takenSlots;

// challenger claims that there is no hearbeat included in the previous minimumPulse periods
// TODO: figure out what happens in slot rotation
Expand Down Expand Up @@ -437,19 +449,19 @@ contract PoaOperator is Adminable {
return slotId;
}

function countSigs(uint256 _sigs, uint256 _epochLength) internal pure returns (uint256 count) {
// an exact amount of sigs is needed, so that if one is proven to be invalid,
// then the amount of signatures drops below the 2/3 quorum => period is deleted
function _checkSigs(uint256 _sigs, uint256 _activeSlots, uint256 _epochLength) internal pure returns (bool) {
uint256 i = 256;
uint256 active = 0;
uint256 found = 0;
do {
i--;
count += uint8(_sigs >> i) & 0x01;
found += uint8(_sigs >> i) & 0x01;
active += uint8(_activeSlots >> i) & 0x01;
} while (i > 256 - _epochLength);
}

// an exact amount of sigs is needed, so that if one is proven to be invalid,
// then the amount of signatures drops below the 2/3 quorum => period is deleted
function neededSigs(uint256 _epochLength) internal pure returns (uint256 needed) {
// calculate n = 3f + 1
return (_epochLength * 2 / 3) + 1;
return ((active * 2 / 3) + 1 == found);
}

function _submitPeriod(
Expand Down Expand Up @@ -513,5 +525,5 @@ contract PoaOperator is Adminable {
}

// solium-disable-next-line mixedcase
uint256[15] private ______gap;
uint256[14] private ______gap;
}
4 changes: 4 additions & 0 deletions contracts/mocks/PoaOperatorMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,8 @@ contract PoaOperatorMock is PoaOperator {
lastCompleteEpoch = _lastCompleteEpoch;
}

function setActiveSlotsMap(uint256 _activeSlots) public {
takenSlots = _activeSlots;
}

}
2 changes: 1 addition & 1 deletion test/heartbeats.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ contract('PoaOperator Heartbeats', (accounts) => {
const alice = accounts[0];
const alicePriv = '0x278a5de700e29faae8e40e366ec5012b5ec63d36ec77e8a2417154cc1d25383f';
const admin = accounts[3];
const CAS = '0xe000000000000000000000000000000000000000000000000000000000000000';
const CAS = '0x8000000000000000000000000000000000000000000000000000000000000000';
const ZERO = '0x0000000000000000000000000000000000000000000000000000000000000000';
const CHALLENGE_DURATION = 3600;
const CHALLENGE_STAKE = '100000000000000000';
Expand Down
85 changes: 51 additions & 34 deletions test/poaOperator.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ contract('PoaOperator', (accounts) => {
const alice = accounts[0];
const bob = accounts[1];
const admin = accounts[3];
const CAS = '0xe000000000000000000000000000000000000000000000000000000000000000';
const ZERO = '0x0000000000000000000000000000000000000000000000000000000000000000';
const CHALLENGE_DURATION = 3600;
const CHALLENGE_STAKE = '100000000000000000';
Expand Down Expand Up @@ -78,48 +77,64 @@ contract('PoaOperator', (accounts) => {
await operator.submitPeriodWithCas(0, p[0], consensusRoot, '0xff', {from: alice}).should.be.rejectedWith(EVMRevert);
});

it('should allow to set slot but prevent submission with CAS bitmap of 0/3', async () => {
it('should allow to set slot but prevent submission with CAS bitmap of 0/1', async () => {
const data = await operator.contract.methods.setSlot(0, alice, alice).encodeABI();
await proxy.applyProposal(data, {from: admin});
const consensusRoot = '0x01';
const casBitmap = '0x00'; // no bits set: 0000 0000
await operator.submitPeriodWithCas(0, p[0], consensusRoot, casBitmap, { from: alice }).should.be.rejectedWith(EVMRevert);
});

it('should prevent submission with CAS bitmap of 1/3', async () => {
it('should allow submission with CAS bitmap of 1/1', async () => {
const consensusRoot = '0x01';
const casBitmap = '0x80'; // first bit set: 1000 0000
await operator.submitPeriodWithCas(0, p[0], consensusRoot, casBitmap, { from: alice }).should.be.rejectedWith(EVMRevert);
await operator.submitPeriodWithCas(0, p[0], consensusRoot, casBitmap, { from: alice }).should.be.fulfilled;
p[1] = await bridge.tipHash();
});

it('should prevent submission with CAS bitmap of 2/3', async () => {
it('should prevent submission with CAS bitmap of 2/1', async () => {
const consensusRoot = '0x01';
const casBitmap = '0xc0'; // first 2 bits set: 1100 0000
await operator.submitPeriodWithCas(0, p[0], consensusRoot, casBitmap, { from: alice }).should.be.rejectedWith(EVMRevert);
});

it('should allow submission with CAS bitmap of 3/3', async () => {
const consensusRoot = '0x01';
const casBitmap = '0xe0'; // first 3 bits set: 1110 0000
await operator.submitPeriodWithCas(0, p[0], consensusRoot, casBitmap, { from: alice }).should.be.fulfilled;
p[1] = await bridge.tipHash();
});

it('should allow to set slot but prevent submission with CAS bitmap of 2/4', async () => {
const data = await operator.contract.methods.setEpochLength(4).encodeABI();
it('should allow to set slot but prevent submission with CAS bitmap of 1/2', async () => {
let data = await operator.contract.methods.setEpochLength(4).encodeABI();
await proxy.applyProposal(data, {from: admin});
data = await operator.contract.methods.setSlot(2, bob, bob).encodeABI();
await proxy.applyProposal(data, {from: admin});
const consensusRoot = '0x02';
const casBitmap = '0xc0'; // two bits set: 1100 0000
const casBitmap = '0x80'; // first bit set: 1000 0000
await operator.submitPeriodWithCas(0, p[1], consensusRoot, casBitmap, { from: alice }).should.be.rejectedWith(EVMRevert);
});

it('should allow submission with CAS bitmap of 3/4', async () => {
it('should allow submission with CAS bitmap of 2/2', async () => {
const consensusRoot = '0x02';
const casBitmap = '0xe0'; // three bits set: 1110 0000
const casBitmap = '0xc0'; // first 2 bits set: 1100 0000
await operator.submitPeriodWithCas(0, p[1], consensusRoot, casBitmap, { from: alice }).should.be.fulfilled;
});

it('should prevent submission with CAS bitmap of 4/4', async () => {
it('should prevent submission with CAS bitmap of 3/2', async () => {
const consensusRoot = '0x03';
const casBitmap = '0xe0'; // 3 bits set: 1110 0000
await operator.submitPeriodWithCas(0, p[1], consensusRoot, casBitmap, { from: alice }).should.be.rejectedWith(EVMRevert);
});

it('should allow to set slot but prevent submission with CAS bitmap of 2/3', async () => {
const data = await operator.contract.methods.setSlot(3, admin, admin).encodeABI();
await proxy.applyProposal(data, {from: admin});
const consensusRoot = '0x03';
const casBitmap = '0xc0'; // first 2 bits set: 1100 0000
await operator.submitPeriodWithCas(0, p[1], consensusRoot, casBitmap, { from: alice }).should.be.rejectedWith(EVMRevert);
});

it('should allow submission with CAS bitmap of 3/3', async () => {
const consensusRoot = '0x03';
const casBitmap = '0xe0'; // 3 bits set: 1110 0000
await operator.submitPeriodWithCas(0, p[1], consensusRoot, casBitmap, { from: alice }).should.be.fulfilled;
});

it('should prevent submission with CAS bitmap of 4/3', async () => {
const consensusRoot = '0x02';
const casBitmap = '0xf0'; // all bits set: 1111 0000
await operator.submitPeriodWithCas(0, p[1], consensusRoot, casBitmap, { from: alice }).should.be.rejectedWith(EVMRevert);
Expand All @@ -128,8 +143,10 @@ contract('PoaOperator', (accounts) => {
it('should allow to set slot but prevent submission with CAS bitmap of 170/255', async () => {
const data = await operator.contract.methods.setEpochLength(256).encodeABI();
await proxy.applyProposal(data, {from: admin});
await operator.setActiveSlotsMap('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff');
const consensusRoot = '0x03';
const casBitmap = '0xffffffffffffffffffffffffffffffffffffffffffc000000000000000000000'; // 170 bits set
p[1] = await bridge.tipHash();
await operator.submitPeriodWithCas(0, p[1], consensusRoot, casBitmap, { from: alice }).should.be.rejectedWith(EVMRevert);
});

Expand All @@ -148,12 +165,13 @@ contract('PoaOperator', (accounts) => {
});

it('should prevent submission with CAS bitmap of 172/255', async () => {
const consensusRoot = '0x03';
const consensusRoot = '0x04';
const casBitmap = '0xfffffffffffffffffffffffffffffffffffffffffff000000000000000000000'; // 172 bits set
await operator.submitPeriodWithCas(0, p[2], consensusRoot, casBitmap, { from: alice }).should.be.rejectedWith(EVMRevert);
await operator.submitPeriodWithCas(0, p[3], consensusRoot, casBitmap, { from: alice }).should.be.rejectedWith(EVMRevert);
});

it('should allow to set slot and submit period without CAS', async () => {
await operator.setActiveSlotsMap('0x00');
let data = await operator.contract.methods.setEpochLength(3).encodeABI();
await proxy.applyProposal(data, {from: admin});
data = await operator.contract.methods.setSlot(0, alice, alice).encodeABI();
Expand All @@ -165,16 +183,15 @@ contract('PoaOperator', (accounts) => {
it('period proof should match contract', async () => {
const data = await operator.contract.methods.setSlot(1, bob, bob).encodeABI();
await proxy.applyProposal(data, {from: admin});

const block = new Block(33);
const depositTx = Tx.deposit(0, 1000, alice);
block.addTx(depositTx);
const prevPeriodRoot = await bridge.tipHash();
const period = new Period(prevPeriodRoot, [block]);
period.setValidatorData(1, bob, CAS);
const casBitmap = '0x80'; // first bit set: 1000 0000
period.setValidatorData(1, bob, casBitmap);
const proof = period.proof(depositTx);

await operator.submitPeriodWithCas(1, p[1], period.merkleRoot(), CAS, { from: bob }).should.be.fulfilled;
await operator.submitPeriodWithCas(1, p[1], period.merkleRoot(), casBitmap, { from: bob }).should.be.fulfilled;
p[2] = await bridge.tipHash();
assert.equal(p[2], proof[0]);
});
Expand All @@ -187,23 +204,23 @@ contract('PoaOperator', (accounts) => {
block.addTx(depositTx);
const prevPeriodRoot = await bridge.tipHash();
const period = new Period(prevPeriodRoot, [block]);
period.setValidatorData(1, bob, CAS);
const casBitmap = '0x80'; // first bit set: 1000 0000
period.setValidatorData(1, bob, casBitmap);
const proof = period.proof(depositTx);

await operator.submitPeriodWithCas(1, p[2], period.merkleRoot(), CAS, { from: bob }).should.be.fulfilled;
await operator.submitPeriodWithCas(1, p[2], period.merkleRoot(), casBitmap, { from: bob }).should.be.fulfilled;
p[3] = await bridge.tipHash();
assert.equal(p[3], proof[0]);

const validatorRoot = merkelize(`0x000000000000000000000001${bob.replace('0x', '')}`, ZERO);
const consensusRoot = merkelize(period.merkleRoot(), ZERO);

await operator.challengeCas(CAS, validatorRoot, consensusRoot, 1, {value: '100000000000000000'});
await operator.challengeCas(casBitmap, validatorRoot, consensusRoot, 1, {value: '100000000000000000'});

let challenge = await operator.getChallenge(p[3], 1);
assert.equal(challenge[0], accounts[0]);
assert.equal(challenge[2], bob);

const casRoot = merkelize(CAS, validatorRoot);
const casRoot = merkelize(casBitmap, validatorRoot);
const vote = Tx.periodVote(1, new Input(new Outpoint(consensusRoot, 0)));
vote.sign(['0x7bc8feb5e1ce2927480de19d8bc1dc6874678c016ae53a2eec6a6e9df717bfac']);
await operator.respondCas(consensusRoot, casRoot, 1, vote.inputs[0].v, vote.inputs[0].r, vote.inputs[0].s, accounts[0]);
Expand All @@ -217,23 +234,23 @@ contract('PoaOperator', (accounts) => {
block.addTx(depositTx);
const prevPeriodRoot = await bridge.tipHash();
const period = new Period(prevPeriodRoot, [block]);
period.setValidatorData(1, bob, CAS);
const casBitmap = '0x80'; // first bit set: 1000 0000
period.setValidatorData(1, bob, casBitmap);
const proof = period.proof(depositTx);

await operator.submitPeriodWithCas(1, p[3], period.merkleRoot(), CAS, { from: bob }).should.be.fulfilled;
await operator.submitPeriodWithCas(1, p[3], period.merkleRoot(), casBitmap, { from: bob }).should.be.fulfilled;
p[4] = await bridge.tipHash();
assert.equal(p[4], proof[0]);

const validatorRoot = merkelize(`0x000000000000000000000001${bob.replace('0x', '')}`, ZERO);
const consensusRoot = merkelize(period.merkleRoot(), ZERO);

await operator.challengeCas(CAS, validatorRoot, consensusRoot, 1, {value: '100000000000000000'});
await operator.challengeCas(casBitmap, validatorRoot, consensusRoot, 1, {value: '100000000000000000'});

let challenge = await operator.getChallenge(p[4], 1);
assert.equal(challenge[0], accounts[0]);
assert.equal(challenge[2], bob);

const casRoot = merkelize(CAS, validatorRoot);
const casRoot = merkelize(casBitmap, validatorRoot);
const periodRoot = merkelize(consensusRoot, casRoot);

await operator.timeoutCas(periodRoot, 1).should.be.rejectedWith(EVMRevert);
Expand Down
Loading