Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
38 changes: 33 additions & 5 deletions kaiax/vrank/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,27 +70,46 @@ func NewCollector() *Collector {
}
}

// RemoveOldViews deletes views that are strictly behind threshold.
func (c *Collector) RemoveOldViews(threshold ViewKey) {
// PruneReported deletes views for sequences strictly below upto, so every round of the block just
// reported survives until a later proposal supersedes it.
func (c *Collector) PruneReported(upto uint64) {
c.mu.Lock()
defer c.mu.Unlock()
for vk := range c.prepreparedMap {
if vk.Cmp(threshold) < 0 {
if vk.N < upto {
delete(c.prepreparedMap, vk)
}
}
for vk := range c.blockHashMap {
if vk.Cmp(threshold) < 0 {
if vk.N < upto {
delete(c.blockHashMap, vk)
}
}
for vk := range c.viewMap {
if vk.Cmp(threshold) < 0 {
if vk.N < upto {
delete(c.viewMap, vk)
}
}
}

// PendingEvaluations returns the deduplicated, unsorted sequences preprepared at or after epochStart.
func (c *Collector) PendingEvaluations(epochStart uint64) []uint64 {
c.mu.RLock()
defer c.mu.RUnlock()
seen := make(map[uint64]struct{}, len(c.prepreparedMap))
nums := make([]uint64, 0, len(c.prepreparedMap))
for vk := range c.prepreparedMap {
if vk.N < epochStart {
continue
}
if _, dup := seen[vk.N]; !dup {
seen[vk.N] = struct{}{}
nums = append(nums, vk.N)
}
}
return nums
}

// AddPrepreparedTime records the start time and expected block hash for the view.
// expectedBlockHash is used at GetViewData/report time to validate VRankCandidate.BlockHash (reject liars).
func (c *Collector) AddPrepreparedTime(vk ViewKey, prepreparedAt time.Time, expectedBlockHash common.Hash) {
Expand All @@ -100,6 +119,15 @@ func (c *Collector) AddPrepreparedTime(vk ViewKey, prepreparedAt time.Time, expe
c.blockHashMap[vk] = expectedBlockHash
}

// HasPreprepared reports whether a preprepared time has been recorded for the view, i.e. this node
// proposed it and is expected to collect its VRankCandidate replies.
func (c *Collector) HasPreprepared(vk ViewKey) bool {
c.mu.RLock()
defer c.mu.RUnlock()
_, ok := c.prepreparedMap[vk]
return ok
}

// AddCandMsg stores a VRankCandidate message for the given view. No verification is done here.
// Returns false if the sender already has a message stored for this view (duplicate).
func (c *Collector) AddCandMsg(vk ViewKey, sender common.Address, receivedAt time.Time, msg *VRankCandidate) bool {
Expand Down
22 changes: 16 additions & 6 deletions kaiax/vrank/collector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,20 +90,20 @@ func TestCollector_GetViewData_ReturnsSnapshotCopy(t *testing.T) {
assert.NotContains(t, m2, addr2)
}

func TestCollector_RemoveOldViews(t *testing.T) {
func TestCollector_PruneReported(t *testing.T) {
var (
c = NewCollector()
views = []ViewKey{{N: 1, R: 0}, {N: 1, R: 8}, {N: 2, R: 0}, {N: 2, R: 1}, {N: 3, R: 0}}
threshold = ViewKey{N: 2, R: 1}
wantGone = []bool{true, true, true, false, false}
c = NewCollector()
// Pruning is per sequence: every round of block 1 goes, every round of block 2 stays.
views = []ViewKey{{N: 1, R: 0}, {N: 1, R: 8}, {N: 2, R: 0}, {N: 2, R: 1}, {N: 3, R: 0}}
wantGone = []bool{true, true, false, false, false}
)

for _, v := range views {
c.AddPrepreparedTime(v, time.Now(), common.Hash{})
c.AddCandMsg(v, common.HexToAddress("0x01"), time.Now(), &VRankCandidate{BlockNumber: v.N, Round: v.R})
}

c.RemoveOldViews(threshold)
c.PruneReported(2)
for i, v := range views {
at, _, m := c.GetViewData(v)
if wantGone[i] {
Expand All @@ -116,6 +116,16 @@ func TestCollector_RemoveOldViews(t *testing.T) {
}
}

func TestCollector_PendingEvaluations(t *testing.T) {
c := NewCollector()
for _, v := range []ViewKey{{N: 1, R: 0}, {N: 5, R: 0}, {N: 5, R: 2}} {
c.AddPrepreparedTime(v, time.Now(), common.Hash{})
}

assert.ElementsMatch(t, []uint64{1, 5}, c.PendingEvaluations(0)) // both rounds of 5 collapse
assert.Equal(t, []uint64{5}, c.PendingEvaluations(2))
}

func TestViewKey_Cmp(t *testing.T) {
a, b, c := ViewKey{N: 1, R: 0}, ViewKey{N: 2, R: 0}, ViewKey{N: 1, R: 1}
assert.Less(t, a.Cmp(b), 0)
Expand Down
2 changes: 0 additions & 2 deletions kaiax/vrank/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,12 @@ var (
ErrVRankPreprepareNil = errors.New("VRankPreprepare is nil")
ErrVRankCandidateNil = errors.New("VRankCandidate is nil")
ErrGetCandidateFailed = errors.New("valset.GetCandTesting failed")
ErrPrepreparedViewNotSet = errors.New("preprepared view is not set")
ErrViewMismatch = errors.New("view mismatch")
ErrBlockHashMismatch = errors.New("block hash mismatch")
ErrMsgFromNonProposer = errors.New("message from non-proposer")
ErrInvalidCandidateSig = errors.New("invalid candidate ECDSA signature")
ErrInvalidCandidateBlsSig = errors.New("invalid candidate BLS signature")
ErrInvalidProposerSig = errors.New("invalid proposer signature")
ErrTooFar = errors.New("too far in the future")
ErrRoundOutOfRange = errors.New("round out of range")
ErrHeaderNotFound = errors.New("header not found")
ErrFutureBlock = errors.New("block number is beyond the current chain head")
Expand Down
29 changes: 11 additions & 18 deletions kaiax/vrank/impl/consensus.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,7 @@ import (
// VerifyHeader checks the VRank field in the header:
// - Before the permissionless fork: VRank must be absent.
// - At epoch-start blocks: VRank must be RLPEncode(CandTesting(N)) or RLPEncode([]).
// - Otherwise: VRank must be a valid encoded report whose addresses are sorted,
// deduplicated, and all present in GetCandTesting(N-1), because header(N).VRank
// reports candidate failures observed while building block N-1.
// - Otherwise: a cfReport whose failed addresses are sorted, deduped, and ⊆ CandTesting.
func (v *VRankModule) VerifyHeader(header *types.Header, _ *types.Header) error {
number := header.Number.Uint64()
permissionless := v.ChainConfig.IsPermissionlessForkEnabled(new(big.Int).SetUint64(number))
Expand Down Expand Up @@ -65,6 +63,8 @@ func (v *VRankModule) VerifyHeader(header *types.Header, _ *types.Header) error
if err != nil {
return vrank.ErrInvalidVRankFormat
}
// Failures score against the reporter's own byzantine-filterable column regardless of content,
// so only the failed list is checked (CandTesting is epoch-stable within the epoch).
candidates, err := v.Valset.GetCandTesting(number - 1)
if err != nil {
return err
Expand All @@ -75,7 +75,8 @@ func (v *VRankModule) VerifyHeader(header *types.Header, _ *types.Header) error
// PrepareHeader fills header.VRank per KIP-227.
//
// - At epoch-start blocks: VRank is CandTesting(N), encoded even when empty.
// - Otherwise: VRank is EvaluateCandidates(N-1, parentRound), or nil when the report is empty.
// - Otherwise: VRank is a cfReport about this proposer's own most recent prior proposal in
// the current epoch, or nil when there is no such block or no failures.
func (v *VRankModule) PrepareHeader(header *types.Header) error {
number := header.Number.Uint64()
if !v.ChainConfig.IsPermissionlessForkEnabled(header.Number) {
Expand Down Expand Up @@ -116,25 +117,17 @@ func (v *VRankModule) encodeEpochStartVRank(number uint64) ([]byte, error) {
}

func (v *VRankModule) encodeCandidateFailureVRank(number uint64) ([]byte, error) {
if number == 0 {
targetNum, round, ok := v.selectReportTarget(number)
if !ok {
// No own prior proposal this epoch (first proposal, or restart). Empty report — fail-safe.
return nil, nil
}
parentNum := number - 1
parent := v.Chain.GetHeaderByNumber(parentNum)
if parent == nil {
logger.Error("Failed to read parent header for VRank", "num", number, "parentNum", parentNum)
return nil, vrank.ErrHeaderNotFound
}
parentRound, err := v.RoundReader.Round(parent)
if err != nil {
logger.Error("Failed to read parent round for VRank", "err", err, "parentNum", parentNum)
return nil, err
}
report, err := v.EvaluateCandidates(parentNum, uint64(parentRound))
report, err := v.EvaluateCandidates(targetNum, round)
if err != nil {
logger.Error("Failed to evaluate VRank candidates", "err", err, "prevBlockNum", parentNum, "prevRound", parentRound)
logger.Error("Failed to evaluate VRank candidates", "err", err, "targetNum", targetNum, "round", round)
return nil, err
}
v.collector.PruneReported(targetNum) // drop views older than targetNum; targetNum stays for re-report
if len(report) == 0 {
return nil, nil
}
Expand Down
Loading
Loading