feat: implement lotus-sim

This commit is contained in:
Steven Allen 2021-05-18 17:01:30 -07:00
parent 715176698f
commit e2f5c494b0
27 changed files with 2651 additions and 12 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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:

44
cmd/lotus-sim/create.go Normal file
View File

@ -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
},
}

18
cmd/lotus-sim/delete.go Normal file
View File

@ -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"))
},
}

35
cmd/lotus-sim/list.go Normal file
View File

@ -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()
},
}

87
cmd/lotus-sim/main.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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")
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -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(&params)
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
}

View File

@ -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(&params)
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(&params)
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)
})
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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(&params)
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
}

46
cmd/lotus-sim/step.go Normal file
View File

@ -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
},
}

53
cmd/lotus-sim/upgrade.go Normal file
View File

@ -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: "<network-version> [+]<epochs>",
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))
},
}

18
cmd/lotus-sim/util.go Normal file
View File

@ -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"))
}