diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bd1ad3b9..7ad42d226 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,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 +- Added Rollups end-to-end test using Echo Dapp ### 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..864950e08 100644 --- a/cmd/cartesi-rollups-node/main.go +++ b/cmd/cartesi-rollups-node/main.go @@ -62,7 +62,7 @@ func main() { }() // start supervisor - if err := supervisor.Start(ctx, ready); err != nil { + if err := supervisor.Start(ctx, ready, ""); err != nil { slog.Error("Node exited with an error", "error", err) os.Exit(1) } diff --git a/cmd/gen-devnet/main.go b/cmd/gen-devnet/main.go index 936a03bef..4d487b2f9 100644 --- a/cmd/gen-devnet/main.go +++ b/cmd/gen-devnet/main.go @@ -107,7 +107,7 @@ func run(cmd *cobra.Command, args []string) { s = append(s, newAnvilService(anvilStatePath)) supervisor := newSupervisorService(s) - if err := supervisor.Start(ctx, ready); err != nil { + if err := supervisor.Start(ctx, ready, ""); err != nil { fmt.Printf("%s: %v", supervisor.Name, err) cancel() } 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..a42321ea6 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,11 +18,15 @@ 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 @@ -40,8 +45,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 +62,74 @@ func NewDefaultDepsConfig() *DepsConfig { &DevnetConfig{ DefaultDevnetDockerImage, DefaultDevnetPort, + DefaultBlockTime, + DefaultBlockToWaitForOnStartup, }, } } // Struct to represent the Node dependencies containers type DepsContainers struct { - containers []testcontainers.Container + postgresContainerIndex int + devnetContainerIndex int + containers []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) { + reader, err := logs(ctx, depContainers, depContainers.devnetContainerIndex) + 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) { + reader, err := logs(ctx, depContainers, depContainers.postgresContainerIndex) + if err != nil { + return nil, fmt.Errorf("Error retrieving logs from Postgres Container : %w", err) + } + return reader, nil +} + +func logs(ctx context.Context, depContainers *DepsContainers, + containerIndex int) (io.ReadCloser, error) { + if containerIndex == -1 { + return nil, fmt.Errorf("Container is not present") + } + return depContainers.containers[containerIndex].Logs(ctx) +} + +func (depContainers *DepsContainers) DevnetEndpoint(ctx context.Context, + protocol string) (string, error) { + + endpoint, err := endpoint(ctx, depContainers, depContainers.devnetContainerIndex, 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) { + + endpoint, err := endpoint(ctx, depContainers, depContainers.postgresContainerIndex, protocol) + if err != nil { + return "", fmt.Errorf("Error retrieving endpoint from Postgres Container : %w", err) + } + return endpoint, nil +} + +func endpoint(ctx context.Context, depContainers *DepsContainers, + containerIndex int, protocol string) (string, error) { + + if containerIndex == -1 { + return "", fmt.Errorf("Container is not present") + } + return depContainers.containers[containerIndex].Endpoint(ctx, protocol) +} + // debugLogging implements the testcontainers.Logging interface by printing the log to slog.Debug. type debugLogging struct{} @@ -73,12 +137,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 +155,32 @@ 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 + debugLogger := debugLogging{} + var finishedWaitGroup sync.WaitGroup + containerIndex := 0 + postgresContainerIndex := -1 + devnetContainerIndex := -1 containers := []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 +193,26 @@ func Run(ctx context.Context, depsConfig DepsConfig) (*DepsContainers, error) { if err != nil { return nil, err } - waitGroup.Add(1) + finishedWaitGroup.Add(1) + postgresContainerIndex = containerIndex + containerIndex++ containers = append(containers, 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,29 @@ func Run(ctx context.Context, depsConfig DepsConfig) (*DepsContainers, error) { if err != nil { return nil, err } - waitGroup.Add(1) + finishedWaitGroup.Add(1) + devnetContainerIndex = containerIndex containers = append(containers, devnet) } + if len(containers) < 1 { return nil, fmt.Errorf("configuration is empty") } - return &DepsContainers{containers, &waitGroup}, nil + + return &DepsContainers{containers: containers, + waitGroup: &finishedWaitGroup, + postgresContainerIndex: postgresContainerIndex, + devnetContainerIndex: devnetContainerIndex}, 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..c13431bfd 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: "8000-9000", }, }) if err != nil { diff --git a/internal/services/command.go b/internal/services/command.go index ad33b3910..e54723783 100644 --- a/internal/services/command.go +++ b/internal/services/command.go @@ -40,9 +40,14 @@ type CommandService struct { Env []string } -func (s CommandService) Start(ctx context.Context, ready chan<- struct{}) error { +func (s CommandService) Start(ctx context.Context, ready chan<- struct{}, workDir string) error { cmd := exec.CommandContext(ctx, s.Path, s.Args...) cmd.Env = s.Env + + if workDir != "" { + cmd.Dir = workDir + } + cmd.Stderr = newLineWriter(commandLogger{s.Name}) cmd.Stdout = newLineWriter(commandLogger{s.Name}) cmd.Cancel = func() error { diff --git a/internal/services/command_test.go b/internal/services/command_test.go index e8e0c5eff..f144362e2 100644 --- a/internal/services/command_test.go +++ b/internal/services/command_test.go @@ -52,7 +52,7 @@ func (s *CommandServiceSuite) TestItStopsWhenContextIsCancelled() { result := make(chan error) ready := make(chan struct{}) go func() { - result <- service.Start(ctx, ready) + result <- service.Start(ctx, ready, "") }() // assert service started successfully @@ -82,7 +82,7 @@ func (s *CommandServiceSuite) TestItTimesOut() { result := make(chan error) ready := make(chan struct{}) go func() { - result <- service.Start(ctx, ready) + result <- service.Start(ctx, ready, "") }() // expect timeout because of wrong port @@ -106,7 +106,7 @@ func (s *CommandServiceSuite) TestItFailsToStartIfExecutableNotInPath() { defer cancel() ready := make(chan struct{}) - err := service.Start(ctx, ready) + err := service.Start(ctx, ready, "") s.ErrorIs(err, exec.ErrNotFound, "service exited for the wrong reason: %v", err) } diff --git a/internal/services/http.go b/internal/services/http.go index aac8d52ec..e7cea0ed8 100644 --- a/internal/services/http.go +++ b/internal/services/http.go @@ -21,7 +21,7 @@ func (s HttpService) String() string { return s.Name } -func (s HttpService) Start(ctx context.Context, ready chan<- struct{}) error { +func (s HttpService) Start(ctx context.Context, ready chan<- struct{}, workDir string) error { server := http.Server{ Addr: s.Address, Handler: s.Handler, diff --git a/internal/services/http_test.go b/internal/services/http_test.go index b8ef59fea..93bc42dc2 100644 --- a/internal/services/http_test.go +++ b/internal/services/http_test.go @@ -42,7 +42,7 @@ func (s *HttpServiceSuite) TestItStopsWhenContextIsClosed() { result := make(chan error, 1) ready := make(chan struct{}, 1) go func() { - result <- service.Start(ctx, ready) + result <- service.Start(ctx, ready, "") }() select { @@ -71,7 +71,7 @@ func (s *HttpServiceSuite) TestItRespondsToRequests() { result := make(chan error, 1) ready := make(chan struct{}, 1) go func() { - result <- service.Start(ctx, ready) + result <- service.Start(ctx, ready, "") }() select { @@ -103,7 +103,7 @@ func (s *HttpServiceSuite) TestItRespondsOngoingRequestsAfterContextIsClosed() { result := make(chan error, 1) ready := make(chan struct{}, 1) go func() { - result <- service.Start(ctx, ready) + result <- service.Start(ctx, ready, "") }() select { diff --git a/internal/services/server-manager.go b/internal/services/server-manager.go index cb2719d0c..7220dbf59 100644 --- a/internal/services/server-manager.go +++ b/internal/services/server-manager.go @@ -41,9 +41,12 @@ type ServerManager struct { const waitDelay = 200 * time.Millisecond -func (s ServerManager) Start(ctx context.Context, ready chan<- struct{}) error { +func (s ServerManager) Start(ctx context.Context, ready chan<- struct{}, workDir string) error { cmd := exec.CommandContext(ctx, s.Path, s.Args...) cmd.Env = s.Env + if workDir != "" { + cmd.Dir = workDir + } if s.BypassLog { cmd.Stderr = os.Stderr cmd.Stdout = os.Stdout diff --git a/internal/services/service.go b/internal/services/service.go index caf9083eb..e06dbd877 100644 --- a/internal/services/service.go +++ b/internal/services/service.go @@ -14,5 +14,5 @@ type Service interface { fmt.Stringer // Starts a service and sends a message to the channel when ready - Start(ctx context.Context, ready chan<- struct{}) error + Start(ctx context.Context, ready chan<- struct{}, workDir string) error } diff --git a/internal/services/supervisor.go b/internal/services/supervisor.go index 81c54fe81..809a7f356 100644 --- a/internal/services/supervisor.go +++ b/internal/services/supervisor.go @@ -41,7 +41,7 @@ func (s SupervisorService) String() string { return s.Name } -func (s SupervisorService) Start(ctx context.Context, ready chan<- struct{}) error { +func (s SupervisorService) Start(ctx context.Context, ready chan<- struct{}, workDir string) error { ctx, cancel := context.WithCancel(ctx) defer cancel() group, ctx := errgroup.WithContext(ctx) @@ -64,7 +64,7 @@ Loop: serviceReady := make(chan struct{}, 1) group.Go(func() error { - err := service.Start(ctx, serviceReady) + err := service.Start(ctx, serviceReady, workDir) if err != nil && !errors.Is(err, context.Canceled) { slog.Error("Service exited with error", "service", service, diff --git a/internal/services/supervisor_test.go b/internal/services/supervisor_test.go index 87defb930..51d986a7f 100644 --- a/internal/services/supervisor_test.go +++ b/internal/services/supervisor_test.go @@ -52,7 +52,7 @@ func (s *SupervisorServiceSuite) TestItIsReadyAfterStartingAllServices() { ready := make(chan struct{}) go func() { - _ = supervisor.Start(ctx, ready) + _ = supervisor.Start(ctx, ready, "") }() select { @@ -97,7 +97,7 @@ func (s *SupervisorServiceSuite) TestItStopsAllServicesWhenContextIsCanceled() { result := make(chan error) ready := make(chan struct{}) go func() { - result <- supervisor.Start(ctx, ready) + result <- supervisor.Start(ctx, ready, "") }() select { @@ -153,7 +153,7 @@ func (s *SupervisorServiceSuite) TestItStopsAllServicesIfAServiceStops() { result := make(chan error, 1) ready := make(chan struct{}, 1) go func() { - result <- supervisor.Start(ctx, ready) + result <- supervisor.Start(ctx, ready, "") }() select { @@ -199,7 +199,7 @@ func (s *SupervisorServiceSuite) TestItStopsCreatingServicesIfAServiceFailsToSta result := make(chan error, 1) ready := make(chan struct{}, 1) go func() { - result <- supervisor.Start(ctx, ready) + result <- supervisor.Start(ctx, ready, "") }() select { @@ -244,7 +244,7 @@ func (s *SupervisorServiceSuite) TestItStopsCreatingServicesIfContextIsCanceled( result := make(chan error) ready := make(chan struct{}, 1) go func() { - result <- supervisor.Start(ctx, ready) + result <- supervisor.Start(ctx, ready, "") }() <-time.After(300 * time.Millisecond) @@ -287,7 +287,7 @@ func (s *SupervisorServiceSuite) TestItTimesOutIfServiceTakesTooLongToBeReady() result := make(chan error) ready := make(chan struct{}, 1) go func() { - result <- supervisor.Start(ctx, ready) + result <- supervisor.Start(ctx, ready, "") }() select { @@ -321,7 +321,7 @@ func (s *SupervisorServiceSuite) TestItTimesOutIfServicesTakeTooLongToStop() { result := make(chan error) ready := make(chan struct{}, 1) go func() { - result <- supervisor.Start(ctx, ready) + result <- supervisor.Start(ctx, ready, "") }() <-ready @@ -339,7 +339,7 @@ type MockService struct { ReadyDelay time.Duration } -func (m *MockService) Start(ctx context.Context, ready chan<- struct{}) error { +func (m *MockService) Start(ctx context.Context, ready chan<- struct{}, workdir string) error { if m.ReadyDelay >= 0 { go func() { <-time.After(m.ReadyDelay) 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..f370911ea 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,31 @@ func AddInput( return getInputIndex(ctx, client, book, inputBox, receipt) } +// Convenience function to add an input using Foundry Mneumonic +// This function waits until the transaction is added to a block and return the input index. +func AddInputUsingFoundryMneumonic(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 +186,26 @@ 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..3cd9f9e91 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: "8000-9000", + }, + }) + 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..6afd57695 --- /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": "0x903814fc514f1b10a57bfe717be4db4692f2d061b96cb06c837e87f2f09c6b85", + "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": "0x903814fc514f1b10a57bfe717be4db4692f2d061b96cb06c837e87f2f09c6b85", + "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..136d91af8 --- /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: "5000-6000", + Password: deps.DefaultPostgresPassword, + }, + &deps.DevnetConfig{ + DockerImage: deps.DefaultDevnetDockerImage, + Port: "8000-9000", + 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) + s.Require().Nil(err) + + ready := make(chan struct{}, 1) + supervisorErr := make(chan error, 1) + go func() { + err := supervisor.Start(ctx, ready, tempDir) + 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.AddInputUsingFoundryMneumonic(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)) +}