Skip to content

Commit

Permalink
court: improve and test heartbeat function
Browse files Browse the repository at this point in the history
  • Loading branch information
facuspagnuolo committed Aug 25, 2019
1 parent 6f4dce2 commit 6b36ce4
Show file tree
Hide file tree
Showing 8 changed files with 255 additions and 82 deletions.
93 changes: 56 additions & 37 deletions contracts/Court.sol
Original file line number Diff line number Diff line change
Expand Up @@ -124,19 +124,20 @@ contract Court is IJurorsRegistryOwner, ICRVotingOwner, ISubscriptionsOwner, Tim
// Court state
uint64 internal termId;
uint64 public configChangeTermId;
mapping (uint64 => Term) public terms;
mapping (uint64 => Term) internal terms;
Dispute[] public disputes;

string internal constant ERROR_INVALID_ADDR = "CTBAD_ADDR";
string internal constant ERROR_DEPOSIT_FAILED = "CTDEPOSIT_FAIL";
string internal constant ERROR_TOO_MANY_TRANSITIONS = "CTTOO_MANY_TRANSITIONS";
string internal constant ERROR_UNFINISHED_TERM = "CTUNFINISHED_TERM";
string internal constant ERROR_INVALID_TRANSITION_TERMS = "CT_INVALID_TRANSITION_TERMS";
string internal constant ERROR_PAST_TERM_FEE_CHANGE = "CTPAST_TERM_FEE_CHANGE";
string internal constant ERROR_OVERFLOW = "CTOVERFLOW";
string internal constant ERROR_ROUND_ALREADY_DRAFTED = "CTROUND_ALRDY_DRAFTED";
string internal constant ERROR_NOT_DRAFT_TERM = "CTNOT_DRAFT_TERM";
string internal constant ERROR_TERM_RANDOMNESS_NOT_YET = "CTRANDOM_NOT_YET";
string internal constant ERROR_WRONG_TERM = "CTBAD_TERM";
string internal constant ERROR_BAD_FIRST_TERM_START_TIME = "CT_BAD_FIRST_TERM_START_TIME";
string internal constant ERROR_TERM_RANDOMNESS_UNAVAIL = "CTRANDOM_UNAVAIL";
string internal constant ERROR_INVALID_DISPUTE_STATE = "CTBAD_DISPUTE_STATE";
string internal constant ERROR_INVALID_ADJUDICATION_ROUND = "CTBAD_ADJ_ROUND";
Expand Down Expand Up @@ -231,7 +232,9 @@ contract Court is IJurorsRegistryOwner, ICRVotingOwner, ISubscriptionsOwner, Tim
uint32 _maxRegularAppealRounds,
uint256[5] _subscriptionParams // _periodDuration, _feeAmount, _prePaymentPeriods, _latePaymentPenaltyPct, _governorSharePct
) public {
require(_firstTermStartTime >= _termDuration, ERROR_WRONG_TERM);
require(_firstTermStartTime >= _termDuration, ERROR_BAD_FIRST_TERM_START_TIME);
// TODO: we should add this validation, cannot enable it now due to how tests are mocking timestamps
// require(_firstTermStartTime - _termDuration <= getTimestamp64(), ERROR_BAD_FIRST_TERM_START_TIME);

termDuration = _termDuration;
jurorsRegistry = _jurorsRegistry;
Expand Down Expand Up @@ -611,6 +614,21 @@ contract Court is IJurorsRegistryOwner, ICRVotingOwner, ISubscriptionsOwner, Tim
return termId;
}

/**
* @dev Tell the information related to a term based on its ID. Note that if the term has not been reached, the
* information returned won't be computed yet.
* @param _termId ID of the term being queried
* @return Term start time
* @return Number of drafts depending on the requested term
* @return ID of the court configuration associated to the requested term
* @return Block number used for randomness in the requested term
* @return Randomness computed for the requested term
*/
function getTerm(uint64 _termId) external view returns (uint64, uint64, uint64, uint64, bytes32) {
Term storage term = terms[_termId];
return (term.startTime, term.dependingDrafts, term.courtConfigId, term.randomnessBN, term.randomness);
}

