Skip to content

Commit

Permalink
tortoise, rewards: enforce minimal active set weight (#4516)
Browse files Browse the repository at this point in the history
closes: #4466
depends on: #4438

without minimal active set weight any smesher can be eligible to produce 1 ballot per layer.
this will result in spam and non-optimal resources consumption.

this change introduce minimal active set weight for eligible number of slots computation. if weight of the
atx specified in the first ballot active set is lower than minimal active set weight then protocol will use minimal.
this value must be the same across whole network
  • Loading branch information
dshulyak committed Jun 19, 2023
1 parent 5a38fdc commit 72076af
Show file tree
Hide file tree
Showing 13 changed files with 193 additions and 77 deletions.
16 changes: 11 additions & 5 deletions blocks/generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,15 @@ const (
numUnit = 12
defaultGas = 100
baseTickHeight = 3

layerSize = 10
epochSize = 3
)

func testConfig() Config {
return Config{
LayerSize: 10,
LayersPerEpoch: 3,
LayerSize: layerSize,
LayersPerEpoch: epochSize,
GenBlockInterval: 10 * time.Millisecond,
BlockGasLimit: math.MaxUint64,
OptFilterThreshold: 90,
Expand Down Expand Up @@ -190,9 +193,12 @@ func createProposal(
InnerProposal: types.InnerProposal{
Ballot: types.Ballot{
InnerBallot: types.InnerBallot{
Layer: lid,
AtxID: atxID,
EpochData: &types.EpochData{Beacon: types.RandomBeacon()},
Layer: lid,
AtxID: atxID,
EpochData: &types.EpochData{
Beacon: types.RandomBeacon(),
EligibilityCount: uint32(layerSize * epochSize / len(activeSet)),
},
},
EligibilityProofs: make([]types.VotingEligibility, numEligibility),
ActiveSet: activeSet,
Expand Down
27 changes: 15 additions & 12 deletions miner/oracle.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@ type EpochEligibility struct {

// Oracle provides proposal eligibility proofs for the miner.
type Oracle struct {
avgLayerSize uint32
layersPerEpoch uint32
cdb *datastore.CachedDB
avgLayerSize uint32
layersPerEpoch uint32
minActiveSetWeight uint64
cdb *datastore.CachedDB

vrfSigner *signing.VRFSigner
nodeID types.NodeID
Expand All @@ -44,15 +45,16 @@ type Oracle struct {
cache *EpochEligibility
}

func newMinerOracle(layerSize, layersPerEpoch uint32, cdb *datastore.CachedDB, vrfSigner *signing.VRFSigner, nodeID types.NodeID, log log.Log) *Oracle {
func newMinerOracle(layerSize, layersPerEpoch uint32, minActiveSetWeight uint64, cdb *datastore.CachedDB, vrfSigner *signing.VRFSigner, nodeID types.NodeID, log log.Log) *Oracle {
return &Oracle{
avgLayerSize: layerSize,
layersPerEpoch: layersPerEpoch,
cdb: cdb,
vrfSigner: vrfSigner,
nodeID: nodeID,
log: log,
cache: &EpochEligibility{},
avgLayerSize: layerSize,
layersPerEpoch: layersPerEpoch,
minActiveSetWeight: minActiveSetWeight,
cdb: cdb,
vrfSigner: vrfSigner,
nodeID: nodeID,
log: log,
cache: &EpochEligibility{},
}
}

Expand Down Expand Up @@ -138,7 +140,7 @@ func (o *Oracle) calcEligibilityProofs(atx *types.ActivationTxHeader, epoch type
log.Uint64("total weight", totalWeight),
)

numEligibleSlots, err := proposals.GetNumEligibleSlots(weight, totalWeight, o.avgLayerSize, o.layersPerEpoch)
numEligibleSlots, err := proposals.GetNumEligibleSlots(weight, o.minActiveSetWeight, totalWeight, o.avgLayerSize, o.layersPerEpoch)
if err != nil {
return nil, fmt.Errorf("oracle get num slots: %w", err)
}
Expand All @@ -162,6 +164,7 @@ func (o *Oracle) calcEligibilityProofs(atx *types.ActivationTxHeader, epoch type
epoch,
beacon,
log.Uint64("weight", weight),
log.Uint64("min activeset weight", o.minActiveSetWeight),
log.Uint64("total weight", totalWeight),
log.Uint32("total num slots", numEligibleSlots),
log.Int("num layers eligible", len(eligibilityProofs)),
Expand Down
36 changes: 30 additions & 6 deletions miner/oracle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,15 @@ func genBallotWithEligibility(
return ballot
}

func createTestOracle(tb testing.TB, layerSize, layersPerEpoch uint32) *testOracle {
func createTestOracle(tb testing.TB, layerSize, layersPerEpoch uint32, minActiveSetWeight uint64) *testOracle {
types.SetLayersPerEpoch(layersPerEpoch)

lg := logtest.New(tb)
cdb := datastore.NewCachedDB(sql.InMemory(), lg)
nodeID, edSigner, vrfSigner := generateNodeIDAndSigner(tb)

return &testOracle{
Oracle: newMinerOracle(layerSize, layersPerEpoch, cdb, vrfSigner, nodeID, lg),
Oracle: newMinerOracle(layerSize, layersPerEpoch, minActiveSetWeight, cdb, vrfSigner, nodeID, lg),
nodeID: nodeID,
edSigner: edSigner,
vrfSigner: vrfSigner,
Expand Down Expand Up @@ -143,7 +143,7 @@ func TestMinerOracle(t *testing.T) {
}

func testMinerOracleAndProposalValidator(t *testing.T, layerSize uint32, layersPerEpoch uint32) {
o := createTestOracle(t, layerSize, layersPerEpoch)
o := createTestOracle(t, layerSize, layersPerEpoch, 0)

ctrl := gomock.NewController(t)
mbc := mocks.NewMockBeaconCollector(ctrl)
Expand All @@ -153,7 +153,7 @@ func testMinerOracleAndProposalValidator(t *testing.T, layerSize uint32, layersP
nonceFetcher := proposals.NewMocknonceFetcher(ctrl)
nonce := types.VRFPostIndex(rand.Uint64())

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

Expand Down Expand Up @@ -195,7 +195,7 @@ func testMinerOracleAndProposalValidator(t *testing.T, layerSize uint32, layersP
func TestOracle_OwnATXNotFound(t *testing.T) {
avgLayerSize := uint32(10)
layersPerEpoch := uint32(20)
o := createTestOracle(t, avgLayerSize, layersPerEpoch)
o := createTestOracle(t, avgLayerSize, layersPerEpoch, 0)
lid := types.LayerID(layersPerEpoch * 3)
ee, err := o.GetProposalEligibility(lid, types.RandomBeacon(), types.VRFPostIndex(1))
require.ErrorIs(t, err, errMinerHasNoATXInPreviousEpoch)
Expand All @@ -205,7 +205,7 @@ func TestOracle_OwnATXNotFound(t *testing.T) {
func TestOracle_EligibilityCached(t *testing.T) {
avgLayerSize := uint32(10)
layersPerEpoch := uint32(20)
o := createTestOracle(t, avgLayerSize, layersPerEpoch)
o := createTestOracle(t, avgLayerSize, layersPerEpoch, 0)
lid := types.LayerID(layersPerEpoch * 3)
epochInfo := genATXForTargetEpochs(t, o.cdb, lid.GetEpoch(), lid.GetEpoch()+1, o.edSigner, layersPerEpoch)
info, ok := epochInfo[lid.GetEpoch()]
Expand All @@ -219,3 +219,27 @@ func TestOracle_EligibilityCached(t *testing.T) {
require.NoError(t, err)
require.Equal(t, ee1, ee2)
}

func TestOracle_MinimalActiveSetWeight(t *testing.T) {
avgLayerSize := uint32(10)
layersPerEpoch := uint32(20)

o := createTestOracle(t, avgLayerSize, layersPerEpoch, 0)
lid := types.LayerID(layersPerEpoch * 3)
epochInfo := genATXForTargetEpochs(t, o.cdb, lid.GetEpoch(), lid.GetEpoch()+1, o.edSigner, layersPerEpoch)

info, ok := epochInfo[lid.GetEpoch()]
require.True(t, ok)

ee1, err := o.GetProposalEligibility(lid, info.beacon, types.VRFPostIndex(1))
require.NoError(t, err)
require.NotNil(t, ee1)

o.minActiveSetWeight = 100000
o.cache.Epoch = 0
ee2, err := o.GetProposalEligibility(lid, info.beacon, types.VRFPostIndex(1))
require.NoError(t, err)
require.NotNil(t, ee1)

require.Less(t, ee2.Slots, ee1.Slots)
}
17 changes: 12 additions & 5 deletions miner/proposal_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,11 @@ type ProposalBuilder struct {

// config defines configuration for the ProposalBuilder.
type config struct {
layerSize uint32
layersPerEpoch uint32
hdist uint32
nodeID types.NodeID
layerSize uint32
layersPerEpoch uint32
hdist uint32
minActiveSetWeight uint64
nodeID types.NodeID
}

type defaultFetcher struct {
Expand Down Expand Up @@ -96,6 +97,12 @@ func WithLayerPerEpoch(layers uint32) Opt {
}
}

func WithMinimalActiveSetWeight(weight uint64) Opt {
return func(pb *ProposalBuilder) {
pb.cfg.minActiveSetWeight = weight
}
}

// WithNodeID defines the miner's NodeID.
func WithNodeID(id types.NodeID) Opt {
return func(pb *ProposalBuilder) {
Expand Down Expand Up @@ -162,7 +169,7 @@ func NewProposalBuilder(
}

if pb.proposalOracle == nil {
pb.proposalOracle = newMinerOracle(pb.cfg.layerSize, pb.cfg.layersPerEpoch, cdb, vrfSigner, pb.cfg.nodeID, pb.logger)
pb.proposalOracle = newMinerOracle(pb.cfg.layerSize, pb.cfg.layersPerEpoch, pb.cfg.minActiveSetWeight, cdb, vrfSigner, pb.cfg.nodeID, pb.logger)
}

if pb.nonceFetcher == nil {
Expand Down
12 changes: 7 additions & 5 deletions node/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -672,11 +672,12 @@ func (app *App) initServices(ctx context.Context, poetClients []activation.PoetP
proposalListener := proposals.NewHandler(app.cachedDB, app.edVerifier, app.host, fetcherWrapped, beaconProtocol, msh, trtl, vrfVerifier, app.clock,
proposals.WithLogger(app.addLogger(ProposalListenerLogger, lg)),
proposals.WithConfig(proposals.Config{
LayerSize: layerSize,
LayersPerEpoch: layersPerEpoch,
GoldenATXID: goldenATXID,
MaxExceptions: trtlCfg.MaxExceptions,
Hdist: trtlCfg.Hdist,
LayerSize: layerSize,
LayersPerEpoch: layersPerEpoch,
GoldenATXID: goldenATXID,
MaxExceptions: trtlCfg.MaxExceptions,
Hdist: trtlCfg.Hdist,
MinimalActiveSetWeight: trtlCfg.MinimalActiveSetWeight,
}),
)

Expand Down Expand Up @@ -782,6 +783,7 @@ func (app *App) initServices(ctx context.Context, poetClients []activation.PoetP
miner.WithNodeID(app.edSgn.NodeID()),
miner.WithLayerSize(layerSize),
miner.WithLayerPerEpoch(layersPerEpoch),
miner.WithMinimalActiveSetWeight(app.Config.Tortoise.MinimalActiveSetWeight),
miner.WithHdist(app.Config.Tortoise.Hdist),
miner.WithLogger(app.addLogger(ProposalBuilderLogger, lg)),
)
Expand Down
36 changes: 19 additions & 17 deletions proposals/eligibility_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,15 @@ var (
// Validator validates the eligibility of a Ballot.
// the validation focuses on eligibility only and assumes the Ballot to be valid otherwise.
type Validator struct {
avgLayerSize uint32
layersPerEpoch uint32
cdb *datastore.CachedDB
mesh meshProvider
beacons system.BeaconCollector
logger log.Log
vrfVerifier vrfVerifier
nonceFetcher nonceFetcher
minActiveSetWeight uint64
avgLayerSize uint32
layersPerEpoch uint32
cdb *datastore.CachedDB
mesh meshProvider
beacons system.BeaconCollector
logger log.Log
vrfVerifier vrfVerifier
nonceFetcher nonceFetcher
}

type defaultFetcher struct {
Expand All @@ -60,16 +61,17 @@ func WithNonceFetcher(nf nonceFetcher) ValidatorOpt {

// NewEligibilityValidator returns a new EligibilityValidator.
func NewEligibilityValidator(
avgLayerSize, layersPerEpoch uint32, cdb *datastore.CachedDB, bc system.BeaconCollector, m meshProvider, lg log.Log, vrfVerifier vrfVerifier, opts ...ValidatorOpt,
avgLayerSize, layersPerEpoch uint32, minActiveSetWeight uint64, cdb *datastore.CachedDB, bc system.BeaconCollector, m meshProvider, lg log.Log, vrfVerifier vrfVerifier, opts ...ValidatorOpt,
) *Validator {
v := &Validator{
avgLayerSize: avgLayerSize,
layersPerEpoch: layersPerEpoch,
cdb: cdb,
mesh: m,
beacons: bc,
logger: lg,
vrfVerifier: vrfVerifier,
minActiveSetWeight: minActiveSetWeight,
avgLayerSize: avgLayerSize,
layersPerEpoch: layersPerEpoch,
cdb: cdb,
mesh: m,
beacons: bc,
logger: lg,
vrfVerifier: vrfVerifier,
}
for _, opt := range opts {
opt(v)
Expand Down Expand Up @@ -141,7 +143,7 @@ func (v *Validator) CheckEligibility(ctx context.Context, ballot *types.Ballot)

atxWeight = owned.GetWeight()

numEligibleSlots, err := GetNumEligibleSlots(atxWeight, totalWeight, v.avgLayerSize, v.layersPerEpoch)
numEligibleSlots, err := GetNumEligibleSlots(atxWeight, v.minActiveSetWeight, totalWeight, v.avgLayerSize, v.layersPerEpoch)
if err != nil {
return false, err
}
Expand Down
42 changes: 40 additions & 2 deletions proposals/eligibility_validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ func createTestValidator(tb testing.TB) *testValidator {
lg := logtest.New(tb)

return &testValidator{
Validator: NewEligibilityValidator(layerAvgSize, layersPerEpoch, datastore.NewCachedDB(sql.InMemory(), lg), ms.mbc, ms.mm, lg, ms.mvrf,
Validator: NewEligibilityValidator(layerAvgSize, layersPerEpoch, 0, datastore.NewCachedDB(sql.InMemory(), lg), ms.mbc, ms.mm, lg, ms.mvrf,
WithNonceFetcher(ms.mNonce),
),
mockSet: ms,
Expand All @@ -91,7 +91,7 @@ func createTestValidator(tb testing.TB) *testValidator {

func createBallots(tb testing.TB, signer *signing.EdSigner, activeSet types.ATXIDList, beacon types.Beacon) []*types.Ballot {
totalWeight := uint64(len(activeSet)-1)*uint64(defaultATXUnit) + uint64(testedATXUnit)
slots, err := GetNumEligibleSlots(uint64(testedATXUnit), totalWeight, layerAvgSize, layersPerEpoch)
slots, err := GetNumEligibleSlots(uint64(testedATXUnit), 0, totalWeight, layerAvgSize, layersPerEpoch)
require.NoError(tb, err)
require.Equal(tb, eligibleSlots, slots)
eligibilityProofs := map[types.LayerID][]types.VotingEligibility{}
Expand Down Expand Up @@ -501,3 +501,41 @@ func TestCheckEligibility_AtxNotIncluded(t *testing.T) {
require.ErrorContains(t, err, "is not included into the active set")
require.False(t, eligibile)
}

func TestCheckEligibility_MinActiveSetWeight(t *testing.T) {
tv := createTestValidator(t)

atx := &types.ActivationTx{
InnerActivationTx: types.InnerActivationTx{
NumUnits: 2,
},
}
atx.PositioningATX = types.ATXID{2}
atx.SetID(types.ATXID{1})
atx.SetEffectiveNumUnits(atx.NumUnits)
atx.SetReceived(time.Time{}.Add(1))
vatx, err := atx.Verify(0, 100)
require.NoError(t, err)
require.NoError(t, atxs.Add(tv.cdb, vatx))

tv.minActiveSetWeight = 2 * vatx.GetWeight()

ballot := &types.Ballot{}
ballot.EligibilityProofs = []types.VotingEligibility{{}}
ballot.SetID(types.BallotID{1})
ballot.AtxID = vatx.ID()
ballot.Layer = vatx.TargetEpoch().FirstLayer()
activeSet := types.ATXIDList{vatx.ID()}
computed, err := GetNumEligibleSlots(vatx.GetWeight(), 0, vatx.GetWeight(), tv.avgLayerSize, tv.layersPerEpoch)
require.NoError(t, err)
ballot.EpochData = &types.EpochData{
ActiveSetHash: activeSet.Hash(),
Beacon: types.Beacon{1},
EligibilityCount: computed,
}
ballot.ActiveSet = activeSet

eligibile, err := tv.CheckEligibility(context.Background(), ballot)
require.ErrorContains(t, err, "ballot has incorrect eligibility count: expected 15, got: 30")
require.False(t, eligibile)
}
13 changes: 7 additions & 6 deletions proposals/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,12 @@ type Handler struct {

// Config defines configuration for the handler.
type Config struct {
LayerSize uint32
LayersPerEpoch uint32
GoldenATXID types.ATXID
MaxExceptions int
Hdist uint32
LayerSize uint32
LayersPerEpoch uint32
GoldenATXID types.ATXID
MaxExceptions int
Hdist uint32
MinimalActiveSetWeight uint64
}

// defaultConfig for BlockHandler.
Expand Down Expand Up @@ -127,7 +128,7 @@ func NewHandler(
opt(b)
}
if b.validator == nil {
b.validator = NewEligibilityValidator(b.cfg.LayerSize, b.cfg.LayersPerEpoch, cdb, bc, m, b.logger, verifier)
b.validator = NewEligibilityValidator(b.cfg.LayerSize, b.cfg.LayersPerEpoch, b.cfg.MinimalActiveSetWeight, cdb, bc, m, b.logger, verifier)
}
return b
}
Expand Down
Loading

0 comments on commit 72076af

Please sign in to comment.