From ded8e77ee5d6466a760c475795555bc7c3a8318e Mon Sep 17 00:00:00 2001 From: Francisco Moura Date: Tue, 9 Apr 2024 18:44:50 -0300 Subject: [PATCH] feat: Add Echo App end-to-end test --- CHANGELOG.md | 10 +- cmd/cartesi-rollups-cli/root/deps/deps.go | 24 ++- .../root/increasetime/increasetime.go | 32 +-- cmd/cartesi-rollups-node/main.go | 2 +- docs/cli/cartesi-rollups-cli_run-deps.md | 4 +- internal/deps/deps.go | 146 ++++++++++--- internal/node/machinehash_test.go | 26 ++- internal/node/node.go | 4 +- internal/node/services.go | 52 +++-- internal/services/command.go | 8 + internal/services/server-manager.go | 6 + pkg/addresses/addresses.go | 4 + pkg/ethutil/ethutil.go | 59 ++++++ pkg/ethutil/ethutil_test.go | 38 ++-- test/config.go | 86 ++++++++ .../expected_input_with_proofs.json | 152 ++++++++++++++ test/echo_test.go | 191 ++++++++++++++++++ 17 files changed, 731 insertions(+), 113 deletions(-) create mode 100644 test/config.go create mode 100644 test/data/echo_input/expected_input_with_proofs.json create mode 100644 test/echo_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 046ff25f0..c5dc5f549 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [unreleased] + +### Added + +- Added Rollups end-to-end test using Echo Dapp + ## [1.4.0] 2024-04-09 ### Added @@ -14,7 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added verification to ensure the Cartesi Machine snapshot hash matches the template hash from the CartesiDApp contract - Added support for `CARTESI_AUTH_PRIVATE_KEY` and `CARTESI_AUTH_PRIVATE_KEY_FILE` - Added `CARTESI_AUTH_KIND` environment variable to select the blockchain authetication method -- Added structured logging with slog. Colored logs can now be enabled with `CARTESI_LOG_PRETTY` environment variable. +- Added structured logging with slog. Colored logs can now be enabled with `CARTESI_LOG_PRETTY` environment variable + ### Changed diff --git a/cmd/cartesi-rollups-cli/root/deps/deps.go b/cmd/cartesi-rollups-cli/root/deps/deps.go index 70b963c32..e262bab80 100644 --- a/cmd/cartesi-rollups-cli/root/deps/deps.go +++ b/cmd/cartesi-rollups-cli/root/deps/deps.go @@ -6,10 +6,13 @@ package deps import ( "context" "log/slog" + "os" "os/signal" "syscall" "github.com/cartesi/rollups-node/internal/deps" + "github.com/lmittmann/tint" + "github.com/mattn/go-isatty" "github.com/spf13/cobra" ) @@ -24,6 +27,7 @@ const examples = `# Run all deps: cartesi-rollups-cli run-deps` var depsConfig = deps.NewDefaultDepsConfig() +var verbose = false func init() { Cmd.Flags().StringVar(&depsConfig.Postgres.DockerImage, "postgres-docker-image", @@ -44,13 +48,31 @@ func init() { Cmd.Flags().StringVar(&depsConfig.Devnet.Port, "devnet-mapped-port", deps.DefaultDevnetPort, - "devnet local listening port number") + "Devnet local listening port number") + + Cmd.Flags().StringVar(&depsConfig.Devnet.BlockTime, "devnet-block-time", + deps.DefaultBlockTime, + "Devnet mining block time") + + Cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "verbose logs") } func run(cmd *cobra.Command, args []string) { ctx, cancel := signal.NotifyContext(cmd.Context(), syscall.SIGINT, syscall.SIGTERM) defer cancel() + if verbose { + // setup log + opts := &tint.Options{ + Level: slog.LevelDebug, + AddSource: true, + NoColor: false || !isatty.IsTerminal(os.Stdout.Fd()), + } + handler := tint.NewHandler(os.Stdout, opts) + logger := slog.New(handler) + slog.SetDefault(logger) + } + depsContainers, err := deps.Run(ctx, *depsConfig) cobra.CheckErr(err) diff --git a/cmd/cartesi-rollups-cli/root/increasetime/increasetime.go b/cmd/cartesi-rollups-cli/root/increasetime/increasetime.go index 5b18522f7..2c9393555 100644 --- a/cmd/cartesi-rollups-cli/root/increasetime/increasetime.go +++ b/cmd/cartesi-rollups-cli/root/increasetime/increasetime.go @@ -4,12 +4,10 @@ package increasetime import ( - "fmt" - "io" - "net/http" - "strconv" - "strings" + "context" + "log/slog" + "github.com/cartesi/rollups-node/pkg/ethutil" "github.com/spf13/cobra" ) @@ -39,24 +37,8 @@ func init() { } func run(cmd *cobra.Command, args []string) { - client := &http.Client{} - var data = strings.NewReader(`{ - "id":1337, - "jsonrpc":"2.0", - "method":"evm_increaseTime", - "params":[` + strconv.Itoa(time) + `] - }`) - - req, err := http.NewRequest("POST", anvilEndpoint, data) - cobra.CheckErr(err) - - req.Header.Set("Content-Type", "application/json") - resp, err := client.Do(req) - cobra.CheckErr(err) - - defer resp.Body.Close() - bodyText, err := io.ReadAll(resp.Body) - cobra.CheckErr(err) - - fmt.Printf("%s\n", bodyText) + + cobra.CheckErr(ethutil.AdvanceDevnetTime(context.Background(), anvilEndpoint, time)) + + slog.Info("Ok") } diff --git a/cmd/cartesi-rollups-node/main.go b/cmd/cartesi-rollups-node/main.go index e45a39d7f..d82391541 100644 --- a/cmd/cartesi-rollups-node/main.go +++ b/cmd/cartesi-rollups-node/main.go @@ -44,7 +44,7 @@ func main() { slog.Info("Starting the Cartesi Rollups Node", "version", buildVersion, "config", config) // create the node supervisor - supervisor, err := node.Setup(ctx, config) + supervisor, err := node.Setup(ctx, config, "") if err != nil { slog.Error("Node exited with an error", "error", err) os.Exit(1) diff --git a/docs/cli/cartesi-rollups-cli_run-deps.md b/docs/cli/cartesi-rollups-cli_run-deps.md index 87fdd2d4f..333808433 100644 --- a/docs/cli/cartesi-rollups-cli_run-deps.md +++ b/docs/cli/cartesi-rollups-cli_run-deps.md @@ -16,12 +16,14 @@ cartesi-rollups-cli run-deps ### Options ``` + --devnet-block-time string Devnet mining block time (default "1") --devnet-docker-image string Devnet docker image name (default "cartesi/rollups-node-devnet:devel") - --devnet-mapped-port string devnet local listening port number (default "8545") + --devnet-mapped-port string Devnet local listening port number (default "8545") -h, --help help for run-deps --postgres-docker-image string Postgress docker image name (default "postgres:16-alpine") --postgres-mapped-port string Postgres local listening port number (default "5432") --postgres-password string Postgres password (default "password") + -v, --verbose verbose logs ``` ### SEE ALSO diff --git a/internal/deps/deps.go b/internal/deps/deps.go index c38b38e5b..194cb49dd 100644 --- a/internal/deps/deps.go +++ b/internal/deps/deps.go @@ -7,6 +7,7 @@ package deps import ( "context" "fmt" + "io" "log/slog" "strings" "sync" @@ -17,16 +18,25 @@ import ( ) const ( - DefaultPostgresDockerImage = "postgres:16-alpine" - DefaultPostgresPort = "5432" - DefaultPostgresPassword = "password" - DefaultDevnetDockerImage = "cartesi/rollups-node-devnet:devel" - DefaultDevnetPort = "8545" + DefaultPostgresDatabase = "postgres" + DefaultPostgresDockerImage = "postgres:16-alpine" + DefaultPostgresPort = "5432" + DefaultPostgresUser = "postgres" + DefaultPostgresPassword = "password" + DefaultDevnetDockerImage = "cartesi/rollups-node-devnet:devel" + DefaultDevnetPort = "8545" + DefaultBlockTime = "1" + DefaultBlockToWaitForOnStartup = "21" numPostgresCheckReadyAttempts = 2 pollInterval = 5 * time.Second ) +const ( + postgresKey = iota + devnetKey +) + // Struct to hold Node dependencies containers configurations type DepsConfig struct { Postgres *PostgresConfig @@ -40,8 +50,10 @@ type PostgresConfig struct { } type DevnetConfig struct { - DockerImage string - Port string + DockerImage string + Port string + BlockTime string + BlockToWaitForOnStartup string } // Builds a DepsConfig struct with default values @@ -55,17 +67,74 @@ func NewDefaultDepsConfig() *DepsConfig { &DevnetConfig{ DefaultDevnetDockerImage, DefaultDevnetPort, + DefaultBlockTime, + DefaultBlockToWaitForOnStartup, }, } } // Struct to represent the Node dependencies containers type DepsContainers struct { - containers []testcontainers.Container + containers map[int]testcontainers.Container //Literal copies lock value from waitGroup as sync.WaitGroup contains sync.noCopy waitGroup *sync.WaitGroup } +func (depContainers *DepsContainers) DevnetLogs(ctx context.Context) (io.ReadCloser, error) { + container, ok := depContainers.containers[devnetKey] + if !ok { + return nil, fmt.Errorf("Container Devnet is not present") + } + reader, err := container.Logs(ctx) + if err != nil { + return nil, fmt.Errorf("Error retrieving logs from Devnet Container : %w", err) + } + return reader, nil +} + +func (depContainers *DepsContainers) PostgresLogs(ctx context.Context) (io.ReadCloser, error) { + container, ok := depContainers.containers[postgresKey] + if !ok { + return nil, fmt.Errorf("Container Postgres is not present") + } + reader, err := container.Logs(ctx) + if err != nil { + return nil, fmt.Errorf("Error retrieving logs from Postgres Container : %w", err) + } + return reader, nil +} + +func (depContainers *DepsContainers) DevnetEndpoint( + ctx context.Context, + protocol string, +) (string, error) { + container, ok := depContainers.containers[devnetKey] + if !ok { + return "", fmt.Errorf("Container Devnet is not present") + } + endpoint, err := container.Endpoint(ctx, protocol) + if err != nil { + return "", fmt.Errorf("Error retrieving endpoint from Devnet Container : %w", err) + } + return endpoint, nil +} + +func (depContainers *DepsContainers) PostgresEndpoint( + ctx context.Context, + protocol string, +) (string, error) { + + container, ok := depContainers.containers[postgresKey] + if !ok { + return "", fmt.Errorf("Container Postgres is not present") + } + endpoint, err := container.Endpoint(ctx, protocol) + if err != nil { + return "", fmt.Errorf("Error retrieving endpoint from Postgres Container : %w", err) + } + return endpoint, nil +} + // debugLogging implements the testcontainers.Logging interface by printing the log to slog.Debug. type debugLogging struct{} @@ -73,12 +142,13 @@ func (d debugLogging) Printf(format string, v ...interface{}) { slog.Debug(fmt.Sprintf(format, v...)) } -func createHook(waitGroup *sync.WaitGroup) []testcontainers.ContainerLifecycleHooks { +func createHook(finishedWaitGroup *sync.WaitGroup) []testcontainers.ContainerLifecycleHooks { return []testcontainers.ContainerLifecycleHooks{ { + PostTerminates: []testcontainers.ContainerHook{ func(ctx context.Context, container testcontainers.Container) error { - waitGroup.Done() + finishedWaitGroup.Done() return nil }, }, @@ -90,25 +160,29 @@ func createHook(waitGroup *sync.WaitGroup) []testcontainers.ContainerLifecycleHo // The returned DepContainers struct can be used to gracefully // terminate the containers using the Terminate method func Run(ctx context.Context, depsConfig DepsConfig) (*DepsContainers, error) { - debugLogger := debugLogging{} - var waitGroup sync.WaitGroup - containers := []testcontainers.Container{} + debugLogger := debugLogging{} + var finishedWaitGroup sync.WaitGroup + containers := make(map[int]testcontainers.Container) if depsConfig.Postgres != nil { // wait strategy copied from testcontainers docs postgresWaitStrategy := wait.ForLog("database system is ready to accept connections"). WithOccurrence(numPostgresCheckReadyAttempts). WithPollInterval(pollInterval) + + postgresExposedPorts := "5432/tcp" + if depsConfig.Postgres.Port != "" { + postgresExposedPorts = strings.Join([]string{ + depsConfig.Postgres.Port, ":", postgresExposedPorts}, "") + } postgresReq := testcontainers.ContainerRequest{ - Image: depsConfig.Postgres.DockerImage, - ExposedPorts: []string{strings.Join([]string{ - depsConfig.Postgres.Port, ":5432/tcp"}, "")}, - WaitingFor: postgresWaitStrategy, - Name: "rollups-node-dep-postgres", + Image: depsConfig.Postgres.DockerImage, + ExposedPorts: []string{postgresExposedPorts}, + WaitingFor: postgresWaitStrategy, Env: map[string]string{ "POSTGRES_PASSWORD": depsConfig.Postgres.Password, }, - LifecycleHooks: createHook(&waitGroup), + LifecycleHooks: createHook(&finishedWaitGroup), } postgres, err := testcontainers.GenericContainer( ctx, @@ -121,20 +195,24 @@ func Run(ctx context.Context, depsConfig DepsConfig) (*DepsContainers, error) { if err != nil { return nil, err } - waitGroup.Add(1) - containers = append(containers, postgres) + finishedWaitGroup.Add(1) + containers[postgresKey] = postgres } if depsConfig.Devnet != nil { + + devnetExposedPort := "8545/tcp" + if depsConfig.Devnet.Port != "" { + devnetExposedPort = strings.Join([]string{ + depsConfig.Devnet.Port, ":", devnetExposedPort}, "") + } devNetReq := testcontainers.ContainerRequest{ Image: depsConfig.Devnet.DockerImage, - ExposedPorts: []string{strings.Join([]string{depsConfig.Devnet.Port, ":8545/tcp"}, "")}, - WaitingFor: wait.ForLog("Listening on 0.0.0.0:8545"), - Name: "rollups-node-dep-devnet", - Env: map[string]string{ - "ANVIL_IP_ADDR": "0.0.0.0", - }, - LifecycleHooks: createHook(&waitGroup), + ExposedPorts: []string{devnetExposedPort}, + WaitingFor: wait.ForLog("Block Number: " + depsConfig.Devnet.BlockToWaitForOnStartup), + Cmd: []string{"anvil", "--block-time", + depsConfig.Devnet.BlockTime, "--load-state", "/usr/share/devnet/anvil_state.json"}, + LifecycleHooks: createHook(&finishedWaitGroup), } devnet, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: devNetReq, @@ -144,25 +222,27 @@ func Run(ctx context.Context, depsConfig DepsConfig) (*DepsContainers, error) { if err != nil { return nil, err } - waitGroup.Add(1) - containers = append(containers, devnet) + finishedWaitGroup.Add(1) + containers[devnetKey] = devnet } + if len(containers) < 1 { return nil, fmt.Errorf("configuration is empty") } - return &DepsContainers{containers, &waitGroup}, nil + + return &DepsContainers{containers: containers, + waitGroup: &finishedWaitGroup, + }, nil } // Terminate terminates all dependencies containers. This method waits for all the containers // to terminate or gives an error if it fails to terminate one of the containers func Terminate(ctx context.Context, depContainers *DepsContainers) error { - for _, depContainer := range depContainers.containers { terr := depContainer.Terminate(ctx) if terr != nil { return terr } } - depContainers.waitGroup.Wait() return nil } diff --git a/internal/node/machinehash_test.go b/internal/node/machinehash_test.go index 614aa0f44..fae7eac20 100644 --- a/internal/node/machinehash_test.go +++ b/internal/node/machinehash_test.go @@ -16,8 +16,6 @@ import ( "github.com/stretchr/testify/suite" ) -const BlockchainHttpEndpoint = "http://0.0.0.0:" + deps.DefaultDevnetPort - type ValidateMachineHashSuite struct { suite.Suite } @@ -52,18 +50,21 @@ func (s *ValidateMachineHashSuite) TestItFailsWhenContextIsCanceled() { s.Require().Nil(err) defer os.RemoveAll(machineDir) - devnet, err := startDevnet() + dependencies, err := startDevnet() s.Require().Nil(err) defer func() { - err = deps.Terminate(context.Background(), devnet) + err = deps.Terminate(context.Background(), dependencies) s.Nil(err) }() + blockchainHttpEndpoint, err := dependencies.DevnetEndpoint(context.Background(), "http") + s.Require().Nil(err) + err = validateMachineHash( ctx, machineDir, addresses.GetTestBook().CartesiDApp.String(), - BlockchainHttpEndpoint, + blockchainHttpEndpoint, ) s.NotNil(err) s.ErrorIs(err, context.DeadlineExceeded) @@ -76,18 +77,21 @@ func (s *ValidateMachineHashSuite) TestItSucceedsWhenHashesAreEqual() { s.Require().Nil(err) defer os.RemoveAll(machineDir) - devnet, err := startDevnet() + dependencies, err := startDevnet() s.Require().Nil(err) defer func() { - err = deps.Terminate(context.Background(), devnet) + err = deps.Terminate(context.Background(), dependencies) s.Nil(err) }() + blockchainHttpEndpoint, err := dependencies.DevnetEndpoint(context.Background(), "http") + s.Require().Nil(err) + err = validateMachineHash( ctx, machineDir, addresses.GetTestBook().CartesiDApp.String(), - BlockchainHttpEndpoint, + blockchainHttpEndpoint, ) s.Nil(err) } @@ -132,8 +136,10 @@ func createMachineSnapshot() (string, error) { func startDevnet() (*deps.DepsContainers, error) { container, err := deps.Run(context.Background(), deps.DepsConfig{ Devnet: &deps.DevnetConfig{ - DockerImage: deps.DefaultDevnetDockerImage, - Port: deps.DefaultDevnetPort, + DockerImage: deps.DefaultDevnetDockerImage, + BlockTime: deps.DefaultBlockTime, + BlockToWaitForOnStartup: deps.DefaultBlockToWaitForOnStartup, + Port: "", }, }) if err != nil { diff --git a/internal/node/node.go b/internal/node/node.go index be613d10f..2ec878388 100644 --- a/internal/node/node.go +++ b/internal/node/node.go @@ -11,7 +11,7 @@ import ( ) // Setup creates the Node top-level supervisor. -func Setup(ctx context.Context, c config.NodeConfig) (services.Service, error) { +func Setup(ctx context.Context, c config.NodeConfig, workDir string) (services.Service, error) { // checks err := validateChainId(ctx, c.BlockchainID, c.BlockchainHttpEndpoint.Value) if err != nil { @@ -30,5 +30,5 @@ func Setup(ctx context.Context, c config.NodeConfig) (services.Service, error) { } // create service - return newSupervisorService(c), nil + return newSupervisorService(c, workDir), nil } diff --git a/internal/node/services.go b/internal/node/services.go index 44d2a990d..23de90986 100644 --- a/internal/node/services.go +++ b/internal/node/services.go @@ -68,7 +68,7 @@ func getRustLog(c config.NodeConfig, rustModule string) string { } } -func newAdvanceRunner(c config.NodeConfig) services.CommandService { +func newAdvanceRunner(c config.NodeConfig, workDir string) services.CommandService { var s services.CommandService s.Name = "advance-runner" s.HealthcheckPort = getPort(c, portOffsetAdvanceRunner) @@ -95,10 +95,11 @@ func newAdvanceRunner(c config.NodeConfig) services.CommandService { s.Env = append(s.Env, fmt.Sprintf("MACHINE_SNAPSHOT_PATH=%v", c.SnapshotDir)) } s.Env = append(s.Env, os.Environ()...) + s.WorkDir = workDir return s } -func newAuthorityClaimer(c config.NodeConfig) services.CommandService { +func newAuthorityClaimer(c config.NodeConfig, workDir string) services.CommandService { var s services.CommandService s.Name = "authority-claimer" s.HealthcheckPort = getPort(c, portOffsetAuthorityClaimer) @@ -136,10 +137,11 @@ func newAuthorityClaimer(c config.NodeConfig) services.CommandService { panic("invalid auth config") } s.Env = append(s.Env, os.Environ()...) + s.WorkDir = workDir return s } -func newDispatcher(c config.NodeConfig) services.CommandService { +func newDispatcher(c config.NodeConfig, workDir string) services.CommandService { var s services.CommandService s.Name = "dispatcher" s.HealthcheckPort = getPort(c, portOffsetDispatcher) @@ -164,10 +166,11 @@ func newDispatcher(c config.NodeConfig) services.CommandService { s.Env = append(s.Env, fmt.Sprintf("DISPATCHER_HTTP_SERVER_PORT=%v", getPort(c, portOffsetDispatcher))) s.Env = append(s.Env, os.Environ()...) + s.WorkDir = workDir return s } -func newGraphQLServer(c config.NodeConfig) services.CommandService { +func newGraphQLServer(c config.NodeConfig, workDir string) services.CommandService { var s services.CommandService s.Name = "graphql-server" s.HealthcheckPort = getPort(c, portOffsetGraphQLHealthcheck) @@ -181,10 +184,11 @@ func newGraphQLServer(c config.NodeConfig) services.CommandService { s.Env = append(s.Env, fmt.Sprintf("GRAPHQL_HEALTHCHECK_PORT=%v", getPort(c, portOffsetGraphQLHealthcheck))) s.Env = append(s.Env, os.Environ()...) + s.WorkDir = workDir return s } -func newHostRunner(c config.NodeConfig) services.CommandService { +func newHostRunner(c config.NodeConfig, workDir string) services.CommandService { var s services.CommandService s.Name = "host-runner" s.HealthcheckPort = getPort(c, portOffsetHostRunnerHealthcheck) @@ -201,10 +205,11 @@ func newHostRunner(c config.NodeConfig) services.CommandService { s.Env = append(s.Env, fmt.Sprintf("HOST_RUNNER_HEALTHCHECK_PORT=%v", getPort(c, portOffsetHostRunnerHealthcheck))) s.Env = append(s.Env, os.Environ()...) + s.WorkDir = workDir return s } -func newIndexer(c config.NodeConfig) services.CommandService { +func newIndexer(c config.NodeConfig, workdir string) services.CommandService { var s services.CommandService s.Name = "indexer" s.HealthcheckPort = getPort(c, portOffsetIndexer) @@ -220,10 +225,11 @@ func newIndexer(c config.NodeConfig) services.CommandService { s.Env = append(s.Env, fmt.Sprintf("INDEXER_HEALTHCHECK_PORT=%v", getPort(c, portOffsetIndexer))) s.Env = append(s.Env, os.Environ()...) + s.WorkDir = workdir return s } -func newInspectServer(c config.NodeConfig) services.CommandService { +func newInspectServer(c config.NodeConfig, workDir string) services.CommandService { var s services.CommandService s.Name = "inspect-server" s.HealthcheckPort = getPort(c, portOffsetInspectHealthcheck) @@ -239,10 +245,11 @@ func newInspectServer(c config.NodeConfig) services.CommandService { s.Env = append(s.Env, fmt.Sprintf("INSPECT_SERVER_HEALTHCHECK_PORT=%v", getPort(c, portOffsetInspectHealthcheck))) s.Env = append(s.Env, os.Environ()...) + s.WorkDir = workDir return s } -func newRedis(c config.NodeConfig) services.CommandService { +func newRedis(c config.NodeConfig, workDir string) services.CommandService { var s services.CommandService s.Name = "redis" s.HealthcheckPort = getPort(c, portOffsetRedis) @@ -252,10 +259,11 @@ func newRedis(c config.NodeConfig) services.CommandService { s.Args = append(s.Args, "--save", "") s.Args = append(s.Args, "--appendonly", "no") s.Env = append(s.Env, os.Environ()...) + s.WorkDir = workDir return s } -func newServerManager(c config.NodeConfig) services.ServerManager { +func newServerManager(c config.NodeConfig, workDir string) services.ServerManager { var s services.ServerManager s.Name = "server-manager" s.HealthcheckPort = getPort(c, portOffsetServerManager) @@ -270,10 +278,11 @@ func newServerManager(c config.NodeConfig) services.ServerManager { } s.Env = append(s.Env, os.Environ()...) s.BypassLog = c.ExperimentalServerManagerBypassLog + s.WorkDir = workDir return s } -func newStateServer(c config.NodeConfig) services.CommandService { +func newStateServer(c config.NodeConfig, workDir string) services.CommandService { var s services.CommandService s.Name = "state-server" s.HealthcheckPort = getPort(c, portOffsetStateServer) @@ -292,38 +301,39 @@ func newStateServer(c config.NodeConfig) services.CommandService { s.Env = append(s.Env, fmt.Sprintf("SS_SERVER_ADDRESS=%v:%v", localhost, getPort(c, portOffsetStateServer))) s.Env = append(s.Env, os.Environ()...) + s.WorkDir = workDir return s } -func newSupervisorService(c config.NodeConfig) services.SupervisorService { +func newSupervisorService(c config.NodeConfig, workDir string) services.SupervisorService { var s []services.Service if !c.ExperimentalSunodoValidatorEnabled { // add Redis first - s = append(s, newRedis(c)) + s = append(s, newRedis(c, workDir)) } // add services without dependencies - s = append(s, newGraphQLServer(c)) - s = append(s, newIndexer(c)) - s = append(s, newStateServer(c)) + s = append(s, newGraphQLServer(c, workDir)) + s = append(s, newIndexer(c, workDir)) + s = append(s, newStateServer(c, workDir)) // start either the server manager or host runner if c.FeatureHostMode { - s = append(s, newHostRunner(c)) + s = append(s, newHostRunner(c, workDir)) } else { - s = append(s, newServerManager(c)) + s = append(s, newServerManager(c, workDir)) } // enable claimer if reader mode and sunodo validator mode are disabled if !c.FeatureDisableClaimer && !c.ExperimentalSunodoValidatorEnabled { - s = append(s, newAuthorityClaimer(c)) + s = append(s, newAuthorityClaimer(c, workDir)) } // add services with dependencies - s = append(s, newAdvanceRunner(c)) // Depends on the server-manager/host-runner - s = append(s, newDispatcher(c)) // Depends on the state server - s = append(s, newInspectServer(c)) // Depends on the server-manager/host-runner + s = append(s, newAdvanceRunner(c, workDir)) // Depends on the server-manager/host-runner + s = append(s, newDispatcher(c, workDir)) // Depends on the state server + s = append(s, newInspectServer(c, workDir)) // Depends on the server-manager/host-runner s = append(s, newHttpService(c)) diff --git a/internal/services/command.go b/internal/services/command.go index ad33b3910..9eb9aa0d0 100644 --- a/internal/services/command.go +++ b/internal/services/command.go @@ -38,6 +38,9 @@ type CommandService struct { // Environment variables. Env []string + + // Working Directory + WorkDir string } func (s CommandService) Start(ctx context.Context, ready chan<- struct{}) error { @@ -52,6 +55,11 @@ func (s CommandService) Start(ctx context.Context, ready chan<- struct{}) error } return err } + + if s.WorkDir != "" { + cmd.Dir = s.WorkDir + } + go s.pollTcp(ctx, ready) err := cmd.Run() diff --git a/internal/services/server-manager.go b/internal/services/server-manager.go index cb2719d0c..3ff61ac76 100644 --- a/internal/services/server-manager.go +++ b/internal/services/server-manager.go @@ -37,6 +37,9 @@ type ServerManager struct { // Bypass the log and write directly to stdout/stderr. BypassLog bool + + // Working Directory + WorkDir string } const waitDelay = 200 * time.Millisecond @@ -44,6 +47,9 @@ const waitDelay = 200 * time.Millisecond func (s ServerManager) Start(ctx context.Context, ready chan<- struct{}) error { cmd := exec.CommandContext(ctx, s.Path, s.Args...) cmd.Env = s.Env + if s.WorkDir != "" { + cmd.Dir = s.WorkDir + } if s.BypassLog { cmd.Stderr = os.Stderr cmd.Stdout = os.Stdout diff --git a/pkg/addresses/addresses.go b/pkg/addresses/addresses.go index 1a8238a63..8989e65b6 100644 --- a/pkg/addresses/addresses.go +++ b/pkg/addresses/addresses.go @@ -29,6 +29,8 @@ type Book struct { EtherPortal common.Address InputBox common.Address CartesiDApp common.Address + HistoryAddress common.Address + AuthorityAddress common.Address } // Get the addresses for the test environment. @@ -45,6 +47,8 @@ func GetTestBook() *Book { EtherPortal: common.HexToAddress("0xFfdbe43d4c855BF7e0f105c400A50857f53AB044"), InputBox: common.HexToAddress("0x59b22D57D4f067708AB0c00552767405926dc768"), CartesiDApp: common.HexToAddress("0x7C54E3f7A8070a54223469965A871fB8f6f88c22"), + HistoryAddress: common.HexToAddress("0x325272217ae6815b494bF38cED004c5Eb8a7CdA7"), + AuthorityAddress: common.HexToAddress("0x58c93F83fb3304730C95aad2E360cdb88b782010"), } } diff --git a/pkg/ethutil/ethutil.go b/pkg/ethutil/ethutil.go index bf84932fb..89e884c1e 100644 --- a/pkg/ethutil/ethutil.go +++ b/pkg/ethutil/ethutil.go @@ -14,8 +14,10 @@ import ( "github.com/cartesi/rollups-node/pkg/contracts" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/rpc" ) // Gas limit when sending transactions. @@ -59,6 +61,34 @@ func AddInput( return getInputIndex(ctx, client, book, inputBox, receipt) } +// Convenience function to add an input using Foundry Mnemonic +// This function waits until the transaction is added to a block and return the input index. +func AddInputUsingFoundryMnemonic( + ctx context.Context, + blockchainHttpEnpoint string, + payload string, +) (int, error) { + + // Send Input + client, err := ethclient.DialContext(ctx, blockchainHttpEnpoint) + if err != nil { + return 0, err + } + defer client.Close() + + signer, err := NewMnemonicSigner(ctx, client, FoundryMnemonic, 0) + if err != nil { + return 0, err + } + book := addresses.GetTestBook() + + payloadBytes, err := hexutil.Decode(payload) + if err != nil { + panic(err) + } + return AddInput(ctx, client, book, signer, payloadBytes) +} + // Get input index in the transaction by looking at the event logs. func getInputIndex( ctx context.Context, @@ -159,3 +189,32 @@ func ExecuteVoucher( return &receipt.TxHash, nil } + +// Advances the Devnet timestamp +func AdvanceDevnetTime(ctx context.Context, + blockchainHttpEnpoint string, + timeInSeconds int, +) error { + client, err := rpc.DialContext(ctx, blockchainHttpEnpoint) + if err != nil { + return err + } + defer client.Close() + return client.CallContext(ctx, nil, "evm_increaseTime", timeInSeconds) + +} + +// Sets the timestamp for the next block at Devnet +func SetNextDevnetBlockTimestamp( + ctx context.Context, + blockchainHttpEnpoint string, + timestamp int64, +) error { + + client, err := rpc.DialContext(ctx, blockchainHttpEnpoint) + if err != nil { + return err + } + defer client.Close() + return client.CallContext(ctx, nil, "evm_setNextBlockTimestamp", timestamp) +} diff --git a/pkg/ethutil/ethutil_test.go b/pkg/ethutil/ethutil_test.go index d8a8d9853..dbb0f2b35 100644 --- a/pkg/ethutil/ethutil_test.go +++ b/pkg/ethutil/ethutil_test.go @@ -9,12 +9,11 @@ import ( "testing" "time" + "github.com/cartesi/rollups-node/internal/deps" "github.com/cartesi/rollups-node/pkg/addresses" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" "github.com/stretchr/testify/suite" - "github.com/testcontainers/testcontainers-go" - "github.com/testcontainers/testcontainers-go/wait" ) const testTimeout = 300 * time.Second @@ -25,7 +24,7 @@ type EthUtilSuite struct { suite.Suite ctx context.Context cancel context.CancelFunc - devNet testcontainers.Container + deps *deps.DepsContainers client *ethclient.Client signer Signer book *addresses.Book @@ -35,10 +34,10 @@ func (s *EthUtilSuite) SetupTest() { s.ctx, s.cancel = context.WithTimeout(context.Background(), testTimeout) var err error - s.devNet, err = newDevNetContainer(s.ctx) + s.deps, err = newDevNetContainer(context.Background()) s.Require().Nil(err) - endpoint, err := s.devNet.Endpoint(s.ctx, "ws") + endpoint, err := s.deps.DevnetEndpoint(s.ctx, "ws") s.Require().Nil(err) s.client, err = ethclient.DialContext(s.ctx, endpoint) @@ -51,7 +50,7 @@ func (s *EthUtilSuite) SetupTest() { } func (s *EthUtilSuite) TearDownTest() { - err := s.devNet.Terminate(s.ctx) + err := deps.Terminate(context.Background(), s.deps) s.Nil(err) s.cancel() } @@ -76,7 +75,7 @@ func (s *EthUtilSuite) TestAddInput() { // Log the output of the given container func (s *EthUtilSuite) logDevnetOutput() { - reader, err := s.devNet.Logs(s.ctx) + reader, err := s.deps.DevnetLogs(s.ctx) s.Require().Nil(err) defer reader.Close() @@ -89,17 +88,20 @@ func TestEthUtilSuite(t *testing.T) { suite.Run(t, new(EthUtilSuite)) } -// We use the sunodo devnet docker image to test the client. +// We use the node devnet docker image to test the client. // This image starts an anvil node with the Rollups contracts already deployed. -func newDevNetContainer(ctx context.Context) (testcontainers.Container, error) { - req := testcontainers.ContainerRequest{ - Image: "cartesi/rollups-node-devnet:devel", - ExposedPorts: []string{"8545/tcp"}, - WaitingFor: wait.ForLog("Listening on 0.0.0.0:8545"), +func newDevNetContainer(ctx context.Context) (*deps.DepsContainers, error) { + + container, err := deps.Run(ctx, deps.DepsConfig{ + Devnet: &deps.DevnetConfig{ + DockerImage: deps.DefaultDevnetDockerImage, + BlockTime: deps.DefaultBlockTime, + BlockToWaitForOnStartup: deps.DefaultBlockToWaitForOnStartup, + Port: "", + }, + }) + if err != nil { + return nil, err } - genericReq := testcontainers.GenericContainerRequest{ - ContainerRequest: req, - Started: true, - } - return testcontainers.GenericContainer(ctx, genericReq) + return container, nil } diff --git a/test/config.go b/test/config.go new file mode 100644 index 000000000..b491fd8cf --- /dev/null +++ b/test/config.go @@ -0,0 +1,86 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +// Package endtoendtests +package endtoendtests + +import ( + "fmt" + "log/slog" + "time" + + "github.com/cartesi/rollups-node/internal/node/config" + "github.com/cartesi/rollups-node/pkg/addresses" + "github.com/cartesi/rollups-node/pkg/ethutil" +) + +const ( + LocalBlockchainID = 31337 + LocalApplicationDeploymentBlockNumber = 20 + LocalInputBoxDeploymentBlockNumber = 20 + LocalHttpAddress = "0.0.0.0" + LocalHttpPort = 10000 + LocalBlockTimeout = 120 + LocalFinalityOffset = 1 + LocalEpochDurationInSeconds = 240 +) + +func NewLocalNodeConfig(localPostgresEnpoint string, localBlockchainHttpEndpoint string, + localBlockchainWsEnpoint string, snapshotDir string) config.NodeConfig { + + var nodeConfig config.NodeConfig + + book := addresses.GetTestBook() + + //Log + nodeConfig.LogLevel = slog.LevelInfo + nodeConfig.LogPretty = false + + //Postgres + nodeConfig.PostgresEndpoint = + config.Redacted[string]{Value: localPostgresEnpoint} + + //Epoch + nodeConfig.RollupsEpochDuration, _ = + time.ParseDuration(fmt.Sprintf("%ds", LocalEpochDurationInSeconds)) + + //Blochain + nodeConfig.BlockchainID = LocalBlockchainID + nodeConfig.BlockchainHttpEndpoint = + config.Redacted[string]{Value: localBlockchainHttpEndpoint} + nodeConfig.BlockchainWsEndpoint = + config.Redacted[string]{Value: localBlockchainWsEnpoint} + nodeConfig.BlockchainIsLegacy = false + nodeConfig.BlockchainFinalityOffset = LocalFinalityOffset + nodeConfig.BlockchainBlockTimeout = LocalBlockTimeout + + //Contracts + nodeConfig.ContractsHistoryAddress = book.HistoryAddress.Hex() + nodeConfig.ContractsAuthorityAddress = book.AuthorityAddress.Hex() + nodeConfig.ContractsApplicationAddress = book.CartesiDApp.Hex() + nodeConfig.ContractsApplicationDeploymentBlockNumber = LocalApplicationDeploymentBlockNumber + nodeConfig.ContractsInputBoxAddress = book.InputBox.Hex() + nodeConfig.ContractsInputBoxDeploymentBlockNumber = LocalInputBoxDeploymentBlockNumber + + //HTTP endpoint + nodeConfig.HttpAddress = LocalHttpAddress + nodeConfig.HttpPort = LocalHttpPort + + //Features + nodeConfig.FeatureHostMode = false + nodeConfig.FeatureDisableClaimer = false + nodeConfig.FeatureDisableMachineHashCheck = false + + //Experimental + nodeConfig.ExperimentalSunodoValidatorEnabled = false + + //Auth + nodeConfig.Auth = config.AuthMnemonic{ + Mnemonic: config.Redacted[string]{Value: ethutil.FoundryMnemonic}, + AccountIndex: config.Redacted[int]{Value: 0}, + } + + nodeConfig.SnapshotDir = snapshotDir + + return nodeConfig +} diff --git a/test/data/echo_input/expected_input_with_proofs.json b/test/data/echo_input/expected_input_with_proofs.json new file mode 100644 index 000000000..0d2912f00 --- /dev/null +++ b/test/data/echo_input/expected_input_with_proofs.json @@ -0,0 +1,152 @@ +{ + "index": 0, + "status": "ACCEPTED", + "msgSender": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "timestamp": 7000000000, + "blockNumber": 22, + "payload": "0xdeadbeef", + "notices": [ + { + "index": 0, + "inputIndex": 0, + "payload": "0xdeadbeef", + "proof": { + "inputIndexWithinEpoch": 0, + "outputIndexWithinInput": 0, + "outputHashesRootHash": "0x660c2d35b0a43d8179792345211d0eab28d88f47fafadd8334b80196cad41ded", + "vouchersEpochRootHash": "0x7d0423c65ec1c03578e3c6182956c51ae617b372c504cc9d272e71a2a3385dcb", + "noticesEpochRootHash": "0x63a367741b1feb9c2dc64bda8ac4a083ebbe5fd1f7bb4746e94597c988f30197", + "machineStateHash": "0x99036b33c8bb8b588b6a1dfbf742567f64039e38080bdc593c9d5c5a5481a48b", + "outputHashInOutputHashesSiblings": [ + "0xae39ce8537aca75e2eff3e38c98011dfe934e700a0967732fc07b430dd656a23", + "0x3fc9a15f5b4869c872f81087bb6104b7d63e6f9ab47f2c43f3535eae7172aa7f", + "0x17d2dd614cddaa4d879276b11e0672c9560033d3e8453a1d045339d34ba601b9", + "0xc37b8b13ca95166fb7af16988a70fcc90f38bf9126fd833da710a47fb37a55e6", + "0x8e7a427fa943d9966b389f4f257173676090c6e95f43e2cb6d65f8758111e309", + "0x30b0b9deb73e155c59740bacf14a6ff04b64bb8e201a506409c3fe381ca4ea90", + "0xcd5deac729d0fdaccc441d09d7325f41586ba13c801b7eccae0f95d8f3933efe", + "0xd8b96e5b7f6f459e9cb6a2f41bf276c7b85c10cd4662c04cbbb365434726c0a0", + "0xc9695393027fb106a8153109ac516288a88b28a93817899460d6310b71cf1e61", + "0x63e8806fa0d4b197a259e8c3ac28864268159d0ac85f8581ca28fa7d2c0c03eb", + "0x91e3eee5ca7a3da2b3053c9770db73599fb149f620e3facef95e947c0ee860b7", + "0x2122e31e4bbd2b7c783d79cc30f60c6238651da7f0726f767d22747264fdb046", + "0xf7549f26cc70ed5e18baeb6c81bb0625cb95bb4019aeecd40774ee87ae29ec51", + "0x7a71f6ee264c5d761379b3d7d617ca83677374b49d10aec50505ac087408ca89", + "0x2b573c267a712a52e1d06421fe276a03efb1889f337201110fdc32a81f8e1524", + "0x99af665835aabfdc6740c7e2c3791a31c3cdc9f5ab962f681b12fc092816a62f" + ], + "outputHashesInEpochSiblings": [ + "0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563", + "0x633dc4d7da7256660a892f8f1604a44b5432649cc8ec5cb3ced4c4e6ac94dd1d", + "0x890740a8eb06ce9be422cb8da5cdafc2b58c0a5e24036c578de2a433c828ff7d", + "0x3b8ec09e026fdc305365dfc94e189a81b38c7597b3d941c279f042e8206e0bd8", + "0xecd50eee38e386bd62be9bedb990706951b65fe053bd9d8a521af753d139e2da", + "0xdefff6d330bb5403f63b14f33b578274160de3a50df4efecf0e0db73bcdd3da5", + "0x617bdd11f7c0a11f49db22f629387a12da7596f9d1704d7465177c63d88ec7d7", + "0x292c23a9aa1d8bea7e2435e555a4a60e379a5a35f3f452bae60121073fb6eead", + "0xe1cea92ed99acdcb045a6726b2f87107e8a61620a232cf4d7d5b5766b3952e10", + "0x7ad66c0a68c72cb89e4fb4303841966e4062a76ab97451e3b9fb526a5ceb7f82", + "0xe026cc5a4aed3c22a58cbd3d2ac754c9352c5436f638042dca99034e83636516", + "0x3d04cffd8b46a874edf5cfae63077de85f849a660426697b06a829c70dd1409c", + "0xad676aa337a485e4728a0b240d92b3ef7b3c372d06d189322bfd5f61f1e7203e", + "0xa2fca4a49658f9fab7aa63289c91b7c7b6c832a6d0e69334ff5b0a3483d09dab", + "0x4ebfd9cd7bca2505f7bef59cc1c12ecc708fff26ae4af19abe852afe9e20c862", + "0x2def10d13dd169f550f578bda343d9717a138562e0093b380a1120789d53cf10", + "0x776a31db34a1a0a7caaf862cffdfff1789297ffadc380bd3d39281d340abd3ad", + "0xe2e7610b87a5fdf3a72ebe271287d923ab990eefac64b6e59d79f8b7e08c46e3", + "0x504364a5c6858bf98fff714ab5be9de19ed31a976860efbd0e772a2efe23e2e0", + "0x4f05f4acb83f5b65168d9fef89d56d4d77b8944015e6b1eed81b0238e2d0dba3", + "0x44a6d974c75b07423e1d6d33f481916fdd45830aea11b6347e700cd8b9f0767c", + "0xedf260291f734ddac396a956127dde4c34c0cfb8d8052f88ac139658ccf2d507", + "0x6075c657a105351e7f0fce53bc320113324a522e8fd52dc878c762551e01a46e", + "0x6ca6a3f763a9395f7da16014725ca7ee17e4815c0ff8119bf33f273dee11833b", + "0x1c25ef10ffeb3c7d08aa707d17286e0b0d3cbcb50f1bd3b6523b63ba3b52dd0f", + "0xfffc43bd08273ccf135fd3cacbeef055418e09eb728d727c4d5d5c556cdea7e3", + "0xc5ab8111456b1f28f3c7a0a604b4553ce905cb019c463ee159137af83c350b22", + "0x0ff273fcbf4ae0f2bd88d6cf319ff4004f8d7dca70d4ced4e74d2c74139739e6", + "0x7fa06ba11241ddd5efdc65d4e39c9f6991b74fd4b81b62230808216c876f827c", + "0x7e275adf313a996c7e2950cac67caba02a5ff925ebf9906b58949f3e77aec5b9", + "0x8f6162fa308d2b3a15dc33cffac85f13ab349173121645aedf00f471663108be", + "0x78ccaaab73373552f207a63599de54d7d8d0c1805f86ce7da15818d09f4cff62" + ], + "context": "0x0000000000000000000000000000000000000000000000000000000000000000" + } + } + ], + "vouchers": [ + { + "index": 0, + "inputIndex": 0, + "destination": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "payload": "0xdeadbeef", + "proof": { + "inputIndexWithinEpoch": 0, + "outputIndexWithinInput": 0, + "outputHashesRootHash": "0xae4004e727f8d55ebd8171010f56e467fe19042f1f84304a379bac9a7d2d8215", + "vouchersEpochRootHash": "0x7d0423c65ec1c03578e3c6182956c51ae617b372c504cc9d272e71a2a3385dcb", + "noticesEpochRootHash": "0x63a367741b1feb9c2dc64bda8ac4a083ebbe5fd1f7bb4746e94597c988f30197", + "machineStateHash": "0x99036b33c8bb8b588b6a1dfbf742567f64039e38080bdc593c9d5c5a5481a48b", + "outputHashInOutputHashesSiblings": [ + "0xae39ce8537aca75e2eff3e38c98011dfe934e700a0967732fc07b430dd656a23", + "0x3fc9a15f5b4869c872f81087bb6104b7d63e6f9ab47f2c43f3535eae7172aa7f", + "0x17d2dd614cddaa4d879276b11e0672c9560033d3e8453a1d045339d34ba601b9", + "0xc37b8b13ca95166fb7af16988a70fcc90f38bf9126fd833da710a47fb37a55e6", + "0x8e7a427fa943d9966b389f4f257173676090c6e95f43e2cb6d65f8758111e309", + "0x30b0b9deb73e155c59740bacf14a6ff04b64bb8e201a506409c3fe381ca4ea90", + "0xcd5deac729d0fdaccc441d09d7325f41586ba13c801b7eccae0f95d8f3933efe", + "0xd8b96e5b7f6f459e9cb6a2f41bf276c7b85c10cd4662c04cbbb365434726c0a0", + "0xc9695393027fb106a8153109ac516288a88b28a93817899460d6310b71cf1e61", + "0x63e8806fa0d4b197a259e8c3ac28864268159d0ac85f8581ca28fa7d2c0c03eb", + "0x91e3eee5ca7a3da2b3053c9770db73599fb149f620e3facef95e947c0ee860b7", + "0x2122e31e4bbd2b7c783d79cc30f60c6238651da7f0726f767d22747264fdb046", + "0xf7549f26cc70ed5e18baeb6c81bb0625cb95bb4019aeecd40774ee87ae29ec51", + "0x7a71f6ee264c5d761379b3d7d617ca83677374b49d10aec50505ac087408ca89", + "0x2b573c267a712a52e1d06421fe276a03efb1889f337201110fdc32a81f8e1524", + "0x99af665835aabfdc6740c7e2c3791a31c3cdc9f5ab962f681b12fc092816a62f" + ], + "outputHashesInEpochSiblings": [ + "0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563", + "0x633dc4d7da7256660a892f8f1604a44b5432649cc8ec5cb3ced4c4e6ac94dd1d", + "0x890740a8eb06ce9be422cb8da5cdafc2b58c0a5e24036c578de2a433c828ff7d", + "0x3b8ec09e026fdc305365dfc94e189a81b38c7597b3d941c279f042e8206e0bd8", + "0xecd50eee38e386bd62be9bedb990706951b65fe053bd9d8a521af753d139e2da", + "0xdefff6d330bb5403f63b14f33b578274160de3a50df4efecf0e0db73bcdd3da5", + "0x617bdd11f7c0a11f49db22f629387a12da7596f9d1704d7465177c63d88ec7d7", + "0x292c23a9aa1d8bea7e2435e555a4a60e379a5a35f3f452bae60121073fb6eead", + "0xe1cea92ed99acdcb045a6726b2f87107e8a61620a232cf4d7d5b5766b3952e10", + "0x7ad66c0a68c72cb89e4fb4303841966e4062a76ab97451e3b9fb526a5ceb7f82", + "0xe026cc5a4aed3c22a58cbd3d2ac754c9352c5436f638042dca99034e83636516", + "0x3d04cffd8b46a874edf5cfae63077de85f849a660426697b06a829c70dd1409c", + "0xad676aa337a485e4728a0b240d92b3ef7b3c372d06d189322bfd5f61f1e7203e", + "0xa2fca4a49658f9fab7aa63289c91b7c7b6c832a6d0e69334ff5b0a3483d09dab", + "0x4ebfd9cd7bca2505f7bef59cc1c12ecc708fff26ae4af19abe852afe9e20c862", + "0x2def10d13dd169f550f578bda343d9717a138562e0093b380a1120789d53cf10", + "0x776a31db34a1a0a7caaf862cffdfff1789297ffadc380bd3d39281d340abd3ad", + "0xe2e7610b87a5fdf3a72ebe271287d923ab990eefac64b6e59d79f8b7e08c46e3", + "0x504364a5c6858bf98fff714ab5be9de19ed31a976860efbd0e772a2efe23e2e0", + "0x4f05f4acb83f5b65168d9fef89d56d4d77b8944015e6b1eed81b0238e2d0dba3", + "0x44a6d974c75b07423e1d6d33f481916fdd45830aea11b6347e700cd8b9f0767c", + "0xedf260291f734ddac396a956127dde4c34c0cfb8d8052f88ac139658ccf2d507", + "0x6075c657a105351e7f0fce53bc320113324a522e8fd52dc878c762551e01a46e", + "0x6ca6a3f763a9395f7da16014725ca7ee17e4815c0ff8119bf33f273dee11833b", + "0x1c25ef10ffeb3c7d08aa707d17286e0b0d3cbcb50f1bd3b6523b63ba3b52dd0f", + "0xfffc43bd08273ccf135fd3cacbeef055418e09eb728d727c4d5d5c556cdea7e3", + "0xc5ab8111456b1f28f3c7a0a604b4553ce905cb019c463ee159137af83c350b22", + "0x0ff273fcbf4ae0f2bd88d6cf319ff4004f8d7dca70d4ced4e74d2c74139739e6", + "0x7fa06ba11241ddd5efdc65d4e39c9f6991b74fd4b81b62230808216c876f827c", + "0x7e275adf313a996c7e2950cac67caba02a5ff925ebf9906b58949f3e77aec5b9", + "0x8f6162fa308d2b3a15dc33cffac85f13ab349173121645aedf00f471663108be", + "0x78ccaaab73373552f207a63599de54d7d8d0c1805f86ce7da15818d09f4cff62" + ], + "context": "0x0000000000000000000000000000000000000000000000000000000000000000" + } + } + ], + "reports": [ + { + "index": 0, + "inputIndex": 0, + "payload": "0xdeadbeef" + } + ] + } \ No newline at end of file diff --git a/test/echo_test.go b/test/echo_test.go new file mode 100644 index 000000000..c7b42b904 --- /dev/null +++ b/test/echo_test.go @@ -0,0 +1,191 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +//go:build endtoendtests +// +build endtoendtests + +package endtoendtests + +import ( + "context" + _ "embed" + "encoding/json" + "net/url" + "os" + "path" + "testing" + "time" + + "github.com/Khan/genqlient/graphql" + "github.com/cartesi/rollups-node/internal/deps" + "github.com/cartesi/rollups-node/internal/machine" + "github.com/cartesi/rollups-node/internal/node" + "github.com/cartesi/rollups-node/pkg/ethutil" + "github.com/cartesi/rollups-node/pkg/readerclient" + "github.com/stretchr/testify/suite" +) + +const ( + payload = "0xdeadbeef" + maxReadInputAttempts = 10 + blockTimestampinSeconds = 7000000000 + testTimeout = 300 * time.Second + devNetAdvanceTimeInSeconds = 120 + devNetMiningBlockTimeInSeconds = "2" + graphqlEndpoint = "http://localhost:10000/graphql" +) + +type EchoInputTestSuite struct { + suite.Suite + containers *deps.DepsContainers + ctx context.Context + cancel context.CancelFunc + tempDir string + supervisorErr chan error + blockchainHttpEndpoint string +} + +//go:embed data/echo_input/expected_input_with_proofs.json +var expectedInputJsonBytes []byte + +func (s *EchoInputTestSuite) SetupTest() { + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + + // Create Tempdir + tempDir, err := os.MkdirTemp("", "echo-test") + s.Require().Nil(err) + snapshotDir := path.Join(tempDir, "machine-snapshot") + + machine.Save("cartesi/rollups-node-snapshot:devel", snapshotDir, "test-echo-app") + + // Run deps + var depsConfig = deps.DepsConfig{ + &deps.PostgresConfig{ + DockerImage: deps.DefaultPostgresDockerImage, + Port: "", + Password: deps.DefaultPostgresPassword, + }, + &deps.DevnetConfig{ + DockerImage: deps.DefaultDevnetDockerImage, + Port: "", + BlockTime: devNetMiningBlockTimeInSeconds, + BlockToWaitForOnStartup: deps.DefaultBlockToWaitForOnStartup, + }, + } + + depsContainers, err := deps.Run(ctx, depsConfig) + s.Require().Nil(err) + + // Capture endpoints + postgressEndpoint, err := depsContainers.PostgresEndpoint(ctx, "postgres") + s.Require().Nil(err) + + postgresUrl, err := url.Parse(postgressEndpoint) + s.Require().Nil(err) + + postgresUrl.User = url.UserPassword(deps.DefaultPostgresUser, deps.DefaultPostgresPassword) + postgresUrl = postgresUrl.JoinPath(deps.DefaultPostgresDatabase) + + devnetHttpEndpoint, err := depsContainers.DevnetEndpoint(ctx, "http") + s.Require().Nil(err) + devnetWsEndpoint, err := depsContainers.DevnetEndpoint(ctx, "ws") + s.Require().Nil(err) + + s.blockchainHttpEndpoint = devnetHttpEndpoint + + // Fix the Blochain timestamp. Must be "in the future" + err = ethutil.SetNextDevnetBlockTimestamp(ctx, devnetHttpEndpoint, blockTimestampinSeconds) + s.Require().Nil(err) + + // Run Node Service + nodeConfig := NewLocalNodeConfig(postgresUrl.String(), devnetHttpEndpoint, devnetWsEndpoint, snapshotDir) + + supervisor, err := node.Setup(ctx, nodeConfig, tempDir) + s.Require().Nil(err) + + ready := make(chan struct{}, 1) + supervisorErr := make(chan error, 1) + go func() { + err := supervisor.Start(ctx, ready) + if err != nil { + supervisorErr <- err + } + }() + + select { + case err := <-supervisorErr: + s.Require().Nil(err) + case <-ready: + break + } + + // Configure Suite for tear down + s.containers = depsContainers + s.tempDir = tempDir + s.ctx = ctx + s.cancel = cancel + s.supervisorErr = supervisorErr + +} + +func (s *EchoInputTestSuite) TearDownTest() { + + // Stop Node services + s.cancel() + + // Remove temp files + os.RemoveAll(s.tempDir) + + // Terminate deps + ctx := context.Background() + err := deps.Terminate(ctx, s.containers) + s.Require().Nil(err) + +} + +func (s *EchoInputTestSuite) TestSendInput() { + + inputIndex, err := ethutil.AddInputUsingFoundryMnemonic(s.ctx, s.blockchainHttpEndpoint, payload) + s.Require().Nil(err) + + // Check input was correctly added to the blockchain + s.Require().Equal(0, inputIndex) + + s.Require().Nil(ethutil.AdvanceDevnetTime(s.ctx, s.blockchainHttpEndpoint, devNetAdvanceTimeInSeconds)) + + // Get Input with vouchers and proofs + graphQlClient := graphql.NewClient(graphqlEndpoint, nil) + var input *readerclient.Input + attempts := 0 + for ; attempts < maxReadInputAttempts; attempts++ { + time.Sleep(2 * time.Second) + input, err = readerclient.GetInput(s.ctx, graphQlClient, inputIndex) + if err == nil && input.Status == "ACCEPTED" && + input.Vouchers != nil && input.Vouchers[0].Proof != nil && + input.Notices != nil && input.Notices[0].Proof != nil { + break + } + } + if attempts == maxReadInputAttempts { + select { + case err = <-s.supervisorErr: + s.Require().Nil(err) + break + case <-time.After(1 * time.Second): + s.Require().FailNow("Reached max read attempts") + } + } + + //Check Input + var expectedInput readerclient.Input + err = json.Unmarshal(expectedInputJsonBytes, &expectedInput) + s.Require().Nil(err) + s.Require().EqualValues(&expectedInput, input) + +} + +func TestEchoInput(t *testing.T) { + + suite.Run(t, new(EchoInputTestSuite)) +}