feat: implement lotus-sim
This commit is contained in:
parent
715176698f
commit
e2f5c494b0
@ -97,9 +97,13 @@ type State interface {
|
|||||||
FindSector(abi.SectorNumber) (*SectorLocation, error)
|
FindSector(abi.SectorNumber) (*SectorLocation, error)
|
||||||
GetSectorExpiration(abi.SectorNumber) (*SectorExpiration, error)
|
GetSectorExpiration(abi.SectorNumber) (*SectorExpiration, error)
|
||||||
GetPrecommittedSector(abi.SectorNumber) (*SectorPreCommitOnChainInfo, error)
|
GetPrecommittedSector(abi.SectorNumber) (*SectorPreCommitOnChainInfo, error)
|
||||||
|
ForEachPrecommittedSector(func(SectorPreCommitOnChainInfo) error) error
|
||||||
LoadSectors(sectorNos *bitfield.BitField) ([]*SectorOnChainInfo, error)
|
LoadSectors(sectorNos *bitfield.BitField) ([]*SectorOnChainInfo, error)
|
||||||
NumLiveSectors() (uint64, error)
|
NumLiveSectors() (uint64, error)
|
||||||
IsAllocated(abi.SectorNumber) (bool, 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
|
// Note that ProvingPeriodStart is deprecated and will be renamed / removed in a future version of actors
|
||||||
GetProvingPeriodStart() (abi.ChainEpoch, error)
|
GetProvingPeriodStart() (abi.ChainEpoch, error)
|
||||||
|
@ -156,9 +156,13 @@ type State interface {
|
|||||||
FindSector(abi.SectorNumber) (*SectorLocation, error)
|
FindSector(abi.SectorNumber) (*SectorLocation, error)
|
||||||
GetSectorExpiration(abi.SectorNumber) (*SectorExpiration, error)
|
GetSectorExpiration(abi.SectorNumber) (*SectorExpiration, error)
|
||||||
GetPrecommittedSector(abi.SectorNumber) (*SectorPreCommitOnChainInfo, error)
|
GetPrecommittedSector(abi.SectorNumber) (*SectorPreCommitOnChainInfo, error)
|
||||||
|
ForEachPrecommittedSector(func(SectorPreCommitOnChainInfo) error) error
|
||||||
LoadSectors(sectorNos *bitfield.BitField) ([]*SectorOnChainInfo, error)
|
LoadSectors(sectorNos *bitfield.BitField) ([]*SectorOnChainInfo, error)
|
||||||
NumLiveSectors() (uint64, error)
|
NumLiveSectors() (uint64, error)
|
||||||
IsAllocated(abi.SectorNumber) (bool, 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
|
// Note that ProvingPeriodStart is deprecated and will be renamed / removed in a future version of actors
|
||||||
GetProvingPeriodStart() (abi.ChainEpoch, error)
|
GetProvingPeriodStart() (abi.ChainEpoch, error)
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
{{end}}
|
{{end}}
|
||||||
"github.com/filecoin-project/go-address"
|
"github.com/filecoin-project/go-address"
|
||||||
"github.com/filecoin-project/go-bitfield"
|
"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/abi"
|
||||||
"github.com/filecoin-project/go-state-types/dline"
|
"github.com/filecoin-project/go-state-types/dline"
|
||||||
"github.com/ipfs/go-cid"
|
"github.com/ipfs/go-cid"
|
||||||
@ -209,6 +210,26 @@ func (s *state{{.v}}) GetPrecommittedSector(num abi.SectorNumber) (*SectorPreCom
|
|||||||
return &ret, nil
|
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) {
|
func (s *state{{.v}}) LoadSectors(snos *bitfield.BitField) ([]*SectorOnChainInfo, error) {
|
||||||
sectors, err := miner{{.v}}.LoadSectors(s.store, s.State.Sectors)
|
sectors, err := miner{{.v}}.LoadSectors(s.store, s.State.Sectors)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -242,9 +263,15 @@ func (s *state{{.v}}) LoadSectors(snos *bitfield.BitField) ([]*SectorOnChainInfo
|
|||||||
return infos, nil
|
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
|
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
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -255,6 +282,42 @@ func (s *state{{.v}}) GetProvingPeriodStart() (abi.ChainEpoch, error) {
|
|||||||
return s.State.ProvingPeriodStart, nil
|
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) {
|
func (s *state{{.v}}) LoadDeadline(idx uint64) (Deadline, error) {
|
||||||
dls, err := s.State.LoadDeadlines(s.store)
|
dls, err := s.State.LoadDeadlines(s.store)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/filecoin-project/go-address"
|
"github.com/filecoin-project/go-address"
|
||||||
"github.com/filecoin-project/go-bitfield"
|
"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/abi"
|
||||||
"github.com/filecoin-project/go-state-types/dline"
|
"github.com/filecoin-project/go-state-types/dline"
|
||||||
"github.com/ipfs/go-cid"
|
"github.com/ipfs/go-cid"
|
||||||
@ -206,6 +207,22 @@ func (s *state0) GetPrecommittedSector(num abi.SectorNumber) (*SectorPreCommitOn
|
|||||||
return &ret, nil
|
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) {
|
func (s *state0) LoadSectors(snos *bitfield.BitField) ([]*SectorOnChainInfo, error) {
|
||||||
sectors, err := miner0.LoadSectors(s.store, s.State.Sectors)
|
sectors, err := miner0.LoadSectors(s.store, s.State.Sectors)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -239,9 +256,15 @@ func (s *state0) LoadSectors(snos *bitfield.BitField) ([]*SectorOnChainInfo, err
|
|||||||
return infos, nil
|
return infos, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *state0) IsAllocated(num abi.SectorNumber) (bool, error) {
|
func (s *state0) loadAllocatedSectorNumbers() (bitfield.BitField, error) {
|
||||||
var allocatedSectors bitfield.BitField
|
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
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,6 +275,42 @@ func (s *state0) GetProvingPeriodStart() (abi.ChainEpoch, error) {
|
|||||||
return s.State.ProvingPeriodStart, nil
|
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) {
|
func (s *state0) LoadDeadline(idx uint64) (Deadline, error) {
|
||||||
dls, err := s.State.LoadDeadlines(s.store)
|
dls, err := s.State.LoadDeadlines(s.store)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/filecoin-project/go-address"
|
"github.com/filecoin-project/go-address"
|
||||||
"github.com/filecoin-project/go-bitfield"
|
"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/abi"
|
||||||
"github.com/filecoin-project/go-state-types/dline"
|
"github.com/filecoin-project/go-state-types/dline"
|
||||||
"github.com/ipfs/go-cid"
|
"github.com/ipfs/go-cid"
|
||||||
@ -204,6 +205,22 @@ func (s *state2) GetPrecommittedSector(num abi.SectorNumber) (*SectorPreCommitOn
|
|||||||
return &ret, nil
|
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) {
|
func (s *state2) LoadSectors(snos *bitfield.BitField) ([]*SectorOnChainInfo, error) {
|
||||||
sectors, err := miner2.LoadSectors(s.store, s.State.Sectors)
|
sectors, err := miner2.LoadSectors(s.store, s.State.Sectors)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -237,9 +254,15 @@ func (s *state2) LoadSectors(snos *bitfield.BitField) ([]*SectorOnChainInfo, err
|
|||||||
return infos, nil
|
return infos, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *state2) IsAllocated(num abi.SectorNumber) (bool, error) {
|
func (s *state2) loadAllocatedSectorNumbers() (bitfield.BitField, error) {
|
||||||
var allocatedSectors bitfield.BitField
|
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
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -250,6 +273,42 @@ func (s *state2) GetProvingPeriodStart() (abi.ChainEpoch, error) {
|
|||||||
return s.State.ProvingPeriodStart, nil
|
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) {
|
func (s *state2) LoadDeadline(idx uint64) (Deadline, error) {
|
||||||
dls, err := s.State.LoadDeadlines(s.store)
|
dls, err := s.State.LoadDeadlines(s.store)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/filecoin-project/go-address"
|
"github.com/filecoin-project/go-address"
|
||||||
"github.com/filecoin-project/go-bitfield"
|
"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/abi"
|
||||||
"github.com/filecoin-project/go-state-types/dline"
|
"github.com/filecoin-project/go-state-types/dline"
|
||||||
"github.com/ipfs/go-cid"
|
"github.com/ipfs/go-cid"
|
||||||
@ -206,6 +207,22 @@ func (s *state3) GetPrecommittedSector(num abi.SectorNumber) (*SectorPreCommitOn
|
|||||||
return &ret, nil
|
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) {
|
func (s *state3) LoadSectors(snos *bitfield.BitField) ([]*SectorOnChainInfo, error) {
|
||||||
sectors, err := miner3.LoadSectors(s.store, s.State.Sectors)
|
sectors, err := miner3.LoadSectors(s.store, s.State.Sectors)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -239,9 +256,15 @@ func (s *state3) LoadSectors(snos *bitfield.BitField) ([]*SectorOnChainInfo, err
|
|||||||
return infos, nil
|
return infos, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *state3) IsAllocated(num abi.SectorNumber) (bool, error) {
|
func (s *state3) loadAllocatedSectorNumbers() (bitfield.BitField, error) {
|
||||||
var allocatedSectors bitfield.BitField
|
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
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,6 +275,42 @@ func (s *state3) GetProvingPeriodStart() (abi.ChainEpoch, error) {
|
|||||||
return s.State.ProvingPeriodStart, nil
|
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) {
|
func (s *state3) LoadDeadline(idx uint64) (Deadline, error) {
|
||||||
dls, err := s.State.LoadDeadlines(s.store)
|
dls, err := s.State.LoadDeadlines(s.store)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/filecoin-project/go-address"
|
"github.com/filecoin-project/go-address"
|
||||||
"github.com/filecoin-project/go-bitfield"
|
"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/abi"
|
||||||
"github.com/filecoin-project/go-state-types/dline"
|
"github.com/filecoin-project/go-state-types/dline"
|
||||||
"github.com/ipfs/go-cid"
|
"github.com/ipfs/go-cid"
|
||||||
@ -206,6 +207,22 @@ func (s *state4) GetPrecommittedSector(num abi.SectorNumber) (*SectorPreCommitOn
|
|||||||
return &ret, nil
|
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) {
|
func (s *state4) LoadSectors(snos *bitfield.BitField) ([]*SectorOnChainInfo, error) {
|
||||||
sectors, err := miner4.LoadSectors(s.store, s.State.Sectors)
|
sectors, err := miner4.LoadSectors(s.store, s.State.Sectors)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -239,9 +256,15 @@ func (s *state4) LoadSectors(snos *bitfield.BitField) ([]*SectorOnChainInfo, err
|
|||||||
return infos, nil
|
return infos, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *state4) IsAllocated(num abi.SectorNumber) (bool, error) {
|
func (s *state4) loadAllocatedSectorNumbers() (bitfield.BitField, error) {
|
||||||
var allocatedSectors bitfield.BitField
|
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
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,6 +275,42 @@ func (s *state4) GetProvingPeriodStart() (abi.ChainEpoch, error) {
|
|||||||
return s.State.ProvingPeriodStart, nil
|
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) {
|
func (s *state4) LoadDeadline(idx uint64) (Deadline, error) {
|
||||||
dls, err := s.State.LoadDeadlines(s.store)
|
dls, err := s.State.LoadDeadlines(s.store)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/filecoin-project/go-address"
|
"github.com/filecoin-project/go-address"
|
||||||
"github.com/filecoin-project/go-bitfield"
|
"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/abi"
|
||||||
"github.com/filecoin-project/go-state-types/dline"
|
"github.com/filecoin-project/go-state-types/dline"
|
||||||
"github.com/ipfs/go-cid"
|
"github.com/ipfs/go-cid"
|
||||||
@ -206,6 +207,22 @@ func (s *state5) GetPrecommittedSector(num abi.SectorNumber) (*SectorPreCommitOn
|
|||||||
return &ret, nil
|
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) {
|
func (s *state5) LoadSectors(snos *bitfield.BitField) ([]*SectorOnChainInfo, error) {
|
||||||
sectors, err := miner5.LoadSectors(s.store, s.State.Sectors)
|
sectors, err := miner5.LoadSectors(s.store, s.State.Sectors)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -239,9 +256,15 @@ func (s *state5) LoadSectors(snos *bitfield.BitField) ([]*SectorOnChainInfo, err
|
|||||||
return infos, nil
|
return infos, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *state5) IsAllocated(num abi.SectorNumber) (bool, error) {
|
func (s *state5) loadAllocatedSectorNumbers() (bitfield.BitField, error) {
|
||||||
var allocatedSectors bitfield.BitField
|
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
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,6 +275,42 @@ func (s *state5) GetProvingPeriodStart() (abi.ChainEpoch, error) {
|
|||||||
return s.State.ProvingPeriodStart, nil
|
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) {
|
func (s *state5) LoadDeadline(idx uint64) (Deadline, error) {
|
||||||
dls, err := s.State.LoadDeadlines(s.store)
|
dls, err := s.State.LoadDeadlines(s.store)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -669,6 +669,12 @@ func (vm *VM) Flush(ctx context.Context) (cid.Cid, error) {
|
|||||||
return root, nil
|
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 {
|
func linksForObj(blk block.Block, cb func(cid.Cid)) error {
|
||||||
switch blk.Cid().Prefix().Codec {
|
switch blk.Cid().Prefix().Codec {
|
||||||
case cid.DagCBOR:
|
case cid.DagCBOR:
|
||||||
|
44
cmd/lotus-sim/create.go
Normal file
44
cmd/lotus-sim/create.go
Normal 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
18
cmd/lotus-sim/delete.go
Normal 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
35
cmd/lotus-sim/list.go
Normal 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
87
cmd/lotus-sim/main.go
Normal 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
|
||||||
|
}
|
187
cmd/lotus-sim/simulation/commit_queue.go
Normal file
187
cmd/lotus-sim/simulation/commit_queue.go
Normal 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
|
||||||
|
}
|
81
cmd/lotus-sim/simulation/messages.go
Normal file
81
cmd/lotus-sim/simulation/messages.go
Normal 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
|
||||||
|
}
|
136
cmd/lotus-sim/simulation/mock.go
Normal file
136
cmd/lotus-sim/simulation/mock.go
Normal 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")
|
||||||
|
}
|
167
cmd/lotus-sim/simulation/node.go
Normal file
167
cmd/lotus-sim/simulation/node.go
Normal 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
|
||||||
|
}
|
58
cmd/lotus-sim/simulation/power.go
Normal file
58
cmd/lotus-sim/simulation/power.go
Normal 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()
|
||||||
|
}
|
205
cmd/lotus-sim/simulation/precommit.go
Normal file
205
cmd/lotus-sim/simulation/precommit.go
Normal 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(¶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
|
||||||
|
}
|
219
cmd/lotus-sim/simulation/provecommit.go
Normal file
219
cmd/lotus-sim/simulation/provecommit.go
Normal 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(¶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)
|
||||||
|
})
|
||||||
|
}
|
274
cmd/lotus-sim/simulation/simulation.go
Normal file
274
cmd/lotus-sim/simulation/simulation.go
Normal 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)
|
||||||
|
}
|
190
cmd/lotus-sim/simulation/state.go
Normal file
190
cmd/lotus-sim/simulation/state.go
Normal 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
|
||||||
|
}
|
196
cmd/lotus-sim/simulation/step.go
Normal file
196
cmd/lotus-sim/simulation/step.go
Normal 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
|
||||||
|
}
|
253
cmd/lotus-sim/simulation/wdpost.go
Normal file
253
cmd/lotus-sim/simulation/wdpost.go
Normal 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(¶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
|
||||||
|
}
|
46
cmd/lotus-sim/step.go
Normal file
46
cmd/lotus-sim/step.go
Normal 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
53
cmd/lotus-sim/upgrade.go
Normal 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
18
cmd/lotus-sim/util.go
Normal 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"))
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user