github.com/filecoin-project/specs-actors/v4@v4.0.2/support/agent/deal_client_agent.go (about) 1 package agent 2 3 import ( 4 "crypto/sha256" 5 mh "github.com/multiformats/go-multihash" 6 "math/bits" 7 "math/rand" 8 "strconv" 9 10 "github.com/filecoin-project/go-address" 11 "github.com/filecoin-project/go-state-types/abi" 12 "github.com/filecoin-project/go-state-types/big" 13 "github.com/filecoin-project/go-state-types/cbor" 14 "github.com/filecoin-project/go-state-types/crypto" 15 "github.com/filecoin-project/specs-actors/v4/actors/builtin" 16 "github.com/filecoin-project/specs-actors/v4/actors/builtin/market" 17 "github.com/filecoin-project/specs-actors/v4/actors/builtin/power" 18 "github.com/filecoin-project/specs-actors/v4/actors/builtin/reward" 19 "github.com/ipfs/go-cid" 20 ) 21 22 type DealClientConfig struct { 23 DealRate float64 // deals made per epoch 24 MinPieceSize uint64 // minimum piece size (actual piece size be rounded up to power of 2) 25 MaxPieceSize uint64 // maximum piece size 26 MinStoragePrice abi.TokenAmount // minimum price per epoch a client will pay for storage (may be zero) 27 MaxStoragePrice abi.TokenAmount // maximum price per epoch a client will pay for storage 28 MinMarketBalance abi.TokenAmount // balance below which client will top up funds in market actor 29 MaxMarketBalance abi.TokenAmount // balance to which client will top up funds in market actor 30 } 31 32 type DealClientAgent struct { 33 DealCount int 34 35 account address.Address 36 config DealClientConfig 37 dealEvents *RateIterator 38 rnd *rand.Rand 39 40 // tracks funds expected to be locked for client deal payment 41 expectedMarketBalance abi.TokenAmount 42 } 43 44 func AddDealClientsForAccounts(s SimState, accounts []address.Address, seed int64, config DealClientConfig) []*DealClientAgent { 45 rnd := rand.New(rand.NewSource(seed)) 46 var agents []*DealClientAgent 47 for _, account := range accounts { 48 agent := NewDealClientAgent(account, rnd.Int63(), config) 49 agents = append(agents, agent) 50 s.AddAgent(agent) 51 } 52 return agents 53 } 54 55 func NewDealClientAgent(account address.Address, seed int64, config DealClientConfig) *DealClientAgent { 56 rnd := rand.New(rand.NewSource(seed)) 57 return &DealClientAgent{ 58 account: account, 59 config: config, 60 rnd: rnd, 61 expectedMarketBalance: big.Zero(), 62 dealEvents: NewRateIterator(config.DealRate, rnd.Int63()), 63 } 64 } 65 66 func (dca *DealClientAgent) Tick(s SimState) ([]message, error) { 67 // aggregate all deals into one message 68 if err := dca.dealEvents.Tick(func() error { 69 provider := s.ChooseDealProvider() 70 71 // provider will be nil if called before any miners are added to system 72 if provider == nil { 73 return nil 74 } 75 76 if err := dca.createDeal(s, provider); err != nil { 77 return err 78 } 79 return nil 80 }); err != nil { 81 return nil, err 82 } 83 84 // add message to update market balance if necessary 85 messages := dca.updateMarketBalance() 86 87 return messages, nil 88 } 89 90 func (dca *DealClientAgent) updateMarketBalance() []message { 91 if dca.expectedMarketBalance.GreaterThanEqual(dca.config.MinMarketBalance) { 92 return []message{} 93 } 94 95 balanceToAdd := big.Sub(dca.config.MaxMarketBalance, dca.expectedMarketBalance) 96 97 return []message{{ 98 From: dca.account, 99 To: builtin.StorageMarketActorAddr, 100 Value: balanceToAdd, 101 Method: builtin.MethodsMarket.AddBalance, 102 Params: &dca.account, 103 ReturnHandler: func(v SimState, msg message, ret cbor.Marshaler) error { 104 dca.expectedMarketBalance = dca.config.MaxMarketBalance 105 return nil 106 }, 107 }} 108 } 109 110 // Create a proposal 111 // Return false if provider if deal can't be performed because client or provider lacks funds 112 func (dca *DealClientAgent) createDeal(s SimState, provider DealProvider) error { 113 pieceCid, err := dca.generatePieceCID() 114 if err != nil { 115 return err 116 } 117 118 pieceSize := dca.config.MinPieceSize + 119 uint64(dca.rnd.Int63n(int64(dca.config.MaxPieceSize-dca.config.MinPieceSize))) 120 121 // round to next power of two 122 if bits.OnesCount64(pieceSize) > 1 { 123 pieceSize = 1 << bits.Len64(pieceSize) 124 } 125 126 providerCollateral, err := calculateProviderCollateral(s, abi.PaddedPieceSize(pieceSize)) 127 if err != nil { 128 return err 129 } 130 131 // if provider does not have enough collateral, just skip this deal 132 if provider.AvailableCollateral().LessThan(providerCollateral) { 133 return nil 134 } 135 136 // storage price is uniformly distributed between min an max 137 price := big.Add(dca.config.MinStoragePrice, 138 big.NewInt(dca.rnd.Int63n(int64(big.Sub(dca.config.MaxStoragePrice, dca.config.MinStoragePrice).Uint64())))) 139 140 // deal start is earliest possible epoch, deal end is uniformly distributed between start and max. 141 dealStart, maxDealEnd := provider.DealRange(s.GetEpoch()) 142 dealEnd := dealStart + abi.ChainEpoch(dca.rnd.Int63n(int64(maxDealEnd-dealStart))) 143 if dealEnd-dealStart < market.DealMinDuration { 144 dealEnd = dealStart + market.DealMinDuration 145 } 146 147 // lower expected balance in anticipation of market actor locking storage fee 148 storageFee := big.Mul(big.NewInt(int64(dealEnd-dealStart)), price) 149 150 // if this client does not have enough balance for storage fee, just skip this deal 151 if dca.expectedMarketBalance.LessThan(storageFee) { 152 return nil 153 } 154 155 dca.expectedMarketBalance = big.Sub(dca.expectedMarketBalance, storageFee) 156 157 proposal := market.DealProposal{ 158 PieceCID: pieceCid, 159 PieceSize: abi.PaddedPieceSize(pieceSize), 160 VerifiedDeal: false, 161 Client: dca.account, 162 Provider: provider.Address(), 163 Label: dca.account.String() + ":" + strconv.Itoa(dca.DealCount), 164 StartEpoch: dealStart, 165 EndEpoch: dealEnd, 166 StoragePricePerEpoch: price, 167 ProviderCollateral: providerCollateral, 168 ClientCollateral: big.Zero(), 169 } 170 171 provider.CreateDeal(market.ClientDealProposal{ 172 Proposal: proposal, 173 ClientSignature: crypto.Signature{}, 174 }) 175 dca.DealCount++ 176 return nil 177 } 178 179 func (dca *DealClientAgent) generatePieceCID() (cid.Cid, error) { 180 data := make([]byte, 10) 181 if _, err := dca.rnd.Read(data); err != nil { 182 return cid.Cid{}, err 183 } 184 185 sum := sha256.Sum256(data) 186 hash, err := mh.Encode(sum[:], market.PieceCIDPrefix.MhType) 187 if err != nil { 188 panic(err) 189 } 190 return cid.NewCidV1(market.PieceCIDPrefix.Codec, hash), nil 191 } 192 193 // Always choose the minimum collateral. This appears to be realistic, and there's is not an obvious way to model a 194 // more complex distribution. 195 func calculateProviderCollateral(s SimState, pieceSize abi.PaddedPieceSize) (abi.TokenAmount, error) { 196 var powerSt power.State 197 if err := s.GetState(builtin.StoragePowerActorAddr, &powerSt); err != nil { 198 return big.Zero(), err 199 } 200 201 var rewardSt reward.State 202 if err := s.GetState(builtin.RewardActorAddr, &rewardSt); err != nil { 203 return big.Zero(), err 204 } 205 206 min, _ := market.DealProviderCollateralBounds(pieceSize, false, powerSt.TotalRawBytePower, 207 powerSt.TotalQualityAdjPower, rewardSt.ThisEpochBaselinePower, s.NetworkCirculatingSupply()) 208 return min, nil 209 }