function getLastEnsuredTermId() external view returns (uint64) {
return termId;
}
Expand Down Expand Up @@ -652,38 +670,35 @@ contract Court is IJurorsRegistryOwner, ICRVotingOwner, ISubscriptionsOwner, Tim
/**
* @notice Send a heartbeat to the Court to transition up to `_termTransitions`
*/
function heartbeat(uint64 _termTransitions) public {
require(canTransitionTerm(), ERROR_UNFINISHED_TERM);

Term storage prevTerm = terms[termId];
termId += 1;
Term storage nextTerm = terms[termId];
address heartbeatSender = msg.sender;

// Set fee structure for term
if (nextTerm.courtConfigId == 0) {
nextTerm.courtConfigId = prevTerm.courtConfigId;
} else {
configChangeTermId = ZERO_TERM_ID; // fee structure changed in this term
}

// TODO: skip period if you can

// Set the start time of the term (ensures equally long terms, regardless of heartbeats)
nextTerm.startTime = prevTerm.startTime + termDuration;
nextTerm.randomnessBN = getBlockNumber64() + 1; // randomness source set to next block (content unknown when heartbeat happens)

CourtConfig storage courtConfig = courtConfigs[nextTerm.courtConfigId];
uint256 totalFee = nextTerm.dependingDrafts * courtConfig.heartbeatFee;

if (totalFee > 0) {
accounting.assign(courtConfig.feeToken, heartbeatSender, totalFee);
function heartbeat(uint64 _maxRequestedTransitions) public {
uint64 neededTransitions = neededTermTransitions();
uint256 transitions = uint256(_maxRequestedTransitions < neededTransitions ? _maxRequestedTransitions : neededTransitions);
require(transitions > uint256(0), ERROR_INVALID_TRANSITION_TERMS);

// Transition the minimum number of terms between the amount requested and the amount actually needed
uint256 totalFee;
for (uint256 transition = 1; transition <= transitions; transition++) {
Term storage previousTerm = terms[termId++];
Term storage nextTerm = terms[termId];

// TODO: allow config to be changed for a future term id
nextTerm.courtConfigId = previousTerm.courtConfigId;
// Set the start time of the new term. Note that we are using a constant term duration value to guarantee
// equally long terms, regardless of heartbeats.
nextTerm.startTime = previousTerm.startTime + termDuration;
// In order to draft a random number of jurors in a term, we use a randomness factor for each term based on a
// block number that is set once the term has started. Note that this information could not be known beforehand.
nextTerm.randomnessBN = getBlockNumber64() + 1;
emit NewTerm(termId, msg.sender);

// Add amount of fees to be paid for the transitioned term
CourtConfig storage config = courtConfigs[nextTerm.courtConfigId];
totalFee = totalFee.add(config.heartbeatFee.mul(uint256(nextTerm.dependingDrafts)));
}

emit NewTerm(termId, heartbeatSender);

if (_termTransitions > 1 && canTransitionTerm()) {
heartbeat(_termTransitions - 1);
// Pay heartbeat fees to the caller of this function
if (totalFee > uint256(0)) {
accounting.assign(config.feeToken, msg.sender, totalFee);
}
}

Expand All @@ -696,10 +711,6 @@ contract Court is IJurorsRegistryOwner, ICRVotingOwner, ISubscriptionsOwner, Tim
return round.filledSeats == round.jurorNumber;
}

function canTransitionTerm() public view returns (bool) {
return neededTermTransitions() >= 1;
}

function neededTermTransitions() public view returns (uint64) {
return (getTimestamp64() - terms[termId].startTime) / termDuration;
}
Expand Down Expand Up @@ -1171,6 +1182,14 @@ contract Court is IJurorsRegistryOwner, ICRVotingOwner, ISubscriptionsOwner, Tim
return courtConfigs[courtConfigId];
}

/**
* @dev Internal function to compute the randomness that will be used to draft jurors for the given term. This
* function assumes the given term exists. To determine the randomness factor for a term we use the hash of a
* block number that is set once the term has started to ensure it cannot be known beforehand. Note that the
* hash function being used only works for the 256 most recent block numbers.
* @param _term Term to compute the randomness of
* @return Randomness computed for the given term
*/
function _getTermRandomness(Term storage _term) internal view returns (bytes32) {
require(getBlockNumber64() > _term.randomnessBN, ERROR_TERM_RANDOMNESS_NOT_YET);
return blockhash(_term.randomnessBN);
Expand Down
1 change: 1 addition & 0 deletions contracts/test/lib/TimeHelpersMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ contract TimeHelpersMock is TimeHelpers {
using SafeMath for uint256;
using SafeMath64 for uint64;

// TODO: Current mocks need to start from timestamp 0 and blocknumber 1 due to how tests are built, fix tests to be able to start with current values
uint256 mockedTimestamp;
uint256 mockedBlockNumber;

Expand Down
2 changes: 1 addition & 1 deletion test/court-batches.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ contract('Court: Batches', ([ rich, arbitrable, juror1, juror2, juror3, juror4,
await this.courtHelper.increaseTime(terms * termDuration)
await this.court.heartbeat(terms)

assert.isFalse(await this.court.canTransitionTerm(), 'all terms transitioned')
assert.isTrue((await this.court.neededTermTransitions()).eq(0), 'all terms transitioned')
}

context('on multiple draft batches', () => {
Expand Down
2 changes: 1 addition & 1 deletion test/court-disputes.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ contract('Court: Disputes', ([ rich, juror1, juror2, juror3, other, appealMaker,
await this.courtHelper.increaseTime(terms * termDuration)
await this.court.heartbeat(terms)

assert.isFalse(await this.court.canTransitionTerm(), 'all terms transitioned')
assert.isTrue((await this.court.neededTermTransitions()).eq(0), 'all terms transitioned')
}

beforeEach(async () => {
Expand Down
2 changes: 1 addition & 1 deletion test/court-final-appeal-non-exact.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ contract('Court: final appeal (non-exact)', ([ poor, rich, juror1, juror2, juror
await this.courtHelper.increaseTime(terms * termDuration)
await this.court.heartbeat(terms)

assert.isFalse(await this.court.canTransitionTerm(), 'all terms transitioned')
assert.isTrue((await this.court.neededTermTransitions()).eq(0), 'all terms transitioned')
}

context('Final appeal, non-exact stakes', () => {
Expand Down
2 changes: 1 addition & 1 deletion test/court-final-appeal.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ contract('Court: final appeal', ([ poor, rich, juror1, juror2, juror3, juror4, j
await this.courtHelper.increaseTime(terms * termDuration)
await this.court.heartbeat(terms)

assert.isFalse(await this.court.canTransitionTerm(), 'all terms transitioned')
assert.isTrue((await this.court.neededTermTransitions()).eq(0), 'all terms transitioned')
}

// TODO: Fix when making the court settings configurable
Expand Down
42 changes: 1 addition & 41 deletions test/court-lifecycle.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,25 +121,6 @@ contract('Court: Lifecycle', ([ poor, rich, governor, juror1, juror2 ]) => {
await assertEqualBN(this.court.getLastEnsuredTermId(), 0, 'court term #0')
})

it('transitions to term #1 on heartbeat', async () => {
await this.jurorsRegistry.mockSetTimestamp(15)
await this.court.mockSetTimestamp(15)
await assertLogs(this.court.heartbeat(1), NEW_TERM_EVENT)

await assertEqualBN(this.court.getLastEnsuredTermId(), 1, 'court term #1')
const [
startTime,
dependingDraws,
courtConfigId,
randomnessBn
] = await this.court.terms(1)

await assertEqualBN(startTime, firstTermStart, 'first term start')
await assertEqualBN(dependingDraws, 0, 'depending draws')
await assertEqualBN(courtConfigId, 1, 'court config id')
await assertEqualBN(randomnessBn, (await this.court.getBlockNumberExt()), 'randomeness bn')
})

it('can activate during period before heartbeat', async () => {
await this.jurorsRegistry.mockSetTimestamp(firstTermStart - 1)
await this.court.mockSetTimestamp(firstTermStart - 1)
Expand Down Expand Up @@ -168,13 +149,6 @@ contract('Court: Lifecycle', ([ poor, rich, governor, juror1, juror2 ]) => {
await assertRevert(this.jurorsRegistry.activate(0, { from: poor }), 'JR_INVALID_ZERO_AMOUNT')
await assertRevert(this.jurorsRegistry.activate(10, { from: poor }), 'JR_INVALID_ACTIVATION_AMOUNT')
})

it("doesn't perform more transitions than requested", async () => {
await this.jurorsRegistry.mockSetTimestamp(firstTermStart + termDuration * 100)
await this.court.mockSetTimestamp(firstTermStart + termDuration * 100)
await this.court.heartbeat(3)
await assertEqualBN(this.court.getLastEnsuredTermId(), 3, 'current term')
})
})

context('on regular court terms', () => {
Expand All @@ -184,7 +158,7 @@ contract('Court: Lifecycle', ([ poor, rich, governor, juror1, juror2 ]) => {
await this.court.mockIncreaseTime(terms * termDuration)
await this.court.heartbeat(terms)

assert.isFalse(await this.court.canTransitionTerm(), 'all terms transitioned')
assert.isTrue((await this.court.neededTermTransitions()).eq(0), 'all terms transitioned')
}

beforeEach(async () => {
Expand All @@ -197,20 +171,6 @@ contract('Court: Lifecycle', ([ poor, rich, governor, juror1, juror2 ]) => {
await assertEqualBN(this.court.getLastEnsuredTermId(), term, 'term #3')
})

it('has correct term state', async () => {
const [
startTime,
dependingDraws,
courtConfigId,
randomnessBn
] = await this.court.terms(term)

await assertEqualBN(startTime, firstTermStart + (term - 1) * termDuration, 'term start')
await assertEqualBN(dependingDraws, 0, 'depending draws')
await assertEqualBN(courtConfigId, 1, 'court config id')
await assertEqualBN(randomnessBn, (await this.court.getBlockNumberExt()), 'randomeness bn')
})

it('jurors can activate', async () => {
await this.jurorsRegistry.activate(0, { from: juror1 })
await this.jurorsRegistry.activate(0, { from: juror2 })
Expand Down
Loading

0 comments on commit 6b36ce4

Please sign in to comment.