Skip to content

Commit

Permalink
optimize ballot eligibility validation (#4923)
Browse files Browse the repository at this point in the history
closes: #4883
related: #4841

active set is validated only for reference ballot in current epoch, for all other ballots eligibilities validated against declared slots in the reference ballot. beside making validation more efficient, it will allow to prune active set data in future changes. reference ballots is still loading data from cached datastore, i plan to remove eventually but not in this change.

additionally i rewrote all tests for eligibility validator to get to 100% coverage and make them less dependent on the implementation.
  • Loading branch information
dshulyak committed Aug 31, 2023
1 parent 115afb8 commit 2c917b9
Show file tree
Hide file tree
Showing 8 changed files with 687 additions and 607 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ Otherwise protocol uses significantly less traffic (atlest x20), and will allow
to set lower expected latency in the network, eventually reducing layer time.

### Improvements

* [#4879](https://github.com/spacemeshos/go-spacemesh/pull/4795) Makes majority calculation weighted for optimistic filtering.
The network will start using the new algorithm at layer 18_000 (2023-09-14 20:00:00 +0000 UTC)
* [#4923](https://github.com/spacemeshos/go-spacemesh/pull/4923) Faster ballot eligibility validation. Improves sync speed.

## v1.1.2

Expand Down
3 changes: 2 additions & 1 deletion miner/oracle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ func testMinerOracleAndProposalValidator(t *testing.T, layerSize, layersPerEpoch
nonceFetcher := proposals.NewMocknonceFetcher(ctrl)
nonce := types.VRFPostIndex(rand.Uint64())

validator := proposals.NewEligibilityValidator(layerSize, layersPerEpoch, 0, o.cdb, mbc, nil, o.log.WithName("blkElgValidator"), vrfVerifier,
validator := proposals.NewEligibilityValidator(layerSize, layersPerEpoch, 0, o.mClock, o.cdb, mbc, o.log.WithName("blkElgValidator"), vrfVerifier,
proposals.WithNonceFetcher(nonceFetcher),
)

Expand All @@ -206,6 +206,7 @@ func testMinerOracleAndProposalValidator(t *testing.T, layerSize, layersPerEpoch
for _, proof := range ee.Proofs[layer] {
b := genBallotWithEligibility(t, o.edSigner, info.beacon, layer, ee)
b.SmesherID = o.edSigner.NodeID()
o.mClock.EXPECT().CurrentLayer().Return(layer)
mbc.EXPECT().ReportBeaconFromBallot(layer.GetEpoch(), b, info.beacon, gomock.Any()).Times(1)
nonceFetcher.EXPECT().VRFNonce(b.SmesherID, layer.GetEpoch()).Return(nonce, nil).Times(1)
eligible, err := validator.CheckEligibility(context.Background(), b)
Expand Down
200 changes: 91 additions & 109 deletions proposals/eligibility_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,25 +31,13 @@ type Validator struct {
avgLayerSize uint32
layersPerEpoch uint32
cdb *datastore.CachedDB
mesh meshProvider
clock layerClock
beacons system.BeaconCollector
logger log.Log
vrfVerifier vrfVerifier
nonceFetcher nonceFetcher
}

type defaultFetcher struct {
cdb *datastore.CachedDB
}

func (f defaultFetcher) VRFNonce(nodeID types.NodeID, epoch types.EpochID) (types.VRFPostIndex, error) {
nonce, err := f.cdb.VRFNonce(nodeID, epoch)
if err != nil {
return types.VRFPostIndex(0), fmt.Errorf("get vrf nonce: %w", err)
}
return nonce, nil
}

// ValidatorOpt for configuring Validator.
type ValidatorOpt func(h *Validator)

Expand All @@ -61,146 +49,140 @@ func WithNonceFetcher(nf nonceFetcher) ValidatorOpt {

// NewEligibilityValidator returns a new EligibilityValidator.
func NewEligibilityValidator(
avgLayerSize, layersPerEpoch uint32, minActiveSetWeight uint64, cdb *datastore.CachedDB, bc system.BeaconCollector, m meshProvider, lg log.Log, vrfVerifier vrfVerifier, opts ...ValidatorOpt,
avgLayerSize, layersPerEpoch uint32, minActiveSetWeight uint64, clock layerClock, cdb *datastore.CachedDB, bc system.BeaconCollector, lg log.Log, vrfVerifier vrfVerifier, opts ...ValidatorOpt,
) *Validator {
v := &Validator{
minActiveSetWeight: minActiveSetWeight,
avgLayerSize: avgLayerSize,
layersPerEpoch: layersPerEpoch,
cdb: cdb,
mesh: m,
nonceFetcher: cdb,
clock: clock,
beacons: bc,
logger: lg,
vrfVerifier: vrfVerifier,
}
for _, opt := range opts {
opt(v)
}

if v.nonceFetcher == nil {
v.nonceFetcher = defaultFetcher{cdb: cdb}
}

return v
}

// CheckEligibility checks that a ballot is eligible in the layer that it specifies.
func (v *Validator) CheckEligibility(ctx context.Context, ballot *types.Ballot) (bool, error) {
var (
atxWeight, totalWeight uint64
err error
refBallot = ballot
epoch = ballot.Layer.GetEpoch()
)
if len(ballot.EligibilityProofs) == 0 {
return false, fmt.Errorf("empty eligibility list is invalid (ballot %s)", ballot.ID())
}

if ballot.RefBallot != types.EmptyBallotID {
if refBallot, err = ballots.Get(v.cdb, ballot.RefBallot); err != nil {
return false, fmt.Errorf("get ref ballot %v: %w", ballot.RefBallot, err)
}
}
if refBallot.EpochData == nil {
return false, fmt.Errorf("%w: ref ballot %v", errMissingEpochData, refBallot.ID())
}
if refBallot.AtxID != ballot.AtxID {
return false, fmt.Errorf("ballot (%v/%v) should be sharing atx with a reference ballot (%v/%v)", ballot.ID(), ballot.AtxID, refBallot.ID(), refBallot.AtxID)
}

beacon := refBallot.EpochData.Beacon
if beacon == types.EmptyBeacon {
return false, fmt.Errorf("%w: ref ballot %v", errMissingBeacon, refBallot.ID())
}

activeSets := refBallot.ActiveSet
if len(activeSets) == 0 {
return false, fmt.Errorf("%w: ref ballot %v", errEmptyActiveSet, refBallot.ID())
}

// todo: optimize by using reference to active set size and cache active set size to not load all atxsIDs from db
var owned *types.ActivationTxHeader
for _, atxID := range activeSets {
atx, err := v.cdb.GetAtxHeader(atxID)
if err != nil {
return false, fmt.Errorf("get ATX header %v: %w", atxID, err)
}
totalWeight += atx.GetWeight()
if atxID == ballot.AtxID {
owned = atx
}
}
if owned == nil {
return false, fmt.Errorf("atx %v from ballot %v (refballot %v) is not included into the active set", ballot.AtxID, ballot.ID(), refBallot.ID())
owned, err := v.cdb.GetAtxHeader(ballot.AtxID)
if err != nil {
return false, fmt.Errorf("failed to load atx %v: %w", ballot.AtxID, err)
}
if targetEpoch := owned.TargetEpoch(); targetEpoch != epoch {
if owned.TargetEpoch() != ballot.Layer.GetEpoch() {
return false, fmt.Errorf("%w: ATX target epoch (%v), ballot publication epoch (%v)",
errTargetEpochMismatch, targetEpoch, epoch)
errTargetEpochMismatch, owned.TargetEpoch(), ballot.Layer.GetEpoch())
}
if ballot.SmesherID != owned.NodeID {
return false, fmt.Errorf("%w: public key (%v), ATX node key (%v)", errPublicKeyMismatch, ballot.SmesherID.String(), owned.NodeID)
}

atxWeight = owned.GetWeight()

numEligibleSlots, err := GetLegacyNumEligible(ballot.Layer, atxWeight, v.minActiveSetWeight, totalWeight, v.avgLayerSize, v.layersPerEpoch)
nonce, err := v.nonceFetcher.VRFNonce(ballot.SmesherID, ballot.Layer.GetEpoch())
if err != nil {
return false, err
return false, fmt.Errorf("no vrf nonce for %v in epoch %v: %w", ballot.SmesherID, ballot.Layer.GetEpoch(), err)
}
if ballot.EpochData != nil && ballot.EpochData.EligibilityCount != numEligibleSlots {
return false, fmt.Errorf("%w: expected %v, got: %v", errIncorrectEligCount, numEligibleSlots, ballot.EpochData.EligibilityCount)
}

var (
last uint32
isFirst = true
)

nonce, err := v.nonceFetcher.VRFNonce(ballot.SmesherID, epoch)
if err != nil {
return false, err
}
for _, proof := range ballot.EligibilityProofs {
counter := proof.J
if counter >= numEligibleSlots {
return false, fmt.Errorf("%w: proof counter (%d) numEligibleBallots (%d), totalWeight (%v)",
errIncorrectCounter, counter, numEligibleSlots, totalWeight)
}
if isFirst {
isFirst = false
} else if counter <= last {
return false, fmt.Errorf("%w: %d <= %d", errInvalidProofsOrder, counter, last)
var data *types.EpochData
if ballot.EpochData != nil && ballot.Layer.GetEpoch() == v.clock.CurrentLayer().GetEpoch() {
var err error
data, err = v.validateReference(ballot, owned)
if err != nil {
return false, err
}
last = counter

message, err := SerializeVRFMessage(beacon, epoch, nonce, counter)
} else {
var err error
data, err = v.validateSecondary(ballot, owned)
if err != nil {
return false, err
}
vrfSig := proof.Sig

beaconStr := beacon.ShortString()
if !v.vrfVerifier.Verify(owned.NodeID, message, vrfSig) {
}
for i, proof := range ballot.EligibilityProofs {
if proof.J >= data.EligibilityCount {
return false, fmt.Errorf("%w: proof counter (%d) numEligibleBallots (%d)",
errIncorrectCounter, proof.J, data.EligibilityCount)
}
if i != 0 && proof.J <= ballot.EligibilityProofs[i-1].J {
return false, fmt.Errorf("%w: %d <= %d", errInvalidProofsOrder, proof.J, ballot.EligibilityProofs[i-1].J)
}
if !v.vrfVerifier.Verify(ballot.SmesherID,
MustSerializeVRFMessage(data.Beacon, ballot.Layer.GetEpoch(), nonce, proof.J), proof.Sig) {
return false, fmt.Errorf("%w: beacon: %v, epoch: %v, counter: %v, vrfSig: %s",
errIncorrectVRFSig, beaconStr, epoch, counter, vrfSig,
errIncorrectVRFSig, data.Beacon.ShortString(), ballot.Layer.GetEpoch(), proof.J, proof.Sig,
)
}

eligibleLayer := CalcEligibleLayer(epoch, v.layersPerEpoch, vrfSig)
if ballot.Layer != eligibleLayer {
if eligible := CalcEligibleLayer(ballot.Layer.GetEpoch(), v.layersPerEpoch, proof.Sig); ballot.Layer != eligible {
return false, fmt.Errorf("%w: ballot layer (%v), eligible layer (%v)",
errIncorrectLayerIndex, ballot.Layer, eligibleLayer)
errIncorrectLayerIndex, ballot.Layer, eligible)
}
}

v.logger.WithContext(ctx).With().Debug("ballot eligibility verified",
ballot.ID(),
ballot.Layer,
epoch,
beacon,
ballot.Layer.GetEpoch(),
data.Beacon,
)

weightPer := fixed.DivUint64(atxWeight, uint64(numEligibleSlots))
v.beacons.ReportBeaconFromBallot(epoch, ballot, beacon, weightPer)
v.beacons.ReportBeaconFromBallot(ballot.Layer.GetEpoch(), ballot, data.Beacon,
fixed.DivUint64(owned.GetWeight(), uint64(data.EligibilityCount)))
return true, nil
}

// validateReference executed for reference ballots in latest epoch.
func (v *Validator) validateReference(ballot *types.Ballot, owned *types.ActivationTxHeader) (*types.EpochData, error) {
if ballot.EpochData.Beacon == types.EmptyBeacon {
return nil, fmt.Errorf("%w: ref ballot %v", errMissingBeacon, ballot.ID())
}
if len(ballot.ActiveSet) == 0 {
return nil, fmt.Errorf("%w: ref ballot %v", errEmptyActiveSet, ballot.ID())
}
var totalWeight uint64
for _, atxID := range ballot.ActiveSet {
atx, err := v.cdb.GetAtxHeader(atxID)
if err != nil {
return nil, fmt.Errorf("atx in active set is missing %v: %w", atxID, err)
}
totalWeight += atx.GetWeight()
}
numEligibleSlots, err := GetLegacyNumEligible(ballot.Layer, owned.GetWeight(), v.minActiveSetWeight, totalWeight, v.avgLayerSize, v.layersPerEpoch)
if err != nil {
return nil, err
}
if ballot.EpochData.EligibilityCount != numEligibleSlots {
return nil, fmt.Errorf("%w: expected %v, got: %v", errIncorrectEligCount, numEligibleSlots, ballot.EpochData.EligibilityCount)
}
return ballot.EpochData, nil
}

// validateSecondary executed for non-reference ballots in latest epoch and all ballots in past epochs.
func (v *Validator) validateSecondary(ballot *types.Ballot, owned *types.ActivationTxHeader) (*types.EpochData, error) {
var refballot *types.Ballot
if ballot.RefBallot == types.EmptyBallotID {
refballot = ballot
} else {
var err error
refballot, err = ballots.Get(v.cdb, ballot.RefBallot)
if err != nil {
return nil, fmt.Errorf("ref ballot is missing %v: %w", ballot.RefBallot, err)
}
}
if refballot.EpochData == nil {
return nil, fmt.Errorf("%w: ref ballot %v", errMissingEpochData, refballot.ID())
}
if refballot.AtxID != ballot.AtxID {
return nil, fmt.Errorf("ballot (%v/%v) should be sharing atx with a reference ballot (%v/%v)", ballot.ID(), ballot.AtxID, refballot.ID(), refballot.AtxID)
}
if refballot.SmesherID != ballot.SmesherID {
return nil, fmt.Errorf("mismatched smesher id with refballot in ballot %v", ballot.ID())
}
if refballot.Layer.GetEpoch() != ballot.Layer.GetEpoch() {
return nil, fmt.Errorf("ballot %v targets mismatched epoch %d", ballot.ID(), ballot.Layer.GetEpoch())
}
return refballot.EpochData, nil
}

0 comments on commit 2c917b9

Please sign in to comment.