diff --git a/chain/actors/builtin/miner/actor.go.template b/chain/actors/builtin/miner/actor.go.template index 619dc699d..8c0b10cb0 100644 --- a/chain/actors/builtin/miner/actor.go.template +++ b/chain/actors/builtin/miner/actor.go.template @@ -97,9 +97,13 @@ type State interface { FindSector(abi.SectorNumber) (*SectorLocation, error) GetSectorExpiration(abi.SectorNumber) (*SectorExpiration, error) GetPrecommittedSector(abi.SectorNumber) (*SectorPreCommitOnChainInfo, error) + ForEachPrecommittedSector(func(SectorPreCommitOnChainInfo) error) error LoadSectors(sectorNos *bitfield.BitField) ([]*SectorOnChainInfo, error) NumLiveSectors() (uint64, error) IsAllocated(abi.SectorNumber) (bool, error) + // UnallocatedSectorNumbers returns up to count unallocated sector numbers (or less than + // count if there aren't enough). + UnallocatedSectorNumbers(count int) ([]abi.SectorNumber, error) // Note that ProvingPeriodStart is deprecated and will be renamed / removed in a future version of actors GetProvingPeriodStart() (abi.ChainEpoch, error) diff --git a/chain/actors/builtin/miner/miner.go b/chain/actors/builtin/miner/miner.go index 6e35d4e9f..bb7f80340 100644 --- a/chain/actors/builtin/miner/miner.go +++ b/chain/actors/builtin/miner/miner.go @@ -156,9 +156,13 @@ type State interface { FindSector(abi.SectorNumber) (*SectorLocation, error) GetSectorExpiration(abi.SectorNumber) (*SectorExpiration, error) GetPrecommittedSector(abi.SectorNumber) (*SectorPreCommitOnChainInfo, error) + ForEachPrecommittedSector(func(SectorPreCommitOnChainInfo) error) error LoadSectors(sectorNos *bitfield.BitField) ([]*SectorOnChainInfo, error) NumLiveSectors() (uint64, error) IsAllocated(abi.SectorNumber) (bool, error) + // UnallocatedSectorNumbers returns up to count unallocated sector numbers (or less than + // count if there aren't enough). + UnallocatedSectorNumbers(count int) ([]abi.SectorNumber, error) // Note that ProvingPeriodStart is deprecated and will be renamed / removed in a future version of actors GetProvingPeriodStart() (abi.ChainEpoch, error) diff --git a/chain/actors/builtin/miner/state.go.template b/chain/actors/builtin/miner/state.go.template index b7e5f40df..eb7ab00bf 100644 --- a/chain/actors/builtin/miner/state.go.template +++ b/chain/actors/builtin/miner/state.go.template @@ -8,6 +8,7 @@ import ( {{end}} "github.com/filecoin-project/go-address" "github.com/filecoin-project/go-bitfield" + rle "github.com/filecoin-project/go-bitfield/rle" "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/dline" "github.com/ipfs/go-cid" @@ -209,6 +210,26 @@ func (s *state{{.v}}) GetPrecommittedSector(num abi.SectorNumber) (*SectorPreCom return &ret, nil } +func (s *state{{.v}}) ForEachPrecommittedSector(cb func(SectorPreCommitOnChainInfo) error) error { +{{if (ge .v 3) -}} + precommitted, err := adt{{.v}}.AsMap(s.store, s.State.PreCommittedSectors, builtin{{.v}}.DefaultHamtBitwidth) +{{- else -}} + precommitted, err := adt{{.v}}.AsMap(s.store, s.State.PreCommittedSectors) +{{- end}} + if err != nil { + return err + } + + var info miner{{.v}}.SectorPreCommitOnChainInfo + if err := precommitted.ForEach(&info, func(_ string) error { + return cb(fromV{{.v}}SectorPreCommitOnChainInfo(info)) + }); err != nil { + return err + } + + return nil +} + func (s *state{{.v}}) LoadSectors(snos *bitfield.BitField) ([]*SectorOnChainInfo, error) { sectors, err := miner{{.v}}.LoadSectors(s.store, s.State.Sectors) if err != nil { @@ -242,9 +263,15 @@ func (s *state{{.v}}) LoadSectors(snos *bitfield.BitField) ([]*SectorOnChainInfo return infos, nil } -func (s *state{{.v}}) IsAllocated(num abi.SectorNumber) (bool, error) { +func (s *state{{.v}}) loadAllocatedSectorNumbers() (bitfield.BitField, error) { var allocatedSectors bitfield.BitField - if err := s.store.Get(s.store.Context(), s.State.AllocatedSectors, &allocatedSectors); err != nil { + err := s.store.Get(s.store.Context(), s.State.AllocatedSectors, &allocatedSectors) + return allocatedSectors, err +} + +func (s *state{{.v}}) IsAllocated(num abi.SectorNumber) (bool, error) { + allocatedSectors, err := s.loadAllocatedSectorNumbers() + if err != nil { return false, err } @@ -255,6 +282,42 @@ func (s *state{{.v}}) GetProvingPeriodStart() (abi.ChainEpoch, error) { return s.State.ProvingPeriodStart, nil } +func (s *state{{.v}}) UnallocatedSectorNumbers(count int) ([]abi.SectorNumber, error) { + allocatedSectors, err := s.loadAllocatedSectorNumbers() + if err != nil { + return nil, err + } + + allocatedRuns, err := allocatedSectors.RunIterator() + if err != nil { + return nil, err + } + + unallocatedRuns, err := rle.Subtract( + &rle.RunSliceIterator{Runs: []rle.Run{ {Val: true, Len: abi.MaxSectorNumber} }}, + allocatedRuns, + ) + if err != nil { + return nil, err + } + + iter, err := rle.BitsFromRuns(unallocatedRuns) + if err != nil { + return nil, err + } + + sectors := make([]abi.SectorNumber, 0, count) + for iter.HasNext() && len(sectors) < count { + nextNo, err := iter.Next() + if err != nil { + return nil, err + } + sectors = append(sectors, abi.SectorNumber(nextNo)) + } + + return sectors, nil +} + func (s *state{{.v}}) LoadDeadline(idx uint64) (Deadline, error) { dls, err := s.State.LoadDeadlines(s.store) if err != nil { diff --git a/chain/actors/builtin/miner/v0.go b/chain/actors/builtin/miner/v0.go index 344be1993..c5e887481 100644 --- a/chain/actors/builtin/miner/v0.go +++ b/chain/actors/builtin/miner/v0.go @@ -8,6 +8,7 @@ import ( "github.com/filecoin-project/go-address" "github.com/filecoin-project/go-bitfield" + rle "github.com/filecoin-project/go-bitfield/rle" "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/dline" "github.com/ipfs/go-cid" @@ -206,6 +207,22 @@ func (s *state0) GetPrecommittedSector(num abi.SectorNumber) (*SectorPreCommitOn return &ret, nil } +func (s *state0) ForEachPrecommittedSector(cb func(SectorPreCommitOnChainInfo) error) error { + precommitted, err := adt0.AsMap(s.store, s.State.PreCommittedSectors) + if err != nil { + return err + } + + var info miner0.SectorPreCommitOnChainInfo + if err := precommitted.ForEach(&info, func(_ string) error { + return cb(fromV0SectorPreCommitOnChainInfo(info)) + }); err != nil { + return err + } + + return nil +} + func (s *state0) LoadSectors(snos *bitfield.BitField) ([]*SectorOnChainInfo, error) { sectors, err := miner0.LoadSectors(s.store, s.State.Sectors) if err != nil { @@ -239,9 +256,15 @@ func (s *state0) LoadSectors(snos *bitfield.BitField) ([]*SectorOnChainInfo, err return infos, nil } -func (s *state0) IsAllocated(num abi.SectorNumber) (bool, error) { +func (s *state0) loadAllocatedSectorNumbers() (bitfield.BitField, error) { var allocatedSectors bitfield.BitField - if err := s.store.Get(s.store.Context(), s.State.AllocatedSectors, &allocatedSectors); err != nil { + err := s.store.Get(s.store.Context(), s.State.AllocatedSectors, &allocatedSectors) + return allocatedSectors, err +} + +func (s *state0) IsAllocated(num abi.SectorNumber) (bool, error) { + allocatedSectors, err := s.loadAllocatedSectorNumbers() + if err != nil { return false, err } @@ -252,6 +275,42 @@ func (s *state0) GetProvingPeriodStart() (abi.ChainEpoch, error) { return s.State.ProvingPeriodStart, nil } +func (s *state0) UnallocatedSectorNumbers(count int) ([]abi.SectorNumber, error) { + allocatedSectors, err := s.loadAllocatedSectorNumbers() + if err != nil { + return nil, err + } + + allocatedRuns, err := allocatedSectors.RunIterator() + if err != nil { + return nil, err + } + + unallocatedRuns, err := rle.Subtract( + &rle.RunSliceIterator{Runs: []rle.Run{{Val: true, Len: abi.MaxSectorNumber}}}, + allocatedRuns, + ) + if err != nil { + return nil, err + } + + iter, err := rle.BitsFromRuns(unallocatedRuns) + if err != nil { + return nil, err + } + + sectors := make([]abi.SectorNumber, 0, count) + for iter.HasNext() && len(sectors) < count { + nextNo, err := iter.Next() + if err != nil { + return nil, err + } + sectors = append(sectors, abi.SectorNumber(nextNo)) + } + + return sectors, nil +} + func (s *state0) LoadDeadline(idx uint64) (Deadline, error) { dls, err := s.State.LoadDeadlines(s.store) if err != nil { diff --git a/chain/actors/builtin/miner/v2.go b/chain/actors/builtin/miner/v2.go index 3e76d0b69..45d4a7165 100644 --- a/chain/actors/builtin/miner/v2.go +++ b/chain/actors/builtin/miner/v2.go @@ -6,6 +6,7 @@ import ( "github.com/filecoin-project/go-address" "github.com/filecoin-project/go-bitfield" + rle "github.com/filecoin-project/go-bitfield/rle" "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/dline" "github.com/ipfs/go-cid" @@ -204,6 +205,22 @@ func (s *state2) GetPrecommittedSector(num abi.SectorNumber) (*SectorPreCommitOn return &ret, nil } +func (s *state2) ForEachPrecommittedSector(cb func(SectorPreCommitOnChainInfo) error) error { + precommitted, err := adt2.AsMap(s.store, s.State.PreCommittedSectors) + if err != nil { + return err + } + + var info miner2.SectorPreCommitOnChainInfo + if err := precommitted.ForEach(&info, func(_ string) error { + return cb(fromV2SectorPreCommitOnChainInfo(info)) + }); err != nil { + return err + } + + return nil +} + func (s *state2) LoadSectors(snos *bitfield.BitField) ([]*SectorOnChainInfo, error) { sectors, err := miner2.LoadSectors(s.store, s.State.Sectors) if err != nil { @@ -237,9 +254,15 @@ func (s *state2) LoadSectors(snos *bitfield.BitField) ([]*SectorOnChainInfo, err return infos, nil } -func (s *state2) IsAllocated(num abi.SectorNumber) (bool, error) { +func (s *state2) loadAllocatedSectorNumbers() (bitfield.BitField, error) { var allocatedSectors bitfield.BitField - if err := s.store.Get(s.store.Context(), s.State.AllocatedSectors, &allocatedSectors); err != nil { + err := s.store.Get(s.store.Context(), s.State.AllocatedSectors, &allocatedSectors) + return allocatedSectors, err +} + +func (s *state2) IsAllocated(num abi.SectorNumber) (bool, error) { + allocatedSectors, err := s.loadAllocatedSectorNumbers() + if err != nil { return false, err } @@ -250,6 +273,42 @@ func (s *state2) GetProvingPeriodStart() (abi.ChainEpoch, error) { return s.State.ProvingPeriodStart, nil } +func (s *state2) UnallocatedSectorNumbers(count int) ([]abi.SectorNumber, error) { + allocatedSectors, err := s.loadAllocatedSectorNumbers() + if err != nil { + return nil, err + } + + allocatedRuns, err := allocatedSectors.RunIterator() + if err != nil { + return nil, err + } + + unallocatedRuns, err := rle.Subtract( + &rle.RunSliceIterator{Runs: []rle.Run{{Val: true, Len: abi.MaxSectorNumber}}}, + allocatedRuns, + ) + if err != nil { + return nil, err + } + + iter, err := rle.BitsFromRuns(unallocatedRuns) + if err != nil { + return nil, err + } + + sectors := make([]abi.SectorNumber, 0, count) + for iter.HasNext() && len(sectors) < count { + nextNo, err := iter.Next() + if err != nil { + return nil, err + } + sectors = append(sectors, abi.SectorNumber(nextNo)) + } + + return sectors, nil +} + func (s *state2) LoadDeadline(idx uint64) (Deadline, error) { dls, err := s.State.LoadDeadlines(s.store) if err != nil { diff --git a/chain/actors/builtin/miner/v3.go b/chain/actors/builtin/miner/v3.go index 72986233d..166abe1e7 100644 --- a/chain/actors/builtin/miner/v3.go +++ b/chain/actors/builtin/miner/v3.go @@ -6,6 +6,7 @@ import ( "github.com/filecoin-project/go-address" "github.com/filecoin-project/go-bitfield" + rle "github.com/filecoin-project/go-bitfield/rle" "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/dline" "github.com/ipfs/go-cid" @@ -206,6 +207,22 @@ func (s *state3) GetPrecommittedSector(num abi.SectorNumber) (*SectorPreCommitOn return &ret, nil } +func (s *state3) ForEachPrecommittedSector(cb func(SectorPreCommitOnChainInfo) error) error { + precommitted, err := adt3.AsMap(s.store, s.State.PreCommittedSectors, builtin3.DefaultHamtBitwidth) + if err != nil { + return err + } + + var info miner3.SectorPreCommitOnChainInfo + if err := precommitted.ForEach(&info, func(_ string) error { + return cb(fromV3SectorPreCommitOnChainInfo(info)) + }); err != nil { + return err + } + + return nil +} + func (s *state3) LoadSectors(snos *bitfield.BitField) ([]*SectorOnChainInfo, error) { sectors, err := miner3.LoadSectors(s.store, s.State.Sectors) if err != nil { @@ -239,9 +256,15 @@ func (s *state3) LoadSectors(snos *bitfield.BitField) ([]*SectorOnChainInfo, err return infos, nil } -func (s *state3) IsAllocated(num abi.SectorNumber) (bool, error) { +func (s *state3) loadAllocatedSectorNumbers() (bitfield.BitField, error) { var allocatedSectors bitfield.BitField - if err := s.store.Get(s.store.Context(), s.State.AllocatedSectors, &allocatedSectors); err != nil { + err := s.store.Get(s.store.Context(), s.State.AllocatedSectors, &allocatedSectors) + return allocatedSectors, err +} + +func (s *state3) IsAllocated(num abi.SectorNumber) (bool, error) { + allocatedSectors, err := s.loadAllocatedSectorNumbers() + if err != nil { return false, err } @@ -252,6 +275,42 @@ func (s *state3) GetProvingPeriodStart() (abi.ChainEpoch, error) { return s.State.ProvingPeriodStart, nil } +func (s *state3) UnallocatedSectorNumbers(count int) ([]abi.SectorNumber, error) { + allocatedSectors, err := s.loadAllocatedSectorNumbers() + if err != nil { + return nil, err + } + + allocatedRuns, err := allocatedSectors.RunIterator() + if err != nil { + return nil, err + } + + unallocatedRuns, err := rle.Subtract( + &rle.RunSliceIterator{Runs: []rle.Run{{Val: true, Len: abi.MaxSectorNumber}}}, + allocatedRuns, + ) + if err != nil { + return nil, err + } + + iter, err := rle.BitsFromRuns(unallocatedRuns) + if err != nil { + return nil, err + } + + sectors := make([]abi.SectorNumber, 0, count) + for iter.HasNext() && len(sectors) < count { + nextNo, err := iter.Next() + if err != nil { + return nil, err + } + sectors = append(sectors, abi.SectorNumber(nextNo)) + } + + return sectors, nil +} + func (s *state3) LoadDeadline(idx uint64) (Deadline, error) { dls, err := s.State.LoadDeadlines(s.store) if err != nil { diff --git a/chain/actors/builtin/miner/v4.go b/chain/actors/builtin/miner/v4.go index 96ed21f04..71a2b9f9d 100644 --- a/chain/actors/builtin/miner/v4.go +++ b/chain/actors/builtin/miner/v4.go @@ -6,6 +6,7 @@ import ( "github.com/filecoin-project/go-address" "github.com/filecoin-project/go-bitfield" + rle "github.com/filecoin-project/go-bitfield/rle" "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/dline" "github.com/ipfs/go-cid" @@ -206,6 +207,22 @@ func (s *state4) GetPrecommittedSector(num abi.SectorNumber) (*SectorPreCommitOn return &ret, nil } +func (s *state4) ForEachPrecommittedSector(cb func(SectorPreCommitOnChainInfo) error) error { + precommitted, err := adt4.AsMap(s.store, s.State.PreCommittedSectors, builtin4.DefaultHamtBitwidth) + if err != nil { + return err + } + + var info miner4.SectorPreCommitOnChainInfo + if err := precommitted.ForEach(&info, func(_ string) error { + return cb(fromV4SectorPreCommitOnChainInfo(info)) + }); err != nil { + return err + } + + return nil +} + func (s *state4) LoadSectors(snos *bitfield.BitField) ([]*SectorOnChainInfo, error) { sectors, err := miner4.LoadSectors(s.store, s.State.Sectors) if err != nil { @@ -239,9 +256,15 @@ func (s *state4) LoadSectors(snos *bitfield.BitField) ([]*SectorOnChainInfo, err return infos, nil } -func (s *state4) IsAllocated(num abi.SectorNumber) (bool, error) { +func (s *state4) loadAllocatedSectorNumbers() (bitfield.BitField, error) { var allocatedSectors bitfield.BitField - if err := s.store.Get(s.store.Context(), s.State.AllocatedSectors, &allocatedSectors); err != nil { + err := s.store.Get(s.store.Context(), s.State.AllocatedSectors, &allocatedSectors) + return allocatedSectors, err +} + +func (s *state4) IsAllocated(num abi.SectorNumber) (bool, error) { + allocatedSectors, err := s.loadAllocatedSectorNumbers() + if err != nil { return false, err } @@ -252,6 +275,42 @@ func (s *state4) GetProvingPeriodStart() (abi.ChainEpoch, error) { return s.State.ProvingPeriodStart, nil } +func (s *state4) UnallocatedSectorNumbers(count int) ([]abi.SectorNumber, error) { + allocatedSectors, err := s.loadAllocatedSectorNumbers() + if err != nil { + return nil, err + } + + allocatedRuns, err := allocatedSectors.RunIterator() + if err != nil { + return nil, err + } + + unallocatedRuns, err := rle.Subtract( + &rle.RunSliceIterator{Runs: []rle.Run{{Val: true, Len: abi.MaxSectorNumber}}}, + allocatedRuns, + ) + if err != nil { + return nil, err + } + + iter, err := rle.BitsFromRuns(unallocatedRuns) + if err != nil { + return nil, err + } + + sectors := make([]abi.SectorNumber, 0, count) + for iter.HasNext() && len(sectors) < count { + nextNo, err := iter.Next() + if err != nil { + return nil, err + } + sectors = append(sectors, abi.SectorNumber(nextNo)) + } + + return sectors, nil +} + func (s *state4) LoadDeadline(idx uint64) (Deadline, error) { dls, err := s.State.LoadDeadlines(s.store) if err != nil { diff --git a/chain/actors/builtin/miner/v5.go b/chain/actors/builtin/miner/v5.go index 7996acf32..568834777 100644 --- a/chain/actors/builtin/miner/v5.go +++ b/chain/actors/builtin/miner/v5.go @@ -6,6 +6,7 @@ import ( "github.com/filecoin-project/go-address" "github.com/filecoin-project/go-bitfield" + rle "github.com/filecoin-project/go-bitfield/rle" "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/dline" "github.com/ipfs/go-cid" @@ -206,6 +207,22 @@ func (s *state5) GetPrecommittedSector(num abi.SectorNumber) (*SectorPreCommitOn return &ret, nil } +func (s *state5) ForEachPrecommittedSector(cb func(SectorPreCommitOnChainInfo) error) error { + precommitted, err := adt5.AsMap(s.store, s.State.PreCommittedSectors, builtin5.DefaultHamtBitwidth) + if err != nil { + return err + } + + var info miner5.SectorPreCommitOnChainInfo + if err := precommitted.ForEach(&info, func(_ string) error { + return cb(fromV5SectorPreCommitOnChainInfo(info)) + }); err != nil { + return err + } + + return nil +} + func (s *state5) LoadSectors(snos *bitfield.BitField) ([]*SectorOnChainInfo, error) { sectors, err := miner5.LoadSectors(s.store, s.State.Sectors) if err != nil { @@ -239,9 +256,15 @@ func (s *state5) LoadSectors(snos *bitfield.BitField) ([]*SectorOnChainInfo, err return infos, nil } -func (s *state5) IsAllocated(num abi.SectorNumber) (bool, error) { +func (s *state5) loadAllocatedSectorNumbers() (bitfield.BitField, error) { var allocatedSectors bitfield.BitField - if err := s.store.Get(s.store.Context(), s.State.AllocatedSectors, &allocatedSectors); err != nil { + err := s.store.Get(s.store.Context(), s.State.AllocatedSectors, &allocatedSectors) + return allocatedSectors, err +} + +func (s *state5) IsAllocated(num abi.SectorNumber) (bool, error) { + allocatedSectors, err := s.loadAllocatedSectorNumbers() + if err != nil { return false, err } @@ -252,6 +275,42 @@ func (s *state5) GetProvingPeriodStart() (abi.ChainEpoch, error) { return s.State.ProvingPeriodStart, nil } +func (s *state5) UnallocatedSectorNumbers(count int) ([]abi.SectorNumber, error) { + allocatedSectors, err := s.loadAllocatedSectorNumbers() + if err != nil { + return nil, err + } + + allocatedRuns, err := allocatedSectors.RunIterator() + if err != nil { + return nil, err + } + + unallocatedRuns, err := rle.Subtract( + &rle.RunSliceIterator{Runs: []rle.Run{{Val: true, Len: abi.MaxSectorNumber}}}, + allocatedRuns, + ) + if err != nil { + return nil, err + } + + iter, err := rle.BitsFromRuns(unallocatedRuns) + if err != nil { + return nil, err + } + + sectors := make([]abi.SectorNumber, 0, count) + for iter.HasNext() && len(sectors) < count { + nextNo, err := iter.Next() + if err != nil { + return nil, err + } + sectors = append(sectors, abi.SectorNumber(nextNo)) + } + + return sectors, nil +} + func (s *state5) LoadDeadline(idx uint64) (Deadline, error) { dls, err := s.State.LoadDeadlines(s.store) if err != nil { diff --git a/chain/vm/vm.go b/chain/vm/vm.go index c5bfffc7f..1b8424eee 100644 --- a/chain/vm/vm.go +++ b/chain/vm/vm.go @@ -669,6 +669,12 @@ func (vm *VM) Flush(ctx context.Context) (cid.Cid, error) { return root, nil } +// Get the buffered blockstore associated with the VM. This includes any temporary blocks produced +// during thsi VM's execution. +func (vm *VM) ActorStore(ctx context.Context) adt.Store { + return adt.WrapStore(ctx, vm.cst) +} + func linksForObj(blk block.Block, cb func(cid.Cid)) error { switch blk.Cid().Prefix().Codec { case cid.DagCBOR: diff --git a/cmd/lotus-sim/create.go b/cmd/lotus-sim/create.go new file mode 100644 index 000000000..777f1723c --- /dev/null +++ b/cmd/lotus-sim/create.go @@ -0,0 +1,44 @@ +package main + +import ( + "fmt" + + "github.com/filecoin-project/lotus/chain/types" + lcli "github.com/filecoin-project/lotus/cli" + "github.com/urfave/cli/v2" +) + +var createSimCommand = &cli.Command{ + Name: "create", + ArgsUsage: "[tipset]", + Action: func(cctx *cli.Context) error { + node, err := open(cctx) + if err != nil { + return err + } + defer node.Close() + + var ts *types.TipSet + switch cctx.NArg() { + case 0: + if err := node.Chainstore.Load(); err != nil { + return err + } + ts = node.Chainstore.GetHeaviestTipSet() + case 1: + cids, err := lcli.ParseTipSetString(cctx.Args().Get(1)) + if err != nil { + return err + } + tsk := types.NewTipSetKey(cids...) + ts, err = node.Chainstore.LoadTipSet(tsk) + if err != nil { + return err + } + default: + return fmt.Errorf("expected 0 or 1 arguments") + } + _, err = node.CreateSim(cctx.Context, cctx.String("simulation"), ts) + return err + }, +} diff --git a/cmd/lotus-sim/delete.go b/cmd/lotus-sim/delete.go new file mode 100644 index 000000000..472a35a86 --- /dev/null +++ b/cmd/lotus-sim/delete.go @@ -0,0 +1,18 @@ +package main + +import ( + "github.com/urfave/cli/v2" +) + +var deleteSimCommand = &cli.Command{ + Name: "delete", + Action: func(cctx *cli.Context) error { + node, err := open(cctx) + if err != nil { + return err + } + defer node.Close() + + return node.DeleteSim(cctx.Context, cctx.String("simulation")) + }, +} diff --git a/cmd/lotus-sim/list.go b/cmd/lotus-sim/list.go new file mode 100644 index 000000000..69809b188 --- /dev/null +++ b/cmd/lotus-sim/list.go @@ -0,0 +1,35 @@ +package main + +import ( + "fmt" + "text/tabwriter" + + "github.com/urfave/cli/v2" +) + +var listSimCommand = &cli.Command{ + Name: "list", + Action: func(cctx *cli.Context) error { + node, err := open(cctx) + if err != nil { + return err + } + defer node.Close() + + list, err := node.ListSims(cctx.Context) + if err != nil { + return err + } + tw := tabwriter.NewWriter(cctx.App.Writer, 8, 8, 0, ' ', 0) + for _, name := range list { + sim, err := node.LoadSim(cctx.Context, name) + if err != nil { + return err + } + head := sim.GetHead() + fmt.Fprintf(tw, "%s\t%s\t%s\n", name, head.Height(), head.Key()) + sim.Close() + } + return tw.Flush() + }, +} diff --git a/cmd/lotus-sim/main.go b/cmd/lotus-sim/main.go new file mode 100644 index 000000000..9a4d40699 --- /dev/null +++ b/cmd/lotus-sim/main.go @@ -0,0 +1,87 @@ +package main + +import ( + "fmt" + "os" + + logging "github.com/ipfs/go-log/v2" + "github.com/urfave/cli/v2" + + "github.com/filecoin-project/lotus/chain/actors/builtin/power" + "github.com/filecoin-project/lotus/chain/stmgr" +) + +var root []*cli.Command = []*cli.Command{ + createSimCommand, + deleteSimCommand, + listSimCommand, + stepSimCommand, + setUpgradeCommand, +} + +func main() { + if _, set := os.LookupEnv("GOLOG_LOG_LEVEL"); !set { + _ = logging.SetLogLevel("simulation", "DEBUG") + } + app := &cli.App{ + Name: "lotus-sim", + Usage: "A tool to simulate a network.", + Commands: root, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "repo", + EnvVars: []string{"LOTUS_PATH"}, + Hidden: true, + Value: "~/.lotus", + }, + &cli.StringFlag{ + Name: "simulation", + Aliases: []string{"sim"}, + EnvVars: []string{"LOTUS_SIMULATION"}, + Value: "default", + }, + }, + } + + if err := app.Run(os.Args); err != nil { + fmt.Fprintf(os.Stderr, "Error: %s\n", err) + os.Exit(1) + return + } +} + +func run(cctx *cli.Context) error { + ctx := cctx.Context + + node, err := open(cctx) + if err != nil { + return err + } + defer node.Close() + + if err := node.Chainstore.Load(); err != nil { + return err + } + + ts := node.Chainstore.GetHeaviestTipSet() + + st, err := stmgr.NewStateManagerWithUpgradeSchedule(node.Chainstore, nil) + if err != nil { + return err + } + + powerTableActor, err := st.LoadActor(ctx, power.Address, ts) + if err != nil { + return err + } + powerTable, err := power.Load(node.Chainstore.ActorStore(ctx), powerTableActor) + if err != nil { + return err + } + allMiners, err := powerTable.ListAllMiners() + if err != nil { + return err + } + fmt.Printf("miner count: %d\n", len(allMiners)) + return nil +} diff --git a/cmd/lotus-sim/simulation/commit_queue.go b/cmd/lotus-sim/simulation/commit_queue.go new file mode 100644 index 000000000..957d301cf --- /dev/null +++ b/cmd/lotus-sim/simulation/commit_queue.go @@ -0,0 +1,187 @@ +package simulation + +import ( + "sort" + + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/lotus/chain/actors/builtin/miner" + "github.com/filecoin-project/lotus/chain/actors/policy" +) + +type pendingCommitTracker map[address.Address]minerPendingCommits +type minerPendingCommits map[abi.RegisteredSealProof][]abi.SectorNumber + +func (m minerPendingCommits) finish(proof abi.RegisteredSealProof, count int) { + snos := m[proof] + if len(snos) < count { + panic("not enough sector numbers to finish") + } else if len(snos) == count { + delete(m, proof) + } else { + m[proof] = snos[count:] + } +} + +func (m minerPendingCommits) empty() bool { + return len(m) == 0 +} + +func (m minerPendingCommits) count() int { + count := 0 + for _, snos := range m { + count += len(snos) + } + return count +} + +type commitQueue struct { + minerQueue []address.Address + queue []pendingCommitTracker + offset abi.ChainEpoch +} + +func (q *commitQueue) ready() int { + if len(q.queue) == 0 { + return 0 + } + count := 0 + for _, pending := range q.queue[0] { + count += pending.count() + } + return count +} + +func (q *commitQueue) nextMiner() (address.Address, minerPendingCommits, bool) { + if len(q.queue) == 0 { + return address.Undef, nil, false + } + next := q.queue[0] + + // Go through the queue and find the first non-empty batch. + for len(q.minerQueue) > 0 { + addr := q.minerQueue[0] + q.minerQueue = q.minerQueue[1:] + pending := next[addr] + if !pending.empty() { + return addr, pending, true + } + delete(next, addr) + } + + return address.Undef, nil, false +} + +func (q *commitQueue) advanceEpoch(epoch abi.ChainEpoch) { + if epoch < q.offset { + panic("cannot roll epoch backwards") + } + // Now we "roll forwards", merging each epoch we advance over with the next. + for len(q.queue) > 1 && q.offset < epoch { + curr := q.queue[0] + q.queue[0] = nil + q.queue = q.queue[1:] + q.offset++ + + next := q.queue[0] + + // Cleanup empty entries. + for addr, pending := range curr { + if pending.empty() { + delete(curr, addr) + } + } + + // If the entire level is actually empty, just skip to the next one. + if len(curr) == 0 { + continue + } + + // Otherwise, merge the next into the current. + for addr, nextPending := range next { + currPending := curr[addr] + if currPending.empty() { + curr[addr] = nextPending + continue + } + for ty, nextSnos := range nextPending { + currSnos := currPending[ty] + if len(currSnos) == 0 { + currPending[ty] = nextSnos + continue + } + currPending[ty] = append(currSnos, nextSnos...) + } + } + } + q.offset = epoch + if len(q.queue) == 0 { + return + } + + next := q.queue[0] + seenMiners := make(map[address.Address]struct{}, len(q.minerQueue)) + for _, addr := range q.minerQueue { + seenMiners[addr] = struct{}{} + } + + // Find the new miners not already in the queue. + offset := len(q.minerQueue) + for addr, pending := range next { + if pending.empty() { + delete(next, addr) + continue + } + if _, ok := seenMiners[addr]; ok { + continue + } + q.minerQueue = append(q.minerQueue, addr) + } + + // Sort the new miners only. + newMiners := q.minerQueue[offset:] + sort.Slice(newMiners, func(i, j int) bool { + // eh, escape analysis should be fine here... + return string(newMiners[i].Bytes()) < string(newMiners[j].Bytes()) + }) +} + +func (q *commitQueue) enqueueProveCommit(addr address.Address, preCommitEpoch abi.ChainEpoch, info miner.SectorPreCommitInfo) error { + // Compute the epoch at which we can start trying to commit. + preCommitDelay := policy.GetPreCommitChallengeDelay() + minCommitEpoch := preCommitEpoch + preCommitDelay + 1 + + // Figure out the offset in the queue. + i := int(minCommitEpoch - q.offset) + if i < 0 { + i = 0 + } + + // Expand capacity and insert. + if cap(q.queue) <= i { + pc := make([]pendingCommitTracker, i+1, preCommitDelay*2) + copy(pc, q.queue) + q.queue = pc + } else if len(q.queue) <= i { + q.queue = q.queue[:i+1] + } + tracker := q.queue[i] + if tracker == nil { + tracker = make(pendingCommitTracker) + q.queue[i] = tracker + } + minerPending := tracker[addr] + if minerPending == nil { + minerPending = make(minerPendingCommits) + tracker[addr] = minerPending + } + minerPending[info.SealProof] = append(minerPending[info.SealProof], info.SectorNumber) + return nil +} + +func (q *commitQueue) head() pendingCommitTracker { + if len(q.queue) > 0 { + return q.queue[0] + } + return nil +} diff --git a/cmd/lotus-sim/simulation/messages.go b/cmd/lotus-sim/simulation/messages.go new file mode 100644 index 000000000..76b100d75 --- /dev/null +++ b/cmd/lotus-sim/simulation/messages.go @@ -0,0 +1,81 @@ +package simulation + +import ( + "context" + + "golang.org/x/xerrors" + + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/go-state-types/crypto" + blockadt "github.com/filecoin-project/specs-actors/actors/util/adt" + "github.com/ipfs/go-cid" + cbg "github.com/whyrusleeping/cbor-gen" + + "github.com/filecoin-project/lotus/chain/actors/builtin" + "github.com/filecoin-project/lotus/chain/types" +) + +func toArray(store blockadt.Store, cids []cid.Cid) (cid.Cid, error) { + arr := blockadt.MakeEmptyArray(store) + for i, c := range cids { + oc := cbg.CborCid(c) + if err := arr.Set(uint64(i), &oc); err != nil { + return cid.Undef, err + } + } + return arr.Root() +} + +func (nd *Node) storeMessages(ctx context.Context, messages []*types.Message) (cid.Cid, error) { + var blsMessages, sekpMessages []cid.Cid + fakeSig := make([]byte, 32) + for _, msg := range messages { + protocol := msg.From.Protocol() + + // It's just a very convenient way to fill up accounts. + if msg.From == builtin.BurntFundsActorAddr { + protocol = address.SECP256K1 + } + switch protocol { + case address.SECP256K1: + chainMsg := &types.SignedMessage{ + Message: *msg, + Signature: crypto.Signature{ + Type: crypto.SigTypeSecp256k1, + Data: fakeSig, + }, + } + c, err := nd.Chainstore.PutMessage(chainMsg) + if err != nil { + return cid.Undef, err + } + sekpMessages = append(sekpMessages, c) + case address.BLS: + c, err := nd.Chainstore.PutMessage(msg) + if err != nil { + return cid.Undef, err + } + blsMessages = append(blsMessages, c) + default: + return cid.Undef, xerrors.Errorf("unexpected from address %q of type %d", msg.From, msg.From.Protocol()) + } + } + adtStore := nd.Chainstore.ActorStore(ctx) + blsMsgArr, err := toArray(adtStore, blsMessages) + if err != nil { + return cid.Undef, err + } + sekpMsgArr, err := toArray(adtStore, sekpMessages) + if err != nil { + return cid.Undef, err + } + + msgsCid, err := adtStore.Put(adtStore.Context(), &types.MsgMeta{ + BlsMessages: blsMsgArr, + SecpkMessages: sekpMsgArr, + }) + if err != nil { + return cid.Undef, err + } + return msgsCid, nil +} diff --git a/cmd/lotus-sim/simulation/mock.go b/cmd/lotus-sim/simulation/mock.go new file mode 100644 index 000000000..e8a7b2d4a --- /dev/null +++ b/cmd/lotus-sim/simulation/mock.go @@ -0,0 +1,136 @@ +package simulation + +import ( + "bytes" + "context" + "encoding/binary" + "fmt" + + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/go-state-types/abi" + + proof5 "github.com/filecoin-project/specs-actors/v5/actors/runtime/proof" +) + +// Ideally, we'd use extern/sector-storage/mock. Unfortunately, those mocks are a bit _too_ accurate +// and would force us to load sector info for window post proofs. + +const ( + mockSealProofPrefix = "valid seal proof:" + mockAggregateSealProofPrefix = "valid aggregate seal proof:" + mockPoStProofPrefix = "valid post proof:" +) + +func mockSealProof(proofType abi.RegisteredSealProof, minerAddr address.Address) ([]byte, error) { + plen, err := proofType.ProofSize() + if err != nil { + return nil, err + } + proof := make([]byte, plen) + i := copy(proof, mockSealProofPrefix) + binary.BigEndian.PutUint64(proof[i:], uint64(proofType)) + i += 8 + i += copy(proof[i:], minerAddr.Bytes()) + return proof, nil +} + +func mockAggregateSealProof(proofType abi.RegisteredSealProof, minerAddr address.Address, count int) ([]byte, error) { + proof := make([]byte, aggProofLen(count)) + i := copy(proof, mockAggregateSealProofPrefix) + binary.BigEndian.PutUint64(proof[i:], uint64(proofType)) + i += 8 + binary.BigEndian.PutUint64(proof[i:], uint64(count)) + i += 8 + i += copy(proof[i:], minerAddr.Bytes()) + + return proof, nil +} + +func mockWpostProof(proofType abi.RegisteredPoStProof, minerAddr address.Address) ([]byte, error) { + plen, err := proofType.ProofSize() + if err != nil { + return nil, err + } + proof := make([]byte, plen) + i := copy(proof, mockPoStProofPrefix) + i += copy(proof[i:], minerAddr.Bytes()) + return proof, nil +} + +// TODO: dedup +func aggProofLen(nproofs int) int { + switch { + case nproofs <= 8: + return 11220 + case nproofs <= 16: + return 14196 + case nproofs <= 32: + return 17172 + case nproofs <= 64: + return 20148 + case nproofs <= 128: + return 23124 + case nproofs <= 256: + return 26100 + case nproofs <= 512: + return 29076 + case nproofs <= 1024: + return 32052 + case nproofs <= 2048: + return 35028 + case nproofs <= 4096: + return 38004 + case nproofs <= 8192: + return 40980 + default: + panic("too many proofs") + } +} + +type mockVerifier struct{} + +func (mockVerifier) VerifySeal(proof proof5.SealVerifyInfo) (bool, error) { + addr, err := address.NewIDAddress(uint64(proof.Miner)) + if err != nil { + return false, err + } + mockProof, err := mockSealProof(proof.SealProof, addr) + if err != nil { + return false, err + } + return bytes.Equal(proof.Proof, mockProof), nil +} + +func (mockVerifier) VerifyAggregateSeals(aggregate proof5.AggregateSealVerifyProofAndInfos) (bool, error) { + addr, err := address.NewIDAddress(uint64(aggregate.Miner)) + if err != nil { + return false, err + } + mockProof, err := mockAggregateSealProof(aggregate.SealProof, addr, len(aggregate.Infos)) + if err != nil { + return false, err + } + return bytes.Equal(aggregate.Proof, mockProof), nil +} +func (mockVerifier) VerifyWinningPoSt(ctx context.Context, info proof5.WinningPoStVerifyInfo) (bool, error) { + panic("should not be called") +} +func (mockVerifier) VerifyWindowPoSt(ctx context.Context, info proof5.WindowPoStVerifyInfo) (bool, error) { + if len(info.Proofs) != 1 { + return false, fmt.Errorf("expected exactly one proof") + } + proof := info.Proofs[0] + addr, err := address.NewIDAddress(uint64(info.Prover)) + if err != nil { + return false, err + } + mockProof, err := mockWpostProof(proof.PoStProof, addr) + if err != nil { + return false, err + } + return bytes.Equal(proof.ProofBytes, mockProof), nil +} + +func (mockVerifier) GenerateWinningPoStSectorChallenge(context.Context, abi.RegisteredPoStProof, abi.ActorID, abi.PoStRandomness, uint64) ([]uint64, error) { + panic("should not be called") +} diff --git a/cmd/lotus-sim/simulation/node.go b/cmd/lotus-sim/simulation/node.go new file mode 100644 index 000000000..505f563e9 --- /dev/null +++ b/cmd/lotus-sim/simulation/node.go @@ -0,0 +1,167 @@ +package simulation + +import ( + "context" + "io" + "strings" + + "go.uber.org/multierr" + "golang.org/x/xerrors" + + "github.com/ipfs/go-datastore" + "github.com/ipfs/go-datastore/query" + + "github.com/filecoin-project/lotus/blockstore" + "github.com/filecoin-project/lotus/chain/stmgr" + "github.com/filecoin-project/lotus/chain/store" + "github.com/filecoin-project/lotus/chain/types" + "github.com/filecoin-project/lotus/chain/vm" + "github.com/filecoin-project/lotus/node/repo" +) + +type Node struct { + Repo repo.LockedRepo + Blockstore blockstore.Blockstore + MetadataDS datastore.Batching + Chainstore *store.ChainStore +} + +func OpenNode(ctx context.Context, path string) (*Node, error) { + var node Node + r, err := repo.NewFS(path) + if err != nil { + return nil, err + } + + node.Repo, err = r.Lock(repo.FullNode) + if err != nil { + node.Close() + return nil, err + } + + node.Blockstore, err = node.Repo.Blockstore(ctx, repo.UniversalBlockstore) + if err != nil { + node.Close() + return nil, err + } + + node.MetadataDS, err = node.Repo.Datastore(ctx, "/metadata") + if err != nil { + node.Close() + return nil, err + } + + node.Chainstore = store.NewChainStore(node.Blockstore, node.Blockstore, node.MetadataDS, vm.Syscalls(mockVerifier{}), nil) + return &node, nil +} + +func (nd *Node) Close() error { + var err error + if closer, ok := nd.Blockstore.(io.Closer); ok && closer != nil { + err = multierr.Append(err, closer.Close()) + } + if nd.MetadataDS != nil { + err = multierr.Append(err, nd.MetadataDS.Close()) + } + if nd.Repo != nil { + err = multierr.Append(err, nd.Repo.Close()) + } + return err +} + +func (nd *Node) LoadSim(ctx context.Context, name string) (*Simulation, error) { + sim := &Simulation{ + Node: nd, + name: name, + } + tskBytes, err := nd.MetadataDS.Get(sim.key("head")) + if err != nil { + return nil, xerrors.Errorf("failed to load simulation %s: %w", name, err) + } + tsk, err := types.TipSetKeyFromBytes(tskBytes) + if err != nil { + return nil, xerrors.Errorf("failed to parse simulation %s's tipset %v: %w", name, tskBytes, err) + } + sim.head, err = nd.Chainstore.LoadTipSet(tsk) + if err != nil { + return nil, xerrors.Errorf("failed to load simulation tipset %s: %w", tsk, err) + } + + err = sim.loadConfig() + if err != nil { + return nil, xerrors.Errorf("failed to load config for simulation %s: %w", name, err) + } + + us, err := sim.config.upgradeSchedule() + if err != nil { + return nil, xerrors.Errorf("failed to create upgrade schedule for simulation %s: %w", name, err) + } + sim.sm, err = stmgr.NewStateManagerWithUpgradeSchedule(nd.Chainstore, us) + if err != nil { + return nil, xerrors.Errorf("failed to create state manager for simulation %s: %w", name, err) + } + return sim, nil +} + +func (nd *Node) CreateSim(ctx context.Context, name string, head *types.TipSet) (*Simulation, error) { + if strings.Contains(name, "/") { + return nil, xerrors.Errorf("simulation name %q cannot contain a '/'", name) + } + sim := &Simulation{ + name: name, + Node: nd, + sm: stmgr.NewStateManager(nd.Chainstore), + } + if has, err := nd.MetadataDS.Has(sim.key("head")); err != nil { + return nil, err + } else if has { + return nil, xerrors.Errorf("simulation named %s already exists", name) + } + + if err := sim.SetHead(head); err != nil { + return nil, err + } + + return sim, nil +} + +func (nd *Node) ListSims(ctx context.Context) ([]string, error) { + prefix := simulationPrefix.ChildString("head").String() + items, err := nd.MetadataDS.Query(query.Query{ + Prefix: prefix, + KeysOnly: true, + Orders: []query.Order{query.OrderByKey{}}, + }) + if err != nil { + return nil, xerrors.Errorf("failed to list simulations: %w", err) + } + defer items.Close() + var names []string + for { + select { + case result, ok := <-items.Next(): + if !ok { + return names, nil + } + if result.Error != nil { + return nil, xerrors.Errorf("failed to retrieve next simulation: %w", result.Error) + } + names = append(names, strings.TrimPrefix(result.Key, prefix+"/")) + case <-ctx.Done(): + return nil, ctx.Err() + } + } +} + +func (nd *Node) DeleteSim(ctx context.Context, name string) error { + // TODO: make this a bit more generic? + keys := []datastore.Key{ + simulationPrefix.ChildString("head").ChildString(name), + simulationPrefix.ChildString("config").ChildString(name), + } + var err error + for _, key := range keys { + err = multierr.Append(err, nd.MetadataDS.Delete(key)) + } + return err +} diff --git a/cmd/lotus-sim/simulation/power.go b/cmd/lotus-sim/simulation/power.go new file mode 100644 index 000000000..9a64c3f3a --- /dev/null +++ b/cmd/lotus-sim/simulation/power.go @@ -0,0 +1,58 @@ +package simulation + +import ( + "context" + + "golang.org/x/xerrors" + + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/go-state-types/big" + + "github.com/filecoin-project/lotus/chain/actors/builtin/power" +) + +type powerInfo struct { + powerLookback, powerNow abi.StoragePower +} + +// Load all power claims at the given height. +func (sim *Simulation) loadClaims(ctx context.Context, height abi.ChainEpoch) (map[address.Address]power.Claim, error) { + powerTable := make(map[address.Address]power.Claim) + store := sim.Chainstore.ActorStore(ctx) + + ts, err := sim.Chainstore.GetTipsetByHeight(ctx, height, sim.head, true) + if err != nil { + return nil, xerrors.Errorf("when projecting growth, failed to lookup lookback epoch: %w", err) + } + + powerActor, err := sim.sm.LoadActor(ctx, power.Address, ts) + if err != nil { + return nil, err + } + + powerState, err := power.Load(store, powerActor) + if err != nil { + return nil, err + } + err = powerState.ForEachClaim(func(miner address.Address, claim power.Claim) error { + powerTable[miner] = claim + return nil + }) + if err != nil { + return nil, err + } + return powerTable, nil +} + +// Compute the number of sectors a miner has from their power claim. +func sectorsFromClaim(sectorSize abi.SectorSize, c power.Claim) int64 { + if c.RawBytePower.Int == nil { + return 0 + } + sectorCount := big.Div(c.RawBytePower, big.NewIntUnsigned(uint64(sectorSize))) + if !sectorCount.IsInt64() { + panic("impossible number of sectors") + } + return sectorCount.Int64() +} diff --git a/cmd/lotus-sim/simulation/precommit.go b/cmd/lotus-sim/simulation/precommit.go new file mode 100644 index 000000000..1ede3d5c4 --- /dev/null +++ b/cmd/lotus-sim/simulation/precommit.go @@ -0,0 +1,205 @@ +package simulation + +import ( + "context" + "fmt" + + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/go-state-types/big" + "github.com/filecoin-project/go-state-types/network" + "github.com/filecoin-project/lotus/chain/actors" + "github.com/filecoin-project/lotus/chain/actors/builtin" + "github.com/filecoin-project/lotus/chain/actors/builtin/miner" + "github.com/filecoin-project/lotus/chain/actors/policy" + "github.com/filecoin-project/lotus/chain/types" + "github.com/ipfs/go-cid" + "golang.org/x/xerrors" + + miner5 "github.com/filecoin-project/specs-actors/v5/actors/builtin/miner" + tutils "github.com/filecoin-project/specs-actors/v5/support/testing" +) + +func makeCommR(minerAddr address.Address, sno abi.SectorNumber) cid.Cid { + return tutils.MakeCID(fmt.Sprintf("%s:%d", minerAddr, sno), &miner5.SealedCIDPrefix) +} + +var ( + targetFunds = abi.TokenAmount(types.MustParseFIL("1000FIL")) + minFunds = abi.TokenAmount(types.MustParseFIL("100FIL")) +) + +func (ss *simulationState) packPreCommits(ctx context.Context, cb packFunc) (full bool, _err error) { + var top1Count, top10Count, restCount int + defer func() { + if _err != nil { + return + } + log.Debugw("packed pre commits", + "done", top1Count+top10Count+restCount, + "top1", top1Count, + "top10", top10Count, + "rest", restCount, + "filled-block", full, + ) + }() + + var top1Miners, top10Miners, restMiners int + for i := 0; ; i++ { + var ( + minerAddr address.Address + count *int + ) + switch { + case (i%3) <= 0 && top1Miners < ss.minerDist.top1.len(): + count = &top1Count + minerAddr = ss.minerDist.top1.next() + top1Miners++ + case (i%3) <= 1 && top10Miners < ss.minerDist.top10.len(): + count = &top10Count + minerAddr = ss.minerDist.top10.next() + top10Miners++ + case (i%3) <= 2 && restMiners < ss.minerDist.rest.len(): + count = &restCount + minerAddr = ss.minerDist.rest.next() + restMiners++ + default: + // Well, we've run through all miners. + return false, nil + } + added, full, err := ss.packPreCommitsMiner(ctx, cb, minerAddr, maxProveCommitBatchSize) + if err != nil { + return false, xerrors.Errorf("failed to pack precommits for miner %s: %w", minerAddr, err) + } + *count += added + if full { + return true, nil + } + } +} + +func (ss *simulationState) packPreCommitsMiner(ctx context.Context, cb packFunc, minerAddr address.Address, count int) (int, bool, error) { + epoch := ss.nextEpoch() + nv := ss.sm.GetNtwkVersion(ctx, epoch) + st, err := ss.stateTree(ctx) + if err != nil { + return 0, false, err + } + actor, err := st.GetActor(minerAddr) + if err != nil { + return 0, false, err + } + minerState, err := miner.Load(ss.Chainstore.ActorStore(ctx), actor) + if err != nil { + return 0, false, err + } + + minerInfo, err := ss.getMinerInfo(ctx, minerAddr) + if err != nil { + return 0, false, err + } + + // Make sure the miner is funded. + minerBalance, err := minerState.AvailableBalance(actor.Balance) + if err != nil { + return 0, false, err + } + + if big.Cmp(minerBalance, minFunds) < 0 { + full, err := cb(&types.Message{ + From: builtin.BurntFundsActorAddr, + To: minerAddr, + Value: targetFunds, + Method: builtin.MethodSend, + }) + if err != nil { + return 0, false, xerrors.Errorf("failed to fund miner %s: %w", minerAddr, err) + } + if full { + return 0, true, nil + } + } + + sealType, err := miner.PreferredSealProofTypeFromWindowPoStType( + nv, minerInfo.WindowPoStProofType, + ) + if err != nil { + return 0, false, err + } + + sectorNos, err := minerState.UnallocatedSectorNumbers(count) + if err != nil { + return 0, false, err + } + + expiration := epoch + policy.GetMaxSectorExpirationExtension() + infos := make([]miner.SectorPreCommitInfo, len(sectorNos)) + for i, sno := range sectorNos { + infos[i] = miner.SectorPreCommitInfo{ + SealProof: sealType, + SectorNumber: sno, + SealedCID: makeCommR(minerAddr, sno), + SealRandEpoch: epoch - 1, + Expiration: expiration, + } + } + added := 0 + if nv >= network.Version13 { + targetBatchSize := maxPreCommitBatchSize + for targetBatchSize >= minPreCommitBatchSize && len(infos) >= minPreCommitBatchSize { + batch := infos + if len(batch) > targetBatchSize { + batch = batch[:targetBatchSize] + } + params := miner5.PreCommitSectorBatchParams{ + Sectors: batch, + } + enc, err := actors.SerializeParams(¶ms) + if err != nil { + return 0, false, err + } + if full, err := sendAndFund(cb, &types.Message{ + To: minerAddr, + From: minerInfo.Worker, + Value: abi.NewTokenAmount(0), + Method: miner.Methods.PreCommitSectorBatch, + Params: enc, + }); err != nil { + return 0, false, err + } else if full { + // try again with a smaller batch. + targetBatchSize /= 2 + continue + } + + for _, info := range batch { + if err := ss.commitQueue.enqueueProveCommit(minerAddr, epoch, info); err != nil { + return 0, false, err + } + added++ + } + infos = infos[len(batch):] + } + } + for _, info := range infos { + enc, err := actors.SerializeParams(&info) + if err != nil { + return 0, false, err + } + if full, err := sendAndFund(cb, &types.Message{ + To: minerAddr, + From: minerInfo.Worker, + Value: abi.NewTokenAmount(0), + Method: miner.Methods.PreCommitSector, + Params: enc, + }); full || err != nil { + return added, full, err + } + + if err := ss.commitQueue.enqueueProveCommit(minerAddr, epoch, info); err != nil { + return 0, false, err + } + added++ + } + return added, false, nil +} diff --git a/cmd/lotus-sim/simulation/provecommit.go b/cmd/lotus-sim/simulation/provecommit.go new file mode 100644 index 000000000..0d855bcd1 --- /dev/null +++ b/cmd/lotus-sim/simulation/provecommit.go @@ -0,0 +1,219 @@ +package simulation + +import ( + "context" + + "github.com/filecoin-project/go-bitfield" + "golang.org/x/xerrors" + + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/go-state-types/exitcode" + "github.com/filecoin-project/go-state-types/network" + "github.com/filecoin-project/lotus/chain/actors" + "github.com/filecoin-project/lotus/chain/actors/aerrors" + "github.com/filecoin-project/lotus/chain/actors/builtin" + "github.com/filecoin-project/lotus/chain/actors/builtin/miner" + "github.com/filecoin-project/lotus/chain/actors/policy" + "github.com/filecoin-project/lotus/chain/types" + + miner5 "github.com/filecoin-project/specs-actors/v5/actors/builtin/miner" + power5 "github.com/filecoin-project/specs-actors/v5/actors/builtin/power" +) + +func (ss *simulationState) packProveCommits(ctx context.Context, cb packFunc) (_full bool, _err error) { + ss.commitQueue.advanceEpoch(ss.nextEpoch()) + + var failed, done, unbatched, count int + defer func() { + if _err != nil { + return + } + remaining := ss.commitQueue.ready() + log.Debugw("packed prove commits", + "remaining", remaining, + "done", done, + "failed", failed, + "unbatched", unbatched, + "miners-processed", count, + "filled-block", _full, + ) + }() + + for { + addr, pending, ok := ss.commitQueue.nextMiner() + if !ok { + return false, nil + } + + res, full, err := ss.packProveCommitsMiner(ctx, cb, addr, pending) + if err != nil { + return false, err + } + failed += res.failed + done += res.done + unbatched += res.unbatched + count++ + if full { + return true, nil + } + } +} + +type proveCommitResult struct { + done, failed, unbatched int +} + +func sendAndFund(send packFunc, msg *types.Message) (bool, error) { + full, err := send(msg) + aerr, ok := err.(aerrors.ActorError) + if !ok || aerr.RetCode() != exitcode.ErrInsufficientFunds { + return full, err + } + // Ok, insufficient funds. Let's fund this miner and try again. + full, err = send(&types.Message{ + From: builtin.BurntFundsActorAddr, + To: msg.To, + Value: targetFunds, + Method: builtin.MethodSend, + }) + if err != nil { + return false, xerrors.Errorf("failed to fund %s: %w", msg.To, err) + } + // ok, nothing's going to work. + if full { + return true, nil + } + return send(msg) +} + +// Enqueue a single prove commit from the given miner. +func (ss *simulationState) packProveCommitsMiner( + ctx context.Context, cb packFunc, minerAddr address.Address, + pending minerPendingCommits, +) (res proveCommitResult, full bool, _err error) { + info, err := ss.getMinerInfo(ctx, minerAddr) + if err != nil { + return res, false, err + } + + nv := ss.sm.GetNtwkVersion(ctx, ss.nextEpoch()) + for sealType, snos := range pending { + if nv >= network.Version13 { + for len(snos) > minProveCommitBatchSize { + batchSize := maxProveCommitBatchSize + if len(snos) < batchSize { + batchSize = len(snos) + } + batch := snos[:batchSize] + snos = snos[batchSize:] + + proof, err := mockAggregateSealProof(sealType, minerAddr, batchSize) + if err != nil { + return res, false, err + } + + params := miner5.ProveCommitAggregateParams{ + SectorNumbers: bitfield.New(), + AggregateProof: proof, + } + for _, sno := range batch { + params.SectorNumbers.Set(uint64(sno)) + } + + enc, err := actors.SerializeParams(¶ms) + if err != nil { + return res, false, err + } + + if full, err := sendAndFund(cb, &types.Message{ + From: info.Worker, + To: minerAddr, + Value: abi.NewTokenAmount(0), + Method: miner.Methods.ProveCommitAggregate, + Params: enc, + }); err != nil { + // If we get a random error, or a fatal actor error, bail. + // Otherwise, just log it. + if aerr, ok := err.(aerrors.ActorError); !ok || aerr.IsFatal() { + return res, false, err + } + log.Errorw("failed to prove commit sector(s)", + "error", err, + "miner", minerAddr, + "sectors", batch, + "epoch", ss.nextEpoch(), + ) + res.failed += batchSize + } else if full { + return res, true, nil + } else { + res.done += batchSize + } + pending.finish(sealType, batchSize) + } + } + for len(snos) > 0 && res.unbatched < power5.MaxMinerProveCommitsPerEpoch { + sno := snos[0] + snos = snos[1:] + + proof, err := mockSealProof(sealType, minerAddr) + if err != nil { + return res, false, err + } + params := miner.ProveCommitSectorParams{ + SectorNumber: sno, + Proof: proof, + } + enc, err := actors.SerializeParams(¶ms) + if err != nil { + return res, false, err + } + if full, err := sendAndFund(cb, &types.Message{ + From: info.Worker, + To: minerAddr, + Value: abi.NewTokenAmount(0), + Method: miner.Methods.ProveCommitSector, + Params: enc, + }); err != nil { + if aerr, ok := err.(aerrors.ActorError); !ok || aerr.IsFatal() { + return res, false, err + } + log.Errorw("failed to prove commit sector(s)", + "error", err, + "miner", minerAddr, + "sectors", []abi.SectorNumber{sno}, + "epoch", ss.nextEpoch(), + ) + res.failed++ + } else if full { + return res, true, nil + } else { + res.unbatched++ + res.done++ + } + // mark it as "finished" regardless so we skip it. + pending.finish(sealType, 1) + } + // if we get here, we can't pre-commit anything more. + } + return res, false, nil +} + +// Enqueue all pending prove-commits for the given miner. +func (ss *simulationState) loadProveCommitsMiner(ctx context.Context, addr address.Address, minerState miner.State) error { + // Find all pending prove commits and group by proof type. Really, there should never + // (except during upgrades be more than one type. + nextEpoch := ss.nextEpoch() + nv := ss.sm.GetNtwkVersion(ctx, nextEpoch) + av := actors.VersionForNetwork(nv) + + return minerState.ForEachPrecommittedSector(func(info miner.SectorPreCommitOnChainInfo) error { + msd := policy.GetMaxProveCommitDuration(av, info.Info.SealProof) + if nextEpoch > info.PreCommitEpoch+msd { + log.Warnw("dropping old pre-commit") + return nil + } + return ss.commitQueue.enqueueProveCommit(addr, info.PreCommitEpoch, info.Info) + }) +} diff --git a/cmd/lotus-sim/simulation/simulation.go b/cmd/lotus-sim/simulation/simulation.go new file mode 100644 index 000000000..ac205e1c3 --- /dev/null +++ b/cmd/lotus-sim/simulation/simulation.go @@ -0,0 +1,274 @@ +package simulation + +import ( + "context" + "crypto/sha256" + "encoding/binary" + "encoding/json" + "time" + + "golang.org/x/xerrors" + + "github.com/ipfs/go-datastore" + logging "github.com/ipfs/go-log/v2" + + "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/go-state-types/crypto" + "github.com/filecoin-project/go-state-types/network" + "github.com/filecoin-project/lotus/build" + "github.com/filecoin-project/lotus/chain/actors/builtin" + "github.com/filecoin-project/lotus/chain/state" + "github.com/filecoin-project/lotus/chain/stmgr" + "github.com/filecoin-project/lotus/chain/types" + + miner5 "github.com/filecoin-project/specs-actors/v5/actors/builtin/miner" +) + +var log = logging.Logger("simulation") + +const onboardingProjectionLookback = 2 * 7 * builtin.EpochsInDay // lookback two weeks + +const ( + minPreCommitBatchSize = 1 + maxPreCommitBatchSize = miner5.PreCommitSectorBatchMaxSize + minProveCommitBatchSize = 4 + maxProveCommitBatchSize = miner5.MaxAggregatedSectors +) + +type config struct { + Upgrades map[network.Version]abi.ChainEpoch +} + +func (c *config) upgradeSchedule() (stmgr.UpgradeSchedule, error) { + upgradeSchedule := stmgr.DefaultUpgradeSchedule() + expected := make(map[network.Version]struct{}, len(c.Upgrades)) + for nv := range c.Upgrades { + expected[nv] = struct{}{} + } + newUpgradeSchedule := upgradeSchedule[:0] + for _, upgrade := range upgradeSchedule { + if height, ok := c.Upgrades[upgrade.Network]; ok { + delete(expected, upgrade.Network) + if height < 0 { + continue + } + upgrade.Height = height + } + newUpgradeSchedule = append(newUpgradeSchedule, upgrade) + } + if len(expected) > 0 { + missing := make([]network.Version, 0, len(expected)) + for nv := range expected { + missing = append(missing, nv) + } + return nil, xerrors.Errorf("unknown network versions %v in config", missing) + } + return newUpgradeSchedule, nil +} + +type Simulation struct { + *Node + + name string + config config + sm *stmgr.StateManager + + // head + st *state.StateTree + head *types.TipSet + + // lazy-loaded state + // access through `simState(ctx)` to load on-demand. + state *simulationState +} + +func (sim *Simulation) loadConfig() error { + configBytes, err := sim.MetadataDS.Get(sim.key("config")) + if err == nil { + err = json.Unmarshal(configBytes, &sim.config) + } + switch err { + case nil: + case datastore.ErrNotFound: + sim.config = config{} + default: + return xerrors.Errorf("failed to load config: %w", err) + } + return nil +} + +func (sim *Simulation) stateTree(ctx context.Context) (*state.StateTree, error) { + if sim.st == nil { + st, _, err := sim.sm.TipSetState(ctx, sim.head) + if err != nil { + return nil, err + } + sim.st, err = sim.sm.StateTree(st) + if err != nil { + return nil, err + } + } + return sim.st, nil +} + +// Loads the simulation state. The state is memoized so this will be fast except the first time. +func (sim *Simulation) simState(ctx context.Context) (*simulationState, error) { + if sim.state == nil { + log.Infow("loading simulation") + state, err := loadSimulationState(ctx, sim) + if err != nil { + return nil, xerrors.Errorf("failed to load simulation state: %w", err) + } + sim.state = state + log.Infow("simulation loaded", "miners", len(sim.state.minerInfos)) + } + + return sim.state, nil +} + +var simulationPrefix = datastore.NewKey("/simulation") + +func (sim *Simulation) key(subkey string) datastore.Key { + return simulationPrefix.ChildString(subkey).ChildString(sim.name) +} + +// Load loads the simulation state. This will happen automatically on first use, but it can be +// useful to preload for timing reasons. +func (sim *Simulation) Load(ctx context.Context) error { + _, err := sim.simState(ctx) + return err +} + +func (sim *Simulation) GetHead() *types.TipSet { + return sim.head +} + +func (sim *Simulation) SetHead(head *types.TipSet) error { + if err := sim.MetadataDS.Put(sim.key("head"), head.Key().Bytes()); err != nil { + return xerrors.Errorf("failed to store simulation head: %w", err) + } + sim.st = nil // we'll compute this on-demand. + sim.head = head + return nil +} + +func (sim *Simulation) Name() string { + return sim.name +} + +func (sim *Simulation) postChainCommitInfo(ctx context.Context, epoch abi.ChainEpoch) (abi.Randomness, error) { + commitRand, err := sim.Chainstore.GetChainRandomness( + ctx, sim.head.Cids(), crypto.DomainSeparationTag_PoStChainCommit, epoch, nil, true) + return commitRand, err +} + +const beaconPrefix = "mockbeacon:" + +func (sim *Simulation) nextBeaconEntries() []types.BeaconEntry { + parentBeacons := sim.head.Blocks()[0].BeaconEntries + lastBeacon := parentBeacons[len(parentBeacons)-1] + beaconRound := lastBeacon.Round + 1 + + buf := make([]byte, len(beaconPrefix)+8) + copy(buf, beaconPrefix) + binary.BigEndian.PutUint64(buf[len(beaconPrefix):], beaconRound) + beaconRand := sha256.Sum256(buf) + return []types.BeaconEntry{{ + Round: beaconRound, + Data: beaconRand[:], + }} +} + +func (sim *Simulation) nextTicket() *types.Ticket { + newProof := sha256.Sum256(sim.head.MinTicket().VRFProof) + return &types.Ticket{ + VRFProof: newProof[:], + } +} + +func (sim *Simulation) makeTipSet(ctx context.Context, messages []*types.Message) (*types.TipSet, error) { + parentTs := sim.head + parentState, parentRec, err := sim.sm.TipSetState(ctx, parentTs) + if err != nil { + return nil, xerrors.Errorf("failed to compute parent tipset: %w", err) + } + msgsCid, err := sim.storeMessages(ctx, messages) + if err != nil { + return nil, xerrors.Errorf("failed to store block messages: %w", err) + } + + uts := parentTs.MinTimestamp() + build.BlockDelaySecs + + blks := []*types.BlockHeader{{ + Miner: parentTs.MinTicketBlock().Miner, // keep reusing the same miner. + Ticket: sim.nextTicket(), + BeaconEntries: sim.nextBeaconEntries(), + Parents: parentTs.Cids(), + Height: parentTs.Height() + 1, + ParentStateRoot: parentState, + ParentMessageReceipts: parentRec, + Messages: msgsCid, + ParentBaseFee: baseFee, + Timestamp: uts, + ElectionProof: &types.ElectionProof{WinCount: 1}, + }} + err = sim.Chainstore.PersistBlockHeaders(blks...) + if err != nil { + return nil, xerrors.Errorf("failed to persist block headers: %w", err) + } + newTipSet, err := types.NewTipSet(blks) + if err != nil { + return nil, xerrors.Errorf("failed to create new tipset: %w", err) + } + now := time.Now() + _, _, err = sim.sm.TipSetState(ctx, newTipSet) + if err != nil { + return nil, xerrors.Errorf("failed to compute new tipset: %w", err) + } + duration := time.Since(now) + log.Infow("computed tipset", "duration", duration, "height", newTipSet.Height()) + + return newTipSet, nil +} + +func (sim *Simulation) SetUpgradeHeight(nv network.Version, epoch abi.ChainEpoch) (_err error) { + if epoch <= sim.head.Height() { + return xerrors.Errorf("cannot set upgrade height in the past (%d <= %d)", epoch, sim.head.Height()) + } + + if sim.config.Upgrades == nil { + sim.config.Upgrades = make(map[network.Version]abi.ChainEpoch, 1) + } + + sim.config.Upgrades[nv] = epoch + defer func() { + if _err != nil { + // try to restore the old config on error. + _ = sim.loadConfig() + } + }() + + newUpgradeSchedule, err := sim.config.upgradeSchedule() + if err != nil { + return err + } + sm, err := stmgr.NewStateManagerWithUpgradeSchedule(sim.Chainstore, newUpgradeSchedule) + if err != nil { + return err + } + err = sim.saveConfig() + if err != nil { + return err + } + + sim.sm = sm + return nil +} + +func (sim *Simulation) saveConfig() error { + buf, err := json.Marshal(sim.config) + if err != nil { + return err + } + return sim.MetadataDS.Put(sim.key("config"), buf) +} diff --git a/cmd/lotus-sim/simulation/state.go b/cmd/lotus-sim/simulation/state.go new file mode 100644 index 000000000..ee664166e --- /dev/null +++ b/cmd/lotus-sim/simulation/state.go @@ -0,0 +1,190 @@ +package simulation + +import ( + "context" + "math/rand" + "sort" + + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/lotus/chain/actors/builtin/miner" + "github.com/filecoin-project/lotus/chain/types" +) + +type perm struct { + miners []address.Address + offset int +} + +func (p *perm) shuffle() { + rand.Shuffle(len(p.miners), func(i, j int) { + p.miners[i], p.miners[j] = p.miners[j], p.miners[i] + }) +} + +func (p *perm) next() address.Address { + next := p.miners[p.offset] + p.offset++ + p.offset %= len(p.miners) + return next +} + +func (p *perm) add(addr address.Address) { + p.miners = append(p.miners, addr) +} + +func (p *perm) len() int { + return len(p.miners) +} + +type simulationState struct { + *Simulation + + // TODO Ideally we'd "learn" this distribution from the network. But this is good enough for + // now. The tiers represent the top 1%, top 10%, and everyone else. When sealing sectors, we + // seal a group of sectors for the top 1%, a group (half that size) for the top 10%, and one + // sector for everyone else. We really should pick a better algorithm. + minerDist struct { + top1, top10, rest perm + } + + // We track the window post periods per miner and assume that no new miners are ever added. + wpostPeriods map[int][]address.Address // (epoch % (epochs in a deadline)) -> miner + // We cache all miner infos for active miners and assume no new miners join. + minerInfos map[address.Address]*miner.MinerInfo + + // We record all pending window post messages, and the epoch up through which we've + // generated window post messages. + pendingWposts []*types.Message + nextWpostEpoch abi.ChainEpoch + + commitQueue commitQueue +} + +func loadSimulationState(ctx context.Context, sim *Simulation) (*simulationState, error) { + state := &simulationState{Simulation: sim} + currentEpoch := sim.head.Height() + + // Lookup the current power table and the power table 2 weeks ago (for onboarding rate + // projections). + currentPowerTable, err := sim.loadClaims(ctx, currentEpoch) + if err != nil { + return nil, err + } + + var lookbackEpoch abi.ChainEpoch + //if epoch > onboardingProjectionLookback { + // lookbackEpoch = epoch - onboardingProjectionLookback + //} + // TODO: Fixme? I really want this to not suck with snapshots. + lookbackEpoch = 770139 // hard coded for now. + lookbackPowerTable, err := sim.loadClaims(ctx, lookbackEpoch) + if err != nil { + return nil, err + } + + // Now load miner state info. + store := sim.Chainstore.ActorStore(ctx) + st, err := sim.stateTree(ctx) + if err != nil { + return nil, err + } + + type onboardingInfo struct { + addr address.Address + onboardingRate uint64 + } + + commitRand, err := sim.postChainCommitInfo(ctx, currentEpoch) + if err != nil { + return nil, err + } + + sealList := make([]onboardingInfo, 0, len(currentPowerTable)) + state.wpostPeriods = make(map[int][]address.Address, miner.WPoStChallengeWindow) + state.minerInfos = make(map[address.Address]*miner.MinerInfo, len(currentPowerTable)) + state.commitQueue.advanceEpoch(state.nextEpoch()) + for addr, claim := range currentPowerTable { + // Load the miner state. + minerActor, err := st.GetActor(addr) + if err != nil { + return nil, err + } + + minerState, err := miner.Load(store, minerActor) + if err != nil { + return nil, err + } + + info, err := minerState.Info() + if err != nil { + return nil, err + } + state.minerInfos[addr] = &info + + // Queue up PoSts + err = state.stepWindowPoStsMiner(ctx, addr, minerState, currentEpoch, commitRand) + if err != nil { + return nil, err + } + + // Qeueu up any pending prove commits. + err = state.loadProveCommitsMiner(ctx, addr, minerState) + if err != nil { + return nil, err + } + + // Record when we need to prove for this miner. + dinfo, err := minerState.DeadlineInfo(state.nextEpoch()) + if err != nil { + return nil, err + } + dinfo = dinfo.NextNotElapsed() + + ppOffset := int(dinfo.PeriodStart % miner.WPoStChallengeWindow) + state.wpostPeriods[ppOffset] = append(state.wpostPeriods[ppOffset], addr) + + sectorsAdded := sectorsFromClaim(info.SectorSize, claim) + if lookbackClaim, ok := lookbackPowerTable[addr]; !ok { + sectorsAdded -= sectorsFromClaim(info.SectorSize, lookbackClaim) + } + + // NOTE: power _could_ have been lost, but that's too much of a pain to care + // about. We _could_ look for faulty power by iterating through all + // deadlines, but I'd rather not. + if sectorsAdded > 0 { + sealList = append(sealList, onboardingInfo{addr, uint64(sectorsAdded)}) + } + } + // We're already done loading for the _next_ epoch. + // Next time, we need to load for the next, next epoch. + // TODO: fix this insanity. + state.nextWpostEpoch = state.nextEpoch() + 1 + + // Now that we have a list of sealing miners, sort them into percentiles. + sort.Slice(sealList, func(i, j int) bool { + return sealList[i].onboardingRate < sealList[j].onboardingRate + }) + + for i, oi := range sealList { + var dist *perm + if i < len(sealList)/100 { + dist = &state.minerDist.top1 + } else if i < len(sealList)/10 { + dist = &state.minerDist.top10 + } else { + dist = &state.minerDist.rest + } + dist.add(oi.addr) + } + + state.minerDist.top1.shuffle() + state.minerDist.top10.shuffle() + state.minerDist.rest.shuffle() + + return state, nil +} + +func (ss *simulationState) nextEpoch() abi.ChainEpoch { + return ss.GetHead().Height() + 1 +} diff --git a/cmd/lotus-sim/simulation/step.go b/cmd/lotus-sim/simulation/step.go new file mode 100644 index 000000000..b44f3be4d --- /dev/null +++ b/cmd/lotus-sim/simulation/step.go @@ -0,0 +1,196 @@ +package simulation + +import ( + "context" + "reflect" + "runtime" + "strings" + + "github.com/filecoin-project/go-address" + "golang.org/x/xerrors" + + "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/lotus/build" + "github.com/filecoin-project/lotus/chain/actors/builtin/account" + "github.com/filecoin-project/lotus/chain/state" + "github.com/filecoin-project/lotus/chain/stmgr" + "github.com/filecoin-project/lotus/chain/store" + "github.com/filecoin-project/lotus/chain/types" + "github.com/filecoin-project/lotus/chain/vm" +) + +const ( + expectedBlocks = 5 + // TODO: This will produce invalid blocks but it will accurately model the amount of gas + // we're willing to use per-tipset. + // A more correct approach would be to produce 5 blocks. We can do that later. + targetGas = build.BlockGasTarget * expectedBlocks +) + +var baseFee = abi.NewTokenAmount(0) + +// Step steps the simulation forward one step. This may move forward by more than one epoch. +func (sim *Simulation) Step(ctx context.Context) (*types.TipSet, error) { + state, err := sim.simState(ctx) + if err != nil { + return nil, err + } + ts, err := state.step(ctx) + if err != nil { + return nil, xerrors.Errorf("failed to step simulation: %w", err) + } + return ts, nil +} + +func (ss *simulationState) step(ctx context.Context) (*types.TipSet, error) { + log.Infow("step", "epoch", ss.head.Height()+1) + messages, err := ss.popNextMessages(ctx) + if err != nil { + return nil, xerrors.Errorf("failed to select messages for block: %w", err) + } + head, err := ss.makeTipSet(ctx, messages) + if err != nil { + return nil, xerrors.Errorf("failed to make tipset: %w", err) + } + if err := ss.SetHead(head); err != nil { + return nil, xerrors.Errorf("failed to update head: %w", err) + } + return head, nil +} + +type packFunc func(*types.Message) (full bool, err error) +type messageGenerator func(ctx context.Context, cb packFunc) (full bool, err error) + +func (ss *simulationState) popNextMessages(ctx context.Context) ([]*types.Message, error) { + parentTs := ss.head + parentState, _, err := ss.sm.TipSetState(ctx, parentTs) + if err != nil { + return nil, err + } + nextHeight := parentTs.Height() + 1 + prevVer := ss.sm.GetNtwkVersion(ctx, nextHeight-1) + nextVer := ss.sm.GetNtwkVersion(ctx, nextHeight) + if nextVer != prevVer { + // So... we _could_ actually run the migration, but that's a pain. It's easier to + // just have an empty block then let the state manager run the migration as normal. + log.Warnw("packing no messages for version upgrade block", + "old", prevVer, + "new", nextVer, + "epoch", nextHeight, + ) + return nil, nil + } + + // Then we need to execute messages till we run out of gas. Those messages will become the + // block's messages. + r := store.NewChainRand(ss.sm.ChainStore(), parentTs.Cids()) + // TODO: Factor this out maybe? + vmopt := &vm.VMOpts{ + StateBase: parentState, + Epoch: nextHeight, + Rand: r, + Bstore: ss.sm.ChainStore().StateBlockstore(), + Syscalls: ss.sm.ChainStore().VMSys(), + CircSupplyCalc: ss.sm.GetVMCirculatingSupply, + NtwkVersion: ss.sm.GetNtwkVersion, + BaseFee: abi.NewTokenAmount(0), // FREE! + LookbackState: stmgr.LookbackStateGetterForTipset(ss.sm, parentTs), + } + vmi, err := vm.NewVM(ctx, vmopt) + if err != nil { + return nil, err + } + // TODO: This is the wrong store and may not include important state for what we're doing + // here.... + // Maybe we just track nonces separately? Yeah, probably better that way. + vmStore := vmi.ActorStore(ctx) + var gasTotal int64 + var messages []*types.Message + tryPushMsg := func(msg *types.Message) (bool, error) { + if gasTotal >= targetGas { + return true, nil + } + + // Copy the message before we start mutating it. + msgCpy := *msg + msg = &msgCpy + st := vmi.StateTree().(*state.StateTree) + + actor, err := st.GetActor(msg.From) + if err != nil { + return false, err + } + msg.Nonce = actor.Nonce + if msg.From.Protocol() == address.ID { + state, err := account.Load(vmStore, actor) + if err != nil { + return false, err + } + msg.From, err = state.PubkeyAddress() + if err != nil { + return false, err + } + } + + // TODO: Our gas estimation is broken for payment channels due to horrible hacks in + // gasEstimateGasLimit. + if msg.Value == types.EmptyInt { + msg.Value = abi.NewTokenAmount(0) + } + msg.GasPremium = abi.NewTokenAmount(0) + msg.GasFeeCap = abi.NewTokenAmount(0) + msg.GasLimit = build.BlockGasLimit + + // We manually snapshot so we can revert nonce changes, etc. on failure. + st.Snapshot(ctx) + defer st.ClearSnapshot() + + ret, err := vmi.ApplyMessage(ctx, msg) + if err != nil { + _ = st.Revert() + return false, err + } + if ret.ActorErr != nil { + _ = st.Revert() + return false, ret.ActorErr + } + + // Sometimes there are bugs. Let's catch them. + if ret.GasUsed == 0 { + _ = st.Revert() + return false, xerrors.Errorf("used no gas", + "msg", msg, + "ret", ret, + ) + } + + // TODO: consider applying overestimation? We're likely going to "over pack" here by + // ~25% because we're too accurate. + + // Did we go over? Yes, revert. + newTotal := gasTotal + ret.GasUsed + if newTotal > targetGas { + _ = st.Revert() + return true, nil + } + gasTotal = newTotal + + // Update the gas limit. + msg.GasLimit = ret.GasUsed + + messages = append(messages, msg) + return false, nil + } + for _, mgen := range []messageGenerator{ss.packWindowPoSts, ss.packProveCommits, ss.packPreCommits} { + if full, err := mgen(ctx, tryPushMsg); err != nil { + name := runtime.FuncForPC(reflect.ValueOf(mgen).Pointer()).Name() + lastDot := strings.LastIndexByte(name, '.') + fName := name[lastDot+1 : len(name)-3] + return nil, xerrors.Errorf("when packing messages with %s: %w", fName, err) + } else if full { + break + } + } + + return messages, nil +} diff --git a/cmd/lotus-sim/simulation/wdpost.go b/cmd/lotus-sim/simulation/wdpost.go new file mode 100644 index 000000000..7abb9a83a --- /dev/null +++ b/cmd/lotus-sim/simulation/wdpost.go @@ -0,0 +1,253 @@ +package simulation + +import ( + "context" + "math" + "time" + + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/lotus/chain/actors" + "github.com/filecoin-project/lotus/chain/actors/aerrors" + "github.com/filecoin-project/lotus/chain/actors/builtin/miner" + "github.com/filecoin-project/lotus/chain/types" + proof5 "github.com/filecoin-project/specs-actors/v5/actors/runtime/proof" + "golang.org/x/xerrors" +) + +func (ss *simulationState) getMinerInfo(ctx context.Context, addr address.Address) (*miner.MinerInfo, error) { + minerInfo, ok := ss.minerInfos[addr] + if !ok { + st, err := ss.stateTree(ctx) + if err != nil { + return nil, err + } + act, err := st.GetActor(addr) + if err != nil { + return nil, err + } + minerState, err := miner.Load(ss.Chainstore.ActorStore(ctx), act) + if err != nil { + return nil, err + } + info, err := minerState.Info() + if err != nil { + return nil, err + } + minerInfo = &info + ss.minerInfos[addr] = minerInfo + } + return minerInfo, nil +} + +func (ss *simulationState) packWindowPoSts(ctx context.Context, cb packFunc) (full bool, _err error) { + // Push any new window posts into the queue. + if err := ss.queueWindowPoSts(ctx); err != nil { + return false, err + } + done := 0 + failed := 0 + defer func() { + if _err != nil { + return + } + + log.Debugw("packed window posts", + "epoch", ss.nextEpoch(), + "done", done, + "failed", failed, + "remaining", len(ss.pendingWposts), + ) + }() + // Then pack as many as we can. + for len(ss.pendingWposts) > 0 { + next := ss.pendingWposts[0] + if full, err := cb(next); err != nil { + if aerr, ok := err.(aerrors.ActorError); !ok || aerr.IsFatal() { + return false, err + } + log.Errorw("failed to submit windowed post", + "error", err, + "miner", next.To, + "epoch", ss.nextEpoch(), + ) + failed++ + } else if full { + return true, nil + } else { + done++ + } + + ss.pendingWposts = ss.pendingWposts[1:] + } + ss.pendingWposts = nil + return false, nil +} + +// Enqueue all missing window posts for the current epoch for the given miner. +func (ss *simulationState) stepWindowPoStsMiner( + ctx context.Context, + addr address.Address, minerState miner.State, + commitEpoch abi.ChainEpoch, commitRand abi.Randomness, +) error { + + if active, err := minerState.DeadlineCronActive(); err != nil { + return err + } else if !active { + return nil + } + + minerInfo, err := ss.getMinerInfo(ctx, addr) + if err != nil { + return err + } + + di, err := minerState.DeadlineInfo(ss.nextEpoch()) + if err != nil { + return err + } + di = di.NextNotElapsed() + + dl, err := minerState.LoadDeadline(di.Index) + if err != nil { + return err + } + + provenBf, err := dl.PartitionsPoSted() + if err != nil { + return err + } + proven, err := provenBf.AllMap(math.MaxUint64) + if err != nil { + return err + } + + var ( + partitions []miner.PoStPartition + partitionGroups [][]miner.PoStPartition + ) + // Only prove partitions with live sectors. + err = dl.ForEachPartition(func(idx uint64, part miner.Partition) error { + if proven[idx] { + return nil + } + // TODO: set this to the actual limit from specs-actors. + // NOTE: We're mimicing the behavior of wdpost_run.go here. + if len(partitions) > 0 && idx%4 == 0 { + partitionGroups = append(partitionGroups, partitions) + partitions = nil + + } + live, err := part.LiveSectors() + if err != nil { + return err + } + liveCount, err := live.Count() + if err != nil { + return err + } + faulty, err := part.FaultySectors() + if err != nil { + return err + } + faultyCount, err := faulty.Count() + if err != nil { + return err + } + if liveCount-faultyCount > 0 { + partitions = append(partitions, miner.PoStPartition{Index: idx}) + } + return nil + }) + if err != nil { + return err + } + if len(partitions) > 0 { + partitionGroups = append(partitionGroups, partitions) + partitions = nil + } + + proof, err := mockWpostProof(minerInfo.WindowPoStProofType, addr) + if err != nil { + return err + } + for _, group := range partitionGroups { + params := miner.SubmitWindowedPoStParams{ + Deadline: di.Index, + Partitions: group, + Proofs: []proof5.PoStProof{{ + PoStProof: minerInfo.WindowPoStProofType, + ProofBytes: proof, + }}, + ChainCommitEpoch: commitEpoch, + ChainCommitRand: commitRand, + } + enc, aerr := actors.SerializeParams(¶ms) + if aerr != nil { + return xerrors.Errorf("could not serialize submit window post parameters: %w", aerr) + } + msg := &types.Message{ + To: addr, + From: minerInfo.Worker, + Method: miner.Methods.SubmitWindowedPoSt, + Params: enc, + Value: types.NewInt(0), + } + ss.pendingWposts = append(ss.pendingWposts, msg) + } + return nil +} + +// Enqueue missing window posts for all miners with deadlines opening at the current epoch. +func (ss *simulationState) queueWindowPoSts(ctx context.Context) error { + targetHeight := ss.nextEpoch() + + st, err := ss.stateTree(ctx) + if err != nil { + return err + } + + now := time.Now() + was := len(ss.pendingWposts) + count := 0 + defer func() { + log.Debugw("computed window posts", + "miners", count, + "count", len(ss.pendingWposts)-was, + "duration", time.Since(now), + ) + }() + + // Perform a bit of catch up. This lets us do things like skip blocks at upgrades then catch + // up to make the simualtion easier. + for ; ss.nextWpostEpoch <= targetHeight; ss.nextWpostEpoch++ { + if ss.nextWpostEpoch+miner.WPoStChallengeWindow < targetHeight { + log.Warnw("skipping old window post", "epoch", ss.nextWpostEpoch) + continue + } + commitEpoch := ss.nextWpostEpoch - 1 + commitRand, err := ss.postChainCommitInfo(ctx, commitEpoch) + if err != nil { + return err + } + + store := ss.Chainstore.ActorStore(ctx) + + for _, addr := range ss.wpostPeriods[int(ss.nextWpostEpoch%miner.WPoStChallengeWindow)] { + minerActor, err := st.GetActor(addr) + if err != nil { + return err + } + minerState, err := miner.Load(store, minerActor) + if err != nil { + return err + } + if err := ss.stepWindowPoStsMiner(ctx, addr, minerState, commitEpoch, commitRand); err != nil { + return err + } + count++ + } + + } + return nil +} diff --git a/cmd/lotus-sim/step.go b/cmd/lotus-sim/step.go new file mode 100644 index 000000000..c2dc3f9e2 --- /dev/null +++ b/cmd/lotus-sim/step.go @@ -0,0 +1,46 @@ +package main + +import ( + "fmt" + + "github.com/urfave/cli/v2" +) + +var stepSimCommand = &cli.Command{ + Name: "step", + Flags: []cli.Flag{ + &cli.IntFlag{ + Name: "epochs", + Usage: "Advance at least the given number of epochs.", + Value: 1, + }, + }, + Action: func(cctx *cli.Context) error { + node, err := open(cctx) + if err != nil { + return err + } + defer node.Close() + + sim, err := node.LoadSim(cctx.Context, cctx.String("simulation")) + if err != nil { + return err + } + fmt.Fprintln(cctx.App.Writer, "loading simulation") + err = sim.Load(cctx.Context) + if err != nil { + return err + } + fmt.Fprintln(cctx.App.Writer, "running simulation") + targetEpochs := cctx.Int("epochs") + for i := 0; i < targetEpochs; i++ { + ts, err := sim.Step(cctx.Context) + if err != nil { + return err + } + fmt.Fprintf(cctx.App.Writer, "advanced to %d %s\n", ts.Height(), ts.Key()) + } + fmt.Fprintln(cctx.App.Writer, "simulation done") + return err + }, +} diff --git a/cmd/lotus-sim/upgrade.go b/cmd/lotus-sim/upgrade.go new file mode 100644 index 000000000..9fd25cb7d --- /dev/null +++ b/cmd/lotus-sim/upgrade.go @@ -0,0 +1,53 @@ +package main + +import ( + "fmt" + "strconv" + "strings" + + "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/go-state-types/network" + "github.com/urfave/cli/v2" +) + +var setUpgradeCommand = &cli.Command{ + Name: "set-upgrade", + ArgsUsage: " [+]", + Description: "Set a network upgrade height. prefix with '+' to set it relative to the last epoch.", + Action: func(cctx *cli.Context) error { + args := cctx.Args() + if args.Len() != 2 { + return fmt.Errorf("expected 2 arguments") + } + nvString := args.Get(0) + networkVersion, err := strconv.ParseInt(nvString, 10, 64) + if err != nil { + return fmt.Errorf("failed to parse network version %q: %w", nvString, err) + } + heightString := args.Get(1) + relative := false + if strings.HasPrefix(heightString, "+") { + heightString = heightString[1:] + relative = true + } + height, err := strconv.ParseInt(heightString, 10, 64) + if err != nil { + return fmt.Errorf("failed to parse height version %q: %w", heightString, err) + } + + node, err := open(cctx) + if err != nil { + return err + } + defer node.Close() + + sim, err := node.LoadSim(cctx.Context, cctx.String("simulation")) + if err != nil { + return err + } + if relative { + height += int64(sim.GetHead().Height()) + } + return sim.SetUpgradeHeight(network.Version(networkVersion), abi.ChainEpoch(height)) + }, +} diff --git a/cmd/lotus-sim/util.go b/cmd/lotus-sim/util.go new file mode 100644 index 000000000..cd15cca0d --- /dev/null +++ b/cmd/lotus-sim/util.go @@ -0,0 +1,18 @@ +package main + +import ( + "fmt" + + "github.com/urfave/cli/v2" + + "github.com/filecoin-project/lotus/cmd/lotus-sim/simulation" + "github.com/filecoin-project/lotus/lib/ulimit" +) + +func open(cctx *cli.Context) (*simulation.Node, error) { + _, _, err := ulimit.ManageFdLimit() + if err != nil { + fmt.Fprintf(cctx.App.ErrWriter, "ERROR: failed to raise ulimit: %s\n", err) + } + return simulation.OpenNode(cctx.Context, cctx.String("repo")) +}