diff --git a/chain/syncer.go b/chain/syncer.go index 5a89c9ee6b..a5e308686d 100644 --- a/chain/syncer.go +++ b/chain/syncer.go @@ -58,12 +58,14 @@ type syncChainSelector interface { // IsHeaver returns true if tipset a is heavier than tipset b and false if // tipset b is heavier than tipset a. IsHeavier(ctx context.Context, a, b types.TipSet, aStateID, bStateID cid.Cid) (bool, error) + // NewWeight returns the weight of a tipset after the upgrade to version 1 + NewWeight(ctx context.Context, ts types.TipSet, stRoot cid.Cid) (uint64, error) } type syncStateEvaluator interface { // RunStateTransition returns the state root CID resulting from applying the input ts to the // prior `stateRoot`. It returns an error if the transition is invalid. - RunStateTransition(ctx context.Context, ts types.TipSet, tsMessages [][]*types.SignedMessage, tsReceipts [][]*types.MessageReceipt, ancestors []types.TipSet, stateID cid.Cid) (cid.Cid, error) + RunStateTransition(ctx context.Context, ts types.TipSet, tsMessages [][]*types.SignedMessage, tsReceipts [][]*types.MessageReceipt, ancestors []types.TipSet, parentWeight uint64, stateID cid.Cid) (cid.Cid, error) } // Syncer updates its chain.Store according to the methods of its @@ -134,7 +136,7 @@ func NewSyncer(e syncStateEvaluator, cs syncChainSelector, s syncerChainReaderWr // // Precondition: the caller of syncOne must hold the syncer's lock (syncer.mu) to // ensure head is not modified by another goroutine during run. -func (syncer *Syncer) syncOne(ctx context.Context, parent, next types.TipSet) error { +func (syncer *Syncer) syncOne(ctx context.Context, grandParent, parent, next types.TipSet) error { priorHeadKey := syncer.chainStore.GetHead() // if tipset is already priorHeadKey, we've been here before. do nothing. @@ -162,6 +164,7 @@ func (syncer *Syncer) syncOne(ctx context.Context, parent, next types.TipSet) er return err } + // Gather tipset messages var nextMessages [][]*types.SignedMessage var nextReceipts [][]*types.MessageReceipt for i := 0; i < next.Len(); i++ { @@ -178,9 +181,15 @@ func (syncer *Syncer) syncOne(ctx context.Context, parent, next types.TipSet) er nextReceipts = append(nextReceipts, rcpts) } + // Gather validated parent weight + parentWeight, err := syncer.calculateParentWeight(ctx, parent, grandParent) + if err != nil { + return err + } + // Run a state transition to validate the tipset and compute // a new state to add to the store. - root, err := syncer.stateEvaluator.RunStateTransition(ctx, next, nextMessages, nextReceipts, ancestors, stateRoot) + root, err := syncer.stateEvaluator.RunStateTransition(ctx, next, nextMessages, nextReceipts, ancestors, parentWeight, stateRoot) if err != nil { return err } @@ -233,6 +242,44 @@ func (syncer *Syncer) syncOne(ctx context.Context, parent, next types.TipSet) er return nil } +// TODO #3537 this should be stored the first time it is computed and retrieved +// from disk just like aggregate state roots. +func (syncer *Syncer) calculateParentWeight(ctx context.Context, parent, grandParent types.TipSet) (uint64, error) { + if grandParent.Equals(types.UndefTipSet) { + return syncer.chainSelector.NewWeight(ctx, parent, cid.Undef) + } + gpStRoot, err := syncer.chainStore.GetTipSetStateRoot(grandParent.Key()) + if err != nil { + return 0, err + } + return syncer.chainSelector.NewWeight(ctx, parent, gpStRoot) +} + +// ancestorsFromStore returns the parent and grandparent tipsets of `ts` +func (syncer *Syncer) ancestorsFromStore(ts types.TipSet) (types.TipSet, types.TipSet, error) { + parentCids, err := ts.Parents() + if err != nil { + return types.UndefTipSet, types.UndefTipSet, err + } + parent, err := syncer.chainStore.GetTipSet(parentCids) + if err != nil { + return types.UndefTipSet, types.UndefTipSet, err + } + grandParentCids, err := parent.Parents() + if err != nil { + return types.UndefTipSet, types.UndefTipSet, err + } + if grandParentCids.Empty() { + // parent == genesis ==> grandParent undef + return parent, types.UndefTipSet, nil + } + grandParent, err := syncer.chainStore.GetTipSet(grandParentCids) + if err != nil { + return types.UndefTipSet, types.UndefTipSet, err + } + return parent, grandParent, nil +} + func (syncer *Syncer) logReorg(ctx context.Context, curHead, newHead types.TipSet) { curHeadIter := IterAncestors(ctx, syncer.chainStore, curHead) newHeadIter := IterAncestors(ctx, syncer.chainStore, newHead) @@ -329,7 +376,7 @@ func (syncer *Syncer) HandleNewTipSet(ctx context.Context, ci *types.ChainInfo, span.AddAttributes(trace.StringAttribute("tipset", ci.Head.String())) defer tracing.AddErrorEndSpan(ctx, span, &err) - // This lock could last a long time as we fetch all the blocks needed to block the chain. + // This lock could last a long time as we fetch all the blocks needed to sync the chain. // This is justified because the app is pretty useless until it is synced. // It's better for multiple calls to wait here than to try to fetch the chain independently. syncer.mu.Lock() @@ -379,11 +426,7 @@ func (syncer *Syncer) HandleNewTipSet(ctx context.Context, ci *types.ChainInfo, // Fetcher returns chain in Traversal order, reverse it to height order Reverse(chain) - parentCids, err := chain[0].Parents() - if err != nil { - return err - } - parent, err := syncer.chainStore.GetTipSet(parentCids) + parent, grandParent, err := syncer.ancestorsFromStore(chain[0]) if err != nil { return err } @@ -401,7 +444,7 @@ func (syncer *Syncer) HandleNewTipSet(ctx context.Context, ci *types.ChainInfo, } if wts.Defined() { logSyncer.Debug("attempt to sync after widen") - err = syncer.syncOne(ctx, parent, wts) + err = syncer.syncOne(ctx, grandParent, parent, wts) if err != nil { return err } @@ -414,7 +457,7 @@ func (syncer *Syncer) HandleNewTipSet(ctx context.Context, ci *types.ChainInfo, // as a performance optimization, because this tipset cannot be heavier // than the widened first tipset. if !wts.Defined() || len(chain) > 1 { - err = syncer.syncOne(ctx, parent, ts) + err = syncer.syncOne(ctx, grandParent, parent, ts) if err != nil { // While `syncOne` can indeed fail for reasons other than consensus, // adding to the badTipSets at this point is the simplest, since we @@ -428,6 +471,7 @@ func (syncer *Syncer) HandleNewTipSet(ctx context.Context, ci *types.ChainInfo, if i%500 == 0 { logSyncer.Infof("processing block %d of %v for chain with head at %v", i, len(chain), ci.Head.String()) } + grandParent = parent parent = ts } return nil diff --git a/chain/syncer_integration_test.go b/chain/syncer_integration_test.go index 65fbc80313..69a02d321a 100644 --- a/chain/syncer_integration_test.go +++ b/chain/syncer_integration_test.go @@ -6,17 +6,20 @@ import ( "time" bserv "github.com/ipfs/go-blockservice" + "github.com/ipfs/go-cid" "github.com/ipfs/go-hamt-ipld" bstore "github.com/ipfs/go-ipfs-blockstore" "github.com/ipfs/go-ipfs-exchange-offline" "github.com/filecoin-project/go-filecoin/address" "github.com/filecoin-project/go-filecoin/chain" + "github.com/filecoin-project/go-filecoin/consensus" "github.com/filecoin-project/go-filecoin/repo" "github.com/filecoin-project/go-filecoin/state" th "github.com/filecoin-project/go-filecoin/testhelpers" tf "github.com/filecoin-project/go-filecoin/testhelpers/testflags" "github.com/filecoin-project/go-filecoin/types" + "github.com/filecoin-project/go-filecoin/version" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -107,3 +110,207 @@ func TestLoadFork(t *testing.T) { // The left chain is ok without any fetching though. assert.NoError(t, offlineSyncer.HandleNewTipSet(ctx, types.NewChainInfo("", left.Key(), heightFromTip(t, left)), true)) } + +// Power table weight comparisons impact syncer's selection. +// One fork has more blocks but less total power. +// Verify that the heavier fork is the one with more power. +// All blocks in this test follow protocol version 1 upgrade weighting rules. +func TestSyncerWeighsPower(t *testing.T) { + cst := hamt.NewCborStore() + ctx := context.Background() + pvt, err := version.ConfigureProtocolVersions(version.TEST) + require.NoError(t, err) + isb := newIntegrationStateBuilder(t, cst, pvt) + builder := chain.NewBuilderWithState(t, address.Undef, isb) + + // Construct genesis with readable state tree root + gen := builder.BuildOneOn(types.UndefTipSet, func(bb *chain.BlockBuilder) {}) + + // Builder constructs two different blocks with different state trees + // for building two forks. + split := builder.BuildOn(gen, 2, func(bb *chain.BlockBuilder, i int) { + if i == 1 { + keys := types.MustGenerateKeyInfo(1, 42) + mm := types.NewMessageMaker(t, keys) + addr := mm.Addresses()[0] + bb.AddMessages( + []*types.SignedMessage{ + mm.NewSignedMessage(addr, 1), + }, + types.EmptyReceipts(1), + ) + } + }) + fork1 := types.RequireNewTipSet(t, split.At(0)) + fork2 := types.RequireNewTipSet(t, split.At(1)) + + // Builder adds 3 blocks to fork 1 and total storage power 2^0 + // 3 + 3*delta = 3 + 3[V*1 + bits(2^0)] = 3 + 3[2 + 1] = 3 + 9 = 12 + head1 := builder.AppendManyOn(3, fork1) + + // Builder adds 1 block to fork 2 and total storage power 2^9 + // 3 + 1*delta = 3 + 1[V*1 + bits(2^9)] = 3 + 2 + 10 = 15 + head2 := builder.AppendOn(fork2, 1) + + // Verify that the syncer selects fork 2 (15 > 12) + as := newForkSnapshotGen(t, types.NewBytesAmount(1), types.NewBytesAmount(512), isb.c512) + dumpBlocksToCborStore(t, builder, cst, head1, head2) + store := chain.NewStore(repo.NewInMemoryRepo().ChainDatastore(), cst, &state.TreeStateLoader{}, chain.NewStatusReporter(), gen.At(0).Cid()) + require.NoError(t, store.PutTipSetAndState(ctx, &chain.TipSetAndState{gen.At(0).StateRoot, gen})) + require.NoError(t, store.SetHead(ctx, gen)) + syncer := chain.NewSyncer(&integrationStateEvaluator{c512: isb.c512}, consensus.NewChainSelector(cst, as, gen.At(0).Cid(), pvt), store, builder, builder, chain.NewStatusReporter(), th.NewFakeClock(time.Unix(1234567890, 0))) + + // sync fork 1 + assert.NoError(t, syncer.HandleNewTipSet(ctx, types.NewChainInfo("", head1.Key(), heightFromTip(t, head1)), true)) + assert.Equal(t, head1.Key(), store.GetHead()) + // sync fork 2 + assert.NoError(t, syncer.HandleNewTipSet(ctx, types.NewChainInfo("", head2.Key(), heightFromTip(t, head1)), true)) + assert.Equal(t, head2.Key(), store.GetHead()) +} + +// integrationStateBuilder is a chain/testing.go `StateBuilder` used for +// construction of a chain where the state root cids signify the total power +// in the power table without actually needing to construct a valid state +// state machine tree. +// +// All blocks with at least one message are assigned a special cid: c512. +// In TestSyncerWeighsPower this state root is interpreted as having +// 512 bytes of power. +// +// integrationStateBuilder also weighs the chain according to the protocol +// version 1 upgrade. +type integrationStateBuilder struct { + t *testing.T + c512 cid.Cid + cGen cid.Cid + cst *hamt.CborIpldStore + pvt *version.ProtocolVersionTable +} + +func newIntegrationStateBuilder(t *testing.T, cst *hamt.CborIpldStore, pvt *version.ProtocolVersionTable) *integrationStateBuilder { + return &integrationStateBuilder{ + t: t, + c512: cid.Undef, + cst: cst, + cGen: cid.Undef, + } +} + +func (isb *integrationStateBuilder) ComputeState(prev cid.Cid, blocksMessages [][]*types.SignedMessage) (cid.Cid, error) { + // setup genesis with a state we can fetch from cborstor + if prev.Equals(types.CidFromString(isb.t, "null")) { + treeGen := state.TreeFromString(isb.t, "1Power", isb.cst) + genRoot, err := treeGen.Flush(context.Background()) + require.NoError(isb.t, err) + return genRoot, nil + } + // setup fork with state we associate with more power + if len(blocksMessages[0]) > 0 { + treeFork := state.TreeFromString(isb.t, "512Power", isb.cst) + forkRoot, err := treeFork.Flush(context.Background()) + require.NoError(isb.t, err) + isb.c512 = forkRoot + return forkRoot, nil + } + return prev, nil +} + +func (isb *integrationStateBuilder) Weigh(tip types.TipSet, pstate cid.Cid) (uint64, error) { + if tip.Equals(types.UndefTipSet) { + return uint64(0), nil + } + if isb.cGen.Equals(cid.Undef) && tip.Len() == 1 { + isb.cGen = tip.At(0).Cid() + } + + if tip.At(0).Cid().Equals(isb.cGen) { + return uint64(0), nil + } + as := newForkSnapshotGen(isb.t, types.NewBytesAmount(1), types.NewBytesAmount(512), isb.c512) + sel := consensus.NewChainSelector(isb.cst, as, isb.cGen, isb.pvt) + return sel.NewWeight(context.Background(), tip, pstate) +} + +// integrationStateEvaluator returns the parent state root. If there are multiple +// parent blocks and any contain state root c512 then it will return c512. +type integrationStateEvaluator struct { + c512 cid.Cid +} + +func (n *integrationStateEvaluator) RunStateTransition(_ context.Context, ts types.TipSet, _ [][]*types.SignedMessage, _ [][]*types.MessageReceipt, _ []types.TipSet, _ uint64, stateID cid.Cid) (cid.Cid, error) { + for i := 0; i < ts.Len(); i++ { + if ts.At(i).StateRoot.Equals(n.c512) { + return n.c512, nil + } + } + return ts.At(0).StateRoot, nil +} + +// forkSnapshotGen reads power from fake state tree root cids. It reads +// power of `forkPower` from cid `forkRoot` and `defaultPower` from all others. +type forkSnapshotGen struct { + forkPower *types.BytesAmount + defaultPower *types.BytesAmount + forkRoot cid.Cid + t *testing.T +} + +func newForkSnapshotGen(t *testing.T, dp, fp *types.BytesAmount, root cid.Cid) *forkSnapshotGen { + return &forkSnapshotGen{ + t: t, + defaultPower: dp, + forkPower: fp, + forkRoot: root, + } +} + +func (fs *forkSnapshotGen) StateTreeSnapshot(st state.Tree, bh *types.BlockHeight) consensus.ActorStateSnapshot { + totalPower := fs.defaultPower + + root, err := st.Flush(context.Background()) + require.NoError(fs.t, err) + if root.Equals(fs.forkRoot) { + totalPower = fs.forkPower + } + + return &consensus.FakePowerTableViewSnapshot{ + MinerPower: types.NewBytesAmount(0), + TotalPower: totalPower, + MinerToWorker: make(map[address.Address]address.Address), + } +} + +// dumpBlocksToCborStore is a helper method that +// TODO #3078 we can avoid this byte shuffling by creating a simple testing type +// that implements the needed interface and grabs blocks from the builder as +// needed. Once #3078 is in place we will have the flexibility to use a +// testing type as the cbor store. +func dumpBlocksToCborStore(t *testing.T, builder *chain.Builder, cst *hamt.CborIpldStore, heads ...types.TipSet) { + cids := make(map[cid.Cid]struct{}) + // traverse builder frontier adding cids to the map. Traverse + // duplicates over doing anything clever. + var err error + for _, head := range heads { + it := chain.IterAncestors(context.Background(), builder, head) + for ; !it.Complete(); err = it.Next() { + require.NoError(t, err) + for i := 0; i < it.Value().Len(); i++ { + blk := head.At(i) + c := blk.Cid() + cids[c] = struct{}{} + } + } + } + + // get all blocks corresponding to the cids and put to the cst + var searchKey []cid.Cid + for c := range cids { + searchKey = append(searchKey, c) + } + blocks, err := builder.GetBlocks(context.Background(), searchKey) + require.NoError(t, err) + for _, blk := range blocks { + _, err = cst.Put(context.Background(), blk) + require.NoError(t, err) + } +} diff --git a/chain/testing.go b/chain/testing.go index a64e53f19f..5ea8123394 100644 --- a/chain/testing.go +++ b/chain/testing.go @@ -43,9 +43,14 @@ var _ TipSetProvider = (*Builder)(nil) var _ net.Fetcher = (*Builder)(nil) var _ MessageProvider = (*Builder)(nil) -// NewBuilder builds a new chain faker. -// Blocks will have `miner` set as the miner address, or a default if empty. +// NewBuilder builds a new chain faker with default fake state building. func NewBuilder(t *testing.T, miner address.Address) *Builder { + return NewBuilderWithState(t, miner, &FakeStateBuilder{}) +} + +// NewBuilderWithState builds a new chain faker. +// Blocks will have `miner` set as the miner address, or a default if empty. +func NewBuilderWithState(t *testing.T, miner address.Address, sb StateBuilder) *Builder { if miner.Empty() { var err error miner, err = address.NewActorAddress([]byte("miner")) @@ -55,7 +60,7 @@ func NewBuilder(t *testing.T, miner address.Address) *Builder { b := &Builder{ t: t, minerAddress: miner, - stateBuilder: &FakeStateBuilder{}, + stateBuilder: sb, blocks: make(map[cid.Cid]*types.Block), tipStateCids: make(map[string]cid.Cid), messages: make(map[cid.Cid][]*types.SignedMessage), @@ -65,8 +70,7 @@ func NewBuilder(t *testing.T, miner address.Address) *Builder { b.messages[types.EmptyMessagesCID] = []*types.SignedMessage{} b.receipts[types.EmptyReceiptsCID] = []*types.MessageReceipt{} - nullState, err := makeCid("null") - require.NoError(t, err) + nullState := types.CidFromString(t, "null") b.tipStateCids[types.NewTipSetKey().String()] = nullState return b } @@ -351,7 +355,7 @@ type FakeStateEvaluator struct { } // RunStateTransition delegates to StateBuilder.ComputeState. -func (e *FakeStateEvaluator) RunStateTransition(ctx context.Context, tip types.TipSet, messages [][]*types.SignedMessage, receipts [][]*types.MessageReceipt, ancestors []types.TipSet, stateID cid.Cid) (cid.Cid, error) { +func (e *FakeStateEvaluator) RunStateTransition(ctx context.Context, tip types.TipSet, messages [][]*types.SignedMessage, receipts [][]*types.MessageReceipt, ancestors []types.TipSet, parentWeight uint64, stateID cid.Cid) (cid.Cid, error) { return e.ComputeState(stateID, messages) } @@ -375,6 +379,11 @@ func (e *FakeChainSelector) IsHeavier(ctx context.Context, a, b types.TipSet, aS return aw > bw, nil } +// NewWeight delegates to the statebuilder +func (e *FakeChainSelector) NewWeight(ctx context.Context, ts types.TipSet, stID cid.Cid) (uint64, error) { + return e.Weigh(ts, stID) +} + ///// Interface and accessor implementations ///// // GetBlock returns the block identified by `c`. diff --git a/consensus/block_validation.go b/consensus/block_validation.go index 4eb41d9947..83b6e899cf 100644 --- a/consensus/block_validation.go +++ b/consensus/block_validation.go @@ -7,6 +7,7 @@ import ( "github.com/filecoin-project/go-filecoin/clock" "github.com/filecoin-project/go-filecoin/types" + "github.com/filecoin-project/go-filecoin/version" ) // BlockValidator defines an interface used to validate a blocks syntax and @@ -26,7 +27,7 @@ type SyntaxValidator interface { // BlockSemanticValidator defines an interface used to validate a blocks // semantics. type BlockSemanticValidator interface { - ValidateSemantic(ctx context.Context, child *types.Block, parents *types.TipSet) error + ValidateSemantic(ctx context.Context, child *types.Block, parents *types.TipSet, parentWeight uint64) error } // BlockSyntaxValidator defines an interface used to validate a blocks @@ -46,19 +47,21 @@ type MessageSyntaxValidator interface { type DefaultBlockValidator struct { clock.Clock blockTime time.Duration + pvt *version.ProtocolVersionTable } // NewDefaultBlockValidator returns a new DefaultBlockValidator. It uses `blkTime` // to validate blocks and uses the DefaultBlockValidationClock. -func NewDefaultBlockValidator(blkTime time.Duration, c clock.Clock) *DefaultBlockValidator { +func NewDefaultBlockValidator(blkTime time.Duration, c clock.Clock, pvt *version.ProtocolVersionTable) *DefaultBlockValidator { return &DefaultBlockValidator{ Clock: c, blockTime: blkTime, + pvt: pvt, } } // ValidateSemantic validates a block is correctly derived from its parent. -func (dv *DefaultBlockValidator) ValidateSemantic(ctx context.Context, child *types.Block, parents *types.TipSet) error { +func (dv *DefaultBlockValidator) ValidateSemantic(ctx context.Context, child *types.Block, parents *types.TipSet, parentWeight uint64) error { pmin, err := parents.MinTimestamp() if err != nil { return err @@ -69,6 +72,23 @@ func (dv *DefaultBlockValidator) ValidateSemantic(ctx context.Context, child *ty return err } + parentVersion, err := dv.pvt.VersionAt(types.NewBlockHeight(ph)) + if err != nil { + return err + } + // Protocol version 1 upgrade introduces validation of the weight field + // on the header. During protocol version 0 validators do not validate + // that the parent weight written to the header actually corresponds to + // the weight measured by the validators. Introducing this check + // prevents a validator from writing arbitrary parent weight values + // into a header and trivially generating the heaviest chain. + if parentVersion >= version.Protocol1 { + // Protocol Version 1 upgrade + if uint64(child.ParentWeight) != parentWeight { + return fmt.Errorf("block %s has invalid parent weight %d", child.Cid().String(), parentWeight) + } + } + if uint64(child.Height) <= ph { return fmt.Errorf("block %s has invalid height %d", child.Cid().String(), child.Height) } diff --git a/consensus/block_validation_test.go b/consensus/block_validation_test.go index e000f002a7..c242314088 100644 --- a/consensus/block_validation_test.go +++ b/consensus/block_validation_test.go @@ -14,6 +14,7 @@ import ( th "github.com/filecoin-project/go-filecoin/testhelpers" tf "github.com/filecoin-project/go-filecoin/testhelpers/testflags" "github.com/filecoin-project/go-filecoin/types" + "github.com/filecoin-project/go-filecoin/version" ) func TestBlockValidSemantic(t *testing.T) { @@ -23,21 +24,26 @@ func TestBlockValidSemantic(t *testing.T) { ts := time.Unix(1234567890, 0) mclock := th.NewFakeClock(ts) ctx := context.Background() + pvt, err := version.NewProtocolVersionTableBuilder(version.TEST). + Add(version.TEST, version.Protocol0, types.NewBlockHeight(0)). + Add(version.TEST, version.Protocol1, types.NewBlockHeight(300)). + Build() + require.NoError(t, err) - validator := consensus.NewDefaultBlockValidator(blockTime, mclock) + validator := consensus.NewDefaultBlockValidator(blockTime, mclock, pvt) t.Run("reject block with same height as parents", func(t *testing.T) { // passes with valid height c := &types.Block{Height: 2, Timestamp: types.Uint64(ts.Add(blockTime).Unix())} p := &types.Block{Height: 1, Timestamp: types.Uint64(ts.Unix())} parents := consensus.RequireNewTipSet(require.New(t), p) - require.NoError(t, validator.ValidateSemantic(ctx, c, &parents)) + require.NoError(t, validator.ValidateSemantic(ctx, c, &parents, 0)) // invalidate parent by matching child height p = &types.Block{Height: 2, Timestamp: types.Uint64(ts.Unix())} parents = consensus.RequireNewTipSet(require.New(t), p) - err := validator.ValidateSemantic(ctx, c, &parents) + err := validator.ValidateSemantic(ctx, c, &parents, 0) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid height") @@ -48,11 +54,11 @@ func TestBlockValidSemantic(t *testing.T) { c := &types.Block{Height: 2, Timestamp: types.Uint64(ts.Add(blockTime).Unix())} p := &types.Block{Height: 1, Timestamp: types.Uint64(ts.Unix())} parents := consensus.RequireNewTipSet(require.New(t), p) - require.NoError(t, validator.ValidateSemantic(ctx, c, &parents)) + require.NoError(t, validator.ValidateSemantic(ctx, c, &parents, 0)) // fails with invalid timestamp c = &types.Block{Height: 2, Timestamp: types.Uint64(ts.Unix())} - err := validator.ValidateSemantic(ctx, c, &parents) + err := validator.ValidateSemantic(ctx, c, &parents, 0) assert.Error(t, err) assert.Contains(t, err.Error(), "too far") @@ -63,22 +69,44 @@ func TestBlockValidSemantic(t *testing.T) { c := &types.Block{Height: 3, Timestamp: types.Uint64(ts.Add(2 * blockTime).Unix())} p := &types.Block{Height: 1, Timestamp: types.Uint64(ts.Unix())} parents := consensus.RequireNewTipSet(require.New(t), p) - err := validator.ValidateSemantic(ctx, c, &parents) + err := validator.ValidateSemantic(ctx, c, &parents, 0) require.NoError(t, err) // fail when nul block calc is off by one blocktime c = &types.Block{Height: 3, Timestamp: types.Uint64(ts.Add(blockTime).Unix())} - err = validator.ValidateSemantic(ctx, c, &parents) + err = validator.ValidateSemantic(ctx, c, &parents, 0) assert.Error(t, err) assert.Contains(t, err.Error(), "too far") // fail with same timestamp as parent c = &types.Block{Height: 3, Timestamp: types.Uint64(ts.Unix())} - err = validator.ValidateSemantic(ctx, c, &parents) + err = validator.ValidateSemantic(ctx, c, &parents, 0) assert.Error(t, err) assert.Contains(t, err.Error(), "too far") }) + + t.Run("reject block mined with invalid parent weight after protocol 1 upgrade", func(t *testing.T) { + hUpgrade := 300 + c := &types.Block{Height: types.Uint64(hUpgrade) + 50, ParentWeight: 5000, Timestamp: types.Uint64(ts.Add(blockTime).Unix())} + p := &types.Block{Height: types.Uint64(hUpgrade) + 49, Timestamp: types.Uint64(ts.Unix())} + parents := consensus.RequireNewTipSet(require.New(t), p) + + // validator expects parent weight different from 5000 + pwExpectedByValidator := uint64(30) + err := validator.ValidateSemantic(ctx, c, &parents, pwExpectedByValidator) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid parent weight") + }) + + t.Run("accept block mined with invalid parent weight before alphanet upgrade", func(t *testing.T) { + c := &types.Block{Height: 2, ParentWeight: 5000, Timestamp: types.Uint64(ts.Add(blockTime).Unix())} + p := &types.Block{Height: 1, Timestamp: types.Uint64(ts.Unix())} + parents := consensus.RequireNewTipSet(require.New(t), p) + + err := validator.ValidateSemantic(ctx, c, &parents, 30) + assert.NoError(t, err) + }) } func TestBlockValidSyntax(t *testing.T) { @@ -87,10 +115,11 @@ func TestBlockValidSyntax(t *testing.T) { blockTime := consensus.DefaultBlockTime ts := time.Unix(1234567890, 0) mclock := th.NewFakeClock(ts) - ctx := context.Background() + pvt, err := version.ConfigureProtocolVersions(version.TEST) + require.NoError(t, err) - validator := consensus.NewDefaultBlockValidator(blockTime, mclock) + validator := consensus.NewDefaultBlockValidator(blockTime, mclock, pvt) validTs := types.Uint64(ts.Unix()) validSt := types.NewCidForTestGetter()() diff --git a/consensus/chain_selector.go b/consensus/chain_selector.go index ac1996520f..5df184de11 100644 --- a/consensus/chain_selector.go +++ b/consensus/chain_selector.go @@ -6,6 +6,7 @@ package consensus import ( "bytes" "context" + "errors" "math/big" "strings" @@ -14,6 +15,7 @@ import ( "github.com/filecoin-project/go-filecoin/state" "github.com/filecoin-project/go-filecoin/types" + "github.com/filecoin-project/go-filecoin/version" ) // Parameters used by the latest weight function "NewWeight" @@ -37,20 +39,23 @@ const ( ecPrM uint64 = 100 ) -// ChainSelector weighs and compares chains according to the Storage Power -// Consensus Protocol +// ChainSelector weighs and compares chains according to the deprecated v0 +// Storage Power Consensus Protocol type ChainSelector struct { cstore *hamt.CborIpldStore actorState SnapshotGenerator genesisCid cid.Cid + + pvt *version.ProtocolVersionTable } // NewChainSelector is the constructor for chain selection module. -func NewChainSelector(cs *hamt.CborIpldStore, actorState SnapshotGenerator, gCid cid.Cid) *ChainSelector { +func NewChainSelector(cs *hamt.CborIpldStore, actorState SnapshotGenerator, gCid cid.Cid, pvt *version.ProtocolVersionTable) *ChainSelector { return &ChainSelector{ cstore: cs, actorState: actorState, genesisCid: gCid, + pvt: pvt, } } @@ -60,7 +65,7 @@ func NewChainSelector(cs *hamt.CborIpldStore, actorState SnapshotGenerator, gCid // w(i) = w(i-1) + (pi)^(P_n) * [V * num_blks + X ] // P_n(n) = if n < 3:0 else: n, n is number of null rounds // X = log_2(total_storage(pSt)) -func (c *ChainSelector) NewWeight(ctx context.Context, ts types.TipSet, pSt state.Tree) (uint64, error) { +func (c *ChainSelector) NewWeight(ctx context.Context, ts types.TipSet, pStateID cid.Cid) (uint64, error) { ctx = log.Start(ctx, "Expected.Weight") log.LogKV(ctx, "Weight", ts.String()) if ts.Len() > 0 && ts.At(0).Cid().Equals(c.genesisCid) { @@ -71,7 +76,6 @@ func (c *ChainSelector) NewWeight(ctx context.Context, ts types.TipSet, pSt stat if err != nil { return uint64(0), err } - w, err := types.FixedToBig(parentW) if err != nil { return uint64(0), err @@ -84,6 +88,13 @@ func (c *ChainSelector) NewWeight(ctx context.Context, ts types.TipSet, pSt stat innerTerm.Mul(floatECV, floatNumBlocks) // Add bitnum(total storage power) to the weight's inner term + if !pStateID.Defined() { + return uint64(0), errors.New("undefined state passed to chain selector new weight") + } + pSt, err := c.loadStateTree(ctx, pStateID) + if err != nil { + return uint64(0), err + } powerTableView := c.createPowerTableView(pSt) totalBytes, err := powerTableView.Total(ctx) if err != nil { @@ -105,31 +116,39 @@ func (c *ChainSelector) NewWeight(ctx context.Context, ts types.TipSet, pSt stat update := new(big.Float) update.Mul(innerTerm, P) w.Add(w, update) + return types.BigToFixed(w) } // Weight returns the EC weight of this TipSet in uint64 encoded fixed point // representation. -func (c *ChainSelector) Weight(ctx context.Context, ts types.TipSet, pSt state.Tree) (uint64, error) { +func (c *ChainSelector) Weight(ctx context.Context, ts types.TipSet, pStateID cid.Cid) (uint64, error) { ctx = log.Start(ctx, "Expected.Weight") log.LogKV(ctx, "Weight", ts.String()) if ts.Len() == 1 && ts.At(0).Cid().Equals(c.genesisCid) { return uint64(0), nil } + // Compute parent weight. parentW, err := ts.ParentWeight() if err != nil { return uint64(0), err } - w, err := types.FixedToBig(parentW) if err != nil { return uint64(0), err } + if !pStateID.Defined() { + return uint64(0), errors.New("undefined state passed to chain selector weight") + } + pSt, err := c.loadStateTree(ctx, pStateID) + if err != nil { + return uint64(0), err + } powerTableView := c.createPowerTableView(pSt) - // Each block in the tipset adds ecV + ECPrm * miner_power to parent weight. + // Each block in the tipset adds ecV + ecPrm * miner_power to parent weight. totalBytes, err := powerTableView.Total(ctx) if err != nil { return uint64(0), err @@ -160,30 +179,25 @@ func (c *ChainSelector) Weight(ctx context.Context, ts types.TipSet, pSt state.T // TODO BLOCK CID CONCAT TIE BREAKER IS NOT IN THE SPEC AND SHOULD BE // EVALUATED BEFORE GETTING TO PRODUCTION. func (c *ChainSelector) IsHeavier(ctx context.Context, a, b types.TipSet, aStateID, bStateID cid.Cid) (bool, error) { - var aSt, bSt state.Tree - var err error - if aStateID.Defined() { - aSt, err = c.loadStateTree(ctx, aStateID) - if err != nil { - return false, err - } + // Select weighting function based on protocol version + aWfun, err := c.chooseWeightFunc(a) + if err != nil { + return false, err } - if bStateID.Defined() { - bSt, err = c.loadStateTree(ctx, bStateID) - if err != nil { - return false, err - } + + bWfun, err := c.chooseWeightFunc(b) + if err != nil { + return false, err } - aW, err := c.Weight(ctx, a, aSt) + aW, err := aWfun(ctx, a, aStateID) if err != nil { return false, err } - bW, err := c.Weight(ctx, b, bSt) + bW, err := bWfun(ctx, b, bStateID) if err != nil { return false, err } - // Without ties pass along the comparison. if aW != bW { return aW > bW, nil @@ -216,6 +230,22 @@ func (c *ChainSelector) IsHeavier(ctx context.Context, a, b types.TipSet, aState return cmp == 1, nil } +func (c *ChainSelector) chooseWeightFunc(ts types.TipSet) (func(context.Context, types.TipSet, cid.Cid) (uint64, error), error) { + wFun := c.Weight + h, err := ts.Height() + if err != nil { + return nil, err + } + v, err := c.pvt.VersionAt(types.NewBlockHeight(h)) + if err != nil { + return nil, err + } + if v >= version.Protocol1 { + wFun = c.NewWeight + } + return wFun, nil +} + func (c *ChainSelector) createPowerTableView(st state.Tree) PowerTableView { snapshot := c.actorState.StateTreeSnapshot(st, nil) return NewPowerTableView(snapshot) diff --git a/consensus/expected.go b/consensus/expected.go index 41362cc483..d5c4934ed7 100644 --- a/consensus/expected.go +++ b/consensus/expected.go @@ -139,13 +139,13 @@ func (c *Expected) BlockTime() time.Duration { // RunStateTransition applies the messages in a tipset to a state, and persists that new state. // It errors if the tipset was not mined according to the EC rules, or if any of the messages // in the tipset results in an error. -func (c *Expected) RunStateTransition(ctx context.Context, ts types.TipSet, tsMessages [][]*types.SignedMessage, tsReceipts [][]*types.MessageReceipt, ancestors []types.TipSet, priorStateID cid.Cid) (root cid.Cid, err error) { +func (c *Expected) RunStateTransition(ctx context.Context, ts types.TipSet, tsMessages [][]*types.SignedMessage, tsReceipts [][]*types.MessageReceipt, ancestors []types.TipSet, parentWeight uint64, priorStateID cid.Cid) (root cid.Cid, err error) { ctx, span := trace.StartSpan(ctx, "Expected.RunStateTransition") span.AddAttributes(trace.StringAttribute("tipset", ts.String())) defer tracing.AddErrorEndSpan(ctx, span, &err) for i := 0; i < ts.Len(); i++ { - if err := c.BlockValidator.ValidateSemantic(ctx, ts.At(i), &ancestors[0]); err != nil { + if err := c.BlockValidator.ValidateSemantic(ctx, ts.At(i), &ancestors[0], parentWeight); err != nil { return cid.Undef, err } } diff --git a/consensus/expected_test.go b/consensus/expected_test.go index 4495c18429..1e8e996304 100644 --- a/consensus/expected_test.go +++ b/consensus/expected_test.go @@ -117,7 +117,7 @@ func TestExpected_RunStateTransition_validateMining(t *testing.T) { emptyReceipts = append(emptyReceipts, []*types.MessageReceipt{}) } - _, err = exp.RunStateTransition(ctx, tipSet, emptyMessages, emptyReceipts, []types.TipSet{pTipSet}, blocks[0].StateRoot) + _, err = exp.RunStateTransition(ctx, tipSet, emptyMessages, emptyReceipts, []types.TipSet{pTipSet}, 0, blocks[0].StateRoot) assert.NoError(t, err) }) @@ -143,7 +143,7 @@ func TestExpected_RunStateTransition_validateMining(t *testing.T) { emptyReceipts = append(emptyReceipts, []*types.MessageReceipt{}) } - _, err = exp.RunStateTransition(ctx, tipSet, emptyMessages, emptyReceipts, []types.TipSet{pTipSet}, genesisBlock.StateRoot) + _, err = exp.RunStateTransition(ctx, tipSet, emptyMessages, emptyReceipts, []types.TipSet{pTipSet}, 0, genesisBlock.StateRoot) assert.EqualError(t, err, "block author did not win election") }) @@ -170,7 +170,7 @@ func TestExpected_RunStateTransition_validateMining(t *testing.T) { emptyReceipts = append(emptyReceipts, []*types.MessageReceipt{}) } - _, err = exp.RunStateTransition(ctx, tipSet, emptyMessages, emptyReceipts, []types.TipSet{pTipSet}, genesisBlock.StateRoot) + _, err = exp.RunStateTransition(ctx, tipSet, emptyMessages, emptyReceipts, []types.TipSet{pTipSet}, 0, genesisBlock.StateRoot) require.NotNil(t, err) assert.Contains(t, err.Error(), "invalid ticket") assert.Contains(t, err.Error(), "position 0") @@ -196,7 +196,7 @@ func TestExpected_RunStateTransition_validateMining(t *testing.T) { emptyMessages = append(emptyMessages, []*types.SignedMessage{}) emptyReceipts = append(emptyReceipts, []*types.MessageReceipt{}) - _, err = exp.RunStateTransition(ctx, tipSet, emptyMessages, emptyReceipts, []types.TipSet{pTipSet}, blocks[0].StateRoot) + _, err = exp.RunStateTransition(ctx, tipSet, emptyMessages, emptyReceipts, []types.TipSet{pTipSet}, 0, blocks[0].StateRoot) assert.Error(t, err) }) @@ -222,7 +222,7 @@ func TestExpected_RunStateTransition_validateMining(t *testing.T) { emptyReceipts = append(emptyReceipts, []*types.MessageReceipt{}) } - _, err = exp.RunStateTransition(ctx, tipSet, emptyMessages, emptyReceipts, []types.TipSet{pTipSet}, blocks[0].StateRoot) + _, err = exp.RunStateTransition(ctx, tipSet, emptyMessages, emptyReceipts, []types.TipSet{pTipSet}, 0, blocks[0].StateRoot) assert.EqualError(t, err, "block signature invalid") }) } diff --git a/consensus/protocol.go b/consensus/protocol.go index 88c40a0068..7eb9d54bc7 100644 --- a/consensus/protocol.go +++ b/consensus/protocol.go @@ -26,13 +26,13 @@ import ( type Protocol interface { // RunStateTransition returns the state root CID resulting from applying the input ts to the // prior `stateID`. It returns an error if the transition is invalid. - RunStateTransition(ctx context.Context, ts types.TipSet, tsMessages [][]*types.SignedMessage, tsReceipts [][]*types.MessageReceipt, ancestors []types.TipSet, stateID cid.Cid) (cid.Cid, error) + RunStateTransition(ctx context.Context, ts types.TipSet, tsMessages [][]*types.SignedMessage, tsReceipts [][]*types.MessageReceipt, ancestors []types.TipSet, parentWeight uint64, stateID cid.Cid) (cid.Cid, error) // ValidateSyntax validates a single block is correctly formed. ValidateSyntax(ctx context.Context, b *types.Block) error // ValidateSemantic validates a block is correctly derived from its parent. - ValidateSemantic(ctx context.Context, child *types.Block, parents *types.TipSet) error + ValidateSemantic(ctx context.Context, child *types.Block, parents *types.TipSet, parentWeight uint64) error // BlockTime returns the block time used by the consensus protocol. BlockTime() time.Duration diff --git a/consensus/weight_test.go b/consensus/weight_test.go index bf689d8a05..e3cc48d215 100644 --- a/consensus/weight_test.go +++ b/consensus/weight_test.go @@ -13,12 +13,17 @@ import ( "github.com/filecoin-project/go-filecoin/consensus" "github.com/filecoin-project/go-filecoin/state" "github.com/filecoin-project/go-filecoin/types" + "github.com/filecoin-project/go-filecoin/version" ) func TestNewWeight(t *testing.T) { cst := hamt.NewCborStore() ctx := context.Background() - fakeTree := &state.MockStateTree{} + fakeTree := state.TreeFromString(t, "test-NewWeight-StateCid", cst) + fakeRoot, err := fakeTree.Flush(ctx) + require.NoError(t, err) + pvt, err := version.ConfigureProtocolVersions(version.TEST) + require.NoError(t, err) // We only care about total power for the weight function // Total is 16, so bitlen is 5 as := consensus.NewFakeActorStateStore(types.NewBytesAmount(1), types.NewBytesAmount(16), make(map[address.Address]address.Address)) @@ -27,11 +32,11 @@ func TestNewWeight(t *testing.T) { ParentWeight: 0, Tickets: tickets, }) - sel := consensus.NewChainSelector(cst, as, types.CidFromString(t, "genesisCid")) + sel := consensus.NewChainSelector(cst, as, types.CidFromString(t, "genesisCid"), pvt) t.Run("basic happy path", func(t *testing.T) { // 0 + 1[2*1 + 5] = 7 - fixWeight, err := sel.NewWeight(ctx, toWeigh, fakeTree) + fixWeight, err := sel.NewWeight(ctx, toWeigh, fakeRoot) assert.NoError(t, err) assertEqualInt(t, 7, fixWeight) }) @@ -42,20 +47,20 @@ func TestNewWeight(t *testing.T) { asHigherX := consensus.NewFakeActorStateStore(types.NewBytesAmount(1), types.NewBytesAmount(32), make(map[address.Address]address.Address)) // Weight is 1 lower than total = 16 with total = 15 - selLower := consensus.NewChainSelector(cst, asLowerX, types.CidFromString(t, "genesisCid")) - fixWeight, err := selLower.NewWeight(ctx, toWeigh, fakeTree) + selLower := consensus.NewChainSelector(cst, asLowerX, types.CidFromString(t, "genesisCid"), pvt) + fixWeight, err := selLower.NewWeight(ctx, toWeigh, fakeRoot) assert.NoError(t, err) assertEqualInt(t, 6, fixWeight) // Weight is same as total = 16 with total = 31 - selSame := consensus.NewChainSelector(cst, asSameX, types.CidFromString(t, "genesisCid")) - fixWeight, err = selSame.NewWeight(ctx, toWeigh, fakeTree) + selSame := consensus.NewChainSelector(cst, asSameX, types.CidFromString(t, "genesisCid"), pvt) + fixWeight, err = selSame.NewWeight(ctx, toWeigh, fakeRoot) assert.NoError(t, err) assertEqualInt(t, 7, fixWeight) // Weight is 1 higher than total = 16 with total = 32 - selHigher := consensus.NewChainSelector(cst, asHigherX, types.CidFromString(t, "genesisCid")) - fixWeight, err = selHigher.NewWeight(ctx, toWeigh, fakeTree) + selHigher := consensus.NewChainSelector(cst, asHigherX, types.CidFromString(t, "genesisCid"), pvt) + fixWeight, err = selHigher.NewWeight(ctx, toWeigh, fakeRoot) assert.NoError(t, err) assertEqualInt(t, 8, fixWeight) }) @@ -69,7 +74,7 @@ func TestNewWeight(t *testing.T) { }) // 49 + 1[2*1 + 5] = 56 - fixWeight, err := sel.NewWeight(ctx, toWeighWithParent, fakeTree) + fixWeight, err := sel.NewWeight(ctx, toWeighWithParent, fakeRoot) assert.NoError(t, err) assertEqualInt(t, 56, fixWeight) }) @@ -93,7 +98,7 @@ func TestNewWeight(t *testing.T) { }, ) // 0 + 1[2*3 + 5] = 11 - fixWeight, err := sel.NewWeight(ctx, toWeighThreeBlock, fakeTree) + fixWeight, err := sel.NewWeight(ctx, toWeighThreeBlock, fakeRoot) assert.NoError(t, err) assertEqualInt(t, 11, fixWeight) }) @@ -110,7 +115,7 @@ func TestNewWeight(t *testing.T) { }) // 0 + 1[2*1 + 5] = 7 - fixWeight, err := sel.NewWeight(ctx, toWeighTwoTickets, fakeTree) + fixWeight, err := sel.NewWeight(ctx, toWeighTwoTickets, fakeRoot) assert.NoError(t, err) assertEqualInt(t, 7, fixWeight) }) @@ -135,7 +140,7 @@ func TestNewWeight(t *testing.T) { require.NoError(t, err) // 0 + ((0.87)^15)[2*1 + 5] - fixWeight, err := sel.NewWeight(ctx, toWeighFifteenNull, fakeTree) + fixWeight, err := sel.NewWeight(ctx, toWeighFifteenNull, fakeRoot) assert.NoError(t, err) assert.Equal(t, fixExpected, fixWeight) }) diff --git a/message/policy_test.go b/message/policy_test.go index bedd919e20..7c443fc879 100644 --- a/message/policy_test.go +++ b/message/policy_test.go @@ -232,7 +232,7 @@ func TestMessageQueuePolicy(t *testing.T) { types.EmptyReceipts(1), ) b.SetTicket([]byte{2}) - b.SetTimestamp(2) // Tweak if necessary to force CID ordering opposite ticket ordering. + b.SetTimestamp(6) // Tweak if necessary to force CID ordering opposite ticket ordering. }) assert.True(t, bytes.Compare(b1.Cid().Bytes(), b2.Cid().Bytes()) > 0) diff --git a/net/graphsync_fetcher_test.go b/net/graphsync_fetcher_test.go index f72dcf8e87..d4775c5e50 100644 --- a/net/graphsync_fetcher_test.go +++ b/net/graphsync_fetcher_test.go @@ -38,6 +38,7 @@ import ( th "github.com/filecoin-project/go-filecoin/testhelpers" tf "github.com/filecoin-project/go-filecoin/testhelpers/testflags" "github.com/filecoin-project/go-filecoin/types" + "github.com/filecoin-project/go-filecoin/version" ) const visitsPerBlock = 4 @@ -56,7 +57,9 @@ func TestGraphsyncFetcher(t *testing.T) { ctx := context.Background() bs := bstore.NewBlockstore(dss.MutexWrap(datastore.NewMapDatastore())) clock := th.NewFakeClock(time.Now()) - bv := consensus.NewDefaultBlockValidator(5*time.Millisecond, clock) + pvt, err := version.ConfigureProtocolVersions(version.TEST) + require.NoError(t, err) + bv := consensus.NewDefaultBlockValidator(5*time.Millisecond, clock, pvt) pid0 := th.RequireIntPeerID(t, 0) builder := chain.NewBuilder(t, address.Undef) keys := types.MustGenerateKeyInfo(1, 42) diff --git a/net/validators_test.go b/net/validators_test.go index d4b563cb1d..5235eeb856 100644 --- a/net/validators_test.go +++ b/net/validators_test.go @@ -19,6 +19,7 @@ import ( th "github.com/filecoin-project/go-filecoin/testhelpers" tf "github.com/filecoin-project/go-filecoin/testhelpers/testflags" "github.com/filecoin-project/go-filecoin/types" + "github.com/filecoin-project/go-filecoin/version" ) func TestBlockTopicValidator(t *testing.T) { @@ -62,7 +63,10 @@ func TestBlockPubSubValidation(t *testing.T) { blocktime := time.Second * 1 // setup a block validator and a topic validator - bv := consensus.NewDefaultBlockValidator(blocktime, mclock) + pvt, err := version.ConfigureProtocolVersions(version.TEST) + require.NoError(t, err) + + bv := consensus.NewDefaultBlockValidator(blocktime, mclock, pvt) btv := net.NewBlockTopicValidator(bv) // setup a floodsub instance on the host and register the topic validator diff --git a/node/builder.go b/node/builder.go index 17a49c240e..9c6c7e746f 100644 --- a/node/builder.go +++ b/node/builder.go @@ -189,12 +189,17 @@ func (b *Builder) build(ctx context.Context) (*Node, error) { return nil, errors.Wrap(err, "failed to build node.Network") } + nd.VersionTable, err = version.ConfigureProtocolVersions(nd.Network.NetworkName) + if err != nil { + return nil, err + } + nd.Blockservice, err = b.buildBlockservice(ctx, &nd.Blockstore, &nd.Network) if err != nil { return nil, errors.Wrap(err, "failed to build node.Blockservice") } - nd.Chain, err = b.buildChain(ctx, &nd.Blockstore, &nd.Network) + nd.Chain, err = b.buildChain(ctx, &nd.Blockstore, &nd.Network, nd.VersionTable) if err != nil { return nil, errors.Wrap(err, "failed to build node.Chain") } @@ -243,12 +248,6 @@ func (b *Builder) build(ctx context.Context) (*Node, error) { return nil, errors.Wrap(err, "failed to build node.FaultSlasher") } - // TODO: inject protocol upgrade table into code that requires it (#3360) - _, err = version.ConfigureProtocolVersions(nd.Network.NetworkName) - if err != nil { - return nil, err - } - nd.PorcelainAPI = porcelain.New(plumbing.New(&plumbing.APIDeps{ Bitswap: nd.Network.bitswap, Chain: nd.Chain.State, @@ -399,7 +398,7 @@ func (b *Builder) buildBlockservice(ctx context.Context, blockstore *BlockstoreS }, nil } -func (b *Builder) buildChain(ctx context.Context, blockstore *BlockstoreSubmodule, network *NetworkSubmodule) (ChainSubmodule, error) { +func (b *Builder) buildChain(ctx context.Context, blockstore *BlockstoreSubmodule, network *NetworkSubmodule, pvt *version.ProtocolVersionTable) (ChainSubmodule, error) { // initialize chain store chainStatusReporter := chain.NewStatusReporter() chainStore := chain.NewStore(b.Repo.ChainDatastore(), blockstore.cborStore, &state.TreeStateLoader{}, chainStatusReporter, b.genCid) @@ -414,7 +413,7 @@ func (b *Builder) buildChain(ctx context.Context, blockstore *BlockstoreSubmodul // setup block validation // TODO when #2961 is resolved do the needful here. - blkValid := consensus.NewDefaultBlockValidator(b.BlockTime, b.Clock) + blkValid := consensus.NewDefaultBlockValidator(b.BlockTime, b.Clock, pvt) // register block validation on floodsub btv := net.NewBlockTopicValidator(blkValid) @@ -425,7 +424,7 @@ func (b *Builder) buildChain(ctx context.Context, blockstore *BlockstoreSubmodul // set up consensus actorState := consensus.NewActorStateStore(chainStore, blockstore.cborStore, blockstore.Blockstore, processor) nodeConsensus := consensus.NewExpected(blockstore.cborStore, blockstore.Blockstore, processor, blkValid, actorState, b.genCid, b.BlockTime, consensus.ElectionMachine{}, consensus.TicketMachine{}) - nodeChainSelector := consensus.NewChainSelector(blockstore.cborStore, actorState, b.genCid) + nodeChainSelector := consensus.NewChainSelector(blockstore.cborStore, actorState, b.genCid, pvt) // setup fecher graphsyncNetwork := gsnet.NewFromLibp2pHost(network.host) diff --git a/node/helpers.go b/node/helpers.go index db24a349f8..aae629de9e 100644 --- a/node/helpers.go +++ b/node/helpers.go @@ -21,6 +21,7 @@ type nodeChainReader interface { GetHead() types.TipSetKey GetTipSet(types.TipSetKey) (types.TipSet, error) GetTipSetState(ctx context.Context, tsKey types.TipSetKey) (state.Tree, error) + GetTipSetStateRoot(tsKey types.TipSetKey) (cid.Cid, error) HeadEvents() *ps.PubSub Load(context.Context) error Stop() @@ -32,7 +33,8 @@ type nodeChainSyncer interface { } type nodeChainSelector interface { - Weight(context.Context, types.TipSet, state.Tree) (uint64, error) + NewWeight(context.Context, types.TipSet, cid.Cid) (uint64, error) + Weight(context.Context, types.TipSet, cid.Cid) (uint64, error) IsHeavier(ctx context.Context, a, b types.TipSet, aStateID, bStateID cid.Cid) (bool, error) } diff --git a/node/node.go b/node/node.go index 96c0cdd18f..5da9bcf0ed 100644 --- a/node/node.go +++ b/node/node.go @@ -9,6 +9,7 @@ import ( "time" bserv "github.com/ipfs/go-blockservice" + "github.com/ipfs/go-cid" "github.com/ipfs/go-hamt-ipld" logging "github.com/ipfs/go-log" "github.com/libp2p/go-libp2p-core/host" @@ -89,7 +90,7 @@ type Node struct { // Protocols // - VersionTable version.ProtocolVersionTable + VersionTable *version.ProtocolVersionTable HelloProtocol HelloProtocolSubmodule StorageProtocol StorageProtocolSubmodule RetrievalProtocol RetrievalProtocolSubmodule @@ -795,19 +796,34 @@ func (node *Node) getStateTree(ctx context.Context, ts types.TipSet) (state.Tree // getWeight is the default GetWeight function for the mining worker. func (node *Node) getWeight(ctx context.Context, ts types.TipSet) (uint64, error) { + h, err := ts.Height() + if err != nil { + return 0, err + } + var wFun func(context.Context, types.TipSet, cid.Cid) (uint64, error) + v, err := node.VersionTable.VersionAt(types.NewBlockHeight(h)) + if err != nil { + return 0, err + } + if v >= version.Protocol1 { + wFun = node.Chain.ChainSelector.NewWeight + } else { + wFun = node.Chain.ChainSelector.Weight + } + parent, err := ts.Parents() if err != nil { return uint64(0), err } // TODO handle genesis cid more gracefully if parent.Len() == 0 { - return node.Chain.ChainSelector.Weight(ctx, ts, nil) + return wFun(ctx, ts, cid.Undef) } - pSt, err := node.Chain.ChainReader.GetTipSetState(ctx, parent) + root, err := node.Chain.ChainReader.GetTipSetStateRoot(parent) if err != nil { return uint64(0), err } - return node.Chain.ChainSelector.Weight(ctx, ts, pSt) + return wFun(ctx, ts, root) } // getAncestors is the default GetAncestors function for the mining worker. diff --git a/state/testing.go b/state/testing.go index 484fc2c085..b43bc020a3 100644 --- a/state/testing.go +++ b/state/testing.go @@ -3,12 +3,16 @@ package state import ( "context" "fmt" + "testing" + "github.com/filecoin-project/go-filecoin/actor" "github.com/filecoin-project/go-filecoin/address" "github.com/filecoin-project/go-filecoin/exec" "github.com/ipfs/go-cid" + "github.com/ipfs/go-hamt-ipld" "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" ) // MustFlush flushes the StateTree or panics if it can't. @@ -114,3 +118,16 @@ func (m *MockStateTree) GetActorCode(c cid.Cid, protocol uint64) (exec.Executabl return a, nil } + +// TreeFromString sets a state tree based on an int. TODO: this indirection +// can be avoided when we are able to change cborStore to an interface and then +// making a test implementation of the cbor store that can map test cids to test +// states. +func TreeFromString(t *testing.T, s string, cst *hamt.CborIpldStore) Tree { + tree := NewEmptyStateTree(cst) + strAddr, err := address.NewActorAddress([]byte(s)) + require.NoError(t, err) + err = tree.SetActor(context.Background(), strAddr, &actor.Actor{}) + require.NoError(t, err) + return tree +} diff --git a/testhelpers/chain.go b/testhelpers/chain.go index 9e15f635ac..e5bfb62e47 100644 --- a/testhelpers/chain.go +++ b/testhelpers/chain.go @@ -12,12 +12,12 @@ import ( "github.com/filecoin-project/go-filecoin/address" "github.com/filecoin-project/go-filecoin/consensus" "github.com/filecoin-project/go-filecoin/repo" - "github.com/filecoin-project/go-filecoin/state" "github.com/filecoin-project/go-filecoin/types" + "github.com/filecoin-project/go-filecoin/version" ) type chainWeighter interface { - Weight(context.Context, types.TipSet, state.Tree) (uint64, error) + Weight(context.Context, types.TipSet, cid.Cid) (uint64, error) } // FakeChildParams is a wrapper for all the params needed to create fake child blocks. @@ -52,10 +52,16 @@ func MkFakeChild(params FakeChildParams) (*types.Block, error) { bs := bstore.NewBlockstore(repo.NewInMemoryRepo().Datastore()) cst := hamt.NewCborStore() processor := consensus.NewDefaultProcessor() + pvt, err := version.ConfigureProtocolVersions(version.TEST) + if err != nil { + return nil, err + } + actorState := consensus.NewActorStateStore(nil, cst, bs, processor) selector := consensus.NewChainSelector(cst, actorState, params.GenesisCid, + pvt, ) params.ConsensusChainSelection = selector return MkFakeChildWithCon(params) @@ -64,7 +70,7 @@ func MkFakeChild(params FakeChildParams) (*types.Block, error) { // MkFakeChildWithCon creates a chain with the given consensus weight function. func MkFakeChildWithCon(params FakeChildParams) (*types.Block, error) { wFun := func(ts types.TipSet) (uint64, error) { - return params.ConsensusChainSelection.Weight(context.Background(), params.Parent, nil) + return params.ConsensusChainSelection.Weight(context.Background(), params.Parent, cid.Undef) } return MkFakeChildCore(params.Parent, params.StateRoot, diff --git a/testhelpers/consensus.go b/testhelpers/consensus.go index 621db04dd6..a76bae262a 100644 --- a/testhelpers/consensus.go +++ b/testhelpers/consensus.go @@ -96,7 +96,7 @@ func NewFakeBlockValidator() *FakeBlockValidator { } // ValidateSemantic does nothing. -func (fbv *FakeBlockValidator) ValidateSemantic(ctx context.Context, child *types.Block, parents *types.TipSet) error { +func (fbv *FakeBlockValidator) ValidateSemantic(ctx context.Context, child *types.Block, parents *types.TipSet, _ uint64) error { return nil } @@ -131,7 +131,7 @@ func NewStubBlockValidator() *StubBlockValidator { } // ValidateSemantic returns nil or error for stubbed block `child`. -func (mbv *StubBlockValidator) ValidateSemantic(ctx context.Context, child *types.Block, parents *types.TipSet) error { +func (mbv *StubBlockValidator) ValidateSemantic(ctx context.Context, child *types.Block, parents *types.TipSet, _ uint64) error { return mbv.semanticStubs[child.Cid()] } diff --git a/version/protocol_versions.go b/version/protocol_versions.go index 63deb801aa..dbe8f2c106 100644 --- a/version/protocol_versions.go +++ b/version/protocol_versions.go @@ -19,14 +19,19 @@ const TEST = "go-filecoin-test" // Protocol0 is the first protocol version const Protocol0 = 0 +// Protocol1 is the weight upgrade +const Protocol1 = 1 + // ConfigureProtocolVersions configures all protocol upgrades for all known networks. // TODO: support arbitrary network names at "latest" protocol version so that only coordinated // network upgrades need to be represented here. See #3491. func ConfigureProtocolVersions(network string) (*ProtocolVersionTable, error) { return NewProtocolVersionTableBuilder(network). Add(USER, Protocol0, types.NewBlockHeight(0)). + Add(USER, Protocol1, types.NewBlockHeight(43000)). Add(DEVNET4, Protocol0, types.NewBlockHeight(0)). - Add(LOCALNET, Protocol0, types.NewBlockHeight(0)). - Add(TEST, Protocol0, types.NewBlockHeight(0)). + Add(DEVNET4, Protocol1, types.NewBlockHeight(300)). + Add(LOCALNET, Protocol1, types.NewBlockHeight(0)). + Add(TEST, Protocol1, types.NewBlockHeight(0)). Build() }