From c5f3edc465a19355f6b09f7edf94168ceda2bab9 Mon Sep 17 00:00:00 2001 From: Chris Schinnerl Date: Mon, 5 Dec 2022 17:23:52 +0100 Subject: [PATCH] Extend cluster type for testing to allow for adding funded hosts to the cluster --- .github/workflows/test.yml | 10 ++ .gitignore | 1 + bus/bus.go | 54 ++++++++- bus/client.go | 57 +++++++++- go.mod | 2 + go.sum | 2 - internal/node/miner.go | 146 ++++++++++++++++++++++++ internal/node/node.go | 15 ++- internal/testing/cluster.go | 183 +++++++++++++++++++++++++++++-- internal/testing/cluster_test.go | 9 ++ internal/testing/init.go | 89 +++++++++++++++ 11 files changed, 550 insertions(+), 18 deletions(-) create mode 100644 .gitignore create mode 100644 internal/node/miner.go create mode 100644 internal/testing/init.go diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8dcd673cb..fd4c73fa7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,7 +28,17 @@ jobs: - name: Test Windows 1.18 # can't run race detector on windows with go 1.18 or lower due to a bug (https://github.com/golang/go/issues/46099) if: matrix.os == 'windows-latest' && matrix.go-version == '1.18' uses: n8maninger/action-golang-test@v1 + with: + args: "-short" - name: Test + if: matrix.os != 'windows-latest' || matrix.go-version != '1.18' + uses: n8maninger/action-golang-test@v1 + with: + args: "-race;-short" + - name: Test Long Windows 1.18 # can't run race detector on windows with go 1.18 or lower due to a bug (https://github.com/golang/go/issues/46099) + if: matrix.os == 'windows-latest' && matrix.go-version == '1.18' + uses: n8maninger/action-golang-test@v1 + - name: Test Long if: matrix.os != 'windows-latest' || matrix.go-version != '1.18' uses: n8maninger/action-golang-test@v1 with: diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..722d5e71d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vscode diff --git a/bus/bus.go b/bus/bus.go index 56ffd1846..329f06766 100644 --- a/bus/bus.go +++ b/bus/bus.go @@ -25,7 +25,7 @@ type ( // A Syncer can connect to other peers and synchronize the blockchain. Syncer interface { - Addr() string + SyncerAddress() (string, error) Peers() []string Connect(addr string) error BroadcastTransaction(txn types.Transaction, dependsOn []types.Transaction) @@ -86,11 +86,17 @@ type ( SlabsForMigration(n int, failureCutoff time.Time, goodContracts []types.FileContractID) ([]SlabID, error) SlabForMigration(slabID SlabID) (object.Slab, []MigrationContract, error) } + + // A Miner mines blocks and submits them to the network. + Miner interface { + Mine(addr types.UnlockHash, n int) error + } ) type bus struct { s Syncer cm ChainManager + m Miner tp TransactionPool w Wallet hdb HostDB @@ -99,6 +105,14 @@ type bus struct { os ObjectStore } +func (b *bus) syncerAddrHandler(jc jape.Context) { + addr, err := b.s.SyncerAddress() + if jc.Check("failed to fetch syncer's address", err) != nil { + return + } + jc.Encode(addr) +} + func (b *bus) syncerPeersHandler(jc jape.Context) { jc.Encode(b.s.Peers()) } @@ -117,6 +131,11 @@ func (b *bus) consensusStateHandler(jc jape.Context) { }) } +func (b *bus) txpoolFeeHandler(jc jape.Context) { + fee := b.tp.RecommendedFee() + jc.Encode(fee) +} + func (b *bus) txpoolTransactionsHandler(jc jape.Context) { jc.Encode(b.tp.Transactions()) } @@ -556,11 +575,32 @@ func (b *bus) objectsMarkSlabMigrationFailureHandlerPOST(jc jape.Context) { } } -// New returns an HTTP handler that serves the bus API. -func New(s Syncer, cm ChainManager, tp TransactionPool, w Wallet, hdb HostDB, cs ContractStore, css ContractSetStore, os ObjectStore) http.Handler { +// TODO: ideally we can get rid of this handler again once we have a way to +// subscribe to consensus through the API and submit blocks through the API. +func (b *bus) minerMineHandlerPOST(jc jape.Context) { + if b.m == nil { + jc.ResponseWriter.WriteHeader(http.StatusNotFound) // miner not enabled + return + } + var uh types.UnlockHash + if jc.DecodeParam("unlockhash", &uh) != nil { + return + } + var n int + if jc.DecodeForm("numBlocks", &n) != nil { + return + } + if jc.Check("failed to mine blocks", b.m.Mine(uh, n)) != nil { + return + } +} + +// New returns a new Bus. +func New(s Syncer, cm ChainManager, m Miner, tp TransactionPool, w Wallet, hdb HostDB, cs ContractStore, css ContractSetStore, os ObjectStore) http.Handler { b := &bus{ s: s, cm: cm, + m: m, tp: tp, w: w, hdb: hdb, @@ -569,13 +609,15 @@ func New(s Syncer, cm ChainManager, tp TransactionPool, w Wallet, hdb HostDB, cs os: os, } return jape.Mux(map[string]jape.Handler{ + "GET /syncer/address": b.syncerAddrHandler, "GET /syncer/peers": b.syncerPeersHandler, "POST /syncer/connect": b.syncerConnectHandler, "GET /consensus/state": b.consensusStateHandler, - "GET /txpool/transactions": b.txpoolTransactionsHandler, - "POST /txpool/broadcast": b.txpoolBroadcastHandler, + "GET /txpool/recommendedfee": b.txpoolFeeHandler, + "GET /txpool/transactions": b.txpoolTransactionsHandler, + "POST /txpool/broadcast": b.txpoolBroadcastHandler, "GET /wallet/balance": b.walletBalanceHandler, "GET /wallet/address": b.walletAddressHandler, @@ -612,5 +654,7 @@ func New(s Syncer, cm ChainManager, tp TransactionPool, w Wallet, hdb HostDB, cs "GET /migration/slabs": b.objectsMigrationSlabsHandlerGET, "GET /migration/slab/:id": b.objectsMigrationSlabHandlerGET, "POST /migration/failed": b.objectsMarkSlabMigrationFailureHandlerPOST, + + "POST /mine/:unlockhash": b.minerMineHandlerPOST, }) } diff --git a/bus/client.go b/bus/client.go index 25adeeaba..e0edddc6b 100644 --- a/bus/client.go +++ b/bus/client.go @@ -19,6 +19,12 @@ type Client struct { c jape.Client } +// SyncerAddress returns the address the syncer is listening on. +func (c *Client) SyncerAddress() (addr string, err error) { + err = c.c.GET("/syncer/address", &addr) + return +} + // SyncerPeers returns the current peers of the syncer. func (c *Client) SyncerPeers() (resp []string, err error) { err = c.c.GET("/syncer/peers", &resp) @@ -67,6 +73,44 @@ func (c *Client) WalletOutputs() (resp []wallet.SiacoinElement, err error) { return } +// estimatedSiacoinTxnSize estimates the txn size of a siacoin txn without file +// contract given its number of outputs. +func estimatedSiacoinTxnSize(nOutputs uint64) uint64 { + return 1000 + 60*nOutputs +} + +// SendSiacoins is a helper method that sends siacoins to the given outputs. +func (c *Client) SendSiacoins(scos []types.SiacoinOutput) (err error) { + fee, err := c.RecommendedFee() + if err != nil { + return err + } + fee = fee.Mul64(estimatedSiacoinTxnSize(uint64(len(scos)))) + + var value types.Currency + for _, sco := range scos { + value = value.Add(sco.Value) + } + txn := types.Transaction{ + SiacoinOutputs: scos, + MinerFees: []types.Currency{fee}, + } + toSign, parents, err := c.WalletFund(&txn, value) + if err != nil { + return err + } + defer func() { + if err != nil { + _ = c.WalletDiscard(txn) + } + }() + err = c.WalletSign(&txn, toSign, types.FullCoveredFields) + if err != nil { + return err + } + return c.BroadcastTransaction(append(parents, txn)) +} + // WalletTransactions returns all transactions relevant to the wallet. func (c *Client) WalletTransactions(since time.Time, max int) (resp []wallet.Transaction, err error) { err = c.c.GET(fmt.Sprintf("/wallet/transactions?since=%s&max=%d", paramTime(since), max), &resp) @@ -268,8 +312,11 @@ func (c *Client) ContractMetadata(types.FileContractID) (ContractMetadata, error func (c *Client) UpdateContractMetadata(types.FileContractID, ContractMetadata) error { panic("unimplemented") } -func (c *Client) RecommendedFee() (types.Currency, error) { - panic("unimplemented") + +// RecommendedFee returns the recommended fee for a txn. +func (c *Client) RecommendedFee() (fee types.Currency, err error) { + err = c.c.GET("/txpool/recommendedfee", &fee) + return } // ContractsForSlab returns contracts that can be used to download the provided @@ -340,6 +387,12 @@ func (c *Client) UploadParams() (up UploadParams, err error) { panic("unimplemented") } +// MineBlocks updates the latest failure time of the given slabs +// to the current time. +func (c *Client) MineBlocks(uh types.UnlockHash, n int) error { + return c.c.POST(fmt.Sprintf("/mine/%v?numBlocks=%d", uh, n), nil, nil) +} + // NewClient returns a client that communicates with a renterd store server // listening on the specified address. func NewClient(addr, password string) *Client { diff --git a/go.mod b/go.mod index 12e4c4e29..2c428b16f 100644 --- a/go.mod +++ b/go.mod @@ -47,3 +47,5 @@ require ( golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect golang.org/x/text v0.3.6 // indirect ) + +replace go.sia.tech/siad => ./../siad diff --git a/go.sum b/go.sum index 4035f389e..b55b327df 100644 --- a/go.sum +++ b/go.sum @@ -163,8 +163,6 @@ gitlab.com/NebulousLabs/writeaheadlog v0.0.0-20200618142844-c59a90f49130/go.mod go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.sia.tech/jape v0.5.0 h1:6BLMZEePInWwQcJO1mcmrPBpF0QuvkrXmHSWGKeR0tY= go.sia.tech/jape v0.5.0/go.mod h1:bu+ka8FgKq7MNH2JRTTOjfvVNku97V4ffyKr0dKoU90= -go.sia.tech/siad v1.5.9 h1:uhaTYAkJQxXh0NEFRIvgD+9bR/1WmKTWQOPojNPdutA= -go.sia.tech/siad v1.5.9/go.mod h1:ifu7TjXlL9s+47DSmqeMz8LOvthALMysZkJ3Df0daAY= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= diff --git a/internal/node/miner.go b/internal/node/miner.go new file mode 100644 index 000000000..d1cbbccfb --- /dev/null +++ b/internal/node/miner.go @@ -0,0 +1,146 @@ +// TODO: remove this file when we can import it from hostd +package node + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "sync" + + "gitlab.com/NebulousLabs/fastrand" + "go.sia.tech/siad/crypto" + "go.sia.tech/siad/modules" + "go.sia.tech/siad/types" +) + +const solveAttempts = 1e4 + +type ( + // Consensus defines a minimal interface needed by the miner to interact + // with the consensus set + Consensus interface { + AcceptBlock(types.Block) error + } + + // A Miner is a CPU miner that can mine blocks, sending the reward to a + // specified address. + Miner struct { + consensus Consensus + + mu sync.Mutex + height types.BlockHeight + target types.Target + currentBlockID types.BlockID + txnsets map[modules.TransactionSetID][]types.TransactionID + transactions []types.Transaction + } +) + +var errFailedToSolve = errors.New("failed to solve block") + +// ProcessConsensusChange implements modules.ConsensusSetSubscriber. +func (m *Miner) ProcessConsensusChange(cc modules.ConsensusChange) { + m.mu.Lock() + defer m.mu.Unlock() + m.target = cc.ChildTarget + m.currentBlockID = cc.AppliedBlocks[len(cc.AppliedBlocks)-1].ID() + m.height = cc.BlockHeight +} + +// ReceiveUpdatedUnconfirmedTransactions implements modules.TransactionPoolSubscriber +func (m *Miner) ReceiveUpdatedUnconfirmedTransactions(diff *modules.TransactionPoolDiff) { + m.mu.Lock() + defer m.mu.Unlock() + + reverted := make(map[types.TransactionID]bool) + for _, setID := range diff.RevertedTransactions { + for _, txnID := range m.txnsets[setID] { + reverted[txnID] = true + } + } + + filtered := m.transactions[:0] + for _, txn := range m.transactions { + if reverted[txn.ID()] { + continue + } + filtered = append(filtered, txn) + } + + for _, txnset := range diff.AppliedTransactions { + m.txnsets[txnset.ID] = txnset.IDs + filtered = append(filtered, txnset.Transactions...) + } + m.transactions = filtered +} + +// mineBlock attempts to mine a block and add it to the consensus set. +func (m *Miner) mineBlock(addr types.UnlockHash) error { + m.mu.Lock() + block := types.Block{ + ParentID: m.currentBlockID, + Timestamp: types.CurrentTimestamp(), + } + + randBytes := fastrand.Bytes(types.SpecifierLen) + randTxn := types.Transaction{ + ArbitraryData: [][]byte{append(modules.PrefixNonSia[:], randBytes...)}, + } + block.Transactions = append([]types.Transaction{randTxn}, m.transactions...) + block.MinerPayouts = append(block.MinerPayouts, types.SiacoinOutput{ + Value: block.CalculateSubsidy(m.height + 1), + UnlockHash: addr, + }) + target := m.target + m.mu.Unlock() + + merkleRoot := block.MerkleRoot() + header := make([]byte, 80) + copy(header, block.ParentID[:]) + binary.LittleEndian.PutUint64(header[40:48], uint64(block.Timestamp)) + copy(header[48:], merkleRoot[:]) + + var nonce uint64 + var solved bool + for i := 0; i < solveAttempts; i++ { + id := crypto.HashBytes(header) + if bytes.Compare(target[:], id[:]) >= 0 { + block.Nonce = *(*types.BlockNonce)(header[32:40]) + solved = true + break + } + binary.LittleEndian.PutUint64(header[32:], nonce) + nonce += types.ASICHardforkFactor + } + if !solved { + return errFailedToSolve + } + + if err := m.consensus.AcceptBlock(block); err != nil { + return fmt.Errorf("failed to get block accepted: %w", err) + } + return nil +} + +// Mine mines n blocks, sending the reward to addr +func (m *Miner) Mine(addr types.UnlockHash, n int) error { + var err error + for mined := 1; mined <= n; { + // return the error only if the miner failed to solve the block, + // ignore any consensus related errors + if err = m.mineBlock(addr); errors.Is(err, errFailedToSolve) { + return fmt.Errorf("failed to mine block %v: %w", mined, errFailedToSolve) + } + mined++ + } + return nil +} + +// NewMiner initializes a new CPU miner +func NewMiner(consensus Consensus) *Miner { + return &Miner{ + consensus: consensus, + txnsets: make(map[modules.TransactionSetID][]types.TransactionID), + } +} diff --git a/internal/node/node.go b/internal/node/node.go index 9873e3a3b..c4aacb438 100644 --- a/internal/node/node.go +++ b/internal/node/node.go @@ -27,6 +27,7 @@ type WorkerConfig struct { type BusConfig struct { Bootstrap bool GatewayAddr string + Miner bool } type AutopilotConfig struct { @@ -75,6 +76,10 @@ func (s syncer) BroadcastTransaction(txn types.Transaction, dependsOn []types.Tr s.tp.Broadcast(append(dependsOn, txn)) } +func (s syncer) SyncerAddress() (string, error) { + return string(s.g.Address()), nil +} + type txpool struct { tp modules.TransactionPool } @@ -179,6 +184,14 @@ func NewBus(cfg BusConfig, dir string, walletKey consensus.PrivateKey) (http.Han return nil, nil, err } + var m *Miner + if cfg.Miner { + m = NewMiner(cm) + if err := cm.ConsensusSetSubscribe(m, ccid, nil); err != nil { + return nil, nil, err + } + } + contractsDir := filepath.Join(dir, "contracts") if err := os.MkdirAll(contractsDir, 0700); err != nil { return nil, nil, err @@ -204,7 +217,7 @@ func NewBus(cfg BusConfig, dir string, walletKey consensus.PrivateKey) (http.Han return nil } - b := bus.New(syncer{g, tp}, chainManager{cm}, txpool{tp}, w, sqlStore, sqlStore, sqlStore, sqlStore) + b := bus.New(syncer{g, tp}, chainManager{cm}, m, txpool{tp}, w, sqlStore, sqlStore, sqlStore, sqlStore) return b, cleanup, nil } diff --git a/internal/testing/cluster.go b/internal/testing/cluster.go index 7a2e7bbbc..558953bfe 100644 --- a/internal/testing/cluster.go +++ b/internal/testing/cluster.go @@ -3,6 +3,8 @@ package testing import ( "context" "encoding/hex" + "errors" + "fmt" "net" "net/http" "path/filepath" @@ -14,6 +16,11 @@ import ( "go.sia.tech/renterd/bus" "go.sia.tech/renterd/internal/consensus" "go.sia.tech/renterd/internal/node" + "go.sia.tech/siad/modules" + sianode "go.sia.tech/siad/node" + "go.sia.tech/siad/node/api/client" + "go.sia.tech/siad/types" + "go.sia.tech/renterd/worker" "go.sia.tech/siad/siatest" "lukechampine.com/frand" @@ -29,17 +36,19 @@ type TestCluster struct { // Autopilot *autopilot.Client // TODO: add once available cleanups []func() error - listeners []net.Listener shutdowns []func(context.Context) error - dir string - wg sync.WaitGroup + dir string + gatewayAddr string + wg sync.WaitGroup } +// randomPassword creates a random 32 byte password encoded as a string. func randomPassword() string { return hex.EncodeToString(frand.Bytes(32)) } +// newTestCluster creates a new cluster without hosts with a funded bus. func newTestCluster(dir string) (*TestCluster, error) { // Use shared wallet key. wk := consensus.GeneratePrivateKey() @@ -70,8 +79,9 @@ func newTestCluster(dir string) (*TestCluster, error) { // Create bus. var cleanups []func() error b, cleanup, err := node.NewBus(node.BusConfig{ - Bootstrap: true, + Bootstrap: false, GatewayAddr: "127.0.0.1:0", + Miner: true, }, busDir, wk) if err != nil { return nil, err @@ -82,6 +92,10 @@ func newTestCluster(dir string) (*TestCluster, error) { Handler: busAuth(b), } busClient := bus.NewClient(busAddr, busPassword) + gatewayAddr, err := busClient.SyncerAddress() + if err != nil { + return nil, err + } // Create worker. w, cleanup, err := node.NewWorker(node.WorkerConfig{}, busClient, wk) @@ -109,14 +123,15 @@ func newTestCluster(dir string) (*TestCluster, error) { } cluster := &TestCluster{ - dir: dir, + dir: dir, + gatewayAddr: gatewayAddr, - // Autopilot: autopilot.NewClient(autopilotAddr, autopilotPassword), // TODO + //Autopilot: autopilot.NewClient(autopilotAddr, autopilotPassword), // TODO Bus: bus.NewClient(busAddr, busPassword), Worker: worker.NewClient(workerAddr, workerPassword), cleanups: cleanups, - shutdowns: []func(context.Context) error{busServer.Shutdown, workerServer.Shutdown, autopilotServer.Shutdown}, + shutdowns: []func(context.Context) error{busServer.Shutdown, workerServer.Shutdown}, //, autopilotServer.Shutdown}, } // Spin up the servers. @@ -135,13 +150,155 @@ func newTestCluster(dir string) (*TestCluster, error) { _ = autopilotServer.Serve(autopilotListener) cluster.wg.Done() }() + + // Fund the bus by mining beyond the foundation hardfork height. + if err := cluster.MineBlocks(10 + int(types.FoundationHardforkHeight)); err != nil { + return nil, err + } return cluster, nil } +// addStorageFolderToHosts adds a single storage folder to each host. +func addStorageFolderToHost(hosts []*siatest.TestNode) error { + // The following api call is very slow. Using multiple threads speeds that + // process up a lot. + for _, host := range hosts { + storage := 512 * modules.SectorSize + if err := host.HostStorageFoldersAddPost(host.Dir, storage); err != nil { + return err + } + } + return nil +} + +// announceHosts adds storage and a registry to each host and announces them to +// the group +func announceHosts(hosts []*siatest.TestNode) error { + for _, host := range hosts { + if err := host.HostModifySettingPost(client.HostParamAcceptingContracts, true); err != nil { + return err + } + if err := host.HostModifySettingPost(client.HostParamRegistrySize, 1<<18); err != nil { + return err + } + if err := host.HostAnnouncePost(); err != nil { + return err + } + } + return nil +} + +func (c *TestCluster) sync(hosts []*siatest.TestNode) error { + for i := 0; i < 100; i++ { + synced, err := c.synced(hosts) + if err != nil { + return err + } + if synced { + return nil + } + time.Sleep(100 * time.Millisecond) + } + return errors.New("cluster was unable to sync in time") +} + +// synced returns true if bus and hosts are at the same blockheight. +func (c *TestCluster) synced(hosts []*siatest.TestNode) (bool, error) { + cs, err := c.Bus.ConsensusState() + if err != nil { + return false, err + } + for _, h := range hosts { + bh, err := h.BlockHeight() + if err != nil { + return false, err + } + if cs.BlockHeight != uint64(bh) { + return false, nil + } + } + return true, nil +} + +// MineBlocks uses the bus' miner to mine n blocks. +func (c *TestCluster) MineBlocks(n int) error { + addr, err := c.Bus.WalletAddress() + if err != nil { + return err + } + return c.Bus.MineBlocks(addr, n) +} + // AddHosts adds n hosts to the cluster. These hosts will be funded and announce // themselves on the network, ready to form contracts. func (c *TestCluster) AddHosts(n int) error { - panic("not implemented") + // Create hosts. + var newHosts []*siatest.TestNode + for i := 0; i < n; i++ { + hostDir := filepath.Join(c.dir, "hosts", fmt.Sprint(len(c.hosts)+1)) + n, err := siatest.NewCleanNodeAsync(sianode.Host(hostDir)) + if err != nil { + return err + } + c.hosts = append(c.hosts, n) + newHosts = append(newHosts, n) + + // Connect gateways. + time.Sleep(time.Second * 5) + if err := c.Bus.SyncerConnect(string(n.GatewayAddress())); err != nil { + return err + } + } + + // Fund host from bus. + balance, err := c.Bus.WalletBalance() + if err != nil { + return err + } + fundAmt := balance.Div64(2).Div64(uint64(len(newHosts))) // 50% of bus balance + var scos []types.SiacoinOutput + for _, h := range newHosts { + wag, err := h.WalletAddressGet() + if err != nil { + return err + } + scos = append(scos, types.SiacoinOutput{ + Value: fundAmt, + UnlockHash: wag.Address, + }) + } + if err := c.Bus.SendSiacoins(scos); err != nil { + return err + } + + // Mine transaction. + if err := c.MineBlocks(1); err != nil { + return err + } + + // Wait for hosts to sync up with consensus. + if err := c.sync(newHosts); err != nil { + return err + } + + // Announce hosts. + if err := addStorageFolderToHost(newHosts); err != nil { + return err + } + if err := announceHosts(newHosts); err != nil { + return err + } + + // Mine a few more blocks to mine the announcements and sync the + // cluster. + if err := c.MineBlocks(5); err != nil { + return err + } + + // TODO: wait for hosts to show up in hostdb. + + // Return once the whole cluster is synced. + return c.Sync() } // Close performs the cleanup on all servers of the cluster. @@ -151,6 +308,11 @@ func (c *TestCluster) Close() error { return err } } + for _, h := range c.hosts { + if err := h.Close(); err != nil { + return err + } + } return nil } @@ -164,3 +326,8 @@ func (c *TestCluster) Shutdown(ctx context.Context) error { c.wg.Wait() // wait for servers to shut down return nil } + +// Sync blocks until the whole cluster has reached the same block height. +func (c *TestCluster) Sync() error { + return c.sync(c.hosts) +} diff --git a/internal/testing/cluster_test.go b/internal/testing/cluster_test.go index 6a6c76007..384ce902e 100644 --- a/internal/testing/cluster_test.go +++ b/internal/testing/cluster_test.go @@ -12,6 +12,10 @@ import ( // TestNewTestCluster is a smoke test for creating a cluster of Nodes for // testing and shutting them down. func TestNewTestCluster(t *testing.T) { + if testing.Short() { + t.SkipNow() + } + cluster, err := newTestCluster(t.TempDir()) if err != nil { t.Fatal(err) @@ -22,6 +26,11 @@ func TestNewTestCluster(t *testing.T) { } }() + // Add a host. + if err := cluster.AddHosts(1); err != nil { + t.Fatal(err) + } + // Try talking to the bus API by adding an object. b := cluster.Bus hosts, err := b.AllHosts() diff --git a/internal/testing/init.go b/internal/testing/init.go new file mode 100644 index 000000000..dc90b7292 --- /dev/null +++ b/internal/testing/init.go @@ -0,0 +1,89 @@ +package testing + +import ( + "math/big" + + "go.sia.tech/siad/modules" + "go.sia.tech/siad/types" +) + +// TODO: This is quite the hack but until we have a better solution +func init() { + modules.SectorSize = modules.SectorSizeTesting + + types.TaxHardforkHeight = types.BlockHeight(10) + + // 'testing' settings are for automatic testing, and create much faster + // environments than a human can interact with. + types.ASICHardforkHeight = 5 + types.ASICHardforkTotalTarget = types.Target{255, 255} + types.ASICHardforkTotalTime = 10e3 + + types.FoundationHardforkHeight = 50 + types.FoundationSubsidyFrequency = 5 + + initialFoundationUnlockConditions, _ := types.GenerateDeterministicMultisig(2, 3, types.InitialFoundationTestingSalt) + initialFoundationFailsafeUnlockConditions, _ := types.GenerateDeterministicMultisig(3, 5, types.InitialFoundationFailsafeTestingSalt) + types.InitialFoundationUnlockHash = initialFoundationUnlockConditions.UnlockHash() + types.InitialFoundationFailsafeUnlockHash = initialFoundationFailsafeUnlockConditions.UnlockHash() + + types.BlockFrequency = 1 // As fast as possible + types.MaturityDelay = 3 + types.GenesisTimestamp = types.CurrentTimestamp() - 1e6 + types.RootTarget = types.Target{128} // Takes an expected 2 hashes; very fast for testing but still probes 'bad hash' code. + + // A restrictive difficulty clamp prevents the difficulty from climbing + // during testing, as the resolution on the difficulty adjustment is + // only 1 second and testing mining should be happening substantially + // faster than that. + types.TargetWindow = 200 + types.MaxTargetAdjustmentUp = big.NewRat(10001, 10000) + types.MaxTargetAdjustmentDown = big.NewRat(9999, 10000) + types.FutureThreshold = 3 // 3 seconds + types.ExtremeFutureThreshold = 6 // 6 seconds + + types.MinimumCoinbase = 299990 // Minimum coinbase is hit after 10 blocks to make testing minimum-coinbase code easier. + + // Do not let the difficulty change rapidly - blocks will be getting + // mined far faster than the difficulty can adjust to. + types.OakHardforkBlock = 20 + types.OakHardforkFixBlock = 23 + types.OakDecayNum = 9999 + types.OakDecayDenom = 10e3 + types.OakMaxBlockShift = 3 + types.OakMaxRise = big.NewRat(10001, 10e3) + types.OakMaxDrop = big.NewRat(10e3, 10001) + + // Populate the void address with 1 billion siacoins in the genesis block. + types.GenesisSiacoinAllocation = []types.SiacoinOutput{ + { + Value: types.NewCurrency64(1000000000).Mul(types.SiacoinPrecision), + UnlockHash: types.UnlockHash{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + }, + } + + types.GenesisSiafundAllocation = []types.SiafundOutput{ + { + Value: types.NewCurrency64(2000), + UnlockHash: types.UnlockHash{214, 166, 197, 164, 29, 201, 53, 236, 106, 239, 10, 158, 127, 131, 20, 138, 63, 221, 230, 16, 98, 247, 32, 77, 210, 68, 116, 12, 241, 89, 27, 223}, + }, + { + Value: types.NewCurrency64(7000), + UnlockHash: types.UnlockHash{209, 246, 228, 60, 248, 78, 242, 110, 9, 8, 227, 248, 225, 216, 163, 52, 142, 93, 47, 176, 103, 41, 137, 80, 212, 8, 132, 58, 241, 189, 2, 17}, + }, + { + Value: types.NewCurrency64(1000), + UnlockHash: types.UnlockConditions{}.UnlockHash(), + }, + } + + // Create the genesis block. + types.GenesisBlock = types.Block{ + Timestamp: types.GenesisTimestamp, + Transactions: []types.Transaction{ + {SiafundOutputs: types.GenesisSiafundAllocation}, + }, + } + // Calculate the genesis ID. + types.GenesisID = types.GenesisBlock.ID() +}