diff --git a/Dockerfile.gowaves-it b/Dockerfile.gowaves-it index 372db228e..43cf12b60 100644 --- a/Dockerfile.gowaves-it +++ b/Dockerfile.gowaves-it @@ -14,13 +14,13 @@ COPY cmd cmd COPY pkg pkg ARG WITH_RACE_SUFFIX="" -RUN make build-node-linux-amd64${WITH_RACE_SUFFIX} +RUN make build-node-native${WITH_RACE_SUFFIX} FROM alpine:3.21.0 ENV TZ=Etc/UTC \ APP_USER=gowaves -RUN apk add --no-cache bind-tools +RUN apk add --no-cache bind-tools curl RUN addgroup -S $APP_USER \ && adduser -S $APP_USER -G $APP_USER @@ -34,7 +34,9 @@ ENV CONFIG_PATH=/home/gowaves/config/gowaves-it.json \ USER $APP_USER -COPY --from=builder /app/build/bin/linux-amd64/node /app/node +COPY --from=builder /app/build/bin/native/node /app/node + +HEALTHCHECK CMD curl -f http://localhost:6869/node/status || exit 1 STOPSIGNAL SIGINT @@ -64,3 +66,5 @@ CMD /app/node \ -microblock-interval 2s \ -blacklist-residence-time 0 \ -rate-limiter-opts="rps=100&burst=100" \ + -min-peers-mining=2 \ + -disable-miner=$DISABLE_MINER \ diff --git a/Makefile b/Makefile index 55dd7feb7..0bd557ab0 100644 --- a/Makefile +++ b/Makefile @@ -110,6 +110,8 @@ dist-blockcmp: release-blockcmp build-node-native: @go build -o build/bin/native/node -ldflags="-X 'github.com/wavesplatform/gowaves/pkg/versioning.Version=$(VERSION)'" ./cmd/node +build-node-native-with-race: + @go build -race -o build/bin/native/node -ldflags="-X 'github.com/wavesplatform/gowaves/pkg/versioning.Version=$(VERSION)'" ./cmd/node build-node-linux-amd64: @CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o build/bin/linux-amd64/node -ldflags="-X 'github.com/wavesplatform/gowaves/pkg/versioning.Version=$(VERSION)'" ./cmd/node build-node-linux-amd64-with-race: diff --git a/itests/clients/net_client.go b/itests/clients/net_client.go index e6d984adb..f38c0657b 100644 --- a/itests/clients/net_client.go +++ b/itests/clients/net_client.go @@ -4,9 +4,13 @@ import ( "bytes" "context" "encoding/base64" + "errors" + "fmt" "io" "log/slog" + "math/big" "net" + "reflect" "sync" "sync/atomic" "testing" @@ -33,6 +37,7 @@ type NetClient struct { impl Implementation n *networking.Network c *networking.Config + h *handler s *networking.Session closing atomic.Bool @@ -45,7 +50,17 @@ func NewNetClient( n := networking.NewNetwork() p := newProtocol(t, nil) h := newHandler(t, peers) - log := slogt.New(t) + + f := slogt.Factory(func(w io.Writer) slog.Handler { + opts := &slog.HandlerOptions{ + AddSource: true, + Level: slog.LevelInfo, + } + return slog.NewTextHandler(w, opts) + }) + log := slogt.New(t, f) + + slog.SetLogLoggerLevel(slog.LevelError) conf := networking.NewConfig(p, h). WithSlogHandler(log.Handler()). WithWriteTimeout(networkTimeout). @@ -58,7 +73,7 @@ func NewNetClient( s, err := n.NewSession(ctx, conn, conf) require.NoError(t, err, "failed to establish new session to %s node", impl.String()) - cli := &NetClient{ctx: ctx, t: t, impl: impl, n: n, c: conf, s: s} + cli := &NetClient{ctx: ctx, t: t, impl: impl, n: n, c: conf, h: h, s: s} h.client = cli // Set client reference in handler. return cli } @@ -93,9 +108,85 @@ func (c *NetClient) Close() { } err := c.s.Close() require.NoError(c.t, err, "failed to close session to %s node at %q", c.impl.String(), c.s.RemoteAddr()) + c.h.close() }) } +// SubscribeForMessages adds specified types to the message waiting queue. +// Once the awaited message received the corresponding type is removed from the queue. +func (c *NetClient) SubscribeForMessages(messageType ...reflect.Type) error { + for _, mt := range messageType { + if err := c.h.waitFor(mt); err != nil { + return err + } + } + return nil +} + +// AwaitMessage waits for a message from the node for the specified timeout. +func (c *NetClient) AwaitMessage(messageType reflect.Type, timeout time.Duration) (proto.Message, error) { + select { + case <-c.ctx.Done(): + return nil, c.ctx.Err() + case <-time.After(timeout): + return nil, fmt.Errorf("timeout waiting for message of type %q", messageType.String()) + case msg := <-c.h.receiveChan(): + if reflect.TypeOf(msg) != messageType { + return nil, fmt.Errorf("unexpected message type %q, expecting %q", + reflect.TypeOf(msg).String(), messageType.String()) + } + return msg, nil + } +} + +// AwaitGetBlockMessage waits for a GetBlockMessage from the node for the specified timeout and +// returns the requested block ID. +func (c *NetClient) AwaitGetBlockMessage(timeout time.Duration) (proto.BlockID, error) { + msg, err := c.AwaitMessage(reflect.TypeOf(&proto.GetBlockMessage{}), timeout) + if err != nil { + return proto.BlockID{}, err + } + getBlockMessage, ok := msg.(*proto.GetBlockMessage) + if !ok { + return proto.BlockID{}, fmt.Errorf("failed to cast message of type %q to GetBlockMessage", + reflect.TypeOf(msg).String()) + } + return getBlockMessage.BlockID, nil +} + +// AwaitScoreMessage waits for a ScoreMessage from the node for the specified timeout and returns the received score. +func (c *NetClient) AwaitScoreMessage(timeout time.Duration) (*big.Int, error) { + msg, err := c.AwaitMessage(reflect.TypeOf(&proto.ScoreMessage{}), timeout) + if err != nil { + return nil, err + } + scoreMessage, ok := msg.(*proto.ScoreMessage) + if !ok { + return nil, fmt.Errorf("failed to cast message of type %q to ScoreMessage", reflect.TypeOf(msg).String()) + } + score := new(big.Int).SetBytes(scoreMessage.Score) + return score, nil +} + +// AwaitMicroblockRequest waits for a MicroBlockRequestMessage from the node for the specified timeout and +// returns the received block ID. +func (c *NetClient) AwaitMicroblockRequest(timeout time.Duration) (proto.BlockID, error) { + msg, err := c.AwaitMessage(reflect.TypeOf(&proto.MicroBlockRequestMessage{}), timeout) + if err != nil { + return proto.BlockID{}, err + } + mbr, ok := msg.(*proto.MicroBlockRequestMessage) + if !ok { + return proto.BlockID{}, fmt.Errorf("failed to cast message of type %q to MicroBlockRequestMessage", + reflect.TypeOf(msg).String()) + } + r, err := proto.NewBlockIDFromBytes(mbr.TotalBlockSig) + if err != nil { + return proto.BlockID{}, err + } + return r, nil +} + func (c *NetClient) reconnect() { c.t.Logf("Reconnecting to %q", c.s.RemoteAddr().String()) conn, err := net.Dial("tcp", c.s.RemoteAddr().String()) @@ -174,10 +265,13 @@ type handler struct { peers []proto.PeerInfo t testing.TB client *NetClient + queue []reflect.Type + ch chan proto.Message } func newHandler(t testing.TB, peers []proto.PeerInfo) *handler { - return &handler{t: t, peers: peers} + ch := make(chan proto.Message, 1) + return &handler{t: t, peers: peers, ch: ch} } func (h *handler) OnReceive(s *networking.Session, r io.Reader) { @@ -195,7 +289,6 @@ func (h *handler) OnReceive(s *networking.Session, r io.Reader) { } switch msg.(type) { // Only reply with peers on GetPeersMessage. case *proto.GetPeersMessage: - h.t.Logf("Received GetPeersMessage from %q", s.RemoteAddr()) rpl := &proto.PeersMessage{Peers: h.peers} if _, sErr := rpl.WriteTo(s); sErr != nil { h.t.Logf("Failed to send peers message: %v", sErr) @@ -203,6 +296,15 @@ func (h *handler) OnReceive(s *networking.Session, r io.Reader) { return } default: + if len(h.queue) == 0 { // No messages to wait for. + return + } + et := h.queue[0] + if reflect.TypeOf(msg) == et { + h.t.Logf("Received expected message of type %q", reflect.TypeOf(msg).String()) + h.queue = h.queue[1:] // Pop the expected type. + h.ch <- msg + } } } @@ -216,3 +318,25 @@ func (h *handler) OnClose(s *networking.Session) { h.client.reconnect() } } + +func (h *handler) waitFor(messageType reflect.Type) error { + if messageType == nil { + return errors.New("nil message type") + } + if messageType == reflect.TypeOf(proto.GetPeersMessage{}) { + return errors.New("cannot wait for GetPeersMessage") + } + h.queue = append(h.queue, messageType) + return nil +} + +func (h *handler) receiveChan() <-chan proto.Message { + return h.ch +} + +func (h *handler) close() { + if h.ch != nil { + close(h.ch) + h.ch = nil + } +} diff --git a/itests/clients/node_client.go b/itests/clients/node_client.go index 95a54d046..1e6002694 100644 --- a/itests/clients/node_client.go +++ b/itests/clients/node_client.go @@ -43,13 +43,13 @@ func NewNodesClients(ctx context.Context, t *testing.T, goPorts, scalaPorts *d.P } func (c *NodesClients) SendStartMessage(t *testing.T) { - c.GoClient.HTTPClient.PrintMsg(t, "------------- Start test: "+t.Name()+" -------------") - c.ScalaClient.HTTPClient.PrintMsg(t, "------------- Start test: "+t.Name()+" -------------") + c.GoClient.SendStartMessage(t) + c.ScalaClient.SendStartMessage(t) } func (c *NodesClients) SendEndMessage(t *testing.T) { - c.GoClient.HTTPClient.PrintMsg(t, "------------- End test: "+t.Name()+" -------------") - c.ScalaClient.HTTPClient.PrintMsg(t, "------------- End test: "+t.Name()+" -------------") + c.GoClient.SendEndMessage(t) + c.ScalaClient.SendEndMessage(t) } func (c *NodesClients) StateHashCmp(t *testing.T, height uint64) (*proto.StateHash, *proto.StateHash, bool) { @@ -242,7 +242,7 @@ func (c *NodesClients) SynchronizedWavesBalances( ctx, cancel := context.WithTimeout(context.Background(), synchronizedBalancesTimeout) defer cancel() - t.Logf("Initial balacnces request") + t.Logf("Initial balances request") sbs, err := c.requestAvailableBalancesForAddresses(ctx, addresses) if err != nil { t.Logf("Errors while requesting balances: %v", err) diff --git a/itests/clients/universal_client.go b/itests/clients/universal_client.go index 32c20833a..a8fae5ec2 100644 --- a/itests/clients/universal_client.go +++ b/itests/clients/universal_client.go @@ -24,3 +24,20 @@ func NewNodeUniversalClient( Connection: NewNetClient(ctx, t, impl, netPort, peers), } } + +func (c *NodeUniversalClient) SendStartMessage(t *testing.T) { + c.HTTPClient.PrintMsg(t, "------------- Start test: "+t.Name()+" -------------") +} + +func (c *NodeUniversalClient) SendEndMessage(t *testing.T) { + c.HTTPClient.PrintMsg(t, "------------- End test: "+t.Name()+" -------------") +} + +func (c *NodeUniversalClient) Handshake() { + c.Connection.SendHandshake() +} + +func (c *NodeUniversalClient) Close(t testing.TB) { + c.GRPCClient.Close(t) + c.Connection.Close() +} diff --git a/itests/config/blockchain.go b/itests/config/blockchain.go index 0c2c73553..ebe2e0daf 100644 --- a/itests/config/blockchain.go +++ b/itests/config/blockchain.go @@ -61,21 +61,19 @@ func (ra *RewardAddresses) AddressesAfter21() []proto.WavesAddress { return []proto.WavesAddress{} } -// BlockchainOption is a function type that allows to set additional parameters to BlockchainConfig. -type BlockchainOption func(*BlockchainConfig) error - // BlockchainConfig is a struct that contains settings for blockchain. // This configuration is used both for building Scala and Go configuration files. // Also, it's used to produce a Docker container run configurations for both nodes. type BlockchainConfig struct { - accounts []AccountInfo - supported []int16 - desiredReward uint64 - - Settings *settings.BlockchainSettings - Features []FeatureInfo - RewardAddresses RewardAddresses - EnableScalaMining bool + accounts []AccountInfo + supported []int16 + desiredReward uint64 + disableGoMining bool + disableScalaMining bool + + Settings *settings.BlockchainSettings + Features []FeatureInfo + RewardAddresses RewardAddresses } func NewBlockchainConfig(options ...BlockchainOption) (*BlockchainConfig, error) { @@ -174,71 +172,15 @@ func (c *BlockchainConfig) TestConfig() TestConfig { } } -// WithFeatureSettingFromFile is a BlockchainOption that allows to set feature settings from configuration file. -// Feature settings configuration file is a JSON file with the structure of `featureSettings`. -func WithFeatureSettingFromFile(path ...string) BlockchainOption { - return func(cfg *BlockchainConfig) error { - fs, err := NewFeatureSettingsFromFile(path...) - if err != nil { - return errors.Wrap(err, "failed to modify features settings") - } - cfg.supported = fs.SupportedFeatures - if ftErr := cfg.UpdatePreactivatedFeatures(fs.PreactivatedFeatures); ftErr != nil { - return errors.Wrap(ftErr, "failed to modify preactivated features") - } - return nil - } -} - -// WithPaymentsSettingFromFile is a BlockchainOption that allows to set payment settings from configuration file. -// Payment settings configuration file is a JSON file with the structure of `paymentSettings`. -func WithPaymentsSettingFromFile(path ...string) BlockchainOption { - return func(cfg *BlockchainConfig) error { - fs, err := NewPaymentSettingsFromFile(path...) - if err != nil { - return errors.Wrap(err, "failed to modify payments settings") - } - cfg.Settings.PaymentsFixAfterHeight = fs.PaymentsFixAfterHeight - cfg.Settings.InternalInvokePaymentsValidationAfterHeight = fs.InternalInvokePaymentsValidationAfterHeight - cfg.Settings.InternalInvokeCorrectFailRejectBehaviourAfterHeight = - fs.InternalInvokeCorrectFailRejectBehaviourAfterHeight - cfg.Settings.InvokeNoZeroPaymentsAfterHeight = fs.InvokeNoZeroPaymentsAfterHeight - return nil - } -} - -// WithRewardSettingFromFile is a BlockchainOption that allows to set reward settings from configuration file. -// Reward settings configuration file is a JSON file with the structure of `rewardSettings`. -func WithRewardSettingFromFile(path ...string) BlockchainOption { - return func(cfg *BlockchainConfig) error { - rs, err := NewRewardSettingsFromFile(path...) - if err != nil { - return errors.Wrap(err, "failed to modify reward settings") - } - cfg.Settings.InitialBlockReward = rs.InitialBlockReward - cfg.Settings.BlockRewardIncrement = rs.BlockRewardIncrement - cfg.Settings.BlockRewardVotingPeriod = rs.BlockRewardVotingPeriod - cfg.Settings.BlockRewardTermAfter20 = rs.BlockRewardTermAfter20 - cfg.Settings.BlockRewardTerm = rs.BlockRewardTerm - cfg.Settings.MinXTNBuyBackPeriod = rs.MinXTNBuyBackPeriod - - ras, err := NewRewardAddresses(rs.DaoAddress, rs.XtnBuybackAddress) - if err != nil { - return errors.Wrap(err, "failed to modify reward settings") - } - cfg.RewardAddresses = ras - cfg.Settings.RewardAddresses = ras.Addresses() - cfg.Settings.RewardAddressesAfter21 = ras.AddressesAfter21() - cfg.desiredReward = rs.DesiredBlockReward - return nil - } +func (c *BlockchainConfig) DisableGoMiningString() string { + return strconv.FormatBool(c.disableGoMining) } -func WithScalaMining() BlockchainOption { - return func(cfg *BlockchainConfig) error { - cfg.EnableScalaMining = true - return nil +func (c *BlockchainConfig) EnableScalaMiningString() string { + if c.disableScalaMining { + return "no" } + return "yes" } func safeNow() uint64 { diff --git a/itests/config/blockchain_options.go b/itests/config/blockchain_options.go new file mode 100644 index 000000000..7b690fbbe --- /dev/null +++ b/itests/config/blockchain_options.go @@ -0,0 +1,98 @@ +package config + +import "github.com/pkg/errors" + +// BlockchainOption is a function type that allows to set additional parameters to BlockchainConfig. +type BlockchainOption func(*BlockchainConfig) error + +// WithRewardSettingFromFile is a BlockchainOption that allows to set reward settings from configuration file. +// Reward settings configuration file is a JSON file with the structure of `rewardSettings`. +func WithRewardSettingFromFile(path ...string) BlockchainOption { + return func(cfg *BlockchainConfig) error { + rs, err := NewRewardSettingsFromFile(path...) + if err != nil { + return errors.Wrap(err, "failed to modify reward settings") + } + cfg.Settings.InitialBlockReward = rs.InitialBlockReward + cfg.Settings.BlockRewardIncrement = rs.BlockRewardIncrement + cfg.Settings.BlockRewardVotingPeriod = rs.BlockRewardVotingPeriod + cfg.Settings.BlockRewardTermAfter20 = rs.BlockRewardTermAfter20 + cfg.Settings.BlockRewardTerm = rs.BlockRewardTerm + cfg.Settings.MinXTNBuyBackPeriod = rs.MinXTNBuyBackPeriod + + ras, err := NewRewardAddresses(rs.DaoAddress, rs.XtnBuybackAddress) + if err != nil { + return errors.Wrap(err, "failed to modify reward settings") + } + cfg.RewardAddresses = ras + cfg.Settings.RewardAddresses = ras.Addresses() + cfg.Settings.RewardAddressesAfter21 = ras.AddressesAfter21() + cfg.desiredReward = rs.DesiredBlockReward + return nil + } +} + +// WithFeatureSettingFromFile is a BlockchainOption that allows to set feature settings from configuration file. +// Feature settings configuration file is a JSON file with the structure of `featureSettings`. +func WithFeatureSettingFromFile(path ...string) BlockchainOption { + return func(cfg *BlockchainConfig) error { + fs, err := NewFeatureSettingsFromFile(path...) + if err != nil { + return errors.Wrap(err, "failed to modify features settings") + } + cfg.supported = fs.SupportedFeatures + if ftErr := cfg.UpdatePreactivatedFeatures(fs.PreactivatedFeatures); ftErr != nil { + return errors.Wrap(ftErr, "failed to modify preactivated features") + } + return nil + } +} + +// WithPaymentsSettingFromFile is a BlockchainOption that allows to set payment settings from configuration file. +// Payment settings configuration file is a JSON file with the structure of `paymentSettings`. +func WithPaymentsSettingFromFile(path ...string) BlockchainOption { + return func(cfg *BlockchainConfig) error { + fs, err := NewPaymentSettingsFromFile(path...) + if err != nil { + return errors.Wrap(err, "failed to modify payments settings") + } + cfg.Settings.PaymentsFixAfterHeight = fs.PaymentsFixAfterHeight + cfg.Settings.InternalInvokePaymentsValidationAfterHeight = fs.InternalInvokePaymentsValidationAfterHeight + cfg.Settings.InternalInvokeCorrectFailRejectBehaviourAfterHeight = + fs.InternalInvokeCorrectFailRejectBehaviourAfterHeight + cfg.Settings.InvokeNoZeroPaymentsAfterHeight = fs.InvokeNoZeroPaymentsAfterHeight + return nil + } +} + +// WithNoScalaMining disables mining on the Scala node. +func WithNoScalaMining() BlockchainOption { + return func(cfg *BlockchainConfig) error { + cfg.disableScalaMining = true + return nil + } +} + +// WithNoGoMining disables mining on the Go node. +func WithNoGoMining() BlockchainOption { + return func(cfg *BlockchainConfig) error { + cfg.disableGoMining = true + return nil + } +} + +func WithPreactivatedFeatures(features []FeatureInfo) BlockchainOption { + return func(cfg *BlockchainConfig) error { + if ftErr := cfg.UpdatePreactivatedFeatures(features); ftErr != nil { + return errors.Wrap(ftErr, "failed to modify preactivated features") + } + return nil + } +} + +func WithAbsencePeriod(period uint64) BlockchainOption { + return func(cfg *BlockchainConfig) error { + cfg.Settings.LightNodeBlockFieldsAbsenceInterval = period + return nil + } +} diff --git a/itests/config/config.go b/itests/config/config.go index 8d885d927..7a287e10f 100644 --- a/itests/config/config.go +++ b/itests/config/config.go @@ -1,6 +1,7 @@ package config import ( + "encoding/binary" "encoding/json" stderrs "errors" "fmt" @@ -11,6 +12,8 @@ import ( "github.com/ory/dockertest/v3" "github.com/pkg/errors" + "github.com/wavesplatform/gowaves/pkg/crypto" + "github.com/wavesplatform/gowaves/pkg/proto" "github.com/wavesplatform/gowaves/pkg/settings" ) @@ -40,6 +43,48 @@ type TestConfig struct { BlockchainSettings *settings.BlockchainSettings } +func (c *TestConfig) GetRichestAccount() AccountInfo { + r := c.Accounts[0] + for _, a := range c.Accounts { + if a.Amount > r.Amount { + r = a + } + } + return r +} + +func (c *TestConfig) GenesisSH() crypto.Digest { + const uint64Size = 8 + + hash, err := crypto.NewFastHash() + if err != nil { + panic(err) + } + var emptyDigest crypto.Digest + hash.Sum(emptyDigest[:0]) + + // Create binary entries for all genesis transactions. + prevSH := emptyDigest + for _, a := range c.Accounts { + hash.Reset() + buf := make([]byte, proto.WavesAddressSize+uint64Size) + copy(buf, a.Address[:]) + binary.BigEndian.PutUint64(buf[proto.WavesAddressSize:], a.Amount) + hash.Write(buf) + var txSH crypto.Digest + hash.Sum(txSH[:0]) + + hash.Reset() + hash.Write(prevSH[:]) + hash.Write(txSH[:]) + + var newSH crypto.Digest + hash.Sum(newSH[:0]) + prevSH = newSH + } + return prevSH +} + type DockerConfigurator interface { DockerRunOptions() *dockertest.RunOptions } @@ -166,6 +211,7 @@ func (c *GoConfigurator) DockerRunOptions() *dockertest.RunOptions { "WALLET_PASSWORD=itest", "DESIRED_REWARD=" + c.cfg.DesiredBlockRewardString(), "SUPPORTED_FEATURES=" + c.cfg.SupportedFeaturesString(), + "DISABLE_MINER=" + c.cfg.DisableGoMiningString(), }, ExposedPorts: []string{ GRPCAPIPort + NetTCP, diff --git a/itests/config/template.conf b/itests/config/template.conf index b252c1831..71b58b02f 100644 --- a/itests/config/template.conf +++ b/itests/config/template.conf @@ -77,8 +77,8 @@ waves { rewards.desired = {{.DesiredBlockRewardString}} matcher.enable = no miner { - enable = {{if .EnableScalaMining}}yes{{else}}no{{end}} - quorum = 1 + enable = {{.EnableScalaMiningString}} + quorum = 2 interval-after-last-block-then-generation-is-allowed = 1h micro-block-interval = 2s min-micro-block-age = 0s diff --git a/itests/docker/docker.go b/itests/docker/docker.go index 5ed1b624d..326c1ad4a 100644 --- a/itests/docker/docker.go +++ b/itests/docker/docker.go @@ -14,13 +14,16 @@ import ( "github.com/ory/dockertest/v3" dc "github.com/ory/dockertest/v3/docker" "github.com/pkg/errors" + "golang.org/x/sync/errgroup" "github.com/wavesplatform/gowaves/itests/config" "github.com/wavesplatform/gowaves/pkg/client" ) const ( - DefaultTimeout = 16 * time.Second + DefaultTimeout = 16 * time.Second + PoolRetryTimeout = 2 * time.Minute + DefaultAPIKey = "itest-api-key" networkName = "waves-it-network" goNodeLogFileName = "go-node.log" @@ -71,6 +74,24 @@ func (c *NodeContainer) closeFiles() error { return err } +// Close purges container and closes log files. +func (c *NodeContainer) Close() error { + if c.container == nil { + return nil + } + if dcErr := c.container.DisconnectFromNetwork(c.network); dcErr != nil { + return errors.Wrapf(dcErr, "failed to disconnect container %q from network %q", + c.container.Container.ID, c.network.Network.Name) + } + if clErr := c.container.Close(); clErr != nil { + return errors.Wrapf(clErr, "failed to close container %q", c.container.Container.ID) + } + if err := c.closeFiles(); err != nil { + return err + } + return nil +} + type Docker struct { suite string @@ -90,6 +111,7 @@ func NewDocker(suiteName string) (*Docker, error) { if err != nil { return nil, err } + pool.MaxWait = PoolRetryTimeout docker := &Docker{suite: suiteName, pool: pool} if rmErr := docker.removeContainers(); rmErr != nil { return nil, rmErr @@ -114,6 +136,23 @@ func (d *Docker) ScalaNode() *NodeContainer { return d.scalaNode } +// StartNodes start both Go and Scala nodes with the given configurations. +// Note that while starting nodes in parallel it is impossible to retrieve the IP address of the other node in prior. +// So this method is heavily dependent on Docker DNS resolution and Go-node's domain name should be passed to the +// configuration of Scala node before calling this method: +// +// scalaConfigurator.WithGoNode("go-node") +func (d *Docker) StartNodes(ctx context.Context, goCfg, scalaCfg config.DockerConfigurator) error { + eg := errgroup.Group{} + eg.Go(func() error { + return d.StartGoNode(ctx, goCfg) + }) + eg.Go(func() error { + return d.StartScalaNode(ctx, scalaCfg) + }) + return eg.Wait() +} + // StartGoNode starts a Go node container with the given configuration. func (d *Docker) StartGoNode(ctx context.Context, cfg config.DockerConfigurator) error { var err error @@ -135,46 +174,60 @@ func (d *Docker) StartScalaNode(ctx context.Context, cfg config.DockerConfigurat } func (d *Docker) Finish(cancel context.CancelFunc) { + eg := errgroup.Group{} if d.scalaNode != nil { - err := d.pool.Client.KillContainer(dc.KillContainerOptions{ - ID: d.scalaNode.container.Container.ID, - Signal: dc.SIGINT, + eg.Go(func() error { + if stErr := d.stopContainer(d.scalaNode.container.Container.ID); stErr != nil { + log.Printf("Failed to stop Scala node container: %v", stErr) + } + return nil }) - if err != nil { - log.Printf("Failed to stop scala container: %v", err) - } } if d.goNode != nil { - err := d.pool.Client.KillContainer(dc.KillContainerOptions{ - ID: d.goNode.container.Container.ID, - Signal: dc.SIGINT, + eg.Go(func() error { + if stErr := d.stopContainer(d.goNode.container.Container.ID); stErr != nil { + log.Printf("Failed to stop Go node container: %v", stErr) + } + return nil }) - if err != nil { - log.Printf("Failed to stop go container: %v", err) - } } + _ = eg.Wait() if d.scalaNode != nil { - if err := d.pool.Purge(d.scalaNode.container); err != nil { - log.Printf("Failed to purge scala-node: %s", err) - } - if err := d.scalaNode.closeFiles(); err != nil { - log.Printf("Failed to close scala-node files: %s", err) - } + eg.Go(func() error { + if clErr := d.scalaNode.Close(); clErr != nil { + log.Printf("Failed to close scala-node: %s", clErr) + } + return nil + }) } if d.goNode != nil { - if err := d.pool.Purge(d.goNode.container); err != nil { - log.Printf("Failed to purge go-node: %s", err) - } - if err := d.goNode.closeFiles(); err != nil { - log.Printf("Failed to close go-node files: %s", err) - } + eg.Go(func() error { + if clErr := d.goNode.Close(); clErr != nil { + log.Printf("Failed to close go-node: %s", clErr) + } + return nil + }) } + _ = eg.Wait() if err := d.pool.RemoveNetwork(d.network); err != nil { log.Printf("Failed to remove docker network: %s", err) } cancel() } +func (d *Docker) stopContainer(containerID string) error { + const shutdownTimeout = 5 // In seconds. + if stErr := d.pool.Client.StopContainer(containerID, shutdownTimeout); stErr != nil { + if klErr := d.pool.Client.KillContainer(dc.KillContainerOptions{ + ID: containerID, + Signal: dc.SIGKILL, + }); klErr != nil { + return errors.Wrapf(stderrs.Join(stErr, klErr), "failed to stop container %q", containerID) + } + } + return nil +} + func (d *Docker) startNode( ctx context.Context, cfg config.DockerConfigurator, logFilename, errFilename string, ) (*NodeContainer, error) { @@ -182,7 +235,7 @@ func (d *Docker) startNode( opts.Networks = []*dockertest.Network{d.network} res, err := d.pool.RunWithOptions(opts, func(hc *dc.HostConfig) { - hc.AutoRemove = true + hc.AutoRemove = false hc.PublishAllPorts = true }) if err != nil { @@ -235,9 +288,11 @@ func (d *Docker) startNode( ApiKey: DefaultAPIKey, }) if fErr != nil { + log.Printf("Failed to create client for container %q: %v", res.Container.Name, fErr) return fErr } _, _, fErr = nodeClient.Blocks.Height(ctx) + log.Printf("Result requesting height from container %q: %v", res.Container.Name, fErr) return fErr }) if err != nil { @@ -247,9 +302,27 @@ func (d *Docker) startNode( } func (d *Docker) removeContainers() error { - err := d.pool.RemoveContainerByName(d.suite) + containers, err := d.pool.Client.ListContainers(dc.ListContainersOptions{ + All: true, + Filters: map[string][]string{ + "name": {d.suite}, + }, + }) if err != nil { - return errors.Wrapf(err, "failed to remove existing containers for suite %s", d.suite) + return fmt.Errorf("failed to list suite %q containers: %w", d.suite, err) + } + if len(containers) == 0 { + return nil + } + for _, c := range containers { + err = d.pool.Client.RemoveContainer(dc.RemoveContainerOptions{ + ID: c.ID, + Force: true, + RemoveVolumes: true, + }) + if err != nil { + return fmt.Errorf("failed to remove container %q of suite %q: %w", c.ID, d.suite, err) + } } return nil } @@ -269,7 +342,7 @@ func (d *Docker) removeNetworks() error { } func (d *Docker) createNetwork() error { - n, err := d.pool.CreateNetwork(d.suite + "-" + networkName) + n, err := d.pool.CreateNetwork(d.suite+"-"+networkName, WithIPv6Disabled) if err != nil { return errors.Wrapf(err, "failed to create network for suite %s", d.suite) } @@ -288,3 +361,7 @@ func (d *Docker) mkLogsDir() error { } return nil } + +func WithIPv6Disabled(conf *dc.CreateNetworkOptions) { + conf.EnableIPv6 = false +} diff --git a/itests/fixtures/base_fixtures.go b/itests/fixtures/base_fixtures.go index eb8f9a911..69170250d 100644 --- a/itests/fixtures/base_fixtures.go +++ b/itests/fixtures/base_fixtures.go @@ -34,19 +34,14 @@ func (suite *BaseSuite) BaseSetup(options ...config.BlockchainOption) { suite.Require().NoError(err, "couldn't create Go configurator") scalaConfigurator, err := config.NewScalaConfigurator(suiteName, cfg) suite.Require().NoError(err, "couldn't create Scala configurator") - + scalaConfigurator.WithGoNode("go-node") docker, err := d.NewDocker(suiteName) suite.Require().NoError(err, "couldn't create Docker pool") suite.Docker = docker - if gsErr := docker.StartGoNode(suite.MainCtx, goConfigurator); gsErr != nil { - docker.Finish(suite.Cancel) - suite.Require().NoError(gsErr, "couldn't start Go node container") - } - scalaConfigurator.WithGoNode(docker.GoNode().ContainerNetworkIP()) - if ssErr := docker.StartScalaNode(suite.MainCtx, scalaConfigurator); ssErr != nil { + if sErr := docker.StartNodes(suite.MainCtx, goConfigurator, scalaConfigurator); sErr != nil { docker.Finish(suite.Cancel) - suite.Require().NoError(ssErr, "couldn't start Scala node container") + suite.Require().NoError(sErr, "couldn't start nodes") } suite.Clients = clients.NewNodesClients(suite.MainCtx, suite.T(), docker.GoNode().Ports(), docker.ScalaNode().Ports()) @@ -54,7 +49,7 @@ func (suite *BaseSuite) BaseSetup(options ...config.BlockchainOption) { } func (suite *BaseSuite) SetupSuite() { - suite.BaseSetup(config.WithScalaMining()) + suite.BaseSetup() } func (suite *BaseSuite) TearDownSuite() { diff --git a/itests/fixtures/lite_node_feature_fixture.go b/itests/fixtures/lite_node_feature_fixture.go deleted file mode 100644 index 2ca7f9ec2..000000000 --- a/itests/fixtures/lite_node_feature_fixture.go +++ /dev/null @@ -1,19 +0,0 @@ -package fixtures - -import "github.com/wavesplatform/gowaves/itests/config" - -const ( - baseSettingsConfigFolder = "base_feature_settings" -) - -type BaseSettingSuite struct { - BaseSuite -} - -func (suite *BaseSettingSuite) SetupSuite() { - suite.BaseSetup( - config.WithScalaMining(), - config.WithFeatureSettingFromFile(baseSettingsConfigFolder, "lite_node_feature_fixture.json"), - config.WithPaymentsSettingFromFile(baseSettingsConfigFolder, "lite_node_feature_fixture.json"), - ) -} diff --git a/itests/fixtures/reward_preactivated_features_fixtures.go b/itests/fixtures/reward_preactivated_features_fixtures.go index d2108ffcb..56c8f26ce 100644 --- a/itests/fixtures/reward_preactivated_features_fixtures.go +++ b/itests/fixtures/reward_preactivated_features_fixtures.go @@ -21,7 +21,6 @@ type RewardIncreaseDaoXtnPreactivatedSuite struct { func (suite *RewardIncreaseDaoXtnPreactivatedSuite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, preactivatedFeaturesConfigFolder, @@ -44,7 +43,6 @@ type RewardUnchangedDaoXtnPreactivatedSuite struct { func (suite *RewardUnchangedDaoXtnPreactivatedSuite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, preactivatedFeaturesConfigFolder, @@ -67,7 +65,6 @@ type RewardDecreaseDaoXtnPreactivatedSuite struct { func (suite *RewardDecreaseDaoXtnPreactivatedSuite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, preactivatedFeaturesConfigFolder, @@ -90,7 +87,6 @@ type RewardIncreaseDaoPreactivatedSuite struct { func (suite *RewardIncreaseDaoPreactivatedSuite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, preactivatedFeaturesConfigFolder, @@ -113,7 +109,6 @@ type RewardUnchangedXtnPreactivatedSuite struct { func (suite *RewardUnchangedXtnPreactivatedSuite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, preactivatedFeaturesConfigFolder, @@ -136,7 +131,6 @@ type Reward2WUnchangedDaoXtnPreactivatedSuite struct { func (suite *Reward2WUnchangedDaoXtnPreactivatedSuite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, preactivatedFeaturesConfigFolder, @@ -159,7 +153,6 @@ type RewardDecreaseDaoPreactivatedSuite struct { func (suite *RewardDecreaseDaoPreactivatedSuite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, preactivatedFeaturesConfigFolder, @@ -182,7 +175,6 @@ type RewardDecreaseXtnPreactivatedSuite struct { func (suite *RewardDecreaseXtnPreactivatedSuite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, preactivatedFeaturesConfigFolder, @@ -205,7 +197,6 @@ type RewardIncreasePreactivatedSuite struct { func (suite *RewardIncreasePreactivatedSuite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, preactivatedFeaturesConfigFolder, @@ -228,7 +219,6 @@ type RewardDaoXtnPreactivatedWithout19Suite struct { func (suite *RewardDaoXtnPreactivatedWithout19Suite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, preactivatedFeaturesConfigFolder, @@ -251,7 +241,6 @@ type RewardDaoXtnPreactivatedWithout20Suite struct { func (suite *RewardDaoXtnPreactivatedWithout20Suite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, preactivatedFeaturesConfigFolder, @@ -276,7 +265,6 @@ type RewardIncreaseDaoXtnCeaseXTNBuybackPreactivatedSuite struct { func (suite *RewardIncreaseDaoXtnCeaseXTNBuybackPreactivatedSuite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, preactivatedFeaturesWith21ConfigFolder, @@ -299,7 +287,6 @@ type RewardIncreaseXtnCeaseXTNBuybackPreactivatedSuite struct { func (suite *RewardIncreaseXtnCeaseXTNBuybackPreactivatedSuite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, preactivatedFeaturesWith21ConfigFolder, @@ -322,7 +309,6 @@ type RewardUnchangedDaoXtnCeaseXTNBuybackPreactivatedSuite struct { func (suite *RewardUnchangedDaoXtnCeaseXTNBuybackPreactivatedSuite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, preactivatedFeaturesWith21ConfigFolder, @@ -345,7 +331,6 @@ type RewardDecreaseDaoXtnCeaseXTNBuybackPreactivatedSuite struct { func (suite *RewardDecreaseDaoXtnCeaseXTNBuybackPreactivatedSuite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, preactivatedFeaturesWith21ConfigFolder, @@ -368,7 +353,6 @@ type RewardDecreaseXtnCeaseXTNBuybackPreactivatedSuite struct { func (suite *RewardDecreaseXtnCeaseXTNBuybackPreactivatedSuite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, preactivatedFeaturesWith21ConfigFolder, @@ -391,7 +375,6 @@ type Reward2WUnchangedDaoXtnCeaseXTNBuybackPreactivatedSuite struct { func (suite *Reward2WUnchangedDaoXtnCeaseXTNBuybackPreactivatedSuite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, preactivatedFeaturesWith21ConfigFolder, @@ -414,7 +397,6 @@ type Reward5W2MinersIncreaseCeaseXTNBuybackPreactivatedSuite struct { func (suite *Reward5W2MinersIncreaseCeaseXTNBuybackPreactivatedSuite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, preactivatedFeaturesWith21ConfigFolder, @@ -437,7 +419,6 @@ type RewardDaoXtnPreactivatedWith21Suite struct { func (suite *RewardDaoXtnPreactivatedWith21Suite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, preactivatedFeaturesWith21ConfigFolder, @@ -460,7 +441,6 @@ type RewardDaoXtnPreactivatedWithout19And20Suite struct { func (suite *RewardDaoXtnPreactivatedWithout19And20Suite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, preactivatedFeaturesWith21ConfigFolder, diff --git a/itests/fixtures/reward_supported_features_fixtures.go b/itests/fixtures/reward_supported_features_fixtures.go index c72508f29..97a7a63d1 100644 --- a/itests/fixtures/reward_supported_features_fixtures.go +++ b/itests/fixtures/reward_supported_features_fixtures.go @@ -21,7 +21,6 @@ type RewardIncreaseDaoXtnSupportedSuite struct { func (suite *RewardIncreaseDaoXtnSupportedSuite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, supportedFeaturesConfigFolder, @@ -44,7 +43,6 @@ type RewardUnchangedDaoXtnSupportedSuite struct { func (suite *RewardUnchangedDaoXtnSupportedSuite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, supportedFeaturesConfigFolder, @@ -67,7 +65,6 @@ type RewardDecreaseDaoXtnSupportedSuite struct { func (suite *RewardDecreaseDaoXtnSupportedSuite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, supportedFeaturesConfigFolder, @@ -90,7 +87,6 @@ type RewardIncreaseDaoSupportedSuite struct { func (suite *RewardIncreaseDaoSupportedSuite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, supportedFeaturesConfigFolder, @@ -113,7 +109,6 @@ type RewardUnchangedXtnSupportedSuite struct { func (suite *RewardUnchangedXtnSupportedSuite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, supportedFeaturesConfigFolder, @@ -136,7 +131,6 @@ type Reward2WUnchangedDaoXtnSupportedSuite struct { func (suite *Reward2WUnchangedDaoXtnSupportedSuite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, supportedFeaturesConfigFolder, @@ -159,7 +153,6 @@ type RewardDecreaseDaoSupportedSuite struct { func (suite *RewardDecreaseDaoSupportedSuite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, supportedFeaturesConfigFolder, @@ -182,7 +175,6 @@ type RewardDecreaseXtnSupportedSuite struct { func (suite *RewardDecreaseXtnSupportedSuite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, supportedFeaturesConfigFolder, @@ -205,7 +197,6 @@ type RewardIncreaseSupportedSuite struct { func (suite *RewardIncreaseSupportedSuite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, supportedFeaturesConfigFolder, @@ -228,7 +219,6 @@ type RewardDaoXtnSupportedWithout19Suite struct { func (suite *RewardDaoXtnSupportedWithout19Suite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, supportedFeaturesConfigFolder, @@ -253,7 +243,6 @@ type RewardIncreaseDaoXtnCeaseXTNBuybackSupportedSuite struct { func (suite *RewardIncreaseDaoXtnCeaseXTNBuybackSupportedSuite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, supportedFeaturesWith21ConfigFolder, @@ -276,7 +265,6 @@ type RewardIncreaseXtnCeaseXTNBuybackSupportedSuite struct { func (suite *RewardIncreaseXtnCeaseXTNBuybackSupportedSuite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, supportedFeaturesWith21ConfigFolder, @@ -299,7 +287,6 @@ type RewardUnchangedDaoXtnCeaseXTNBuybackSupportedSuite struct { func (suite *RewardUnchangedDaoXtnCeaseXTNBuybackSupportedSuite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, supportedFeaturesWith21ConfigFolder, @@ -322,7 +309,6 @@ type RewardDecreaseDaoXtnCeaseXTNBuybackSupportedSuite struct { func (suite *RewardDecreaseDaoXtnCeaseXTNBuybackSupportedSuite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, supportedFeaturesWith21ConfigFolder, @@ -345,7 +331,6 @@ type RewardDecreaseXtnCeaseXTNBuybackSupportedSuite struct { func (suite *RewardDecreaseXtnCeaseXTNBuybackSupportedSuite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, supportedFeaturesWith21ConfigFolder, @@ -368,7 +353,6 @@ type Reward2WUnchangedDaoXtnCeaseXTNBuybackSupportedSuite struct { func (suite *Reward2WUnchangedDaoXtnCeaseXTNBuybackSupportedSuite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, supportedFeaturesWith21ConfigFolder, @@ -391,7 +375,6 @@ type Reward5W2MinersIncreaseCeaseXTNBuybackSupportedSuite struct { func (suite *Reward5W2MinersIncreaseCeaseXTNBuybackSupportedSuite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, supportedFeaturesWith21ConfigFolder, @@ -414,7 +397,6 @@ type RewardDaoXtnSupportedWithout20Suite struct { func (suite *RewardDaoXtnSupportedWithout20Suite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, supportedFeaturesWith21ConfigFolder, @@ -437,7 +419,6 @@ type RewardDaoXtnSupportedWithout19And20Suite struct { func (suite *RewardDaoXtnSupportedWithout19And20Suite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, supportedFeaturesWith21ConfigFolder, @@ -462,7 +443,6 @@ type RewardIncreaseDaoXtnSupported20Suite struct { func (suite *RewardIncreaseDaoXtnSupported20Suite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, supportedFeature20ConfigFolder, @@ -485,7 +465,6 @@ type RewardDaoXtnSupported19Suite struct { func (suite *RewardDaoXtnSupported19Suite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, supportedFeaturesConfigFolder, @@ -508,7 +487,6 @@ type RewardDistributionRollbackBefore21Suite struct { func (suite *RewardDistributionRollbackBefore21Suite) SetupSuite() { suite.BaseSetup( - config.WithScalaMining(), config.WithFeatureSettingFromFile( rewardSettingsFolder, supportedFeaturesWith21ConfigFolder, diff --git a/itests/fixtures/single_go_node_suite.go b/itests/fixtures/single_go_node_suite.go new file mode 100644 index 000000000..599175835 --- /dev/null +++ b/itests/fixtures/single_go_node_suite.go @@ -0,0 +1,69 @@ +package fixtures + +import ( + "context" + + "github.com/stoewer/go-strcase" + "github.com/stretchr/testify/suite" + + "github.com/wavesplatform/gowaves/itests/clients" + "github.com/wavesplatform/gowaves/itests/config" + d "github.com/wavesplatform/gowaves/itests/docker" + "github.com/wavesplatform/gowaves/pkg/proto" +) + +type SingleGoNodeSuite struct { + suite.Suite + + MainCtx context.Context + Cancel context.CancelFunc + Cfg config.TestConfig + Docker *d.Docker + Client *clients.NodeUniversalClient +} + +func (suite *SingleGoNodeSuite) BaseSetup(options ...config.BlockchainOption) { + suite.MainCtx, suite.Cancel = context.WithCancel(context.Background()) + suiteName := strcase.KebabCase(suite.T().Name()) + cfg, err := config.NewBlockchainConfig(options...) + suite.Require().NoError(err, "couldn't create blockchain config") + suite.Cfg = cfg.TestConfig() + + goConfigurator, err := config.NewGoConfigurator(suiteName, cfg) + suite.Require().NoError(err, "couldn't create Go configurator") + + docker, err := d.NewDocker(suiteName) + suite.Require().NoError(err, "couldn't create Docker pool") + suite.Docker = docker + + if sErr := docker.StartGoNode(suite.MainCtx, goConfigurator); sErr != nil { + docker.Finish(suite.Cancel) + suite.Require().NoError(sErr, "couldn't start Go node container") + } + + gp, err := proto.NewPeerInfoFromString(config.DefaultIP + ":" + docker.GoNode().Ports().BindPort) + suite.Require().NoError(err, "failed to create Go peer info") + peers := []proto.PeerInfo{gp} + suite.Client = clients.NewNodeUniversalClient(suite.MainCtx, suite.T(), clients.NodeGo, + docker.GoNode().Ports().RESTAPIPort, docker.GoNode().Ports().GRPCPort, docker.GoNode().Ports().BindPort, + peers, + ) + suite.Client.Handshake() +} + +func (suite *SingleGoNodeSuite) SetupSuite() { + suite.BaseSetup() +} + +func (suite *SingleGoNodeSuite) TearDownSuite() { + suite.Client.Close(suite.T()) + suite.Docker.Finish(suite.Cancel) +} + +func (suite *SingleGoNodeSuite) SetupTest() { + suite.Client.SendStartMessage(suite.T()) +} + +func (suite *SingleGoNodeSuite) TearDownTest() { + suite.Client.SendEndMessage(suite.T()) +} diff --git a/itests/init_internal_test.go b/itests/init_internal_test.go index 4d9f50b94..8f10825d7 100644 --- a/itests/init_internal_test.go +++ b/itests/init_internal_test.go @@ -36,7 +36,12 @@ func TestMain(m *testing.M) { if err != nil { log.Fatalf("Failed to create docker pool: %v", err) } - if err := pool.Client.PullImage(dc.PullImageOptions{Repository: "wavesplatform/wavesnode", Tag: "latest"}, dc.AuthConfiguration{}); err != nil { + if plErr := pool.Client.PullImage( + dc.PullImageOptions{ + Repository: "wavesplatform/wavesnode", + Tag: "latest", + Platform: "linux/amd64"}, + dc.AuthConfiguration{}); plErr != nil { log.Fatalf("Failed to pull node image: %v", err) } var buildArgs []dc.BuildArg diff --git a/itests/snapshot_internal_test.go b/itests/snapshot_internal_test.go new file mode 100644 index 000000000..6e2c1213d --- /dev/null +++ b/itests/snapshot_internal_test.go @@ -0,0 +1,262 @@ +package itests + +import ( + "encoding/binary" + "math" + "math/big" + "reflect" + "testing" + "time" + + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + + "github.com/wavesplatform/gowaves/itests/config" + "github.com/wavesplatform/gowaves/itests/fixtures" + "github.com/wavesplatform/gowaves/pkg/consensus" + "github.com/wavesplatform/gowaves/pkg/crypto" + "github.com/wavesplatform/gowaves/pkg/proto" + "github.com/wavesplatform/gowaves/pkg/settings" +) + +type SimpleSnapshotSuite struct { + fixtures.SingleGoNodeSuite +} + +func (s *SimpleSnapshotSuite) SetupSuite() { + s.BaseSetup( + config.WithNoGoMining(), + config.WithPreactivatedFeatures([]config.FeatureInfo{{Feature: int16(settings.LightNode), Height: 1}}), + config.WithAbsencePeriod(1), + ) +} + +func (s *SimpleSnapshotSuite) TestSimpleSnapshot() { + const messageTimeout = 5 * time.Second + + acc := s.Cfg.GetRichestAccount() + + // Initialize genesis block ID. + err := s.Cfg.BlockchainSettings.Genesis.GenerateBlockID(s.Cfg.BlockchainSettings.AddressSchemeCharacter) + require.NoError(s.T(), err, "failed to generate genesis block ID") + genesisID := s.Cfg.BlockchainSettings.Genesis.BlockID() + + // Calculate state hash for the key-block. Take into account only miner's reward. + genesisSH := s.Cfg.GenesisSH() + + // Calculate new balance of the richest account (block generator). + newBalance := acc.Amount + s.Cfg.BlockchainSettings.InitialBlockReward + + // Calculate state hash for the key-block. + sh, err := keyBlockSH(genesisSH, acc.Address, newBalance) + require.NoError(s.T(), err, "failed to calculate state hash") + + // Generate key-block + bl, delay := createKeyBlock(s.T(), s.Cfg.BlockchainSettings.Genesis.GenSignature, s.Cfg.BlockchainSettings, + acc.SecretKey, acc.PublicKey, newBalance, genesisID, s.Cfg.BlockchainSettings.Genesis.Timestamp, + s.Cfg.BlockchainSettings.Genesis.BaseTarget, sh) + if delay > 0 { + time.Sleep(delay) + } + + err = s.Client.Connection.SubscribeForMessages( + reflect.TypeOf(&proto.GetBlockIdsMessage{}), + reflect.TypeOf(&proto.GetBlockMessage{}), + reflect.TypeOf(&proto.ScoreMessage{}), + reflect.TypeOf(&proto.MicroBlockRequestMessage{}), + ) + require.NoError(s.T(), err, "failed to subscribe for messages") + + // Calculate new score and send score to the node. + genesisScore := calculateScore(s.Cfg.BlockchainSettings.Genesis.BaseTarget) + blockScore := calculateCumulativeScore(genesisScore, bl.BaseTarget) + scoreMsg := &proto.ScoreMessage{Score: blockScore.Bytes()} + s.Client.Connection.SendMessage(scoreMsg) + + // Wait for the node to request block IDs. + _, err = s.Client.Connection.AwaitMessage(reflect.TypeOf(&proto.GetBlockIdsMessage{}), messageTimeout) + require.NoError(s.T(), err, "failed to wait for block IDs request") + + // Send block IDs to the node. + blocksMsg := &proto.BlockIdsMessage{Blocks: []proto.BlockID{bl.BlockID()}} + s.Client.Connection.SendMessage(blocksMsg) + + // Wait for the node to request the block. + blockID, err := s.Client.Connection.AwaitGetBlockMessage(messageTimeout) + require.NoError(s.T(), err, "failed to wait for block request") + assert.Equal(s.T(), bl.BlockID(), blockID) + + // Marshal the block and send it to the node. + bb, err := bl.MarshalToProtobuf(s.Cfg.BlockchainSettings.AddressSchemeCharacter) + require.NoError(s.T(), err, "failed to marshal block") + blMsg := &proto.PBBlockMessage{PBBlockBytes: bb} + s.Client.Connection.SendMessage(blMsg) + + // Wait for updated score message. + score, err := s.Client.Connection.AwaitScoreMessage(messageTimeout) + require.NoError(s.T(), err, "failed to wait for score") + assert.Equal(s.T(), blockScore, score) + + // Wait for 2.5 seconds and send micro-block (imitate real life). + time.Sleep(2500 * time.Millisecond) + + // Add transactions to block. + tx := proto.NewUnsignedTransferWithProofs(3, acc.PublicKey, + proto.NewOptionalAssetWaves(), proto.NewOptionalAssetWaves(), uint64(time.Now().UnixMilli()), 1_0000_0000, + 100_000, proto.NewRecipientFromAddress(acc.Address), nil) + err = tx.Sign(s.Cfg.BlockchainSettings.AddressSchemeCharacter, acc.SecretKey) + require.NoError(s.T(), err, "failed to sign tx") + + // Create micro-block with the transaction and unchanged state hash. + mb, inv := createMicroBlockAndInv(s.T(), *bl, s.Cfg.BlockchainSettings, tx, acc.SecretKey, acc.PublicKey, sh) + + // Send micro-block inv to the node. + ib, err := inv.MarshalBinary() + require.NoError(s.T(), err, "failed to marshal inv") + invMsg := &proto.MicroBlockInvMessage{Body: ib} + s.Client.Connection.SendMessage(invMsg) + + // Wait for the node to request micro-block. + mbID, err := s.Client.Connection.AwaitMicroblockRequest(messageTimeout) + require.NoError(s.T(), err, "failed to wait for micro-block request") + assert.Equal(s.T(), inv.TotalBlockID, mbID) + + // Marshal the micro-block and send it to the node. + mbb, err := mb.MarshalToProtobuf(s.Cfg.BlockchainSettings.AddressSchemeCharacter) + require.NoError(s.T(), err, "failed to marshal micro block") + mbMsg := &proto.PBMicroBlockMessage{MicroBlockBytes: mbb} + s.Client.Connection.SendMessage(mbMsg) + + h := s.Client.HTTPClient.GetHeight(s.T()) + header := s.Client.HTTPClient.BlockHeader(s.T(), h.Height) + assert.Equal(s.T(), bl.BlockID().String(), header.ID.String()) +} + +func TestSimpleSnapshotSuite(t *testing.T) { + t.Parallel() + suite.Run(t, new(SimpleSnapshotSuite)) +} + +func keyBlockSH(prevSH crypto.Digest, miner proto.WavesAddress, balance uint64) (crypto.Digest, error) { + hash, err := crypto.NewFastHash() + if err != nil { + return crypto.Digest{}, errors.Wrap(err, "failed to calculate key block snapshot hash") + } + + buf := make([]byte, proto.WavesAddressSize+8) + copy(buf, miner[:]) + binary.BigEndian.PutUint64(buf[proto.WavesAddressSize:], balance) + hash.Write(buf) + + var txSHD crypto.Digest + hash.Sum(txSHD[:0]) + + hash.Reset() + hash.Write(prevSH.Bytes()) + hash.Write(txSHD.Bytes()) + + var r crypto.Digest + hash.Sum(r[:0]) + return r, nil +} + +func createKeyBlock(t *testing.T, hitSource []byte, cfg *settings.BlockchainSettings, + generatorSK crypto.SecretKey, generatorPK crypto.PublicKey, generatorBalance uint64, + parentID proto.BlockID, parentTimestamp uint64, parentBaseTarget uint64, + sh crypto.Digest, +) (*proto.Block, time.Duration) { + gsp := consensus.VRFGenerationSignatureProvider + pos := consensus.NewFairPosCalculator(cfg.DelayDelta, cfg.MinBlockTime) + gs, err := gsp.GenerationSignature(generatorSK, hitSource) + require.NoError(t, err, "failed to generate generation signature") + + source, err := gsp.HitSource(generatorSK, hitSource) + require.NoError(t, err, "failed to generate hit source") + + hit, err := consensus.GenHit(source) + require.NoError(t, err, "failed to generate hit from source") + + delay, err := pos.CalculateDelay(hit, parentBaseTarget, generatorBalance) + require.NoError(t, err, "failed to calculate delay") + + ts := parentTimestamp + delay + bt, err := pos.CalculateBaseTarget(cfg.AverageBlockDelaySeconds, 1, parentBaseTarget, parentTimestamp, 0, ts) + require.NoError(t, err, "failed to calculate base target") + + nxt := proto.NxtConsensus{BaseTarget: bt, GenSignature: gs} + + bl, err := proto.CreateBlock(proto.Transactions(nil), ts, parentID, generatorPK, nxt, proto.ProtobufBlockVersion, + nil, int64(cfg.InitialBlockReward), cfg.AddressSchemeCharacter, &sh) + require.NoError(t, err, "failed to create block") + + // Sign the block and generate its ID. + err = bl.Sign(cfg.AddressSchemeCharacter, generatorSK) + require.NoError(t, err, "failed to sing the block") + + err = bl.GenerateBlockID(cfg.AddressSchemeCharacter) + require.NoError(t, err, "failed to generate block ID") + + return bl, time.Until(time.UnixMilli(int64(ts))) +} + +func createMicroBlockAndInv(t *testing.T, b proto.Block, cfg *settings.BlockchainSettings, tx proto.Transaction, + generatorSK crypto.SecretKey, generatorPK crypto.PublicKey, sh crypto.Digest, +) (*proto.MicroBlock, *proto.MicroBlockInv) { + b.Transactions = []proto.Transaction{tx} + b.TransactionCount = len(b.Transactions) + err := b.SetTransactionsRootIfPossible(cfg.AddressSchemeCharacter) + require.NoError(t, err, "failed to set transactions root") + err = b.Sign(cfg.AddressSchemeCharacter, generatorSK) + require.NoError(t, err, "failed to sign block") + err = b.GenerateBlockID(cfg.AddressSchemeCharacter) + require.NoError(t, err, "failed to generate block ID") + + mb := &proto.MicroBlock{ + VersionField: byte(b.Version), + SenderPK: generatorPK, + Transactions: b.Transactions, + TransactionCount: uint32(b.TransactionCount), + Reference: b.ID, + TotalResBlockSigField: b.BlockSignature, + TotalBlockID: b.BlockID(), + StateHash: &sh, + } + + err = mb.Sign(cfg.AddressSchemeCharacter, generatorSK) + require.NoError(t, err, "failed to sign mb block") + + inv := proto.NewUnsignedMicroblockInv(generatorPK, mb.TotalBlockID, mb.Reference) + err = inv.Sign(generatorSK, cfg.AddressSchemeCharacter) + require.NoError(t, err, "failed to sign inv") + + return mb, inv +} + +func calculateScore(baseTarget uint64) *big.Int { + const decimalBase = 10 + + res := big.NewInt(0) + if baseTarget == 0 { + return res + } + if baseTarget > math.MaxInt64 { + panic("base target is too big") + } + bt := big.NewInt(int64(baseTarget)) + maxBlockScore, ok := big.NewInt(0).SetString("18446744073709551616", decimalBase) + if !ok { + return res + } + res.Div(maxBlockScore, bt) + return res +} + +func calculateCumulativeScore(parentScore *big.Int, baseTarget uint64) *big.Int { + s := calculateScore(baseTarget) + if parentScore == nil { + return s + } + return s.Add(s, parentScore) +} diff --git a/itests/testdata/feature_settings/base_feature_settings/lite_node_feature_fixture.json b/itests/testdata/feature_settings/base_feature_settings/lite_node_feature_fixture.json deleted file mode 100644 index 6948940a1..000000000 --- a/itests/testdata/feature_settings/base_feature_settings/lite_node_feature_fixture.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "payments_fix_after_height": 1, - "preactivated_features": [ - { - "feature": 19, - "height": 1 - }, - { - "feature": 20, - "height": 1 - }, - { - "feature": 21, - "height": 1 - }, - { - "feature": 22, - "height": 1 - } - ], - "supported_features": [] -} \ No newline at end of file diff --git a/itests/utilities/common.go b/itests/utilities/common.go index 960823072..0d6f52924 100644 --- a/itests/utilities/common.go +++ b/itests/utilities/common.go @@ -394,8 +394,16 @@ func GetHeightScala(suite *f.BaseSuite) uint64 { } func GetHeight(suite *f.BaseSuite) uint64 { - goHeight := GetHeightGo(suite) - scalaHeight := GetHeightScala(suite) + goCh := make(chan uint64) + scalaCh := make(chan uint64) + go func() { + goCh <- GetHeightGo(suite) + }() + go func() { + scalaCh <- GetHeightScala(suite) + }() + goHeight := <-goCh + scalaHeight := <-scalaCh if goHeight < scalaHeight { return goHeight } @@ -481,30 +489,41 @@ func GetFeatureActivationHeightScala(suite *f.BaseSuite, featureID settings.Feat } func GetFeatureActivationHeight(suite *f.BaseSuite, featureID settings.Feature, height uint64) proto.Height { - var err error - var activationHeight proto.Height - activationHeightGo := GetFeatureActivationHeightGo(suite, featureID, height) - activationHeightScala := GetFeatureActivationHeightScala(suite, featureID, height) + goCh := make(chan proto.Height) + scalaCh := make(chan proto.Height) + go func() { + goCh <- GetFeatureActivationHeightGo(suite, featureID, height) + }() + go func() { + scalaCh <- GetFeatureActivationHeightScala(suite, featureID, height) + }() + activationHeightGo := <-goCh + activationHeightScala := <-scalaCh + if activationHeightGo == activationHeightScala && activationHeightGo > 0 { - activationHeight = activationHeightGo - } else { - err = errors.New("Activation Height from Go and Scala is different") + return activationHeightGo } - require.NoError(suite.T(), err) - return activationHeight + + suite.FailNow("Activation Height from Go and Scala is different") + return 0 } func GetFeatureBlockchainStatus(suite *f.BaseSuite, featureID settings.Feature, height uint64) (string, error) { - var status string - var err error - statusGo := GetFeatureBlockchainStatusGo(suite, featureID, height) - statusScala := GetFeatureBlockchainStatusScala(suite, featureID, height) + goCh := make(chan string) + scalaCh := make(chan string) + go func() { + goCh <- GetFeatureBlockchainStatusGo(suite, featureID, height) + }() + go func() { + scalaCh <- GetFeatureBlockchainStatusScala(suite, featureID, height) + }() + statusGo := <-goCh + statusScala := <-scalaCh + if statusGo == statusScala { - status = statusGo - } else { - err = errors.Errorf("Feature with Id %d has different statuses", featureID) + return statusGo, nil } - return status, err + return "", errors.Errorf("Feature with ID %d has different statuses", featureID) } func GetWaitingBlocks(suite *f.BaseSuite, height uint64, featureID settings.Feature) uint64 { @@ -531,17 +550,26 @@ func GetWaitingBlocks(suite *f.BaseSuite, height uint64, featureID settings.Feat } func WaitForFeatureActivation(suite *f.BaseSuite, featureID settings.Feature, height uint64) proto.Height { - var activationHeight proto.Height waitingBlocks := GetWaitingBlocks(suite, height, featureID) h := WaitForHeight(suite, height+waitingBlocks) - activationHeightGo := GetFeatureActivationHeightGo(suite, featureID, h) - activationHeightScala := GetFeatureActivationHeightScala(suite, featureID, h) + + goCh := make(chan proto.Height) + scalaCh := make(chan proto.Height) + + go func() { + goCh <- GetFeatureActivationHeightGo(suite, featureID, h) + }() + go func() { + scalaCh <- GetFeatureActivationHeightScala(suite, featureID, h) + }() + activationHeightGo := <-goCh + activationHeightScala := <-scalaCh + if activationHeightScala == activationHeightGo { - activationHeight = activationHeightGo - } else { - suite.FailNowf("Feature has different activation heights", "Feature ID is %d", featureID) + return activationHeightGo } - return activationHeight + suite.FailNowf("Feature has different activation heights", "Feature ID is %d", featureID) + return 0 } func FeatureShouldBeActivated(suite *f.BaseSuite, featureID settings.Feature, height uint64) { @@ -884,8 +912,16 @@ func GetRewardTermAtHeightScala(suite *f.BaseSuite, height uint64) uint64 { } func GetRewardTermAtHeight(suite *f.BaseSuite, height uint64) RewardTerm { - termGo := GetRewardTermAtHeightGo(suite, height) - termScala := GetRewardTermAtHeightScala(suite, height) + goCh := make(chan uint64) + scalaCh := make(chan uint64) + go func() { + goCh <- GetRewardTermAtHeightGo(suite, height) + }() + go func() { + scalaCh <- GetRewardTermAtHeightScala(suite, height) + }() + termGo := <-goCh + termScala := <-scalaCh suite.T().Logf("Go: Reward Term: %d, Scala: Reward Term: %d, height: %d", termGo, termScala, height) return NewRewardTerm(termGo, termScala) @@ -917,5 +953,13 @@ func GetRollbackToHeightScala(suite *f.BaseSuite, height uint64, returnTxToUtx b func GetRollbackToHeight(suite *f.BaseSuite, height uint64, returnTxToUtx bool) (*proto.BlockID, *proto.BlockID) { suite.T().Logf("Rollback to height: %d from height: %d", height, GetHeight(suite)) - return GetRollbackToHeightGo(suite, height, returnTxToUtx), GetRollbackToHeightScala(suite, height, returnTxToUtx) + goCh := make(chan *proto.BlockID) + scalaCh := make(chan *proto.BlockID) + go func() { + goCh <- GetRollbackToHeightGo(suite, height, returnTxToUtx) + }() + go func() { + scalaCh <- GetRollbackToHeightScala(suite, height, returnTxToUtx) + }() + return <-goCh, <-scalaCh }