package main import ( "context" "fmt" "io/ioutil" mbig "math/big" "math/rand" "os" "time" "github.com/libp2p/go-libp2p/core/peer" "github.com/testground/sdk-go/sync" "github.com/filecoin-project/go-address" datatransfer "github.com/filecoin-project/go-data-transfer" "github.com/filecoin-project/go-state-types/big" "github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/build" "github.com/filecoin-project/lotus/testplans/lotus-soup/testkit" ) // This is the baseline test; Filecoin 101. // // A network with a bootstrapper, a number of miners, and a number of clients/full nodes // is constructed and connected through the bootstrapper. // Some funds are allocated to each node and a number of sectors are presealed in the genesis block. // // The test plan: // One or more clients store content to one or more miners, testing storage deals. // The plan ensures that the storage deals hit the blockchain and measure the time it took. // Verification: one or more clients retrieve and verify the hashes of stored content. // The plan ensures that all (previously) published content can be correctly retrieved // and measures the time it took. // // Preparation of the genesis block: this is the responsibility of the bootstrapper. // In order to compute the genesis block, we need to collect identities and presealed // sectors from each node. // Then we create a genesis block that allocates some funds to each node and collects // the presealed sectors. func dealsE2E(t *testkit.TestEnvironment) error { t.RecordMessage("running node with role '%s'", t.Role) // Dispatch/forward non-client roles to defaults. if t.Role != "client" { return testkit.HandleDefaultRole(t) } // This is a client role fastRetrieval := t.BooleanParam("fast_retrieval") t.RecordMessage("running client, with fast retrieval set to: %v", fastRetrieval) cl, err := testkit.PrepareClient(t) if err != nil { return err } ctx := context.Background() client := cl.FullApi // select a random miner minerAddr := cl.MinerAddrs[rand.Intn(len(cl.MinerAddrs))] if err := client.NetConnect(ctx, minerAddr.MinerNetAddrs); err != nil { return err } t.D().Counter(fmt.Sprintf("send-data-to,miner=%s", minerAddr.MinerActorAddr)).Inc(1) t.RecordMessage("selected %s as the miner", minerAddr.MinerActorAddr) if fastRetrieval { err = initPaymentChannel(t, ctx, cl, minerAddr) if err != nil { return err } } // give some time to the miner, otherwise, we get errors like: // deal errored deal failed: (State=26) error calling node: publishing deal: GasEstimateMessageGas // error: estimating gas used: message execution failed: exit 19, reason: failed to lock balance: failed to lock client funds: not enough balance to lock for addr t0102: escrow balance 0 < locked 0 + required 640297000 (RetCode=19) time.Sleep(40 * time.Second) time.Sleep(time.Duration(t.GlobalSeq) * 5 * time.Second) // generate 5000000 bytes of random data data := make([]byte, 5000000) rand.New(rand.NewSource(time.Now().UnixNano())).Read(data) file, err := ioutil.TempFile("/tmp", "data") if err != nil { return err } defer os.Remove(file.Name()) _, err = file.Write(data) if err != nil { return err } fcid, err := client.ClientImport(ctx, api.FileRef{Path: file.Name(), IsCAR: false}) if err != nil { return err } t.RecordMessage("file cid: %s", fcid) // Check if we should bounce the connection during data transfers if t.BooleanParam("bounce_conn_data_transfers") { t.RecordMessage("Will bounce connection during push and pull data-transfers") err = bounceConnInTransfers(ctx, t, client, minerAddr.MinerNetAddrs.ID) if err != nil { return err } } // start deal t1 := time.Now() deal := testkit.StartDeal(ctx, minerAddr.MinerActorAddr, client, fcid.Root, fastRetrieval) t.RecordMessage("started deal: %s", deal) // TODO: this sleep is only necessary because deals don't immediately get logged in the dealstore, we should fix this time.Sleep(2 * time.Second) t.RecordMessage("waiting for deal to be sealed") testkit.WaitDealSealed(t, ctx, client, deal) t.D().ResettingHistogram("deal.sealed").Update(int64(time.Since(t1))) // wait for all client deals to be sealed before trying to retrieve t.SyncClient.MustSignalAndWait(ctx, sync.State("done-sealing"), t.IntParam("clients")) carExport := true t.RecordMessage("trying to retrieve %s", fcid) t1 = time.Now() _ = testkit.RetrieveData(t, ctx, client, fcid.Root, nil, carExport, data) t.D().ResettingHistogram("deal.retrieved").Update(int64(time.Since(t1))) t.SyncClient.MustSignalEntry(ctx, testkit.StateStopMining) time.Sleep(10 * time.Second) // wait for metrics to be emitted // TODO broadcast published content CIDs to other clients // TODO select a random piece of content published by some other client and retrieve it t.SyncClient.MustSignalAndWait(ctx, testkit.StateDone, t.TestInstanceCount) return nil } func bounceConnInTransfers(ctx context.Context, t *testkit.TestEnvironment, client api.FullNode, minerPeerID peer.ID) error { storageConnBroken := false retrievalConnBroken := false upds, err := client.ClientDataTransferUpdates(ctx) if err != nil { return err } go func() { for upd := range upds { dir := "push" if !upd.IsSender { dir = "pull" } t.RecordMessage("%s data transfer status: %s, transferred: %d", dir, datatransfer.Statuses[upd.Status], upd.Transferred) // Bounce the connection after the first block is sent for the storage deal if upd.IsSender && upd.Transferred > 0 && !storageConnBroken { storageConnBroken = true bounceConnection(ctx, t, client, minerPeerID) } // Bounce the connection after the first block is received for the retrieval deal if !upd.IsSender && upd.Transferred > 0 && !retrievalConnBroken { retrievalConnBroken = true bounceConnection(ctx, t, client, minerPeerID) } } }() return nil } func bounceConnection(ctx context.Context, t *testkit.TestEnvironment, client api.FullNode, minerPeerID peer.ID) { t.RecordMessage("disconnecting peer %s", minerPeerID) client.NetBlockAdd(ctx, api.NetBlockList{ Peers: []peer.ID{minerPeerID}, }) go func() { time.Sleep(3 * time.Second) t.RecordMessage("reconnecting to peer %s", minerPeerID) client.NetBlockRemove(ctx, api.NetBlockList{ Peers: []peer.ID{minerPeerID}, }) }() } // filToAttoFil converts a fractional filecoin value into AttoFIL, rounding if necessary func filToAttoFil(f float64) big.Int { a := mbig.NewFloat(f) a.Mul(a, mbig.NewFloat(float64(build.FilecoinPrecision))) i, _ := a.Int(nil) return big.Int{Int: i} } func initPaymentChannel(t *testkit.TestEnvironment, ctx context.Context, cl *testkit.LotusClient, minerAddr testkit.MinerAddressesMsg) error { recv := minerAddr balance := filToAttoFil(10) t.RecordMessage("my balance: %d", balance) t.RecordMessage("creating payment channel; from=%s, to=%s, funds=%d", cl.Wallet.Address, recv.WalletAddr, balance) channel, err := cl.FullApi.PaychGet(ctx, cl.Wallet.Address, recv.WalletAddr, balance, api.PaychGetOpts{ OffChain: false, }) if err != nil { return fmt.Errorf("failed to create payment channel: %w", err) } if addr := channel.Channel; addr != address.Undef { return fmt.Errorf("expected an Undef channel address, got: %s", addr) } t.RecordMessage("payment channel created; msg_cid=%s", channel.WaitSentinel) t.RecordMessage("waiting for payment channel message to appear on chain") // wait for the channel creation message to appear on chain. _, err = cl.FullApi.StateWaitMsg(ctx, channel.WaitSentinel, 2, api.LookbackNoLimit, true) if err != nil { return fmt.Errorf("failed while waiting for payment channel creation msg to appear on chain: %w", err) } // need to wait so that the channel is tracked. // the full API waits for build.MessageConfidence (=1 in tests) before tracking the channel. // we wait for 2 confirmations, so we have the assurance the channel is tracked. t.RecordMessage("reloading paych; now it should have an address") channel, err = cl.FullApi.PaychGet(ctx, cl.Wallet.Address, recv.WalletAddr, big.Zero(), api.PaychGetOpts{ OffChain: false, }) if err != nil { return fmt.Errorf("failed to reload payment channel: %w", err) } t.RecordMessage("channel address: %s", channel.Channel) return nil }