From 91d2c027656a2974cab41506224d91a96897e958 Mon Sep 17 00:00:00 2001 From: Aayush Date: Thu, 10 Aug 2023 16:07:08 -0400 Subject: [PATCH 1/5] feat: miner: implement FRC-0051 --- build/params_mainnet.go | 1 + miner/miner.go | 43 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/build/params_mainnet.go b/build/params_mainnet.go index 0b3c0a254..9026056c0 100644 --- a/build/params_mainnet.go +++ b/build/params_mainnet.go @@ -105,6 +105,7 @@ var SupportedProofTypes = []abi.RegisteredSealProof{ var ConsensusMinerMinPower = abi.NewStoragePower(10 << 40) var PreCommitChallengeDelay = abi.ChainEpoch(150) var PropagationDelaySecs = uint64(10) +var EquivocationDelaySecs = uint64(2) func init() { if os.Getenv("LOTUS_USE_TEST_ADDRESSES") != "1" { diff --git a/miner/miner.go b/miner/miner.go index 104cf35fc..b02aba69d 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -10,7 +10,7 @@ import ( "sync" "time" - "github.com/hashicorp/golang-lru/arc/v2" + "github.com/ipfs/go-cid" logging "github.com/ipfs/go-log/v2" "go.opencensus.io/trace" "golang.org/x/xerrors" @@ -373,8 +373,9 @@ minerLoop: // MiningBase is the tipset on top of which we plan to construct our next block. // Refer to godocs on GetBestMiningCandidate. type MiningBase struct { - TipSet *types.TipSet - NullRounds abi.ChainEpoch + TipSet *types.TipSet + ComputeTime time.Time + NullRounds abi.ChainEpoch } // GetBestMiningCandidate implements the fork choice rule from a miner's @@ -412,7 +413,7 @@ func (m *Miner) GetBestMiningCandidate(ctx context.Context) (*MiningBase, error) } } - m.lastWork = &MiningBase{TipSet: bts} + m.lastWork = &MiningBase{TipSet: bts, ComputeTime: time.Now()} return m.lastWork, nil } @@ -560,6 +561,37 @@ func (m *Miner) mineOne(ctx context.Context, base *MiningBase) (minedBlock *type return nil, err } + tEquivocateWait := build.Clock.Now() + + // TODO: make param + m.niceSleep(time.Until(base.ComputeTime.Add(2 * time.Second))) + newBase, err := m.GetBestMiningCandidate(ctx) + if err != nil { + err = xerrors.Errorf("failed to refresh best mining candidate: %w", err) + return nil, err + } + + // Only factor in equivocated blocks if doing so will not risk us missing a block + if newBase.TipSet.Height() == base.TipSet.Height() && newBase.TipSet.MinTicket().Equals(base.TipSet.MinTicket()) { + newBaseMap := map[cid.Cid]struct{}{} + for _, newBaseBlk := range newBase.TipSet.Cids() { + newBaseMap[newBaseBlk] = struct{}{} + } + + refreshedBase := make([]*types.BlockHeader, 0, len(base.TipSet.Cids())) + for _, baseBlk := range base.TipSet.Blocks() { + if _, ok := newBaseMap[baseBlk.Cid()]; ok { + refreshedBase = append(refreshedBase, baseBlk) + } + } + + base.TipSet, err = types.NewTipSet(refreshedBase) + if err != nil { + err = xerrors.Errorf("failed to create new tipset when refreshing: %w", err) + return nil, err + } + } + tPending := build.Clock.Now() // TODO: winning post proof @@ -582,7 +614,8 @@ func (m *Miner) mineOne(ctx context.Context, base *MiningBase) (minedBlock *type "tTicket ", tTicket.Sub(tPowercheck), "tSeed ", tSeed.Sub(tTicket), "tProof ", tProof.Sub(tSeed), - "tPending ", tPending.Sub(tProof), + "tEquivocateWait ", tEquivocateWait.Sub(tProof), + "tPending ", tPending.Sub(tEquivocateWait), "tCreateBlock ", tCreateBlock.Sub(tPending)) } From 668d615be6a8ba3da8670779a2752f6f7f096ec7 Mon Sep 17 00:00:00 2001 From: Aayush Date: Fri, 11 Aug 2023 10:31:18 -0400 Subject: [PATCH 2/5] Add comments, re-select messages if refreshing base --- miner/miner.go | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/miner/miner.go b/miner/miner.go index b02aba69d..86860c03e 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -555,7 +555,7 @@ func (m *Miner) mineOne(ctx context.Context, base *MiningBase) (minedBlock *type tProof := build.Clock.Now() // get pending messages early, - msgs, err := m.api.MpoolSelect(context.TODO(), base.TipSet.Key(), ticket.Quality()) + msgs, err := m.api.MpoolSelect(ctx, base.TipSet.Key(), ticket.Quality()) if err != nil { err = xerrors.Errorf("failed to select messages for block: %w", err) return nil, err @@ -563,6 +563,11 @@ func (m *Miner) mineOne(ctx context.Context, base *MiningBase) (minedBlock *type tEquivocateWait := build.Clock.Now() + // This next block exists to "catch" equivocating miners, + // who submit 2 blocks at the same height at different times in order to split the network. + // To safeguard against this, we make sure it's been EquivocationDelaySecs since our base was calculated, + // then re-calculate it. + // If the daemon detected equivocated blocks, those blocks will no longer be in the new base. // TODO: make param m.niceSleep(time.Until(base.ComputeTime.Add(2 * time.Second))) newBase, err := m.GetBestMiningCandidate(ctx) @@ -571,24 +576,38 @@ func (m *Miner) mineOne(ctx context.Context, base *MiningBase) (minedBlock *type return nil, err } - // Only factor in equivocated blocks if doing so will not risk us missing a block - if newBase.TipSet.Height() == base.TipSet.Height() && newBase.TipSet.MinTicket().Equals(base.TipSet.MinTicket()) { + // If the MinTicket is still the same, we take the _intersection_ of our old base and new base, + // thus ejecting blocks from any equivocating miners, without taking any new blocks. + // If the MinTicket is not the same, then the work we've done so far is no longer valid. + // Instead of choosing to miss a block, we submit our best-effort block anyway. + if !newBase.TipSet.Equals(base.TipSet) && newBase.TipSet.MinTicket().Equals(base.TipSet.MinTicket()) { newBaseMap := map[cid.Cid]struct{}{} for _, newBaseBlk := range newBase.TipSet.Cids() { newBaseMap[newBaseBlk] = struct{}{} } - refreshedBase := make([]*types.BlockHeader, 0, len(base.TipSet.Cids())) + refreshedBaseBlocks := make([]*types.BlockHeader, 0, len(base.TipSet.Cids())) for _, baseBlk := range base.TipSet.Blocks() { if _, ok := newBaseMap[baseBlk.Cid()]; ok { - refreshedBase = append(refreshedBase, baseBlk) + refreshedBaseBlocks = append(refreshedBaseBlocks, baseBlk) } } - base.TipSet, err = types.NewTipSet(refreshedBase) - if err != nil { - err = xerrors.Errorf("failed to create new tipset when refreshing: %w", err) - return nil, err + if len(refreshedBaseBlocks) != len(base.TipSet.Blocks()) { + refreshedBase, err := types.NewTipSet(refreshedBaseBlocks) + if err != nil { + err = xerrors.Errorf("failed to create new tipset when refreshing: %w", err) + return nil, err + } + + base.TipSet = refreshedBase + + // refresh messages, as the selected messages may no longer be valid + msgs, err = m.api.MpoolSelect(ctx, base.TipSet.Key(), ticket.Quality()) + if err != nil { + err = xerrors.Errorf("failed to re-select messages for block: %w", err) + return nil, err + } } } From 43d1d62fc6da20d45b8b89f7a1852e66452b7a5f Mon Sep 17 00:00:00 2001 From: Aayush Date: Fri, 11 Aug 2023 10:36:17 -0400 Subject: [PATCH 3/5] Make EquivocationDelaySecs a build param --- build/params_2k.go | 2 ++ build/params_butterfly.go | 2 ++ build/params_calibnet.go | 2 ++ build/params_interop.go | 2 ++ build/params_mainnet.go | 1 + build/params_testground.go | 6 +----- itests/kit/ensemble.go | 2 ++ miner/miner.go | 21 +++++++++++++-------- 8 files changed, 25 insertions(+), 13 deletions(-) diff --git a/build/params_2k.go b/build/params_2k.go index 4af81f851..5e0f5c60d 100644 --- a/build/params_2k.go +++ b/build/params_2k.go @@ -132,6 +132,8 @@ const BlockDelaySecs = uint64(4) const PropagationDelaySecs = uint64(1) +var EquivocationDelaySecs = uint64(0) + // SlashablePowerDelay is the number of epochs after ElectionPeriodStart, after // which the miner is slashed // diff --git a/build/params_butterfly.go b/build/params_butterfly.go index 3cdb3914f..7d3b613c9 100644 --- a/build/params_butterfly.go +++ b/build/params_butterfly.go @@ -86,6 +86,8 @@ const BlockDelaySecs = uint64(builtin2.EpochDurationSeconds) const PropagationDelaySecs = uint64(6) +var EquivocationDelaySecs = uint64(2) + // BootstrapPeerThreshold is the minimum number peers we need to track for a sync worker to start const BootstrapPeerThreshold = 2 diff --git a/build/params_calibnet.go b/build/params_calibnet.go index da3d228cb..4081d4c1d 100644 --- a/build/params_calibnet.go +++ b/build/params_calibnet.go @@ -123,6 +123,8 @@ const BlockDelaySecs = uint64(builtin2.EpochDurationSeconds) var PropagationDelaySecs = uint64(10) +var EquivocationDelaySecs = uint64(2) + // BootstrapPeerThreshold is the minimum number peers we need to track for a sync worker to start const BootstrapPeerThreshold = 4 diff --git a/build/params_interop.go b/build/params_interop.go index 017f7def6..2b2f36160 100644 --- a/build/params_interop.go +++ b/build/params_interop.go @@ -121,6 +121,8 @@ const BlockDelaySecs = uint64(builtin2.EpochDurationSeconds) const PropagationDelaySecs = uint64(6) +var EquivocationDelaySecs = uint64(2) + // BootstrapPeerThreshold is the minimum number peers we need to track for a sync worker to start const BootstrapPeerThreshold = 2 diff --git a/build/params_mainnet.go b/build/params_mainnet.go index 9026056c0..f35ac45ef 100644 --- a/build/params_mainnet.go +++ b/build/params_mainnet.go @@ -105,6 +105,7 @@ var SupportedProofTypes = []abi.RegisteredSealProof{ var ConsensusMinerMinPower = abi.NewStoragePower(10 << 40) var PreCommitChallengeDelay = abi.ChainEpoch(150) var PropagationDelaySecs = uint64(10) + var EquivocationDelaySecs = uint64(2) func init() { diff --git a/build/params_testground.go b/build/params_testground.go index be380dc60..46d2e16de 100644 --- a/build/params_testground.go +++ b/build/params_testground.go @@ -9,7 +9,6 @@ package build import ( "math/big" - "time" "github.com/ipfs/go-cid" @@ -34,6 +33,7 @@ var ( MinimumBaseFee = int64(100) BlockDelaySecs = uint64(builtin2.EpochDurationSeconds) PropagationDelaySecs = uint64(6) + EquivocationDelaySecs = uint64(2) SupportedProofTypes = []abi.RegisteredSealProof{ abi.RegisteredSealProof_StackedDrg32GiBV1, abi.RegisteredSealProof_StackedDrg64GiBV1, @@ -139,7 +139,3 @@ const BootstrapPeerThreshold = 1 // ChainId defines the chain ID used in the Ethereum JSON-RPC endpoint. // As per https://github.com/ethereum-lists/chains const Eip155ChainId = 31415926 - -// Reducing the delivery delay for equivocation of -// consistent broadcast to just half a second. -var CBDeliveryDelay = 500 * time.Millisecond diff --git a/itests/kit/ensemble.go b/itests/kit/ensemble.go index 19cc163af..18b8fd734 100644 --- a/itests/kit/ensemble.go +++ b/itests/kit/ensemble.go @@ -169,6 +169,8 @@ func NewEnsemble(t *testing.T, opts ...EnsembleOpt) *Ensemble { require.NoError(t, build.UseNetworkBundle("testing")) } + build.EquivocationDelaySecs = 0 + return n } diff --git a/miner/miner.go b/miner/miner.go index 86860c03e..be32d3927 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -568,19 +568,16 @@ func (m *Miner) mineOne(ctx context.Context, base *MiningBase) (minedBlock *type // To safeguard against this, we make sure it's been EquivocationDelaySecs since our base was calculated, // then re-calculate it. // If the daemon detected equivocated blocks, those blocks will no longer be in the new base. - // TODO: make param - m.niceSleep(time.Until(base.ComputeTime.Add(2 * time.Second))) + m.niceSleep(time.Until(base.ComputeTime.Add(time.Duration(build.EquivocationDelaySecs) * time.Second))) newBase, err := m.GetBestMiningCandidate(ctx) if err != nil { err = xerrors.Errorf("failed to refresh best mining candidate: %w", err) return nil, err } - // If the MinTicket is still the same, we take the _intersection_ of our old base and new base, + // If the base has changed, we take the _intersection_ of our old base and new base, // thus ejecting blocks from any equivocating miners, without taking any new blocks. - // If the MinTicket is not the same, then the work we've done so far is no longer valid. - // Instead of choosing to miss a block, we submit our best-effort block anyway. - if !newBase.TipSet.Equals(base.TipSet) && newBase.TipSet.MinTicket().Equals(base.TipSet.MinTicket()) { + if !newBase.TipSet.Equals(base.TipSet) { newBaseMap := map[cid.Cid]struct{}{} for _, newBaseBlk := range newBase.TipSet.Cids() { newBaseMap[newBaseBlk] = struct{}{} @@ -600,14 +597,22 @@ func (m *Miner) mineOne(ctx context.Context, base *MiningBase) (minedBlock *type return nil, err } - base.TipSet = refreshedBase + if !base.TipSet.MinTicket().Equals(refreshedBase.MinTicket()) { + ticket, err = m.computeTicket(ctx, &rbase, round, refreshedBase.MinTicket(), mbi) + if err != nil { + err = xerrors.Errorf("failed to refresh ticket: %w", err) + return nil, err + } + } // refresh messages, as the selected messages may no longer be valid - msgs, err = m.api.MpoolSelect(ctx, base.TipSet.Key(), ticket.Quality()) + msgs, err = m.api.MpoolSelect(ctx, refreshedBase.Key(), ticket.Quality()) if err != nil { err = xerrors.Errorf("failed to re-select messages for block: %w", err) return nil, err } + + base.TipSet = refreshedBase } } From b68ced7b19ee129c7a71341df0bc46159053a2e9 Mon Sep 17 00:00:00 2001 From: Aayush Date: Tue, 22 Aug 2023 11:00:27 -0400 Subject: [PATCH 4/5] address review --- miner/miner.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/miner/miner.go b/miner/miner.go index be32d3927..9fc344e15 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -10,6 +10,7 @@ import ( "sync" "time" + "github.com/hashicorp/golang-lru/arc/v2" "github.com/ipfs/go-cid" logging "github.com/ipfs/go-log/v2" "go.opencensus.io/trace" @@ -577,7 +578,7 @@ func (m *Miner) mineOne(ctx context.Context, base *MiningBase) (minedBlock *type // If the base has changed, we take the _intersection_ of our old base and new base, // thus ejecting blocks from any equivocating miners, without taking any new blocks. - if !newBase.TipSet.Equals(base.TipSet) { + if newBase.TipSet.Height() == base.TipSet.Height() && !newBase.TipSet.Equals(base.TipSet) { newBaseMap := map[cid.Cid]struct{}{} for _, newBaseBlk := range newBase.TipSet.Cids() { newBaseMap[newBaseBlk] = struct{}{} From 114a3bbf3fe595337515b5b0040805243f8ebe2e Mon Sep 17 00:00:00 2001 From: Aayush Date: Mon, 28 Aug 2023 12:10:48 -0400 Subject: [PATCH 5/5] add some logs --- miner/miner.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/miner/miner.go b/miner/miner.go index 9fc344e15..5dc44930a 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -579,6 +579,7 @@ func (m *Miner) mineOne(ctx context.Context, base *MiningBase) (minedBlock *type // If the base has changed, we take the _intersection_ of our old base and new base, // thus ejecting blocks from any equivocating miners, without taking any new blocks. if newBase.TipSet.Height() == base.TipSet.Height() && !newBase.TipSet.Equals(base.TipSet) { + log.Warnf("base changed from %s to %s, taking intersection", base.TipSet.Key(), newBase.TipSet.Key()) newBaseMap := map[cid.Cid]struct{}{} for _, newBaseBlk := range newBase.TipSet.Cids() { newBaseMap[newBaseBlk] = struct{}{} @@ -599,6 +600,8 @@ func (m *Miner) mineOne(ctx context.Context, base *MiningBase) (minedBlock *type } if !base.TipSet.MinTicket().Equals(refreshedBase.MinTicket()) { + log.Warn("recomputing ticket due to base refresh") + ticket, err = m.computeTicket(ctx, &rbase, round, refreshedBase.MinTicket(), mbi) if err != nil { err = xerrors.Errorf("failed to refresh ticket: %w", err) @@ -606,6 +609,7 @@ func (m *Miner) mineOne(ctx context.Context, base *MiningBase) (minedBlock *type } } + log.Warn("re-selecting messages due to base refresh") // refresh messages, as the selected messages may no longer be valid msgs, err = m.api.MpoolSelect(ctx, refreshedBase.Key(), ticket.Quality()) if err != nil {