github.com/filecoin-project/specs-actors/v4@v4.0.2/support/agent/sim.go (about) 1 package agent 2 3 import ( 4 "context" 5 "fmt" 6 "math/rand" 7 "strings" 8 "testing" 9 10 "github.com/filecoin-project/go-address" 11 "github.com/filecoin-project/go-bitfield" 12 "github.com/filecoin-project/go-state-types/abi" 13 "github.com/filecoin-project/go-state-types/big" 14 "github.com/filecoin-project/go-state-types/cbor" 15 "github.com/filecoin-project/go-state-types/dline" 16 "github.com/filecoin-project/go-state-types/exitcode" 17 "github.com/filecoin-project/go-state-types/rt" 18 power2 "github.com/filecoin-project/specs-actors/v2/actors/builtin/power" 19 reward2 "github.com/filecoin-project/specs-actors/v2/actors/builtin/reward" 20 cid "github.com/ipfs/go-cid" 21 ipldcbor "github.com/ipfs/go-ipld-cbor" 22 "github.com/pkg/errors" 23 "golang.org/x/xerrors" 24 25 adt2 "github.com/filecoin-project/specs-actors/v2/actors/util/adt" 26 vm2 "github.com/filecoin-project/specs-actors/v2/support/vm" 27 "github.com/filecoin-project/specs-actors/v4/actors/builtin" 28 "github.com/filecoin-project/specs-actors/v4/actors/builtin/market" 29 "github.com/filecoin-project/specs-actors/v4/actors/builtin/power" 30 power3 "github.com/filecoin-project/specs-actors/v4/actors/builtin/power" 31 "github.com/filecoin-project/specs-actors/v4/actors/builtin/reward" 32 "github.com/filecoin-project/specs-actors/v4/actors/states" 33 "github.com/filecoin-project/specs-actors/v4/actors/util/adt" 34 "github.com/filecoin-project/specs-actors/v4/support/ipld" 35 vm "github.com/filecoin-project/specs-actors/v4/support/vm" 36 ) 37 38 // Sim is a simulation framework to exercise actor code in a network-like environment. 39 // It's goal is to simulate realistic call sequences and interactions to perform invariant analysis 40 // and test performance assumptions prior to shipping actor code out to implementations. 41 // The model is that the simulation will "Tick" once per epoch. Within this tick: 42 // * It will first compute winning tickets from previous state for miners to simulate block mining. 43 // * It will create any agents it is configured to create and generate messages to create their associated actors. 44 // * It will call tick on all it agents. This call will return messages that will get added to the simulated "tipset". 45 // * Messages will be shuffled to simulate network entropy. 46 // * Messages will be applied and an new VM will be created from the resulting state tree for the next tick. 47 type Sim struct { 48 Config SimConfig 49 Agents []Agent 50 DealProviders []DealProvider 51 WinCount uint64 52 MessageCount uint64 53 ComputePowerTable func(SimVM, []Agent) (PowerTable, error) 54 CreateMinerParamsFunc func(address.Address, address.Address, abi.RegisteredSealProof) (interface{}, error) 55 56 v SimVM 57 vmFactory VMFactoryFunc 58 minerStateFactory func(context.Context, cid.Cid) (SimMinerState, error) 59 rnd *rand.Rand 60 statsByMethod map[vm.MethodKey]*vm.CallStats 61 blkStore ipldcbor.IpldBlockstore 62 blkStoreFactory func() ipldcbor.IpldBlockstore 63 ctx context.Context 64 t testing.TB 65 } 66 67 type VMFactoryFunc func(context.Context, vm2.ActorImplLookup, adt.Store, cid.Cid, abi.ChainEpoch) (SimVM, error) 68 69 func NewSim(ctx context.Context, t testing.TB, blockstoreFactory func() ipldcbor.IpldBlockstore, config SimConfig) *Sim { 70 blkStore := blockstoreFactory() 71 metrics := ipld.NewMetricsBlockStore(blkStore) 72 v := vm.NewVMWithSingletons(ctx, t, metrics) 73 vmFactory := func(ctx context.Context, impl vm2.ActorImplLookup, store adt.Store, stateRoot cid.Cid, epoch abi.ChainEpoch) (SimVM, error) { 74 return vm.NewVMAtEpoch(ctx, vm.ActorImplLookup(impl), store, stateRoot, epoch) 75 } 76 v.SetStatsSource(metrics) 77 minerStateFactory := func(ctx context.Context, root cid.Cid) (SimMinerState, error) { 78 return &MinerStateV3{ 79 Ctx: ctx, 80 Root: root, 81 }, nil 82 } 83 return &Sim{ 84 Config: config, 85 Agents: []Agent{}, 86 DealProviders: []DealProvider{}, 87 ComputePowerTable: ComputePowerTableV3, 88 CreateMinerParamsFunc: CreateMinerParamsV3, 89 v: v, 90 vmFactory: vmFactory, 91 minerStateFactory: minerStateFactory, 92 rnd: rand.New(rand.NewSource(config.Seed)), 93 blkStore: blkStore, 94 blkStoreFactory: blockstoreFactory, 95 ctx: ctx, 96 t: t, 97 } 98 } 99 100 func NewSimWithVM(ctx context.Context, t testing.TB, v SimVM, vmFactory VMFactoryFunc, 101 computePowerTable func(SimVM, []Agent) (PowerTable, error), blkStore ipldcbor.IpldBlockstore, 102 blockstoreFactory func() ipldcbor.IpldBlockstore, minerStateFactory func(context.Context, cid.Cid) (SimMinerState, error), 103 config SimConfig, createMinerParams func(address.Address, address.Address, abi.RegisteredSealProof) (interface{}, error), 104 ) *Sim { 105 metrics := ipld.NewMetricsBlockStore(blkStore) 106 v.SetStatsSource(metrics) 107 108 return &Sim{ 109 Config: config, 110 Agents: []Agent{}, 111 DealProviders: []DealProvider{}, 112 ComputePowerTable: computePowerTable, 113 CreateMinerParamsFunc: createMinerParams, 114 v: v, 115 vmFactory: vmFactory, 116 minerStateFactory: minerStateFactory, 117 rnd: rand.New(rand.NewSource(config.Seed)), 118 blkStore: blkStore, 119 blkStoreFactory: blockstoreFactory, 120 ctx: ctx, 121 t: t, 122 } 123 } 124 125 func (s *Sim) SwapVM(v SimVM, vmFactory VMFactoryFunc, minerStateFactory func(context.Context, cid.Cid) (SimMinerState, error), 126 computePowerTable func(SimVM, []Agent) (PowerTable, error), createMinerParams func(address.Address, address.Address, abi.RegisteredSealProof) (interface{}, error), 127 ) { 128 s.v = v 129 s.vmFactory = vmFactory 130 s.minerStateFactory = minerStateFactory 131 s.ComputePowerTable = computePowerTable 132 s.CreateMinerParamsFunc = createMinerParams 133 } 134 135 ////////////////////////////////////////// 136 // 137 // Sim execution 138 // 139 ////////////////////////////////////////// 140 141 func (s *Sim) Tick() error { 142 var err error 143 var blockMessages []message 144 // compute power table before state transition to create block rewards at the end 145 powerTable, err := s.ComputePowerTable(s.v, s.Agents) 146 if err != nil { 147 return err 148 } 149 150 if err := computeCircSupply(s.v); err != nil { 151 return err 152 } 153 154 // add all agent messages 155 for _, agent := range s.Agents { 156 msgs, err := agent.Tick(s) 157 if err != nil { 158 return err 159 } 160 161 blockMessages = append(blockMessages, msgs...) 162 } 163 164 // shuffle messages 165 s.rnd.Shuffle(len(blockMessages), func(i, j int) { 166 blockMessages[i], blockMessages[j] = blockMessages[j], blockMessages[i] 167 }) 168 169 // run messages 170 for _, msg := range blockMessages { 171 ret, code := s.v.ApplyMessage(msg.From, msg.To, msg.Value, msg.Method, msg.Params) 172 173 // for now, assume everything should work 174 if code != exitcode.Ok { 175 return errors.Errorf("exitcode %d: message failed: %v\n%s\n", code, msg, strings.Join(s.v.GetLogs(), "\n")) 176 } 177 178 if msg.ReturnHandler != nil { 179 if err := msg.ReturnHandler(s, msg, ret); err != nil { 180 return err 181 } 182 } 183 } 184 s.MessageCount += uint64(len(blockMessages)) 185 // Apply block rewards 186 // Note that this differs from the specification in that it applies all reward messages at the end, whereas 187 // a real implementation would apply a reward messages at the end of each block in the tipset (thereby 188 // interleaving them with the rest of the messages). 189 for _, miner := range powerTable.minerPower { 190 if powerTable.totalQAPower.GreaterThan(big.Zero()) { 191 wins := WinCount(miner.qaPower, powerTable.totalQAPower, s.rnd.Float64()) 192 s.WinCount += wins 193 err := s.rewardMiner(miner.addr, wins) 194 if err != nil { 195 return err 196 } 197 } 198 } 199 200 // run cron 201 _, code := s.v.ApplyMessage(builtin.SystemActorAddr, builtin.CronActorAddr, big.Zero(), builtin.MethodsCron.EpochTick, nil) 202 if code != exitcode.Ok { 203 return errors.Errorf("exitcode %d: cron message failed:\n%s\n", code, strings.Join(s.v.GetLogs(), "\n")) 204 } 205 206 // store last stats 207 s.statsByMethod = s.v.GetCallStats() 208 209 // dump logs if we have them 210 if len(s.v.GetLogs()) > 0 { 211 fmt.Printf("%s\n", strings.Join(s.v.GetLogs(), "\n")) 212 } 213 214 // create next vm 215 nextEpoch := s.v.GetEpoch() + 1 216 if s.Config.CheckpointEpochs > 0 && uint64(nextEpoch)%s.Config.CheckpointEpochs == 0 { 217 nextStore := s.blkStoreFactory() 218 blks, size, err := BlockstoreCopy(s.blkStore, nextStore, s.v.StateRoot()) 219 if err != nil { 220 return err 221 } 222 fmt.Printf("CHECKPOINT: state blocks: %d, state data size %d\n", blks, size) 223 224 s.blkStore = nextStore 225 metrics := ipld.NewMetricsBlockStore(nextStore) 226 s.v, err = s.vmFactory(s.ctx, s.v.GetActorImpls(), adt.WrapBlockStore(s.ctx, metrics), s.v.StateRoot(), nextEpoch) 227 if err != nil { 228 return err 229 } 230 s.v.SetStatsSource(metrics) 231 232 } else { 233 statsSource := s.v.GetStatsSource() 234 s.v, err = s.vmFactory(s.ctx, s.v.GetActorImpls(), s.v.Store(), s.v.StateRoot(), nextEpoch) 235 if err != nil { 236 return err 237 } 238 s.v.SetStatsSource(statsSource) 239 } 240 241 return err 242 } 243 244 ////////////////////////////////////////////////// 245 // 246 // SimState Methods and other accessors 247 // 248 ////////////////////////////////////////////////// 249 250 func (s *Sim) GetEpoch() abi.ChainEpoch { 251 return s.v.GetEpoch() 252 } 253 254 func (s *Sim) GetState(addr address.Address, out cbor.Unmarshaler) error { 255 return s.v.GetState(addr, out) 256 } 257 258 func (s *Sim) Store() adt.Store { 259 return s.v.Store() 260 } 261 262 func (s *Sim) MinerState(addr address.Address) (SimMinerState, error) { 263 act, found, err := s.v.GetActor(addr) 264 if err != nil { 265 return nil, err 266 } 267 if !found { 268 return nil, xerrors.Errorf("miner %s not found", addr) 269 } 270 return s.minerStateFactory(s.ctx, act.Head) 271 } 272 273 func (s *Sim) AddAgent(a Agent) { 274 s.Agents = append(s.Agents, a) 275 } 276 277 func (s *Sim) AddDealProvider(d DealProvider) { 278 s.DealProviders = append(s.DealProviders, d) 279 } 280 281 func (s *Sim) GetVM() SimVM { 282 return s.v 283 } 284 285 func (s *Sim) GetCallStats() map[vm.MethodKey]*vm.CallStats { 286 return s.statsByMethod 287 } 288 289 func (s *Sim) ChooseDealProvider() DealProvider { 290 if len(s.DealProviders) == 0 { 291 return nil 292 } 293 return s.DealProviders[s.rnd.Int63n(int64(len(s.DealProviders)))] 294 } 295 296 func (s *Sim) NetworkCirculatingSupply() abi.TokenAmount { 297 return s.v.GetCirculatingSupply() 298 } 299 300 func (s *Sim) CreateMinerParams(worker, owner address.Address, sealProof abi.RegisteredSealProof) (interface{}, error) { 301 return s.CreateMinerParamsFunc(worker, owner, sealProof) 302 } 303 304 ////////////////////////////////////////////////// 305 // 306 // Misc Methods 307 // 308 ////////////////////////////////////////////////// 309 310 func (s *Sim) rewardMiner(addr address.Address, wins uint64) error { 311 if wins < 1 { 312 return nil 313 } 314 315 rewardParams := reward.AwardBlockRewardParams{ 316 Miner: addr, 317 Penalty: big.Zero(), 318 GasReward: big.Zero(), 319 WinCount: int64(wins), 320 } 321 _, code := s.v.ApplyMessage(builtin.SystemActorAddr, builtin.RewardActorAddr, big.Zero(), builtin.MethodsReward.AwardBlockReward, &rewardParams) 322 if code != exitcode.Ok { 323 return errors.Errorf("exitcode %d: reward message failed:\n%s\n", code, strings.Join(s.v.GetLogs(), "\n")) 324 } 325 return nil 326 } 327 328 func ComputePowerTableV3(v SimVM, agents []Agent) (PowerTable, error) { 329 pt := PowerTable{} 330 331 var rwst reward.State 332 if err := v.GetState(builtin.RewardActorAddr, &rwst); err != nil { 333 return PowerTable{}, err 334 } 335 pt.blockReward = rwst.ThisEpochReward 336 337 var st power.State 338 if err := v.GetState(builtin.StoragePowerActorAddr, &st); err != nil { 339 return PowerTable{}, err 340 } 341 pt.totalQAPower = st.TotalQualityAdjPower 342 343 for _, agent := range agents { 344 if miner, ok := agent.(*MinerAgent); ok { 345 if claim, found, err := st.GetClaim(v.Store(), miner.IDAddress); err != nil { 346 return pt, err 347 } else if found { 348 if sufficient, err := st.MinerNominalPowerMeetsConsensusMinimum(v.Store(), miner.IDAddress); err != nil { 349 return pt, err 350 } else if sufficient { 351 pt.minerPower = append(pt.minerPower, minerPowerTable{miner.IDAddress, claim.QualityAdjPower}) 352 } 353 } 354 } 355 } 356 return pt, nil 357 } 358 359 func ComputePowerTableV2(v SimVM, agents []Agent) (PowerTable, error) { 360 pt := PowerTable{} 361 362 var rwst reward2.State 363 if err := v.GetState(builtin.RewardActorAddr, &rwst); err != nil { 364 return PowerTable{}, err 365 } 366 pt.blockReward = rwst.ThisEpochReward 367 368 var st power2.State 369 if err := v.GetState(builtin.StoragePowerActorAddr, &st); err != nil { 370 return PowerTable{}, err 371 } 372 pt.totalQAPower = st.TotalQualityAdjPower 373 374 for _, agent := range agents { 375 if miner, ok := agent.(*MinerAgent); ok { 376 if claim, found, err := st.GetClaim(v.Store(), miner.IDAddress); err != nil { 377 return pt, err 378 } else if found { 379 if sufficient, err := st.MinerNominalPowerMeetsConsensusMinimum(v.Store(), miner.IDAddress); err != nil { 380 return pt, err 381 } else if sufficient { 382 pt.minerPower = append(pt.minerPower, minerPowerTable{miner.IDAddress, claim.QualityAdjPower}) 383 } 384 } 385 } 386 } 387 return pt, nil 388 } 389 390 func CreateMinerParamsV2(worker, owner address.Address, sealProof abi.RegisteredSealProof) (interface{}, error) { 391 return &power2.CreateMinerParams{ 392 Owner: owner, 393 Worker: worker, 394 SealProofType: sealProof, 395 }, nil 396 } 397 398 func CreateMinerParamsV3(worker, owner address.Address, sealProof abi.RegisteredSealProof) (interface{}, error) { 399 wPoStProof, err := sealProof.RegisteredWindowPoStProof() 400 if err != nil { 401 return nil, err 402 } 403 404 return &power3.CreateMinerParams{ 405 Owner: owner, 406 Worker: worker, 407 WindowPoStProofType: wPoStProof, 408 }, nil 409 } 410 411 func computeCircSupply(v SimVM) error { 412 // disbursed + reward.State.TotalStoragePowerReward - burnt.Balance - power.State.TotalPledgeCollateral 413 var rewardSt reward.State 414 if err := v.GetState(builtin.RewardActorAddr, &rewardSt); err != nil { 415 return err 416 } 417 418 var powerSt power.State 419 if err := v.GetState(builtin.StoragePowerActorAddr, &powerSt); err != nil { 420 return err 421 } 422 423 burnt, found, err := v.GetActor(builtin.BurntFundsActorAddr) 424 if err != nil { 425 return err 426 } else if !found { 427 return errors.Errorf("burnt actor not found at %v", builtin.BurntFundsActorAddr) 428 } 429 430 v.SetCirculatingSupply(big.Sum(DisbursedAmount, rewardSt.TotalStoragePowerReward, 431 powerSt.TotalPledgeCollateral.Neg(), burnt.Balance.Neg())) 432 return nil 433 } 434 435 ////////////////////////////////////////////// 436 // 437 // Internal Types 438 // 439 ////////////////////////////////////////////// 440 441 type SimState interface { 442 GetEpoch() abi.ChainEpoch 443 GetState(addr address.Address, out cbor.Unmarshaler) error 444 Store() adt.Store 445 AddAgent(a Agent) 446 AddDealProvider(d DealProvider) 447 NetworkCirculatingSupply() abi.TokenAmount 448 MinerState(addr address.Address) (SimMinerState, error) 449 CreateMinerParams(worker, owner address.Address, sealProof abi.RegisteredSealProof) (interface{}, error) 450 451 // randomly select an agent capable of making deals. 452 // Returns nil if no providers exist. 453 ChooseDealProvider() DealProvider 454 } 455 456 type Agent interface { 457 Tick(v SimState) ([]message, error) 458 } 459 460 type DealProvider interface { 461 Address() address.Address 462 DealRange(currentEpoch abi.ChainEpoch) (start abi.ChainEpoch, end abi.ChainEpoch) 463 CreateDeal(proposal market.ClientDealProposal) 464 AvailableCollateral() abi.TokenAmount 465 } 466 467 type SimConfig struct { 468 AccountCount int 469 AccountInitialBalance abi.TokenAmount 470 Seed int64 471 CreateMinerProbability float32 472 CheckpointEpochs uint64 473 } 474 475 type returnHandler func(v SimState, msg message, ret cbor.Marshaler) error 476 477 type message struct { 478 From address.Address 479 To address.Address 480 Value abi.TokenAmount 481 Method abi.MethodNum 482 Params interface{} 483 ReturnHandler returnHandler 484 } 485 486 type minerPowerTable struct { 487 addr address.Address 488 qaPower abi.StoragePower 489 } 490 491 type PowerTable struct { 492 blockReward abi.TokenAmount 493 totalQAPower abi.StoragePower 494 minerPower []minerPowerTable 495 } 496 497 // VM interface allowing a simulation to operate over multiple VM versions 498 type SimVM interface { 499 ApplyMessage(from, to address.Address, value abi.TokenAmount, method abi.MethodNum, params interface{}) (cbor.Marshaler, exitcode.ExitCode) 500 GetCirculatingSupply() abi.TokenAmount 501 GetLogs() []string 502 GetState(addr address.Address, out cbor.Unmarshaler) error 503 SetStatsSource(stats vm2.StatsSource) 504 GetCallStats() map[vm2.MethodKey]*vm2.CallStats 505 GetEpoch() abi.ChainEpoch 506 Store() adt2.Store 507 GetActor(addr address.Address) (*states.Actor, bool, error) 508 SetCirculatingSupply(supply big.Int) 509 GetActorImpls() map[cid.Cid]rt.VMActor 510 StateRoot() cid.Cid 511 GetStatsSource() vm2.StatsSource 512 GetTotalActorBalance() (abi.TokenAmount, error) 513 } 514 515 var _ SimVM = (*vm.VM)(nil) 516 var _ SimVM = (*vm2.VM)(nil) 517 518 type SimMinerState interface { 519 HasSectorNo(adt.Store, abi.SectorNumber) (bool, error) 520 FindSector(adt.Store, abi.SectorNumber) (uint64, uint64, error) 521 ProvingPeriodStart(adt.Store) (abi.ChainEpoch, error) 522 LoadSectorInfo(adt.Store, uint64) (SimSectorInfo, error) 523 DeadlineInfo(adt.Store, abi.ChainEpoch) (*dline.Info, error) 524 FeeDebt(adt.Store) (abi.TokenAmount, error) 525 LoadDeadlineState(adt.Store, uint64) (SimDeadlineState, error) 526 } 527 528 type SimSectorInfo interface { 529 Expiration() abi.ChainEpoch 530 } 531 532 type SimDeadlineState interface { 533 LoadPartition(adt.Store, uint64) (SimPartitionState, error) 534 } 535 536 type SimPartitionState interface { 537 Terminated() bitfield.BitField 538 }