diff --git a/mysticeti/blocks/blck.nim b/mysticeti/blocks/blck.nim index 3d4b68e..0d5036f 100644 --- a/mysticeti/blocks/blck.nim +++ b/mysticeti/blocks/blck.nim @@ -10,14 +10,14 @@ type parents: seq[BlockId[Hashing]] transactions: seq[Transaction] -func new*( - _: type Block, +func new*[Hashing]( + _: type Block[Hashing]; author: CommitteeMember, round: uint64, - parents: seq[BlockId], + parents: seq[BlockId[Hashing]], transactions: seq[Transaction] ): auto = - Block[BlockId.Hashing]( + Block[Hashing]( author: author, round: round, parents: parents, diff --git a/mysticeti/validator/round.nim b/mysticeti/validator/round.nim index 8fd6377..9fa004b 100644 --- a/mysticeti/validator/round.nim +++ b/mysticeti/validator/round.nim @@ -9,6 +9,7 @@ type Round*[Hashing] = ref object slots: seq[ProposerSlot[Hashing]] func new*(T: type Round, number: uint64, slots: int): T = + assert slots > 0 type Slot = ProposerSlot[T.Hashing] let slots = newSeqWith(slots, Slot.new()) T(number: number, slots: slots) @@ -42,7 +43,7 @@ iterator slots*(round: Round): auto = yield round[member] iterator proposals*(round: Round): auto = - for slot in round.slots: + for slot in slots(round): for proposal in slot.proposals: yield proposal @@ -71,11 +72,15 @@ func find*(round: Round, blockId: BlockId): auto = func findAnchor*(round: Round): auto = var next = round.find(round.number + 3) while current =? next: - for slot in current.slots: + for slot in slots(current): if slot.status in [SlotStatus.undecided, SlotStatus.commit]: return some slot next = current.next +func addProposal*(round: Round, blck: Block): auto = + assert blck.round == round.number + round[blck.author].addProposal(blck) + func remove*(round: Round) = if previous =? round.previous: previous.next = round.next @@ -83,6 +88,3 @@ func remove*(round: Round) = next.previous = round.previous round.next = none Round round.previous = none Round - -func addProposal*(round: Round, blck: Block): auto = - round[blck.author].addProposal(blck) diff --git a/tests/mysticeti/examples.nim b/tests/mysticeti/examples.nim index 454f238..b2347f1 100644 --- a/tests/mysticeti/examples.nim +++ b/tests/mysticeti/examples.nim @@ -28,12 +28,14 @@ proc example*(T: type BlockId): T = let hash = Hash[T.Hashing].example BlockId.new(author, round, hash) -proc example*(T: type Block): T = - let author = CommitteeMember.example - let round = uint64.example +proc example*( + T: type Block, + author = CommitteeMember.example, + round = uint64.example +): T = let parents = seq[BlockId[T.Hashing]].example let transactions = seq[Transaction].example - Block.new(author, round, parents, transactions) + T.new(author, round, parents, transactions) proc example*[T](_: type seq[T], length=0..10): seq[T] = let size = rand(length) diff --git a/tests/mysticeti/validator/testRound.nim b/tests/mysticeti/validator/testRound.nim index 0bd8729..91a3217 100644 --- a/tests/mysticeti/validator/testRound.nim +++ b/tests/mysticeti/validator/testRound.nim @@ -1,10 +1,123 @@ import ../basics import mysticeti -import mysticeti/validator/rounds +import mysticeti/blocks +import mysticeti/validator/slots +import mysticeti/validator/round suite "Validator Round": - type Round = rounds.Round[MockHashing] + type Round = round.Round[MockHashing] + type Block = mysticeti.Block[MockHashing] + + test "rounds have a number": + check Round.new(0, 1).number == 0 + check Round.new(42, 1).number == 42 + check Round.new(1337, 1).number == 1337 + + test "round has a fixed number of slots": + check toSeq(Round.new(0, 1).slots).len == 1 + check toSeq(Round.new(0, 42).slots).len == 42 + check toSeq(Round.new(0, 1337).slots).len == 1337 + + test "round requires at least one slot": + expect Defect: + discard Round.new(0, 0) + + test "round has a slot for each committee member": + let round = Round.new(0, 4) + check not isNil round[CommitteeMember(0)] + check not isNil round[CommitteeMember(1)] + check not isNil round[CommitteeMember(2)] + check not isNil round[CommitteeMember(3)] + expect Defect: + discard round[CommitteeMember(4)] + + test "round stores proposed blocks in the corresponding slots": + let round = Round.new(0, 4) + let block1 = Block.example(author = CommitteeMember(1), round = 0) + let block2 = Block.example(author = CommitteeMember(2), round = 0) + let block3 = Block.example(author = CommitteeMember(2), round = 0) + round.addProposal(block1) + round.addProposal(block2) + round.addProposal(block3) + let slot1 = round[CommitteeMember(1)] + check slot1.proposals.len == 1 + check slot1.proposals[0].blck == block1 + let slot2 = round[CommitteeMember(2)] + check slot2.proposals.len == 2 + check slot2.proposals[0].blck == block2 + check slot2.proposals[1].blck == block3 + + test "round does not accept blocks meant for different rounds": + let blck = Block.example(author = CommitteeMember(0), round = 42) + let round42 = Round.new(42, 4) + let round43 = Round.new(43, 4) + round42.addProposal(blck) + expect Defect: + round43.addProposal(blck) + + test "round is part of a doubly linked list": + let first = Round.new(0, 4) + let second = Round.new(first) + let third = Round.new(second) + check first.previous == none Round + check first.next == some second + check second.previous == some first + check second.next == some third + check third.previous == some second + check third.next == none Round + + test "doubly linked list has increasing round numbers": + let first = Round.new(42, 4) + let second = Round.new(first) + let third = Round.new(second) + check first.number == 42 + check second.number == 43 + check third.number == 44 + + test "doubly linked list can be used to find a round by its number": + let first = Round.new(42, 4) + let second = Round.new(first) + let third = Round.new(second) + for round in [first, second, third]: + check round.find(41'u64) == none Round + check round.find(42'u64) == some first + check round.find(43'u64) == some second + check round.find(44'u64) == some third + check round.find(45'u64) == none Round + + test "doubly linked list can be used to find a block": + let first = Round.new(42, 4) + let second = Round.new(first) + let third = Round.new(second) + let block1 = Block.example(author = CommitteeMember(1), round = 42) + let block2 = Block.example(author = CommitteeMember(2), round = 43) + let block3 = Block.example(author = CommitteeMember(2), round = 43) + first.addProposal(block1) + second.addProposal(block2) + second.addProposal(block3) + for round in [first, second, third]: + check round.find(block1.id) == some block1 + check round.find(block2.id) == some block2 + check round.find(block3.id) == some block3 + check round.find(Block.example.id) == none Block + + test "round can be removed from a doubly linked list": + let first = Round.new(42, 4) + let second = Round.new(first) + let third = Round.new(second) + second.remove() + check first.previous == none Round + check first.next == some third + check second.previous == none Round + check second.next == none Round + check third.previous == some first + check third.next == none Round + third.remove() + check first.previous == none Round + check first.next == none Round + check third.previous == none Round + check third.next == none Round test "members are ordered round-robin for each round": var round: Round @@ -18,3 +131,46 @@ suite "Validator Round": check toSeq(round.members) == @[3, 0, 1, 2].mapIt(CommitteeMember(it)) round = Round.new(4, 4) check toSeq(round.members) == @[0, 1, 2, 3].mapIt(CommitteeMember(it)) + + test "slots are ordered round-robin too": + let round = Round.new(2, 4) + let slots = toSeq(round.slots) + check slots[0] == round[CommitteeMember(2)] + check slots[1] == round[CommitteeMember(3)] + check slots[2] == round[CommitteeMember(0)] + check slots[3] == round[CommitteeMember(1)] + + test "proposals are ordered round-robin as well": + var blocks: seq[Block] + blocks.add(Block.example(author = CommitteeMember(0), round = 2)) + blocks.add(Block.example(author = CommitteeMember(0), round = 2)) + blocks.add(Block.example(author = CommitteeMember(1), round = 2)) + blocks.add(Block.example(author = CommitteeMember(1), round = 2)) + blocks.add(Block.example(author = CommitteeMember(2), round = 2)) + blocks.add(Block.example(author = CommitteeMember(2), round = 2)) + blocks.add(Block.example(author = CommitteeMember(3), round = 2)) + blocks.add(Block.example(author = CommitteeMember(3), round = 2)) + let round = Round.new(2, 4) + for blck in blocks: + round.addProposal(blck) + let proposals = toSeq(round.proposals) + check proposals[0].blck == blocks[4] + check proposals[1].blck == blocks[5] + check proposals[2].blck == blocks[6] + check proposals[3].blck == blocks[7] + check proposals[4].blck == blocks[0] + check proposals[5].blck == blocks[1] + check proposals[6].blck == blocks[2] + check proposals[7].blck == blocks[3] + + test "doubly linked list can be used to find the anchor for a round": + let proposing = Round.new(2, 4) + let voting = Round.new(proposing) + let certifying = Round.new(voting) + let anchoring = Round.new(certifying) + let orderedSlots = toSeq(anchoring.slots) + check proposing.findAnchor() == some orderedSlots[0] + orderedSlots[0].skip() + check proposing.findAnchor() == some orderedSlots[1] + orderedSlots[1].skip() + check proposing.findAnchor() == some orderedSlots[2]