generic test plan scaffolding, with baseline plan (#39)

* test plan scaffolding

* generify the testplan role dispatch

* manifest.toml

* initial go.mod and go.sum

* correct name

* gomod: update from build

* node construction in scaffolding

* fix test runner return type

* remove offending comments

* add initial composition, and fix context bug

* debug lines

* check errors from node construction

* specify Repo after Online option

* add power/proof type initialization code

* fix baseline composition

* use new docker-images (build/run) introduced in the #48 PR

* upgrade go-sdk to master (#51)

* fix types for run.InvokeMap

* fix miner actor sequence address

* explictly specify listen address for nodes on the data network

* make a separate full node for the miner

* initialize the wallet for the full node before creating the storage node

* go mod tidy

* also set the listen address for the miner node

* circleci to build the soup testplan

* extract topics

* test runner: pass the role map to doRun for generic runner

* use a wrapper TestEnvironment to encapsulate the runenv and initCtx

* embed RunEnv and InitContext into TestEnvironment for better ergonomics

* remove empty import

* extract stateReady

Co-authored-by: Anton Evangelatov <anton.evangelatov@gmail.com>
This commit is contained in:
vyzo 2020-06-24 13:52:23 +03:00 committed by GitHub
parent 1907fe2a91
commit 448bbf3710
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 2587 additions and 0 deletions

View File

@ -51,3 +51,7 @@ jobs:
- run:
name: "build lotus-testground"
command: pushd lotus-testground && go build .
- run:
name: "build lotus-soup"
command: pushd lotus-soup && go build .

1
lotus-soup/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
lotus-soup

64
lotus-soup/baseline.go Normal file
View File

@ -0,0 +1,64 @@
package main
// This is the basline 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.
// The we create a genesis block that allocates some funds to each node and collects
// the presealed sectors.
var baselineRoles = map[string]func(*TestEnvironment) error{
"bootstrapper": runBaselineBootstrapper,
"miner": runBaselineMiner,
"client": runBaselineClient,
}
func runBaselineBootstrapper(t *TestEnvironment) error {
t.RecordMessage("running bootstrapper")
_, err := prepareBootstrapper(t)
if err != nil {
return err
}
// TODO just wait until completion of test, nothing else to do
return nil
}
func runBaselineMiner(t *TestEnvironment) error {
t.RecordMessage("running miner")
_, err := prepareMiner(t)
if err != nil {
return err
}
// TODO wait a bit for network to bootstrap
// TODO just wait until completion of test, serving requests -- the client does all the job
return nil
}
func runBaselineClient(t *TestEnvironment) error {
t.RecordMessage("running client")
_, err := prepareClient(t)
if err != nil {
return err
}
// TODO generate a number of random "files" and publish them to one or more miners
// TODO broadcast published content CIDs to other clients
// TODO select a random piece of content published by some other client and retreieve it
return nil
}

View File

@ -0,0 +1,58 @@
[metadata]
name = "lotus-soup"
author = ""
[global]
plan = "lotus-soup"
case = "lotus-baseline"
total_instances = 3
builder = "docker:go"
runner = "local:docker"
[[groups]]
id = "bootstrapper"
[groups.resources]
memory = "120Mi"
cpu = "10m"
[groups.instances]
count = 1
percentage = 0.0
[groups.run]
[groups.run.test_params]
role = "bootstrapper"
clients = "1"
miners = "1"
balance = "2000"
sectors = "10"
[[groups]]
id = "miners"
[groups.resources]
memory = "120Mi"
cpu = "10m"
[groups.instances]
count = 1
percentage = 0.0
[groups.run]
[groups.run.test_params]
role = "miner"
clients = "1"
miners = "1"
balance = "2000"
sectors = "10"
[[groups]]
id = "clients"
[groups.resources]
memory = "120Mi"
cpu = "10m"
[groups.instances]
count = 1
percentage = 0.0
[groups.run]
[groups.run.test_params]
role = "client"
clients = "1"
miners = "1"
balance = "2000"
sectors = "10"

20
lotus-soup/go.mod Normal file
View File

@ -0,0 +1,20 @@
module github.com/filecoin-project/oni/lotus-soup
go 1.14
require (
github.com/filecoin-project/go-address v0.0.2-0.20200504173055-8b6f2fb2b3ef
github.com/filecoin-project/go-storedcounter v0.0.0-20200421200003-1c99c62e8a5b
github.com/filecoin-project/lotus v0.4.1-0.20200623104442-68d38eff33e4
github.com/filecoin-project/specs-actors v0.6.2-0.20200617175406-de392ca14121
github.com/ipfs/go-datastore v0.4.4
github.com/ipfs/go-log/v2 v2.1.2-0.20200609205458-f8d20c392cb7
github.com/libp2p/go-libp2p-core v0.6.0
github.com/multiformats/go-multiaddr v0.2.2
github.com/testground/sdk-go v0.2.3-0.20200617132925-2e4d69f9ba38
)
// This will work in all build modes: docker:go, exec:go, and local go build.
// On docker:go and exec:go, it maps to /extra/filecoin-ffi, as it's picked up
// as an "extra source" in the manifest.
replace github.com/filecoin-project/filecoin-ffi => ../extra/filecoin-ffi

1865
lotus-soup/go.sum Normal file

File diff suppressed because it is too large Load Diff

27
lotus-soup/main.go Normal file
View File

@ -0,0 +1,27 @@
package main
import (
"fmt"
"github.com/testground/sdk-go/run"
"github.com/testground/sdk-go/runtime"
)
var testplans = map[string]interface{}{
"lotus-baseline": doRun(baselineRoles),
}
func main() {
run.InvokeMap(testplans)
}
func doRun(roles map[string]func(*TestEnvironment) error) run.InitializedTestCaseFn {
return func(runenv *runtime.RunEnv, initCtx *run.InitContext) error {
role := runenv.StringParam("role")
proc, ok := baselineRoles[role]
if ok {
return proc(&TestEnvironment{RunEnv: runenv, InitContext: initCtx})
}
return fmt.Errorf("Unknown role: %s", role)
}
}

30
lotus-soup/manifest.toml Normal file
View File

@ -0,0 +1,30 @@
name = "lotus-soup"
extra_sources = { "exec:go" = ["../extra/filecoin-ffi"] }
[defaults]
builder = "docker:go"
runner = "local:docker"
[builders."docker:go"]
enabled = true
build_base_image = "iptestground/oni-buildbase:v1"
runtime_image = "iptestground/oni-runtime:v1"
enable_go_build_cache = true
skip_runtime_image = false
[runners."local:docker"]
enabled = true
[runners."cluster:k8s"]
enabled = true
[[testcases]]
name = "lotus-baseline"
instances = { min = 1, max = 100, default = 5 }
[testcases.params]
clients = { type = "int", default = 1 }
miners = { type = "int", default = 1 }
balance = { type = "int", default = 1 }
sectors = { type = "int", default = 1 }
role = { type = "string" }

518
lotus-soup/node.go Normal file
View File

@ -0,0 +1,518 @@
package main
import (
"bytes"
"context"
"crypto/rand"
//"encoding/json"
"fmt"
"io/ioutil"
"time"
"github.com/testground/sdk-go/run"
"github.com/testground/sdk-go/runtime"
"github.com/testground/sdk-go/sync"
logging "github.com/ipfs/go-log/v2"
libp2p_crypto "github.com/libp2p/go-libp2p-core/crypto"
"github.com/libp2p/go-libp2p-core/peer"
ma "github.com/multiformats/go-multiaddr"
"github.com/ipfs/go-datastore"
"github.com/filecoin-project/go-address"
"github.com/filecoin-project/go-storedcounter"
"github.com/filecoin-project/lotus/api"
"github.com/filecoin-project/lotus/build"
"github.com/filecoin-project/lotus/chain/actors"
genesis_chain "github.com/filecoin-project/lotus/chain/gen/genesis"
"github.com/filecoin-project/lotus/chain/types"
"github.com/filecoin-project/lotus/chain/wallet"
"github.com/filecoin-project/lotus/cmd/lotus-seed/seed"
"github.com/filecoin-project/lotus/genesis"
"github.com/filecoin-project/lotus/node"
"github.com/filecoin-project/lotus/node/config"
"github.com/filecoin-project/lotus/node/modules"
"github.com/filecoin-project/lotus/node/modules/dtypes"
"github.com/filecoin-project/lotus/node/modules/lp2p"
modtest "github.com/filecoin-project/lotus/node/modules/testing"
"github.com/filecoin-project/lotus/node/repo"
"github.com/filecoin-project/specs-actors/actors/builtin/verifreg"
"github.com/filecoin-project/specs-actors/actors/abi"
"github.com/filecoin-project/specs-actors/actors/abi/big"
"github.com/filecoin-project/specs-actors/actors/builtin"
saminer "github.com/filecoin-project/specs-actors/actors/builtin/miner"
"github.com/filecoin-project/specs-actors/actors/builtin/power"
"github.com/filecoin-project/specs-actors/actors/crypto"
)
var (
PrepareNodeTimeout = time.Minute
genesisTopic = sync.NewTopic("genesis", &GenesisMsg{})
balanceTopic = sync.NewTopic("balance", &InitialBalanceMsg{})
presealTopic = sync.NewTopic("preseal", &PresealMsg{})
stateReady = sync.State("ready")
)
type TestEnvironment struct {
*runtime.RunEnv
*run.InitContext
}
type Node struct {
fullApi api.FullNode
minerApi api.StorageMiner
stop node.StopFunc
}
type InitialBalanceMsg struct {
Addr address.Address
Balance int
}
type PresealMsg struct {
Miner genesis.Miner
}
type GenesisMsg struct {
Genesis []byte
Bootstrapper []byte
}
func init() {
logging.SetLogLevel("vm", "WARN")
build.DisableBuiltinAssets = true
// Note: I don't understand the significance of this, but the node test does it.
power.ConsensusMinerMinPower = big.NewInt(2048)
saminer.SupportedProofTypes = map[abi.RegisteredSealProof]struct{}{
abi.RegisteredSealProof_StackedDrg2KiBV1: {},
}
verifreg.MinVerifiedDealSize = big.NewInt(256)
}
func prepareBootstrapper(t *TestEnvironment) (*Node, error) {
ctx, cancel := context.WithTimeout(context.Background(), PrepareNodeTimeout)
defer cancel()
clients := t.IntParam("clients")
miners := t.IntParam("miners")
nodes := clients + miners
// the first duty of the boostrapper is to construct the genesis block
// first collect all client and miner balances to assign initial funds
balanceMsgs := make([]*InitialBalanceMsg, 0, nodes)
balanceCh := make(chan *InitialBalanceMsg)
t.SyncClient.MustSubscribe(ctx, balanceTopic, balanceCh)
for i := 0; i < nodes; i++ {
m := <-balanceCh
balanceMsgs = append(balanceMsgs, m)
}
// then collect all preseals from miners
presealMsgs := make([]*PresealMsg, 0, miners)
presealCh := make(chan *PresealMsg)
t.SyncClient.MustSubscribe(ctx, presealTopic, presealCh)
for i := 0; i < miners; i++ {
m := <-presealCh
presealMsgs = append(presealMsgs, m)
}
// now construct the genesis block
var genesisActors []genesis.Actor
var genesisMiners []genesis.Miner
for _, bm := range balanceMsgs {
genesisActors = append(genesisActors,
genesis.Actor{
Type: genesis.TAccount,
Balance: big.Mul(big.NewInt(int64(bm.Balance)), types.NewInt(build.FilecoinPrecision)),
Meta: (&genesis.AccountMeta{Owner: bm.Addr}).ActorMeta(),
})
}
for _, pm := range presealMsgs {
genesisMiners = append(genesisMiners, pm.Miner)
}
genesisTemplate := genesis.Template{
Accounts: genesisActors,
Miners: genesisMiners,
Timestamp: uint64(time.Now().Unix() - 1000), // this needs to be in the past
}
// dump the genesis block
// var jsonBuf bytes.Buffer
// jsonEnc := json.NewEncoder(&jsonBuf)
// err := jsonEnc.Encode(genesisTemplate)
// if err != nil {
// panic(err)
// }
// runenv.RecordMessage(fmt.Sprintf("Genesis template: %s", string(jsonBuf.Bytes())))
// this is horrendously disgusting, we use this contraption to side effect the construction
// of the genesis block in the buffer -- yes, a side effect of dependency injection.
// I remember when software was straightforward...
var genesisBuffer bytes.Buffer
bootstrapperIP := t.NetClient.MustGetDataNetworkIP().String()
n := &Node{}
stop, err := node.New(context.Background(),
node.FullAPI(&n.fullApi),
node.Online(),
node.Repo(repo.NewMemory(nil)),
node.Override(new(modules.Genesis), modtest.MakeGenesisMem(&genesisBuffer, genesisTemplate)),
withListenAddress(bootstrapperIP),
withBootstrapper(nil),
withPubsubConfig(true),
)
if err != nil {
return nil, err
}
n.stop = stop
// this dance to construct the bootstrapper multiaddr is quite vexing.
var bootstrapperAddr ma.Multiaddr
bootstrapperAddrs, err := n.fullApi.NetAddrsListen(ctx)
if err != nil {
stop(context.TODO())
return nil, err
}
for _, a := range bootstrapperAddrs.Addrs {
ip, err := a.ValueForProtocol(ma.P_IP4)
if err != nil {
continue
}
if ip != bootstrapperIP {
continue
}
addrs, err := peer.AddrInfoToP2pAddrs(&peer.AddrInfo{
ID: bootstrapperAddrs.ID,
Addrs: []ma.Multiaddr{a},
})
if err != nil {
panic(err)
}
bootstrapperAddr = addrs[0]
break
}
if bootstrapperAddr == nil {
panic("failed to determine bootstrapper address")
}
genesisMsg := &GenesisMsg{
Genesis: genesisBuffer.Bytes(),
Bootstrapper: bootstrapperAddr.Bytes(),
}
t.SyncClient.MustPublish(ctx, genesisTopic, genesisMsg)
// we are ready; wait for all nodes to be ready
t.SyncClient.MustBarrier(ctx, stateReady, t.TestInstanceCount)
return n, nil
}
func prepareMiner(t *TestEnvironment) (*Node, error) {
ctx, cancel := context.WithTimeout(context.Background(), PrepareNodeTimeout)
defer cancel()
// first create a wallet
walletKey, err := wallet.GenerateKey(crypto.SigTypeBLS)
if err != nil {
return nil, err
}
// publish the account ID/balance
balance := t.IntParam("balance")
balanceMsg := &InitialBalanceMsg{Addr: walletKey.Address, Balance: balance}
t.SyncClient.Publish(ctx, balanceTopic, balanceMsg)
// create and publish the preseal commitment
priv, _, err := libp2p_crypto.GenerateEd25519Key(rand.Reader)
if err != nil {
return nil, err
}
minerID, err := peer.IDFromPrivateKey(priv)
if err != nil {
return nil, err
}
minerAddr, err := address.NewIDAddress(genesis_chain.MinerStart + uint64(t.GroupSeq-1))
if err != nil {
return nil, err
}
presealDir, err := ioutil.TempDir("", "preseal")
if err != nil {
return nil, err
}
sectors := t.IntParam("sectors")
genMiner, _, err := seed.PreSeal(minerAddr, abi.RegisteredSealProof_StackedDrg2KiBV1, 0, sectors, presealDir, []byte("TODO: randomize this"), &walletKey.KeyInfo)
if err != nil {
return nil, err
}
genMiner.PeerId = minerID
t.RecordMessage("Miner Info: Owner: %s Worker: %s", genMiner.Owner, genMiner.Worker)
presealMsg := &PresealMsg{Miner: *genMiner}
t.SyncClient.Publish(ctx, presealTopic, presealMsg)
// then collect the genesis block and bootstrapper address
genesisCh := make(chan *GenesisMsg)
t.SyncClient.MustSubscribe(ctx, genesisTopic, genesisCh)
genesisMsg := <-genesisCh
// prepare the repo
minerRepo := repo.NewMemory(nil)
// V00D00 People DaNC3!
lr, err := minerRepo.Lock(repo.StorageMiner)
if err != nil {
return nil, err
}
ks, err := lr.KeyStore()
if err != nil {
return nil, err
}
kbytes, err := priv.Bytes()
if err != nil {
return nil, err
}
err = ks.Put("libp2p-host", types.KeyInfo{
Type: "libp2p-host",
PrivateKey: kbytes,
})
if err != nil {
return nil, err
}
ds, err := lr.Datastore("/metadata")
if err != nil {
return nil, err
}
err = ds.Put(datastore.NewKey("miner-address"), minerAddr.Bytes())
if err != nil {
return nil, err
}
nic := storedcounter.New(ds, datastore.NewKey(modules.StorageCounterDSPrefix))
for i := 0; i < (sectors + 1); i++ {
_, err = nic.Next()
if err != nil {
return nil, err
}
}
err = lr.Close()
if err != nil {
return nil, err
}
minerIP := t.NetClient.MustGetDataNetworkIP().String()
// create the node
// we need both a full node _and_ and storage miner node
n := &Node{}
stop1, err := node.New(context.Background(),
node.FullAPI(&n.fullApi),
node.Online(),
node.Repo(repo.NewMemory(nil)),
withGenesis(genesisMsg.Genesis),
withListenAddress(minerIP),
withBootstrapper(genesisMsg.Bootstrapper),
withPubsubConfig(false),
)
if err != nil {
return nil, err
}
// set the wallet
err = n.setWallet(ctx, walletKey)
if err != nil {
stop1(context.TODO())
return nil, err
}
stop2, err := node.New(context.Background(),
node.StorageMiner(&n.minerApi),
node.Online(),
node.Repo(minerRepo),
node.Override(new(api.FullNode), n.fullApi),
withMinerListenAddress(minerIP),
)
if err != nil {
stop1(context.TODO())
return nil, err
}
n.stop = func(ctx context.Context) error {
// TODO use a multierror for this
err2 := stop2(ctx)
err1 := stop1(ctx)
if err2 != nil {
return err2
}
return err1
}
// add local storage for presealed sectors
err = n.minerApi.StorageAddLocal(ctx, presealDir)
if err != nil {
n.stop(context.TODO())
return nil, err
}
// set the miner PeerID
minerIDEncoded, err := actors.SerializeParams(&saminer.ChangePeerIDParams{NewID: abi.PeerID(minerID)})
if err != nil {
return nil, err
}
changeMinerID := &types.Message{
To: minerAddr,
From: genMiner.Worker,
Method: builtin.MethodsMiner.ChangePeerID,
Params: minerIDEncoded,
Value: types.NewInt(0),
GasPrice: types.NewInt(0),
GasLimit: 1000000,
}
_, err = n.fullApi.MpoolPushMessage(ctx, changeMinerID)
if err != nil {
n.stop(context.TODO())
return nil, err
}
// we are ready; wait for all nodes to be ready
t.RecordMessage("waiting for all nodes to be ready")
t.SyncClient.MustBarrier(ctx, stateReady, t.TestInstanceCount)
return n, err
}
func prepareClient(t *TestEnvironment) (*Node, error) {
ctx, cancel := context.WithTimeout(context.Background(), PrepareNodeTimeout)
defer cancel()
// first create a wallet
walletKey, err := wallet.GenerateKey(crypto.SigTypeBLS)
if err != nil {
return nil, err
}
// publish the account ID/balance
balance := t.IntParam("balance")
balanceMsg := &InitialBalanceMsg{Addr: walletKey.Address, Balance: balance}
t.SyncClient.Publish(ctx, balanceTopic, balanceMsg)
// then collect the genesis block and bootstrapper address
genesisCh := make(chan *GenesisMsg)
t.SyncClient.MustSubscribe(ctx, genesisTopic, genesisCh)
genesisMsg := <-genesisCh
clientIP := t.NetClient.MustGetDataNetworkIP().String()
// create the node
n := &Node{}
stop, err := node.New(context.Background(),
node.FullAPI(&n.fullApi),
node.Online(),
node.Repo(repo.NewMemory(nil)),
withGenesis(genesisMsg.Genesis),
withListenAddress(clientIP),
withBootstrapper(genesisMsg.Bootstrapper),
withPubsubConfig(false),
)
if err != nil {
return nil, err
}
n.stop = stop
// set the wallet
err = n.setWallet(ctx, walletKey)
if err != nil {
stop(context.TODO())
return nil, err
}
t.RecordMessage("waiting for all nodes to be ready")
// we are ready; wait for all nodes to be ready
t.SyncClient.MustBarrier(ctx, stateReady, t.TestInstanceCount)
return n, nil
}
func (n *Node) setWallet(ctx context.Context, walletKey *wallet.Key) error {
_, err := n.fullApi.WalletImport(ctx, &walletKey.KeyInfo)
if err != nil {
return err
}
err = n.fullApi.WalletSetDefault(ctx, walletKey.Address)
if err != nil {
return err
}
return nil
}
func withGenesis(gb []byte) node.Option {
return node.Override(new(modules.Genesis), modules.LoadGenesis(gb))
}
func withBootstrapper(ab []byte) node.Option {
return node.Override(new(dtypes.BootstrapPeers),
func() (dtypes.BootstrapPeers, error) {
if ab == nil {
return dtypes.BootstrapPeers{}, nil
}
a, err := ma.NewMultiaddrBytes(ab)
if err != nil {
return nil, err
}
ai, err := peer.AddrInfoFromP2pAddr(a)
if err != nil {
return nil, err
}
return dtypes.BootstrapPeers{*ai}, nil
})
}
func withPubsubConfig(bootstrapper bool) node.Option {
return node.Override(new(*config.Pubsub), func() *config.Pubsub {
return &config.Pubsub{
Bootstrapper: bootstrapper,
RemoteTracer: "",
}
})
}
func withListenAddress(ip string) node.Option {
addrs := []string{fmt.Sprintf("/ip4/%s/tcp/4001", ip)}
return node.Override(node.StartListeningKey, lp2p.StartListening(addrs))
}
func withMinerListenAddress(ip string) node.Option {
addrs := []string{fmt.Sprintf("/ip4/%s/tcp/4002", ip)}
return node.Override(node.StartListeningKey, lp2p.StartListening(addrs))
}