diff --git a/internal/validator/validator_test.go b/internal/validator/validator_test.go index f0c115520..d27df9d3d 100644 --- a/internal/validator/validator_test.go +++ b/internal/validator/validator_test.go @@ -47,133 +47,6 @@ func (s *ValidatorSuite) TearDownSubTest() { validator = nil } -func (s *ValidatorSuite) TestItCreatesClaimAndProofs() { - // returns pristine claim and no proofs - s.Run("WhenThereAreNoOutputsAndNoPreviousEpoch", func() { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - epoch := dummyEpochs[0] - - repository.On( - "GetOutputsProducedInBlockRange", - mock.Anything, epoch.AppAddress, epoch.FirstBlock, epoch.LastBlock, - ).Return(nil, nil) - repository.On("GetPreviousEpoch", mock.Anything, epoch).Return(nil, nil) - - claim, outputs, err := validator.createClaimAndProofs(ctx, epoch) - s.Require().Nil(err) - s.Require().NotNil(claim) - - expectedClaim, _, err := merkle.CreateProofs(nil, MAX_OUTPUT_TREE_HEIGHT) - s.Require().Nil(err) - s.Require().NotNil(expectedClaim) - - s.Equal(expectedClaim, *claim) - s.Nil(outputs) - repository.AssertExpectations(s.T()) - }) - - // returns previous epoch claim and no proofs - s.Run("WhenThereAreNoOutputsAndThereIsAPreviousEpoch", func() { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - previousEpoch := dummyEpochs[0] - expectedClaim := randomHash() - previousEpoch.ClaimHash = &expectedClaim - epoch := dummyEpochs[1] - - repository.On( - "GetOutputsProducedInBlockRange", - mock.Anything, epoch.AppAddress, epoch.FirstBlock, epoch.LastBlock, - ).Return(nil, nil) - repository.On("GetPreviousEpoch", mock.Anything, epoch).Return(&previousEpoch, nil) - - claim, outputs, err := validator.createClaimAndProofs(ctx, epoch) - s.Require().Nil(err) - s.Require().NotNil(claim) - - s.Equal(expectedClaim, *claim) - s.Nil(outputs) - repository.AssertExpectations(s.T()) - }) - - // returns new claim and proofs - s.Run("WhenThereAreOutputsAndNoPreviousEpoch", func() { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - epoch := dummyEpochs[0] - outputs := randomOutputs(2, 0, false) - - repository.On( - "GetOutputsProducedInBlockRange", - mock.Anything, epoch.AppAddress, epoch.FirstBlock, epoch.LastBlock, - ).Return(outputs, nil).Once() - repository.On("GetPreviousEpoch", mock.Anything, epoch).Return(nil, nil) - - claim, updatedOutputs, err := validator.createClaimAndProofs(ctx, epoch) - s.Require().Nil(err) - s.Require().NotNil(claim) - - s.Len(updatedOutputs, len(outputs)) - for idx, output := range updatedOutputs { - s.Equal(outputs[idx].Id, output.Id) - s.NotNil(output.Hash) - s.NotNil(output.OutputHashesSiblings) - } - repository.AssertExpectations(s.T()) - }) - - // returns new claim and proofs - s.Run("WhenThereAreOutputsAndAPreviousEpoch", func() { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - previousEpoch := dummyEpochs[0] - previousEpochClaim := randomHash() - previousEpoch.ClaimHash = &previousEpochClaim - epoch := dummyEpochs[1] - previousOutputs := randomOutputs(2, 0, true) - epochOutputs := randomOutputs(2, 2, false) - - repository.On( - "GetOutputsProducedInBlockRange", - mock.Anything, epoch.AppAddress, epoch.FirstBlock, epoch.LastBlock, - ).Return(epochOutputs, nil) - repository.On( - "GetOutputsProducedInBlockRange", - mock.Anything, epoch.AppAddress, inputBoxDeploymentBlock, previousEpoch.LastBlock, - ).Return(previousOutputs, nil) - repository.On("GetPreviousEpoch", mock.Anything, epoch).Return(&previousEpoch, nil) - - claim, updatedOutputs, err := validator.createClaimAndProofs(ctx, epoch) - s.Require().Nil(err) - s.Require().NotNil(claim) - - allOutputs := append(previousOutputs, epochOutputs...) - leaves := make([]Hash, 0, len(allOutputs)) - for _, output := range allOutputs { - leaves = append(leaves, *output.Hash) - } - expectedClaim, allProofs, err := merkle.CreateProofs(leaves, MAX_OUTPUT_TREE_HEIGHT) - s.Require().Nil(err) - - s.NotEqual(previousEpoch.ClaimHash, claim) - s.Equal(&expectedClaim, claim) - s.Len(updatedOutputs, len(epochOutputs)) - - for idx, output := range updatedOutputs { - s.Equal(epochOutputs[idx].Index, output.Index) - s.NotNil(output.Hash) - s.NotNil(output.OutputHashesSiblings) - s.assertProofs(output, allProofs) - } - repository.AssertExpectations(s.T()) - }) -} - func (s *ValidatorSuite) TestItFailsWhenClaimDoesNotMatchMachineOutputsHash() { s.Run("OneAppSingleEpoch", func() { ctx, cancel := context.WithCancel(context.Background()) diff --git a/test/validator/validator_test.go b/test/validator/validator_test.go new file mode 100644 index 000000000..bbf182635 --- /dev/null +++ b/test/validator/validator_test.go @@ -0,0 +1,429 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package validator + +import ( + "context" + "fmt" + "net/url" + "testing" + "time" + + "github.com/cartesi/rollups-node/internal/deps" + "github.com/cartesi/rollups-node/internal/merkle" + "github.com/cartesi/rollups-node/internal/node/model" + "github.com/cartesi/rollups-node/internal/repository" + "github.com/cartesi/rollups-node/internal/repository/schema" + "github.com/cartesi/rollups-node/internal/validator" + "github.com/cartesi/rollups-node/pkg/testutil" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/suite" +) + +const testTimeout = 300 * time.Second + +type ValidatorRepositoryIntegrationSuite struct { + suite.Suite + ctx context.Context + cancel context.CancelFunc + containers *deps.DepsContainers + validator *validator.Validator + database *repository.Database + databaseURL *url.URL + schema *schema.Schema +} + +func TestValidatorRepositoryIntegration(t *testing.T) { + suite.Run(t, new(ValidatorRepositoryIntegrationSuite)) +} + +func (s *ValidatorRepositoryIntegrationSuite) SetupSuite() { + s.ctx, s.cancel = context.WithTimeout(context.Background(), testTimeout) + + var depsConfig = deps.DepsConfig{ + Postgres: &deps.PostgresConfig{ + DockerImage: deps.DefaultPostgresDockerImage, + Port: testutil.GetCartesiTestDepsPortRange(), + Password: deps.DefaultPostgresPassword, + }, + } + + var err error + s.containers, err = deps.Run(s.ctx, depsConfig) + s.Require().Nil(err) + + // build database URL + postgresEndpoint, err := s.containers.PostgresEndpoint(s.ctx, "postgres") + s.Require().Nil(err) + s.databaseURL, err = url.Parse(postgresEndpoint) + s.Require().Nil(err) + s.databaseURL.User = url.UserPassword(deps.DefaultPostgresUser, deps.DefaultPostgresPassword) + s.databaseURL = s.databaseURL.JoinPath(deps.DefaultPostgresDatabase) +} + +func (s *ValidatorRepositoryIntegrationSuite) SetupSubTest() { + var err error + s.database, err = repository.Connect(s.ctx, s.databaseURL.String()) + s.Require().Nil(err) + + s.schema, err = schema.New( + fmt.Sprintf("%v?sslmode=disable", s.databaseURL.String()), + ) + s.Require().Nil(err) + + s.validator = validator.NewValidator(s.database, 0) + + err = s.schema.Upgrade() + s.Require().Nil(err) +} + +func (s *ValidatorRepositoryIntegrationSuite) TearDownSubTest() { + s.validator = nil + + err := s.schema.Downgrade() + s.Require().Nil(err) + s.schema.Close() + + s.database.Close() +} + +func (s *ValidatorRepositoryIntegrationSuite) TearDownSuite() { + s.cancel() + + err := deps.Terminate(context.Background(), s.containers) + s.Require().Nil(err) +} + +func (s *ValidatorRepositoryIntegrationSuite) TestItReturnsPristineClaim() { + s.Run("WhenThereAreNoOutputsAndNoPreviousEpoch", func() { + app := &model.Application{ + ContractAddress: common.BytesToAddress([]byte("deadbeef")), + Status: model.ApplicationStatusRunning, + } + err := s.database.InsertApplication(s.ctx, app) + s.Require().Nil(err) + + epoch := &model.Epoch{ + AppAddress: app.ContractAddress, + Status: model.EpochStatusProcessedAllInputs, + FirstBlock: 0, + LastBlock: 9, + } + epoch.Id, err = s.database.InsertEpoch(s.ctx, epoch) + s.Require().Nil(err) + + // if there are no outputs and no previous claim, + // a pristine claim is expected with no proofs + expectedClaim, _, err := merkle.CreateProofs(nil, validator.MAX_OUTPUT_TREE_HEIGHT) + s.Require().Nil(err) + + input := &model.Input{ + AppAddress: app.ContractAddress, + EpochId: epoch.Id, + BlockNumber: 9, + RawData: []byte("data"), + OutputsHash: &expectedClaim, + CompletionStatus: model.InputStatusAccepted, + } + input.Id, err = s.database.InsertInput(s.ctx, input) + s.Require().Nil(err) + + err = s.validator.Run(s.ctx) + s.Require().Nil(err) + + updatedEpoch, err := s.database.GetEpoch(s.ctx, epoch.Index, epoch.AppAddress) + s.Require().Nil(err) + s.Require().NotNil(updatedEpoch) + s.Require().NotNil(updatedEpoch.ClaimHash) + + // epoch status was updated + s.Equal(model.EpochStatusClaimComputed, updatedEpoch.Status) + // claim is pristine claim + s.Equal(expectedClaim, *updatedEpoch.ClaimHash) + }) +} + +func (s *ValidatorRepositoryIntegrationSuite) TestItReturnsPreviousClaim() { + s.Run("WhenThereAreNoOutputsAndThereIsAPreviousEpoch", func() { + app := &model.Application{ + ContractAddress: common.BytesToAddress([]byte("deadbeef")), + Status: model.ApplicationStatusRunning, + } + err := s.database.InsertApplication(s.ctx, app) + s.Require().Nil(err) + + // insert the first epoch with a claim + firstEpochClaim := common.BytesToHash([]byte("claim")) + firstEpoch := &model.Epoch{ + AppAddress: app.ContractAddress, + Status: model.EpochStatusClaimComputed, + ClaimHash: &firstEpochClaim, + FirstBlock: 0, + LastBlock: 9, + } + firstEpoch.Id, err = s.database.InsertEpoch(s.ctx, firstEpoch) + s.Require().Nil(err) + + // we add an input to the epoch because they must have at least one and + // because without it the claim hash check will fail + firstEpochInput := &model.Input{ + AppAddress: app.ContractAddress, + EpochId: firstEpoch.Id, + BlockNumber: 9, + RawData: []byte("data"), + OutputsHash: &firstEpochClaim, + CompletionStatus: model.InputStatusAccepted, + } + firstEpochInput.Id, err = s.database.InsertInput(s.ctx, firstEpochInput) + s.Require().Nil(err) + + // create the second epoch with no outputs + secondEpoch := &model.Epoch{ + Index: 1, + AppAddress: app.ContractAddress, + Status: model.EpochStatusProcessedAllInputs, + FirstBlock: 10, + LastBlock: 19, + } + secondEpoch.Id, err = s.database.InsertEpoch(s.ctx, secondEpoch) + s.Require().Nil(err) + + secondEpochInput := &model.Input{ + Index: 1, + AppAddress: app.ContractAddress, + EpochId: secondEpoch.Id, + BlockNumber: 19, + RawData: []byte("data2"), + // since there are no new outputs in the second epoch, + // the machine OutputsHash will remain the same + OutputsHash: &firstEpochClaim, + CompletionStatus: model.InputStatusAccepted, + } + secondEpochInput.Id, err = s.database.InsertInput(s.ctx, secondEpochInput) + s.Require().Nil(err) + + err = s.validator.Run(s.ctx) + s.Require().Nil(err) + + updatedEpoch, err := s.database.GetEpoch(s.ctx, secondEpoch.Index, secondEpoch.AppAddress) + s.Require().Nil(err) + s.Require().NotNil(updatedEpoch) + s.Require().NotNil(updatedEpoch.ClaimHash) + + // epoch status was updated + s.Equal(model.EpochStatusClaimComputed, updatedEpoch.Status) + // claim is the same from previous epoch + s.Equal(firstEpochClaim, *updatedEpoch.ClaimHash) + }) +} + +func (s *ValidatorRepositoryIntegrationSuite) TestItReturnsANewClaimAndProofs() { + s.Run("WhenThereAreOutputsAndNoPreviousEpoch", func() { + app := &model.Application{ + ContractAddress: common.BytesToAddress([]byte("deadbeef")), + Status: model.ApplicationStatusRunning, + } + err := s.database.InsertApplication(s.ctx, app) + s.Require().Nil(err) + + epoch := &model.Epoch{ + AppAddress: app.ContractAddress, + Status: model.EpochStatusProcessedAllInputs, + FirstBlock: 0, + LastBlock: 9, + } + epoch.Id, err = s.database.InsertEpoch(s.ctx, epoch) + s.Require().Nil(err) + + input := &model.Input{ + AppAddress: app.ContractAddress, + EpochId: epoch.Id, + BlockNumber: 9, + RawData: []byte("data"), + CompletionStatus: model.InputStatusAccepted, + } + + outputRawData := []byte("output") + output := model.Output{RawData: outputRawData} + + // calculate the expected claim and proofs + expectedOutputHash := crypto.Keccak256Hash(outputRawData) + expectedClaim, expectedProofs, err := merkle.CreateProofs( + []model.Hash{expectedOutputHash}, + validator.MAX_OUTPUT_TREE_HEIGHT, + ) + s.Require().Nil(err) + s.Require().NotNil(expectedClaim) + s.Require().NotNil(expectedProofs) + + // update the input with its OutputsHash and insert it in the db + input.OutputsHash = &expectedClaim + input.Id, err = s.database.InsertInput(s.ctx, input) + s.Require().Nil(err) + + // update the output with its input id and insert it in the db + output.InputId = input.Id + output.Id, err = s.database.InsertOutput(s.ctx, &output) + s.Require().Nil(err) + + err = s.validator.Run(s.ctx) + s.Require().Nil(err) + + updatedEpoch, err := s.database.GetEpoch(s.ctx, epoch.Index, epoch.AppAddress) + s.Require().Nil(err) + s.Require().NotNil(updatedEpoch) + s.Require().NotNil(updatedEpoch.ClaimHash) + + // epoch status was updated + s.Equal(model.EpochStatusClaimComputed, updatedEpoch.Status) + // claim is the expected new claim + s.Equal(expectedClaim, *updatedEpoch.ClaimHash) + + updatedOutput, err := s.database.GetOutput(s.ctx, output.Index, app.ContractAddress) + s.Require().Nil(err) + s.Require().NotNil(updatedOutput) + s.Require().NotNil(updatedOutput.Hash) + + // output was updated with its hash + s.Equal(expectedOutputHash, *updatedOutput.Hash) + // output has proof + s.Len(updatedOutput.OutputHashesSiblings, validator.MAX_OUTPUT_TREE_HEIGHT) + }) + + s.Run("WhenThereAreOutputsAndAPreviousEpoch", func() { + app := &model.Application{ + ContractAddress: common.BytesToAddress([]byte("deadbeef")), + Status: model.ApplicationStatusRunning, + } + err := s.database.InsertApplication(s.ctx, app) + s.Require().Nil(err) + + firstEpoch := &model.Epoch{ + Index: 0, + AppAddress: app.ContractAddress, + Status: model.EpochStatusClaimComputed, + FirstBlock: 0, + LastBlock: 9, + } + + firstInput := &model.Input{ + AppAddress: app.ContractAddress, + EpochId: firstEpoch.Id, + BlockNumber: 9, + RawData: []byte("data"), + CompletionStatus: model.InputStatusAccepted, + } + + firstOutputData := []byte("output1") + firstOutputHash := crypto.Keccak256Hash(firstOutputData) + firstOutput := model.Output{ + RawData: firstOutputData, + Hash: &firstOutputHash, + } + + // calculate first epoch claim + firstEpochClaim, firstEpochProofs, err := merkle.CreateProofs( + []model.Hash{firstOutputHash}, + validator.MAX_OUTPUT_TREE_HEIGHT, + ) + s.Require().Nil(err) + s.Require().NotNil(firstEpochClaim) + + // update epoch with its claim and insert it in the db + firstEpoch.ClaimHash = &firstEpochClaim + firstEpoch.Id, err = s.database.InsertEpoch(s.ctx, firstEpoch) + s.Require().Nil(err) + + // update input with its epoch id and OuputsHash and insert it in the db + firstInput.EpochId = firstEpoch.Id + firstInput.OutputsHash = &firstEpochClaim + firstInput.Id, err = s.database.InsertInput(s.ctx, firstInput) + s.Require().Nil(err) + + // update output with its input id and insert it in the database + firstOutput.InputId = firstInput.Id + firstOutput.OutputHashesSiblings = firstEpochProofs + firstOutput.Id, err = s.database.InsertOutput(s.ctx, &firstOutput) + s.Require().Nil(err) + + // setup second epoch + secondEpoch := &model.Epoch{ + Index: 1, + AppAddress: app.ContractAddress, + Status: model.EpochStatusProcessedAllInputs, + FirstBlock: 10, + LastBlock: 19, + } + secondEpoch.Id, err = s.database.InsertEpoch(s.ctx, secondEpoch) + s.Require().Nil(err) + + secondInput := &model.Input{ + Index: 1, + AppAddress: app.ContractAddress, + EpochId: secondEpoch.Id, + BlockNumber: 19, + RawData: []byte("data2"), + CompletionStatus: model.InputStatusAccepted, + } + + secondOutputData := []byte("output2") + secondOutput := model.Output{ + Index: 1, + RawData: secondOutputData, + } + + // calculate the expected claim + secondOutputHash := crypto.Keccak256Hash(secondOutputData) + expectedEpochClaim, expectedProofs, err := merkle.CreateProofs( + []model.Hash{firstOutputHash, secondOutputHash}, + validator.MAX_OUTPUT_TREE_HEIGHT, + ) + s.Require().Nil(err) + s.Require().NotNil(expectedEpochClaim) + s.Require().NotNil(expectedProofs) + + // update second input with its OutputsHash and insert it in the db + secondInput.OutputsHash = &expectedEpochClaim + secondInput.Id, err = s.database.InsertInput(s.ctx, secondInput) + s.Require().Nil(err) + + // update second output with its input id and insert it in the database + secondOutput.InputId = secondInput.Id + secondOutput.Id, err = s.database.InsertOutput(s.ctx, &secondOutput) + s.Require().Nil(err) + + err = s.validator.Run(s.ctx) + s.Require().Nil(err) + + updatedSecondEpoch, err := s.database.GetEpoch( + s.ctx, + secondEpoch.Index, + secondEpoch.AppAddress, + ) + s.Require().Nil(err) + s.Require().NotNil(updatedSecondEpoch) + s.Require().NotNil(updatedSecondEpoch.ClaimHash) + + // assert epoch status was changed + s.Equal(model.EpochStatusClaimComputed, updatedSecondEpoch.Status) + // assert second epoch claim is a new claim + s.NotEqual(firstEpochClaim, *updatedSecondEpoch.ClaimHash) + s.Equal(expectedEpochClaim, *updatedSecondEpoch.ClaimHash) + + updatedSecondOutput, err := s.database.GetOutput( + s.ctx, + secondOutput.Index, + app.ContractAddress, + ) + s.Require().Nil(err) + s.Require().NotNil(updatedSecondOutput) + s.Require().NotNil(updatedSecondOutput.Hash) + + // assert output hash was updated + s.Equal(secondOutputHash, *updatedSecondOutput.Hash) + // assert output has proof + s.Len(updatedSecondOutput.OutputHashesSiblings, validator.MAX_OUTPUT_TREE_HEIGHT) + }) +}