From e93e1b234e0ee14ea6bd1429099992dcf47bd33e Mon Sep 17 00:00:00 2001 From: Joan Esteban <129153821+joanestebanr@users.noreply.github.com> Date: Mon, 25 Nov 2024 10:55:59 +0100 Subject: [PATCH] feat: check agglayer certificate and use as initial if db is empty (#192) - integration `interop_getLatestKnownCertificateHeader` end-point (aggsender and e2e tests) - Check agglayer and aggsender are on the same page - Fix wrong DBPath on default config - Fix colors on script `local_config` - Changes on config file: new fields `aggsender.MaxRetriesStoreCertificate` and `aggsender.DelayBeetweenRetries` - Partial solution to bug CDK-603 on PreviousLER (just fix initial case that first cert fails) --------- Co-authored-by: Goran Rojovic --- .github/workflows/test-e2e.yml | 1 + agglayer/client.go | 22 ++ agglayer/client_test.go | 71 +++- agglayer/mock_agglayer_client.go | 58 +++ agglayer/types.go | 39 +- aggsender/aggsender.go | 221 ++++++++++-- aggsender/aggsender_test.go | 340 ++++++++++++++++-- aggsender/block_notifier_polling_test.go | 7 +- aggsender/config.go | 8 +- aggsender/db/aggsender_db_storage.go | 28 +- aggsender/db/aggsender_db_storage_test.go | 28 +- aggsender/mocks/agg_sender_storage.go | 54 +-- aggsender/types/types.go | 50 ++- config/default.go | 10 +- scripts/local_config | 2 +- test/bats/pp/e2e-pp.bats | 21 +- test/combinations/fork12-pessimistic.yml | 5 +- test/run-e2e.sh | 9 +- test/scripts/agglayer_certificates_monitor.sh | 73 ++-- 19 files changed, 864 insertions(+), 183 deletions(-) diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index abde0c6b..9f024498 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -82,6 +82,7 @@ jobs: env: KURTOSIS_FOLDER: ${{ github.workspace }}/kurtosis-cdk BATS_LIB_PATH: /usr/lib/ + agglayer_prover_sp1_key: ${{ secrets.SP1_PRIVATE_KEY }} - name: Dump enclave logs if: failure() diff --git a/agglayer/client.go b/agglayer/client.go index 8396fc9e..8a186be4 100644 --- a/agglayer/client.go +++ b/agglayer/client.go @@ -30,6 +30,7 @@ type AgglayerClientInterface interface { WaitTxToBeMined(hash common.Hash, ctx context.Context) error SendCertificate(certificate *SignedCertificate) (common.Hash, error) GetCertificateHeader(certificateHash common.Hash) (*CertificateHeader, error) + GetLatestKnownCertificateHeader(networkID uint32) (*CertificateHeader, error) AggLayerClientGetEpochConfiguration } @@ -158,3 +159,24 @@ func (c *AggLayerClient) GetEpochConfiguration() (*ClockConfiguration, error) { return result, nil } + +// GetLatestKnownCertificateHeader returns the last certificate header submitted by networkID +func (c *AggLayerClient) GetLatestKnownCertificateHeader(networkID uint32) (*CertificateHeader, error) { + response, err := jSONRPCCall(c.url, "interop_getLatestKnownCertificateHeader", networkID) + if err != nil { + return nil, fmt.Errorf("GetLatestKnownCertificateHeader error jSONRPCCall. Err: %w", err) + } + + if response.Error != nil { + return nil, fmt.Errorf("GetLatestKnownCertificateHeader rpc returns an error: code=%d msg=%s", + response.Error.Code, response.Error.Message) + } + + var result *CertificateHeader + err = json.Unmarshal(response.Result, &result) + if err != nil { + return nil, fmt.Errorf("GetLatestKnownCertificateHeader error Unmashal. Err: %w", err) + } + + return result, nil +} diff --git a/agglayer/client_test.go b/agglayer/client_test.go index 82baea85..09bea14e 100644 --- a/agglayer/client_test.go +++ b/agglayer/client_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/0xPolygon/cdk-rpc/rpc" + "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" ) @@ -14,11 +15,40 @@ const ( func TestExploratoryClient(t *testing.T) { t.Skip("This test is for exploratory purposes only") - sut := NewAggLayerClient("http://127.0.0.1:32853") + sut := NewAggLayerClient("http://127.0.0.1:32781") config, err := sut.GetEpochConfiguration() require.NoError(t, err) require.NotNil(t, config) fmt.Printf("Config: %s", config.String()) + + lastCert, err := sut.GetLatestKnownCertificateHeader(1) + require.NoError(t, err) + require.NotNil(t, lastCert) + fmt.Printf("LastCert: %s", lastCert.String()) +} + +func TestExploratoryGetCertificateHeader(t *testing.T) { + t.Skip("This test is exploratory and should be skipped") + aggLayerClient := NewAggLayerClient("http://localhost:32796") + certificateID := common.HexToHash("0xf153e75e24591432ac5deafaeaafba3fec0fd851261c86051b9c0d540b38c369") + certificateHeader, err := aggLayerClient.GetCertificateHeader(certificateID) + require.NoError(t, err) + fmt.Print(certificateHeader) +} +func TestExploratoryGetEpochConfiguration(t *testing.T) { + t.Skip("This test is exploratory and should be skipped") + aggLayerClient := NewAggLayerClient("http://localhost:32796") + clockConfig, err := aggLayerClient.GetEpochConfiguration() + require.NoError(t, err) + fmt.Print(clockConfig) +} + +func TestExploratoryGetLatestKnownCertificateHeader(t *testing.T) { + t.Skip("This test is exploratory and should be skipped") + aggLayerClient := NewAggLayerClient("http://localhost:32781") + cert, err := aggLayerClient.GetLatestKnownCertificateHeader(1) + require.NoError(t, err) + fmt.Print(cert) } func TestGetEpochConfigurationResponseWithError(t *testing.T) { @@ -74,3 +104,42 @@ func TestGetEpochConfigurationOkResponse(t *testing.T) { GenesisBlock: 1, }, *clockConfig) } + +func TestGetLatestKnownCertificateHeaderOkResponse(t *testing.T) { + sut := NewAggLayerClient(testURL) + response := rpc.Response{ + Result: []byte(`{"network_id":1,"height":0,"epoch_number":223,"certificate_index":0,"certificate_id":"0xf9179d2fbe535814b5a14496e2eed474f49c6131227a9dfc5d2d8caf9e212054","new_local_exit_root":"0x7ae06f4a5d0b6da7dd4973fb6ef40d82c9f2680899b3baaf9e564413b59cc160","metadata":"0x00000000000000000000000000000000000000000000000000000000000001a7","status":"Settled"}`), + } + jSONRPCCall = func(url, method string, params ...interface{}) (rpc.Response, error) { + return response, nil + } + cert, err := sut.GetLatestKnownCertificateHeader(1) + require.NotNil(t, cert) + require.NoError(t, err) +} +func TestGetLatestKnownCertificateHeaderErrorResponse(t *testing.T) { + sut := NewAggLayerClient(testURL) + jSONRPCCall = func(url, method string, params ...interface{}) (rpc.Response, error) { + return rpc.Response{}, fmt.Errorf("unittest error") + } + + cert, err := sut.GetLatestKnownCertificateHeader(1) + + require.Nil(t, cert) + require.Error(t, err) +} + +func TestGetLatestKnownCertificateHeaderResponseBadJson(t *testing.T) { + sut := NewAggLayerClient(testURL) + response := rpc.Response{ + Result: []byte(`{`), + } + jSONRPCCall = func(url, method string, params ...interface{}) (rpc.Response, error) { + return response, nil + } + + cert, err := sut.GetLatestKnownCertificateHeader(1) + + require.Nil(t, cert) + require.Error(t, err) +} diff --git a/agglayer/mock_agglayer_client.go b/agglayer/mock_agglayer_client.go index b7f70ee8..6c5a3fbf 100644 --- a/agglayer/mock_agglayer_client.go +++ b/agglayer/mock_agglayer_client.go @@ -138,6 +138,64 @@ func (_c *AgglayerClientMock_GetEpochConfiguration_Call) RunAndReturn(run func() return _c } +// GetLatestKnownCertificateHeader provides a mock function with given fields: networkID +func (_m *AgglayerClientMock) GetLatestKnownCertificateHeader(networkID uint32) (*CertificateHeader, error) { + ret := _m.Called(networkID) + + if len(ret) == 0 { + panic("no return value specified for GetLatestKnownCertificateHeader") + } + + var r0 *CertificateHeader + var r1 error + if rf, ok := ret.Get(0).(func(uint32) (*CertificateHeader, error)); ok { + return rf(networkID) + } + if rf, ok := ret.Get(0).(func(uint32) *CertificateHeader); ok { + r0 = rf(networkID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*CertificateHeader) + } + } + + if rf, ok := ret.Get(1).(func(uint32) error); ok { + r1 = rf(networkID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// AgglayerClientMock_GetLatestKnownCertificateHeader_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetLatestKnownCertificateHeader' +type AgglayerClientMock_GetLatestKnownCertificateHeader_Call struct { + *mock.Call +} + +// GetLatestKnownCertificateHeader is a helper method to define mock.On call +// - networkID uint32 +func (_e *AgglayerClientMock_Expecter) GetLatestKnownCertificateHeader(networkID interface{}) *AgglayerClientMock_GetLatestKnownCertificateHeader_Call { + return &AgglayerClientMock_GetLatestKnownCertificateHeader_Call{Call: _e.mock.On("GetLatestKnownCertificateHeader", networkID)} +} + +func (_c *AgglayerClientMock_GetLatestKnownCertificateHeader_Call) Run(run func(networkID uint32)) *AgglayerClientMock_GetLatestKnownCertificateHeader_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(uint32)) + }) + return _c +} + +func (_c *AgglayerClientMock_GetLatestKnownCertificateHeader_Call) Return(_a0 *CertificateHeader, _a1 error) *AgglayerClientMock_GetLatestKnownCertificateHeader_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *AgglayerClientMock_GetLatestKnownCertificateHeader_Call) RunAndReturn(run func(uint32) (*CertificateHeader, error)) *AgglayerClientMock_GetLatestKnownCertificateHeader_Call { + _c.Call.Return(run) + return _c +} + // SendCertificate provides a mock function with given fields: certificate func (_m *AgglayerClientMock) SendCertificate(certificate *SignedCertificate) (common.Hash, error) { ret := _m.Called(certificate) diff --git a/agglayer/types.go b/agglayer/types.go index aece93f0..9a56d878 100644 --- a/agglayer/types.go +++ b/agglayer/types.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "math/big" + "slices" "strings" "github.com/0xPolygon/cdk/bridgesync" @@ -24,11 +25,36 @@ const ( Settled ) +var ( + NonSettledStatuses = []CertificateStatus{Pending, Candidate, Proven} + ClosedStatuses = []CertificateStatus{Settled, InError} +) + // String representation of the enum func (c CertificateStatus) String() string { return [...]string{"Pending", "Proven", "Candidate", "InError", "Settled"}[c] } +// IsClosed returns true if the certificate is closed (settled or inError) +func (c CertificateStatus) IsClosed() bool { + return !c.IsOpen() +} + +// IsSettled returns true if the certificate is settled +func (c CertificateStatus) IsSettled() bool { + return c == Settled +} + +// IsInError returns true if the certificate is in error +func (c CertificateStatus) IsInError() bool { + return c == InError +} + +// IsOpen returns true if the certificate is open (pending, candidate or proven) +func (c CertificateStatus) IsOpen() bool { + return slices.Contains(NonSettledStatuses, c) +} + // UnmarshalJSON is the implementation of the json.Unmarshaler interface func (c *CertificateStatus) UnmarshalJSON(data []byte) error { dataStr := string(data) @@ -550,7 +576,18 @@ type CertificateHeader struct { Error PPError `json:"-"` } -func (c CertificateHeader) String() string { +// ID returns a string with the ident of this cert (height/certID) +func (c *CertificateHeader) ID() string { + if c == nil { + return "nil" + } + return fmt.Sprintf("%d/%s", c.Height, c.CertificateID.String()) +} + +func (c *CertificateHeader) String() string { + if c == nil { + return "nil" + } errors := "" if c.Error != nil { errors = c.Error.String() diff --git a/aggsender/aggsender.go b/aggsender/aggsender.go index 08730572..39cd6867 100644 --- a/aggsender/aggsender.go +++ b/aggsender/aggsender.go @@ -8,7 +8,6 @@ import ( "fmt" "math/big" "os" - "slices" "time" "github.com/0xPolygon/cdk/agglayer" @@ -28,8 +27,7 @@ var ( errNoBridgesAndClaims = errors.New("no bridges and claims to build certificate") errInvalidSignatureSize = errors.New("invalid signature size") - zeroLER = common.HexToHash("0x27ae5ba08d7291c96c8cbddcc148bf48a6d68c7974b94356f53754ef6171d757") - nonSettledStatuses = []agglayer.CertificateStatus{agglayer.Pending, agglayer.Candidate, agglayer.Proven} + zeroLER = common.HexToHash("0x27ae5ba08d7291c96c8cbddcc148bf48a6d68c7974b94356f53754ef6171d757") ) // AggSender is a component that will send certificates to the aggLayer @@ -83,9 +81,31 @@ func New( // Start starts the AggSender func (a *AggSender) Start(ctx context.Context) { + a.log.Info("AggSender started") + a.checkInitialStatus(ctx) a.sendCertificates(ctx) } +// checkInitialStatus check local status vs agglayer status +func (a *AggSender) checkInitialStatus(ctx context.Context) { + ticker := time.NewTicker(a.cfg.DelayBeetweenRetries.Duration) + defer ticker.Stop() + + for { + if err := a.checkLastCertificateFromAgglayer(ctx); err != nil { + log.Errorf("error checking initial status: %w, retrying in %s", err, a.cfg.DelayBeetweenRetries.String()) + } else { + log.Info("Initial status checked successfully") + return + } + select { + case <-ctx.Done(): + return + case <-ticker.C: + } + } +} + // sendCertificates sends certificates to the aggLayer func (a *AggSender) sendCertificates(ctx context.Context) { chEpoch := a.epochNotifier.Subscribe("aggsender") @@ -132,6 +152,10 @@ func (a *AggSender) sendCertificate(ctx context.Context) (*agglayer.SignedCertif if err != nil { return nil, err } + if lastSentCertificateInfo == nil { + // There are no certificates, so we set that to a empty one + lastSentCertificateInfo = &types.CertificateInfo{} + } previousToBlock := lastSentCertificateInfo.ToBlock if lastSentCertificateInfo.Status == agglayer.InError { @@ -166,7 +190,7 @@ func (a *AggSender) sendCertificate(ctx context.Context) (*agglayer.SignedCertif a.log.Infof("building certificate for block: %d to block: %d", fromBlock, toBlock) - certificate, err := a.buildCertificate(ctx, bridges, claims, lastSentCertificateInfo, toBlock) + certificate, err := a.buildCertificate(ctx, bridges, claims, *lastSentCertificateInfo, toBlock) if err != nil { return nil, fmt.Errorf("error building certificate: %w", err) } @@ -202,8 +226,10 @@ func (a *AggSender) sendCertificate(ctx context.Context) (*agglayer.SignedCertif UpdatedAt: createdTime, SignedCertificate: string(raw), } - - if err := a.storage.SaveLastSentCertificate(ctx, certInfo); err != nil { + // TODO: Improve this case, if a cert is not save in the storage, we are going to settle a unknown certificate + err = a.saveCertificateToStorage(ctx, certInfo, a.cfg.MaxRetriesStoreCertificate) + if err != nil { + a.log.Errorf("error saving certificate to storage: %w", err) return nil, fmt.Errorf("error saving last sent certificate %s in db: %w", certInfo.String(), err) } @@ -213,6 +239,26 @@ func (a *AggSender) sendCertificate(ctx context.Context) (*agglayer.SignedCertif return signedCertificate, nil } +// saveCertificateToStorage saves the certificate to the storage +// it retries if it fails. if param retries == 0 it retries indefinitely +func (a *AggSender) saveCertificateToStorage(ctx context.Context, cert types.CertificateInfo, maxRetries int) error { + retries := 1 + err := fmt.Errorf("initial_error") + for err != nil { + if err = a.storage.SaveLastSentCertificate(ctx, cert); err != nil { + // If this happens we can't work as normal, because local DB is outdated, we have to retry + a.log.Errorf("error saving last sent certificate %s in db: %w", cert.String(), err) + if retries == maxRetries { + return fmt.Errorf("error saving last sent certificate %s in db: %w", cert.String(), err) + } else { + retries++ + time.Sleep(a.cfg.DelayBeetweenRetries.Duration) + } + } + } + return nil +} + // saveCertificate saves the certificate to a tmp file func (a *AggSender) saveCertificateToFile(signedCertificate *agglayer.SignedCertificate) { if signedCertificate == nil || a.cfg.SaveCertificatesToFilesPath == "" { @@ -235,7 +281,7 @@ func (a *AggSender) saveCertificateToFile(signedCertificate *agglayer.SignedCert func (a *AggSender) getNextHeightAndPreviousLER( lastSentCertificateInfo *types.CertificateInfo) (uint64, common.Hash) { height := lastSentCertificateInfo.Height + 1 - if lastSentCertificateInfo.Status == agglayer.InError { + if lastSentCertificateInfo.Status.IsInError() { // previous certificate was in error, so we need to resend it a.log.Debugf("Last certificate %s failed so reusing height %d", lastSentCertificateInfo.CertificateID, lastSentCertificateInfo.Height) @@ -243,7 +289,8 @@ func (a *AggSender) getNextHeightAndPreviousLER( } previousLER := lastSentCertificateInfo.NewLocalExitRoot - if lastSentCertificateInfo.NewLocalExitRoot == (common.Hash{}) { + if lastSentCertificateInfo.NewLocalExitRoot == (common.Hash{}) || + lastSentCertificateInfo.Height == 0 && lastSentCertificateInfo.Status.IsInError() { // meaning this is the first certificate height = 0 previousLER = zeroLER @@ -489,54 +536,72 @@ func (a *AggSender) signCertificate(certificate *agglayer.Certificate) (*agglaye // It returns: // bool -> if there are pending certificates func (a *AggSender) checkPendingCertificatesStatus(ctx context.Context) bool { - pendingCertificates, err := a.storage.GetCertificatesByStatus(nonSettledStatuses) + pendingCertificates, err := a.storage.GetCertificatesByStatus(agglayer.NonSettledStatuses) if err != nil { - err = fmt.Errorf("error getting pending certificates: %w", err) - a.log.Error(err) + a.log.Errorf("error getting pending certificates: %w", err) return true } - thereArePendingCerts := false + a.log.Debugf("checkPendingCertificatesStatus num of pendingCertificates: %d", len(pendingCertificates)) + thereArePendingCerts := false + for _, certificate := range pendingCertificates { certificateHeader, err := a.aggLayerClient.GetCertificateHeader(certificate.CertificateID) if err != nil { - err = fmt.Errorf("error getting certificate header of %d/%s from agglayer: %w", - certificate.Height, certificate.String(), err) - a.log.Error(err) + a.log.Errorf("error getting certificate header of %s from agglayer: %w", + certificate.ID(), err) return true } - elapsedTime := time.Now().UTC().Sub(time.UnixMilli(certificate.CreatedAt)) + a.log.Debugf("aggLayerClient.GetCertificateHeader status [%s] of certificate %s elapsed time:%s", certificateHeader.Status, - certificateHeader.String(), - elapsedTime) - - if certificateHeader.Status != certificate.Status { - a.log.Infof("certificate %s changed status from [%s] to [%s] elapsed time: %s", - certificateHeader.String(), certificate.Status, certificateHeader.Status, elapsedTime) + certificateHeader.ID(), + certificate.ElapsedTimeSinceCreation()) - certificate.Status = certificateHeader.Status - certificate.UpdatedAt = time.Now().UTC().UnixMilli() - - if err := a.storage.UpdateCertificateStatus(ctx, *certificate); err != nil { - err = fmt.Errorf("error updating certificate %s status in storage: %w", certificateHeader.String(), err) - a.log.Error(err) - return true - } + if err := a.updateCertificateStatus(ctx, certificate, certificateHeader); err != nil { + a.log.Errorf("error updating certificate %s status in storage: %w", certificateHeader.String(), err) + return true } - if slices.Contains(nonSettledStatuses, certificateHeader.Status) { + + if !certificate.IsClosed() { a.log.Infof("certificate %s is still pending, elapsed time:%s ", - certificateHeader.String(), elapsedTime) + certificateHeader.ID(), certificate.ElapsedTimeSinceCreation()) thereArePendingCerts = true } } return thereArePendingCerts } +// updateCertificate updates the certificate status in the storage +func (a *AggSender) updateCertificateStatus(ctx context.Context, + localCert *types.CertificateInfo, + agglayerCert *agglayer.CertificateHeader) error { + if localCert.Status == agglayerCert.Status { + return nil + } + a.log.Infof("certificate %s changed status from [%s] to [%s] elapsed time: %s full_cert: %s", + localCert.ID(), localCert.Status, agglayerCert.Status, localCert.ElapsedTimeSinceCreation(), + localCert.String()) + + // That is a strange situation + if agglayerCert.Status.IsOpen() == localCert.Status.IsClosed() { + a.log.Warnf("certificate %s is reopen! from [%s] to [%s]", + localCert.ID(), localCert.Status, agglayerCert.Status) + } + + localCert.Status = agglayerCert.Status + localCert.UpdatedAt = time.Now().UTC().UnixMilli() + if err := a.storage.UpdateCertificate(ctx, *localCert); err != nil { + a.log.Errorf("error updating certificate %s status in storage: %w", agglayerCert.ID(), err) + return fmt.Errorf("error updating certificate. Err: %w", err) + } + return nil +} + // shouldSendCertificate checks if a certificate should be sent at given time // if we have pending certificates, then we wait until they are settled func (a *AggSender) shouldSendCertificate() (bool, error) { - pendingCertificates, err := a.storage.GetCertificatesByStatus(nonSettledStatuses) + pendingCertificates, err := a.storage.GetCertificatesByStatus(agglayer.NonSettledStatuses) if err != nil { return false, fmt.Errorf("error getting pending certificates: %w", err) } @@ -544,6 +609,74 @@ func (a *AggSender) shouldSendCertificate() (bool, error) { return len(pendingCertificates) == 0, nil } +// checkLastCertificateFromAgglayer checks the last certificate from agglayer +func (a *AggSender) checkLastCertificateFromAgglayer(ctx context.Context) error { + networkID := a.l2Syncer.OriginNetwork() + a.log.Infof("recovery: checking last certificate from AggLayer for network %d", networkID) + aggLayerLastCert, err := a.aggLayerClient.GetLatestKnownCertificateHeader(networkID) + if err != nil { + return fmt.Errorf("recovery: error getting latest known certificate header from agglayer: %w", err) + } + a.log.Infof("recovery: last certificate from AggLayer: %s", aggLayerLastCert.String()) + localLastCert, err := a.storage.GetLastSentCertificate() + if err != nil { + return fmt.Errorf("recovery: error getting last sent certificate from local storage: %w", err) + } + a.log.Infof("recovery: last certificate in storage: %s", localLastCert.String()) + + // CASE 1: No certificates in local storage and agglayer + if localLastCert == nil && aggLayerLastCert == nil { + a.log.Info("recovery: No certificates in local storage and agglayer: initial state") + return nil + } + // CASE 2: No certificates in local storage but agglayer has one + if localLastCert == nil && aggLayerLastCert != nil { + a.log.Info("recovery: No certificates in local storage but agglayer have one: recovery aggSender cert: %s", + aggLayerLastCert.String()) + if _, err := a.updateLocalStorageWithAggLayerCert(ctx, aggLayerLastCert); err != nil { + return fmt.Errorf("recovery: error updating local storage with agglayer certificate: %w", err) + } + return nil + } + // CASE 3: aggsender stopped between sending to agglayer and storing on DB + if aggLayerLastCert.Height == localLastCert.Height+1 { + a.log.Infof("recovery: AggLayer have next cert (height:%d), so is a recovery case: storing cert: %s", + aggLayerLastCert.Height, localLastCert.String()) + // we need to store the certificate in the local storage. + localLastCert, err = a.updateLocalStorageWithAggLayerCert(ctx, aggLayerLastCert) + if err != nil { + log.Errorf("recovery: error updating status certificate: %s status: %w", aggLayerLastCert.String(), err) + return fmt.Errorf("recovery: error updating certificate status: %w", err) + } + } + // CASE 4: AggSender and AggLayer are not on the same page + // note: we don't need to check individual fields of the certificate + // because CertificateID is a hash of all the fields + if localLastCert.CertificateID != aggLayerLastCert.CertificateID { + a.log.Errorf("recovery: Local certificate:\n %s \n is different from agglayer certificate:\n %s", + localLastCert.String(), aggLayerLastCert.String()) + return fmt.Errorf("recovery: mismatch between local and agglayer certificates") + } + // CASE 5: AggSender and AggLayer are at same page + // just update status + err = a.updateCertificateStatus(ctx, localLastCert, aggLayerLastCert) + if err != nil { + a.log.Errorf("recovery: error updating status certificate: %s status: %w", aggLayerLastCert.String(), err) + return fmt.Errorf("recovery: error updating certificate status: %w", err) + } + + a.log.Infof("recovery: successfully checked last certificate from AggLayer for network %d", networkID) + return nil +} + +// updateLocalStorageWithAggLayerCert updates the local storage with the certificate from the AggLayer +func (a *AggSender) updateLocalStorageWithAggLayerCert(ctx context.Context, + aggLayerCert *agglayer.CertificateHeader) (*types.CertificateInfo, error) { + certInfo := NewCertificateInfoFromAgglayerCertHeader(aggLayerCert) + log.Infof("setting initial certificate from AggLayer: %s", certInfo.String()) + return certInfo, a.storage.SaveLastSentCertificate(ctx, *certInfo) +} + // extractSignatureData extracts the R, S, and V from a 65-byte signature func extractSignatureData(signature []byte) (r, s common.Hash, isOddParity bool, err error) { if len(signature) != signatureSize { @@ -562,3 +695,25 @@ func extractSignatureData(signature []byte) (r, s common.Hash, isOddParity bool, func createCertificateMetadata(toBlock uint64) common.Hash { return common.BigToHash(new(big.Int).SetUint64(toBlock)) } + +func extractFromCertificateMetadataToBlock(metadata common.Hash) uint64 { + return metadata.Big().Uint64() +} + +func NewCertificateInfoFromAgglayerCertHeader(c *agglayer.CertificateHeader) *types.CertificateInfo { + if c == nil { + return nil + } + now := time.Now().UTC().UnixMilli() + return &types.CertificateInfo{ + Height: c.Height, + CertificateID: c.CertificateID, + NewLocalExitRoot: c.NewLocalExitRoot, + FromBlock: 0, + ToBlock: extractFromCertificateMetadataToBlock(c.Metadata), + Status: c.Status, + CreatedAt: now, + UpdatedAt: now, + SignedCertificate: "na/agglayer header", + } +} diff --git a/aggsender/aggsender_test.go b/aggsender/aggsender_test.go index b9242bdf..6d84c683 100644 --- a/aggsender/aggsender_test.go +++ b/aggsender/aggsender_test.go @@ -8,10 +8,12 @@ import ( "fmt" "math/big" "os" + "runtime" "testing" "time" "github.com/0xPolygon/cdk/agglayer" + "github.com/0xPolygon/cdk/aggsender/db" "github.com/0xPolygon/cdk/aggsender/mocks" aggsendertypes "github.com/0xPolygon/cdk/aggsender/types" "github.com/0xPolygon/cdk/bridgesync" @@ -25,21 +27,13 @@ import ( "github.com/stretchr/testify/require" ) -func TestExploratoryGetCertificateHeader(t *testing.T) { - t.Skip("This test is exploratory and should be skipped") - aggLayerClient := agglayer.NewAggLayerClient("http://localhost:32796") - certificateID := common.HexToHash("0xf153e75e24591432ac5deafaeaafba3fec0fd851261c86051b9c0d540b38c369") - certificateHeader, err := aggLayerClient.GetCertificateHeader(certificateID) - require.NoError(t, err) - fmt.Print(certificateHeader) -} -func TestExploratoryGetEpochConfiguration(t *testing.T) { - t.Skip("This test is exploratory and should be skipped") - aggLayerClient := agglayer.NewAggLayerClient("http://localhost:32796") - clockConfig, err := aggLayerClient.GetEpochConfiguration() - require.NoError(t, err) - fmt.Print(clockConfig) -} +const ( + networkIDTest = uint32(1234) +) + +var ( + errTest = errors.New("unitest error") +) func TestConfigString(t *testing.T) { config := Config{ @@ -59,7 +53,6 @@ func TestConfigString(t *testing.T) { "BlockGetInterval: 10s\n" + "CheckSettledInterval: 20s\n" + "AggsenderPrivateKeyPath: /path/to/key\n" + - "AggsenderPrivateKeyPassword: password\n" + "URLRPCL2: http://l2.rpc.url\n" + "BlockFinality: latestBlock\n" + "EpochNotificationPercentage: 50\n" + @@ -281,7 +274,7 @@ func TestGetBridgeExits(t *testing.T) { } func TestAggSenderStart(t *testing.T) { - AggLayerMock := agglayer.NewAgglayerClientMock(t) + aggLayerMock := agglayer.NewAgglayerClientMock(t) epochNotifierMock := mocks.NewEpochNotifier(t) bridgeL2SyncerMock := mocks.NewL2BridgeSyncer(t) ctx, cancel := context.WithCancel(context.Background()) @@ -290,9 +283,10 @@ func TestAggSenderStart(t *testing.T) { ctx, log.WithFields("test", "unittest"), Config{ - StoragePath: "file::memory:?cache=shared", + StoragePath: "file::memory:?cache=shared", + DelayBeetweenRetries: types.Duration{Duration: 1 * time.Microsecond}, }, - AggLayerMock, + aggLayerMock, nil, bridgeL2SyncerMock, epochNotifierMock) @@ -300,7 +294,9 @@ func TestAggSenderStart(t *testing.T) { require.NotNil(t, aggSender) ch := make(chan aggsendertypes.EpochEvent) epochNotifierMock.EXPECT().Subscribe("aggsender").Return(ch) + bridgeL2SyncerMock.EXPECT().OriginNetwork().Return(uint32(1)) bridgeL2SyncerMock.EXPECT().GetLastProcessedBlock(mock.Anything).Return(uint64(0), nil) + aggLayerMock.EXPECT().GetLatestKnownCertificateHeader(mock.Anything).Return(nil, nil) go aggSender.Start(ctx) ch <- aggsendertypes.EpochEvent{ @@ -902,15 +898,15 @@ func TestCheckIfCertificatesAreSettled(t *testing.T) { mockAggLayerClient := agglayer.NewAgglayerClientMock(t) mockLogger := log.WithFields("test", "unittest") - mockStorage.On("GetCertificatesByStatus", nonSettledStatuses).Return( + mockStorage.On("GetCertificatesByStatus", agglayer.NonSettledStatuses).Return( tt.pendingCertificates, tt.getFromDBError) for certID, header := range tt.certificateHeaders { mockAggLayerClient.On("GetCertificateHeader", certID).Return(header, tt.clientError) } if tt.updateDBError != nil { - mockStorage.On("UpdateCertificateStatus", mock.Anything, mock.Anything).Return(tt.updateDBError) + mockStorage.On("UpdateCertificate", mock.Anything, mock.Anything).Return(tt.updateDBError) } else if tt.clientError == nil && tt.getFromDBError == nil { - mockStorage.On("UpdateCertificateStatus", mock.Anything, mock.Anything).Return(nil) + mockStorage.On("UpdateCertificate", mock.Anything, mock.Anything).Return(nil) } aggSender := &AggSender{ @@ -961,7 +957,7 @@ func TestSendCertificate(t *testing.T) { var ( aggsender = &AggSender{ log: log.WithFields("aggsender", 1), - cfg: Config{}, + cfg: Config{MaxRetriesStoreCertificate: 3}, sequencerKey: cfg.sequencerKey, } mockStorage *mocks.AggSenderStorage @@ -973,8 +969,8 @@ func TestSendCertificate(t *testing.T) { if cfg.shouldSendCertificate != nil || cfg.getLastSentCertificate != nil || cfg.saveLastSentCertificate != nil { mockStorage = mocks.NewAggSenderStorage(t) - mockStorage.On("GetCertificatesByStatus", nonSettledStatuses). - Return(cfg.shouldSendCertificate...).Once() + mockStorage.On("GetCertificatesByStatus", agglayer.NonSettledStatuses). + Return(cfg.shouldSendCertificate...) aggsender.storage = mockStorage @@ -983,7 +979,7 @@ func TestSendCertificate(t *testing.T) { } if cfg.saveLastSentCertificate != nil { - mockStorage.On("SaveLastSentCertificate", mock.Anything, mock.Anything).Return(cfg.saveLastSentCertificate...).Once() + mockStorage.On("SaveLastSentCertificate", mock.Anything, mock.Anything).Return(cfg.saveLastSentCertificate...) } } @@ -1055,14 +1051,14 @@ func TestSendCertificate(t *testing.T) { name: "error getting last sent certificate", shouldSendCertificate: []interface{}{[]*aggsendertypes.CertificateInfo{}, nil}, lastL2BlockProcessed: []interface{}{uint64(8), nil}, - getLastSentCertificate: []interface{}{aggsendertypes.CertificateInfo{}, errors.New("error getting last sent certificate")}, + getLastSentCertificate: []interface{}{&aggsendertypes.CertificateInfo{}, errors.New("error getting last sent certificate")}, expectedError: "error getting last sent certificate", }, { name: "no new blocks to send certificate", shouldSendCertificate: []interface{}{[]*aggsendertypes.CertificateInfo{}, nil}, lastL2BlockProcessed: []interface{}{uint64(41), nil}, - getLastSentCertificate: []interface{}{aggsendertypes.CertificateInfo{ + getLastSentCertificate: []interface{}{&aggsendertypes.CertificateInfo{ Height: 41, CertificateID: common.HexToHash("0x111"), NewLocalExitRoot: common.HexToHash("0x13223"), @@ -1074,7 +1070,7 @@ func TestSendCertificate(t *testing.T) { name: "get bridges error", shouldSendCertificate: []interface{}{[]*aggsendertypes.CertificateInfo{}, nil}, lastL2BlockProcessed: []interface{}{uint64(59), nil}, - getLastSentCertificate: []interface{}{aggsendertypes.CertificateInfo{ + getLastSentCertificate: []interface{}{&aggsendertypes.CertificateInfo{ Height: 50, CertificateID: common.HexToHash("0x1111"), NewLocalExitRoot: common.HexToHash("0x132233"), @@ -1088,7 +1084,7 @@ func TestSendCertificate(t *testing.T) { name: "no bridges", shouldSendCertificate: []interface{}{[]*aggsendertypes.CertificateInfo{}, nil}, lastL2BlockProcessed: []interface{}{uint64(69), nil}, - getLastSentCertificate: []interface{}{aggsendertypes.CertificateInfo{ + getLastSentCertificate: []interface{}{&aggsendertypes.CertificateInfo{ Height: 60, CertificateID: common.HexToHash("0x11111"), NewLocalExitRoot: common.HexToHash("0x1322233"), @@ -1101,7 +1097,7 @@ func TestSendCertificate(t *testing.T) { name: "get claims error", shouldSendCertificate: []interface{}{[]*aggsendertypes.CertificateInfo{}, nil}, lastL2BlockProcessed: []interface{}{uint64(79), nil}, - getLastSentCertificate: []interface{}{aggsendertypes.CertificateInfo{ + getLastSentCertificate: []interface{}{&aggsendertypes.CertificateInfo{ Height: 70, CertificateID: common.HexToHash("0x121111"), NewLocalExitRoot: common.HexToHash("0x13122233"), @@ -1123,7 +1119,7 @@ func TestSendCertificate(t *testing.T) { name: "error getting info by global exit root", shouldSendCertificate: []interface{}{[]*aggsendertypes.CertificateInfo{}, nil}, lastL2BlockProcessed: []interface{}{uint64(89), nil}, - getLastSentCertificate: []interface{}{aggsendertypes.CertificateInfo{ + getLastSentCertificate: []interface{}{&aggsendertypes.CertificateInfo{ Height: 80, CertificateID: common.HexToHash("0x1321111"), NewLocalExitRoot: common.HexToHash("0x131122233"), @@ -1150,7 +1146,7 @@ func TestSendCertificate(t *testing.T) { name: "error getting L1 Info tree root by index", shouldSendCertificate: []interface{}{[]*aggsendertypes.CertificateInfo{}, nil}, lastL2BlockProcessed: []interface{}{uint64(89), nil}, - getLastSentCertificate: []interface{}{aggsendertypes.CertificateInfo{ + getLastSentCertificate: []interface{}{&aggsendertypes.CertificateInfo{ Height: 80, CertificateID: common.HexToHash("0x1321111"), NewLocalExitRoot: common.HexToHash("0x131122233"), @@ -1187,7 +1183,7 @@ func TestSendCertificate(t *testing.T) { name: "error getting L1 Info tree merkle proof from index to root", shouldSendCertificate: []interface{}{[]*aggsendertypes.CertificateInfo{}, nil}, lastL2BlockProcessed: []interface{}{uint64(89), nil}, - getLastSentCertificate: []interface{}{aggsendertypes.CertificateInfo{ + getLastSentCertificate: []interface{}{&aggsendertypes.CertificateInfo{ Height: 80, CertificateID: common.HexToHash("0x1321111"), NewLocalExitRoot: common.HexToHash("0x131122233"), @@ -1226,7 +1222,7 @@ func TestSendCertificate(t *testing.T) { name: "send certificate error", shouldSendCertificate: []interface{}{[]*aggsendertypes.CertificateInfo{}, nil}, lastL2BlockProcessed: []interface{}{uint64(99), nil}, - getLastSentCertificate: []interface{}{aggsendertypes.CertificateInfo{ + getLastSentCertificate: []interface{}{&aggsendertypes.CertificateInfo{ Height: 90, CertificateID: common.HexToHash("0x1121111"), NewLocalExitRoot: common.HexToHash("0x111122211"), @@ -1253,7 +1249,7 @@ func TestSendCertificate(t *testing.T) { name: "store last sent certificate error", shouldSendCertificate: []interface{}{[]*aggsendertypes.CertificateInfo{}, nil}, lastL2BlockProcessed: []interface{}{uint64(109), nil}, - getLastSentCertificate: []interface{}{aggsendertypes.CertificateInfo{ + getLastSentCertificate: []interface{}{&aggsendertypes.CertificateInfo{ Height: 100, CertificateID: common.HexToHash("0x11121111"), NewLocalExitRoot: common.HexToHash("0x1211122211"), @@ -1281,7 +1277,7 @@ func TestSendCertificate(t *testing.T) { name: "successful sending of certificate", shouldSendCertificate: []interface{}{[]*aggsendertypes.CertificateInfo{}, nil}, lastL2BlockProcessed: []interface{}{uint64(119), nil}, - getLastSentCertificate: []interface{}{aggsendertypes.CertificateInfo{ + getLastSentCertificate: []interface{}{&aggsendertypes.CertificateInfo{ Height: 110, CertificateID: common.HexToHash("0x12121111"), NewLocalExitRoot: common.HexToHash("0x1221122211"), @@ -1570,8 +1566,8 @@ func TestSendCertificate_NoClaims(t *testing.T) { }, } - mockStorage.On("GetCertificatesByStatus", nonSettledStatuses).Return([]*aggsendertypes.CertificateInfo{}, nil).Once() - mockStorage.On("GetLastSentCertificate").Return(aggsendertypes.CertificateInfo{ + mockStorage.On("GetCertificatesByStatus", agglayer.NonSettledStatuses).Return([]*aggsendertypes.CertificateInfo{}, nil).Once() + mockStorage.On("GetLastSentCertificate").Return(&aggsendertypes.CertificateInfo{ NewLocalExitRoot: common.HexToHash("0x123"), Height: 1, FromBlock: 0, @@ -1611,3 +1607,269 @@ func TestSendCertificate_NoClaims(t *testing.T) { mockAggLayerClient.AssertExpectations(t) mockL1InfoTreeSyncer.AssertExpectations(t) } + +func TestMetadataConversions(t *testing.T) { + toBlock := uint64(123567890) + c := createCertificateMetadata(toBlock) + extractBlock := extractFromCertificateMetadataToBlock(c) + require.Equal(t, toBlock, extractBlock) +} + +func TestExtractFromCertificateMetadataToBlock(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + metadata common.Hash + expected uint64 + }{ + { + name: "Valid metadata", + metadata: common.BigToHash(big.NewInt(123567890)), + expected: 123567890, + }, + { + name: "Zero metadata", + metadata: common.BigToHash(big.NewInt(0)), + expected: 0, + }, + { + name: "Max uint64 metadata", + metadata: common.BigToHash(new(big.Int).SetUint64(^uint64(0))), + expected: ^uint64(0), + }, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + result := extractFromCertificateMetadataToBlock(tt.metadata) + require.Equal(t, tt.expected, result) + }) + } +} + +func TestCheckLastCertificateFromAgglayer_ErrorAggLayer(t *testing.T) { + testData := newAggsenderTestData(t, testDataFlagMockStorage) + testData.l2syncerMock.EXPECT().OriginNetwork().Return(networkIDTest).Once() + testData.agglayerClientMock.EXPECT().GetLatestKnownCertificateHeader(networkIDTest).Return(nil, fmt.Errorf("unittest error")).Once() + + err := testData.sut.checkLastCertificateFromAgglayer(testData.ctx) + + require.Error(t, err) +} + +func TestCheckLastCertificateFromAgglayer_ErrorStorageGetLastSentCertificate(t *testing.T) { + testData := newAggsenderTestData(t, testDataFlagMockStorage) + testData.l2syncerMock.EXPECT().OriginNetwork().Return(networkIDTest).Once() + testData.agglayerClientMock.EXPECT().GetLatestKnownCertificateHeader(networkIDTest).Return(nil, nil).Once() + testData.storageMock.EXPECT().GetLastSentCertificate().Return(nil, fmt.Errorf("unittest error")) + + err := testData.sut.checkLastCertificateFromAgglayer(testData.ctx) + + require.Error(t, err) +} + +// TestCheckLastCertificateFromAgglayer_Case1NoCerts +// CASE 1: No certificates in local storage and agglayer +// Aggsender and agglayer are empty so it's ok +func TestCheckLastCertificateFromAgglayer_Case1NoCerts(t *testing.T) { + testData := newAggsenderTestData(t, testDataFlagNone) + testData.l2syncerMock.EXPECT().OriginNetwork().Return(networkIDTest).Once() + testData.agglayerClientMock.EXPECT().GetLatestKnownCertificateHeader(networkIDTest).Return(nil, nil).Once() + + err := testData.sut.checkLastCertificateFromAgglayer(testData.ctx) + + require.NoError(t, err) +} + +// TestCheckLastCertificateFromAgglayer_Case2NoCertLocalCertRemote +// CASE 2: No certificates in local storage but agglayer has one +// The local DB is empty and we set the lastCert reported by AggLayer +func TestCheckLastCertificateFromAgglayer_Case2NoCertLocalCertRemote(t *testing.T) { + testData := newAggsenderTestData(t, testDataFlagNone) + testData.l2syncerMock.EXPECT().OriginNetwork().Return(networkIDTest).Once() + testData.agglayerClientMock.EXPECT().GetLatestKnownCertificateHeader(networkIDTest). + Return(certInfoToCertHeader(&testData.testCerts[0], networkIDTest), nil).Once() + + err := testData.sut.checkLastCertificateFromAgglayer(testData.ctx) + + require.NoError(t, err) + localCert, err := testData.sut.storage.GetLastSentCertificate() + require.NoError(t, err) + require.Equal(t, testData.testCerts[0].CertificateID, localCert.CertificateID) +} + +// TestCheckLastCertificateFromAgglayer_Case2NoCertLocalCertRemoteErrorStorage +// sub case of previous one that fails to update local storage +func TestCheckLastCertificateFromAgglayer_Case2NoCertLocalCertRemoteErrorStorage(t *testing.T) { + testData := newAggsenderTestData(t, testDataFlagMockStorage) + testData.l2syncerMock.EXPECT().OriginNetwork().Return(networkIDTest).Once() + testData.agglayerClientMock.EXPECT().GetLatestKnownCertificateHeader(networkIDTest). + Return(certInfoToCertHeader(&testData.testCerts[0], networkIDTest), nil).Once() + testData.storageMock.EXPECT().GetLastSentCertificate().Return(nil, nil) + testData.storageMock.EXPECT().SaveLastSentCertificate(mock.Anything, mock.Anything).Return(errTest).Once() + err := testData.sut.checkLastCertificateFromAgglayer(testData.ctx) + + require.Error(t, err) +} + +// CASE 3: AggSender and AggLayer not same certificateID. AggLayer has a new certificate +func TestCheckLastCertificateFromAgglayer_Case3Mismatch(t *testing.T) { + testData := newAggsenderTestData(t, testDataFlagMockStorage) + testData.l2syncerMock.EXPECT().OriginNetwork().Return(networkIDTest).Once() + testData.agglayerClientMock.EXPECT().GetLatestKnownCertificateHeader(networkIDTest). + Return(certInfoToCertHeader(&testData.testCerts[1], networkIDTest), nil).Once() + testData.storageMock.EXPECT().GetLastSentCertificate().Return(&testData.testCerts[0], nil) + testData.storageMock.EXPECT().SaveLastSentCertificate(mock.Anything, mock.Anything).Return(nil).Once() + + err := testData.sut.checkLastCertificateFromAgglayer(testData.ctx) + + require.NoError(t, err) +} + +// CASE 4: AggSender and AggLayer not same certificateID +func TestCheckLastCertificateFromAgglayer_Case4Mismatch(t *testing.T) { + testData := newAggsenderTestData(t, testDataFlagMockStorage) + testData.l2syncerMock.EXPECT().OriginNetwork().Return(networkIDTest).Once() + testData.agglayerClientMock.EXPECT().GetLatestKnownCertificateHeader(networkIDTest). + Return(certInfoToCertHeader(&testData.testCerts[0], networkIDTest), nil).Once() + testData.storageMock.EXPECT().GetLastSentCertificate().Return(&testData.testCerts[1], nil) + + err := testData.sut.checkLastCertificateFromAgglayer(testData.ctx) + + require.Error(t, err) +} + +// CASE 5: AggSender and AggLayer same certificateID and same status +func TestCheckLastCertificateFromAgglayer_Case5SameStatus(t *testing.T) { + testData := newAggsenderTestData(t, testDataFlagMockStorage) + testData.l2syncerMock.EXPECT().OriginNetwork().Return(networkIDTest).Once() + testData.agglayerClientMock.EXPECT().GetLatestKnownCertificateHeader(networkIDTest). + Return(certInfoToCertHeader(&testData.testCerts[0], networkIDTest), nil).Once() + testData.storageMock.EXPECT().GetLastSentCertificate().Return(&testData.testCerts[0], nil) + + err := testData.sut.checkLastCertificateFromAgglayer(testData.ctx) + + require.NoError(t, err) +} + +// CASE 5: AggSender and AggLayer same certificateID and differ on status +func TestCheckLastCertificateFromAgglayer_Case5UpdateStatus(t *testing.T) { + testData := newAggsenderTestData(t, testDataFlagMockStorage) + testData.l2syncerMock.EXPECT().OriginNetwork().Return(networkIDTest).Once() + aggLayerCert := certInfoToCertHeader(&testData.testCerts[0], networkIDTest) + aggLayerCert.Status = agglayer.Settled + testData.agglayerClientMock.EXPECT().GetLatestKnownCertificateHeader(networkIDTest). + Return(aggLayerCert, nil).Once() + testData.storageMock.EXPECT().GetLastSentCertificate().Return(&testData.testCerts[0], nil) + testData.storageMock.EXPECT().UpdateCertificate(mock.Anything, mock.Anything).Return(nil).Once() + + err := testData.sut.checkLastCertificateFromAgglayer(testData.ctx) + + require.NoError(t, err) +} + +// CASE 4: AggSender and AggLayer same certificateID and differ on status but fails update +func TestCheckLastCertificateFromAgglayer_Case4ErrorUpdateStatus(t *testing.T) { + testData := newAggsenderTestData(t, testDataFlagMockStorage) + testData.l2syncerMock.EXPECT().OriginNetwork().Return(networkIDTest).Once() + aggLayerCert := certInfoToCertHeader(&testData.testCerts[0], networkIDTest) + aggLayerCert.Status = agglayer.Settled + testData.agglayerClientMock.EXPECT().GetLatestKnownCertificateHeader(networkIDTest). + Return(aggLayerCert, nil).Once() + testData.storageMock.EXPECT().GetLastSentCertificate().Return(&testData.testCerts[0], nil) + testData.storageMock.EXPECT().UpdateCertificate(mock.Anything, mock.Anything).Return(errTest).Once() + + err := testData.sut.checkLastCertificateFromAgglayer(testData.ctx) + + require.Error(t, err) +} + +type testDataFlags = int + +const ( + testDataFlagNone testDataFlags = 0 + testDataFlagMockStorage testDataFlags = 1 +) + +type aggsenderTestData struct { + ctx context.Context + agglayerClientMock *agglayer.AgglayerClientMock + l2syncerMock *mocks.L2BridgeSyncer + l1InfoTreeSyncerMock *mocks.L1InfoTreeSyncer + storageMock *mocks.AggSenderStorage + sut *AggSender + testCerts []aggsendertypes.CertificateInfo +} + +func certInfoToCertHeader(certInfo *aggsendertypes.CertificateInfo, networkID uint32) *agglayer.CertificateHeader { + if certInfo == nil { + return nil + } + return &agglayer.CertificateHeader{ + Height: certInfo.Height, + NetworkID: networkID, + CertificateID: certInfo.CertificateID, + NewLocalExitRoot: certInfo.NewLocalExitRoot, + Status: agglayer.Pending, + Metadata: createCertificateMetadata(certInfo.ToBlock), + } +} + +func newAggsenderTestData(t *testing.T, creationFlags testDataFlags) *aggsenderTestData { + t.Helper() + l2syncerMock := mocks.NewL2BridgeSyncer(t) + agglayerClientMock := agglayer.NewAgglayerClientMock(t) + l1InfoTreeSyncerMock := mocks.NewL1InfoTreeSyncer(t) + logger := log.WithFields("aggsender-test", "checkLastCertificateFromAgglayer") + var storageMock *mocks.AggSenderStorage + var storage db.AggSenderStorage + var err error + if creationFlags&testDataFlagMockStorage != 0 { + storageMock = mocks.NewAggSenderStorage(t) + storage = storageMock + } else { + pc, _, _, _ := runtime.Caller(1) + part := runtime.FuncForPC(pc) + dbPath := fmt.Sprintf("file:%d?mode=memory&cache=shared", part.Entry()) + storage, err = db.NewAggSenderSQLStorage(logger, dbPath) + require.NoError(t, err) + } + + ctx := context.TODO() + sut := &AggSender{ + log: logger, + l2Syncer: l2syncerMock, + aggLayerClient: agglayerClientMock, + storage: storage, + l1infoTreeSyncer: l1InfoTreeSyncerMock, + } + testCerts := []aggsendertypes.CertificateInfo{ + { + Height: 1, + CertificateID: common.HexToHash("0x1"), + NewLocalExitRoot: common.HexToHash("0x2"), + Status: agglayer.Pending, + }, + { + Height: 2, + CertificateID: common.HexToHash("0x1a111"), + NewLocalExitRoot: common.HexToHash("0x2a2"), + Status: agglayer.Pending, + }, + } + + return &aggsenderTestData{ + ctx: ctx, + agglayerClientMock: agglayerClientMock, + l2syncerMock: l2syncerMock, + l1InfoTreeSyncerMock: l1InfoTreeSyncerMock, + storageMock: storageMock, + sut: sut, + testCerts: testCerts, + } +} diff --git a/aggsender/block_notifier_polling_test.go b/aggsender/block_notifier_polling_test.go index 83b3b643..e4f15ad7 100644 --- a/aggsender/block_notifier_polling_test.go +++ b/aggsender/block_notifier_polling_test.go @@ -32,11 +32,8 @@ func TestExploratoryBlockNotifierPolling(t *testing.T) { require.NoError(t, errSut) go sut.Start(context.Background()) ch := sut.Subscribe("test") - for { - select { - case block := <-ch: - fmt.Println(block) - } + for block := range ch { + fmt.Println(block) } } diff --git a/aggsender/config.go b/aggsender/config.go index 8ae0b759..b36fbe7a 100644 --- a/aggsender/config.go +++ b/aggsender/config.go @@ -29,6 +29,13 @@ type Config struct { EpochNotificationPercentage uint `mapstructure:"EpochNotificationPercentage"` // SaveCertificatesToFilesPath if != "" tells the AggSender to save the certificates to a file in this path SaveCertificatesToFilesPath string `mapstructure:"SaveCertificatesToFilesPath"` + + // MaxRetriesStoreCertificate is the maximum number of retries to store a certificate + // 0 is infinite + MaxRetriesStoreCertificate int `mapstructure:"MaxRetriesStoreCertificate"` + // DelayBeetweenRetries is the delay between retries: + // is used on store Certificate and also in initial check + DelayBeetweenRetries types.Duration `mapstructure:"DelayBeetweenRetries"` } // String returns a string representation of the Config @@ -38,7 +45,6 @@ func (c Config) String() string { "BlockGetInterval: " + c.BlockGetInterval.String() + "\n" + "CheckSettledInterval: " + c.CheckSettledInterval.String() + "\n" + "AggsenderPrivateKeyPath: " + c.AggsenderPrivateKey.Path + "\n" + - "AggsenderPrivateKeyPassword: " + c.AggsenderPrivateKey.Password + "\n" + "URLRPCL2: " + c.URLRPCL2 + "\n" + "BlockFinality: " + c.BlockFinality + "\n" + "EpochNotificationPercentage: " + fmt.Sprintf("%d", c.EpochNotificationPercentage) + "\n" + diff --git a/aggsender/db/aggsender_db_storage.go b/aggsender/db/aggsender_db_storage.go index 15866c29..00440f4a 100644 --- a/aggsender/db/aggsender_db_storage.go +++ b/aggsender/db/aggsender_db_storage.go @@ -21,17 +21,17 @@ const errWhileRollbackFormat = "error while rolling back tx: %w" // AggSenderStorage is the interface that defines the methods to interact with the storage type AggSenderStorage interface { // GetCertificateByHeight returns a certificate by its height - GetCertificateByHeight(height uint64) (types.CertificateInfo, error) + GetCertificateByHeight(height uint64) (*types.CertificateInfo, error) // GetLastSentCertificate returns the last certificate sent to the aggLayer - GetLastSentCertificate() (types.CertificateInfo, error) + GetLastSentCertificate() (*types.CertificateInfo, error) // SaveLastSentCertificate saves the last certificate sent to the aggLayer SaveLastSentCertificate(ctx context.Context, certificate types.CertificateInfo) error // DeleteCertificate deletes a certificate from the storage DeleteCertificate(ctx context.Context, certificateID common.Hash) error // GetCertificatesByStatus returns a list of certificates by their status GetCertificatesByStatus(status []agglayer.CertificateStatus) ([]*types.CertificateInfo, error) - // UpdateCertificateStatus updates the status of a certificate - UpdateCertificateStatus(ctx context.Context, certificate types.CertificateInfo) error + // UpdateCertificate updates certificate in db + UpdateCertificate(ctx context.Context, certificate types.CertificateInfo) error } var _ AggSenderStorage = (*AggSenderSQLStorage)(nil) @@ -88,31 +88,31 @@ func (a *AggSenderSQLStorage) GetCertificatesByStatus( } // GetCertificateByHeight returns a certificate by its height -func (a *AggSenderSQLStorage) GetCertificateByHeight(height uint64) (types.CertificateInfo, error) { +func (a *AggSenderSQLStorage) GetCertificateByHeight(height uint64) (*types.CertificateInfo, error) { return getCertificateByHeight(a.db, height) } // getCertificateByHeight returns a certificate by its height using the provided db func getCertificateByHeight(db meddler.DB, - height uint64) (types.CertificateInfo, error) { + height uint64) (*types.CertificateInfo, error) { var certificateInfo types.CertificateInfo if err := meddler.QueryRow(db, &certificateInfo, "SELECT * FROM certificate_info WHERE height = $1;", height); err != nil { - return types.CertificateInfo{}, getSelectQueryError(height, err) + return nil, getSelectQueryError(height, err) } - return certificateInfo, nil + return &certificateInfo, nil } // GetLastSentCertificate returns the last certificate sent to the aggLayer -func (a *AggSenderSQLStorage) GetLastSentCertificate() (types.CertificateInfo, error) { +func (a *AggSenderSQLStorage) GetLastSentCertificate() (*types.CertificateInfo, error) { var certificateInfo types.CertificateInfo if err := meddler.QueryRow(a.db, &certificateInfo, "SELECT * FROM certificate_info ORDER BY height DESC LIMIT 1;"); err != nil { - return types.CertificateInfo{}, getSelectQueryError(0, err) + return nil, getSelectQueryError(0, err) } - return certificateInfo, nil + return &certificateInfo, nil } // SaveLastSentCertificate saves the last certificate sent to the aggLayer @@ -134,7 +134,7 @@ func (a *AggSenderSQLStorage) SaveLastSentCertificate(ctx context.Context, certi return err } - if cert.CertificateID != (common.Hash{}) { + if cert != nil { // we already have a certificate with this height // we need to delete it before inserting the new one if err = deleteCertificate(tx, cert.CertificateID); err != nil { @@ -191,8 +191,8 @@ func deleteCertificate(db meddler.DB, certificateID common.Hash) error { return nil } -// UpdateCertificateStatus updates the status of a certificate -func (a *AggSenderSQLStorage) UpdateCertificateStatus(ctx context.Context, certificate types.CertificateInfo) error { +// UpdateCertificate updates a certificate +func (a *AggSenderSQLStorage) UpdateCertificate(ctx context.Context, certificate types.CertificateInfo) error { tx, err := db.NewTx(ctx, a.db) if err != nil { return err diff --git a/aggsender/db/aggsender_db_storage_test.go b/aggsender/db/aggsender_db_storage_test.go index a0a20894..642c8ae7 100644 --- a/aggsender/db/aggsender_db_storage_test.go +++ b/aggsender/db/aggsender_db_storage_test.go @@ -45,7 +45,7 @@ func Test_Storage(t *testing.T) { certificateFromDB, err := storage.GetCertificateByHeight(certificate.Height) require.NoError(t, err) - require.Equal(t, certificate, certificateFromDB) + require.Equal(t, certificate, *certificateFromDB) require.NoError(t, storage.clean()) }) @@ -66,7 +66,7 @@ func Test_Storage(t *testing.T) { certificateFromDB, err := storage.GetCertificateByHeight(certificate.Height) require.ErrorIs(t, err, db.ErrNotFound) - require.Equal(t, types.CertificateInfo{}, certificateFromDB) + require.Nil(t, certificateFromDB) require.NoError(t, storage.clean()) }) @@ -74,7 +74,7 @@ func Test_Storage(t *testing.T) { // try getting a certificate that doesn't exist certificateFromDB, err := storage.GetLastSentCertificate() require.NoError(t, err) - require.Equal(t, types.CertificateInfo{}, certificateFromDB) + require.Nil(t, certificateFromDB) // try getting a certificate that exists certificate := types.CertificateInfo{ @@ -91,8 +91,8 @@ func Test_Storage(t *testing.T) { certificateFromDB, err = storage.GetLastSentCertificate() require.NoError(t, err) - - require.Equal(t, certificate, certificateFromDB) + require.NotNil(t, certificateFromDB) + require.Equal(t, certificate, *certificateFromDB) require.NoError(t, storage.clean()) }) @@ -100,12 +100,12 @@ func Test_Storage(t *testing.T) { // try getting height 0 certificateFromDB, err := storage.GetCertificateByHeight(0) require.NoError(t, err) - require.Equal(t, types.CertificateInfo{}, certificateFromDB) + require.Nil(t, certificateFromDB) // try getting a certificate that doesn't exist certificateFromDB, err = storage.GetCertificateByHeight(4) require.ErrorIs(t, err, db.ErrNotFound) - require.Equal(t, types.CertificateInfo{}, certificateFromDB) + require.Nil(t, certificateFromDB) // try getting a certificate that exists certificate := types.CertificateInfo{ @@ -122,8 +122,8 @@ func Test_Storage(t *testing.T) { certificateFromDB, err = storage.GetCertificateByHeight(certificate.Height) require.NoError(t, err) - - require.Equal(t, certificate, certificateFromDB) + require.NotNil(t, certificateFromDB) + require.Equal(t, certificate, *certificateFromDB) require.NoError(t, storage.clean()) }) @@ -213,7 +213,7 @@ func Test_Storage(t *testing.T) { // Update the status of the certificate certificate.Status = agglayer.Settled - require.NoError(t, storage.UpdateCertificateStatus(ctx, certificate)) + require.NoError(t, storage.UpdateCertificate(ctx, certificate)) // Fetch the certificate and verify the status has been updated certificateFromDB, err := storage.GetCertificateByHeight(certificate.Height) @@ -251,7 +251,7 @@ func Test_SaveLastSentCertificate(t *testing.T) { certificateFromDB, err := storage.GetCertificateByHeight(certificate.Height) require.NoError(t, err) - require.Equal(t, certificate, certificateFromDB) + require.Equal(t, certificate, *certificateFromDB) require.NoError(t, storage.clean()) }) @@ -281,7 +281,7 @@ func Test_SaveLastSentCertificate(t *testing.T) { certificateFromDB, err := storage.GetCertificateByHeight(updatedCertificate.Height) require.NoError(t, err) - require.Equal(t, updatedCertificate, certificateFromDB) + require.Equal(t, updatedCertificate, *certificateFromDB) require.NoError(t, storage.clean()) }) @@ -310,7 +310,7 @@ func Test_SaveLastSentCertificate(t *testing.T) { certificateFromDB, err := storage.GetCertificateByHeight(certificate.Height) require.ErrorIs(t, err, db.ErrNotFound) - require.Equal(t, types.CertificateInfo{}, certificateFromDB) + require.Nil(t, certificateFromDB) require.NoError(t, storage.clean()) }) @@ -362,7 +362,7 @@ func Test_SaveLastSentCertificate(t *testing.T) { certificateFromDB, err := storage.GetCertificateByHeight(certificate.Height) require.NoError(t, err) - require.Equal(t, certificate, certificateFromDB) + require.Equal(t, certificate, *certificateFromDB) require.Equal(t, raw, []byte(certificateFromDB.SignedCertificate)) require.NoError(t, storage.clean()) diff --git a/aggsender/mocks/agg_sender_storage.go b/aggsender/mocks/agg_sender_storage.go index 1816d4a3..b6337180 100644 --- a/aggsender/mocks/agg_sender_storage.go +++ b/aggsender/mocks/agg_sender_storage.go @@ -74,22 +74,24 @@ func (_c *AggSenderStorage_DeleteCertificate_Call) RunAndReturn(run func(context } // GetCertificateByHeight provides a mock function with given fields: height -func (_m *AggSenderStorage) GetCertificateByHeight(height uint64) (types.CertificateInfo, error) { +func (_m *AggSenderStorage) GetCertificateByHeight(height uint64) (*types.CertificateInfo, error) { ret := _m.Called(height) if len(ret) == 0 { panic("no return value specified for GetCertificateByHeight") } - var r0 types.CertificateInfo + var r0 *types.CertificateInfo var r1 error - if rf, ok := ret.Get(0).(func(uint64) (types.CertificateInfo, error)); ok { + if rf, ok := ret.Get(0).(func(uint64) (*types.CertificateInfo, error)); ok { return rf(height) } - if rf, ok := ret.Get(0).(func(uint64) types.CertificateInfo); ok { + if rf, ok := ret.Get(0).(func(uint64) *types.CertificateInfo); ok { r0 = rf(height) } else { - r0 = ret.Get(0).(types.CertificateInfo) + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.CertificateInfo) + } } if rf, ok := ret.Get(1).(func(uint64) error); ok { @@ -119,12 +121,12 @@ func (_c *AggSenderStorage_GetCertificateByHeight_Call) Run(run func(height uint return _c } -func (_c *AggSenderStorage_GetCertificateByHeight_Call) Return(_a0 types.CertificateInfo, _a1 error) *AggSenderStorage_GetCertificateByHeight_Call { +func (_c *AggSenderStorage_GetCertificateByHeight_Call) Return(_a0 *types.CertificateInfo, _a1 error) *AggSenderStorage_GetCertificateByHeight_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *AggSenderStorage_GetCertificateByHeight_Call) RunAndReturn(run func(uint64) (types.CertificateInfo, error)) *AggSenderStorage_GetCertificateByHeight_Call { +func (_c *AggSenderStorage_GetCertificateByHeight_Call) RunAndReturn(run func(uint64) (*types.CertificateInfo, error)) *AggSenderStorage_GetCertificateByHeight_Call { _c.Call.Return(run) return _c } @@ -188,22 +190,24 @@ func (_c *AggSenderStorage_GetCertificatesByStatus_Call) RunAndReturn(run func([ } // GetLastSentCertificate provides a mock function with given fields: -func (_m *AggSenderStorage) GetLastSentCertificate() (types.CertificateInfo, error) { +func (_m *AggSenderStorage) GetLastSentCertificate() (*types.CertificateInfo, error) { ret := _m.Called() if len(ret) == 0 { panic("no return value specified for GetLastSentCertificate") } - var r0 types.CertificateInfo + var r0 *types.CertificateInfo var r1 error - if rf, ok := ret.Get(0).(func() (types.CertificateInfo, error)); ok { + if rf, ok := ret.Get(0).(func() (*types.CertificateInfo, error)); ok { return rf() } - if rf, ok := ret.Get(0).(func() types.CertificateInfo); ok { + if rf, ok := ret.Get(0).(func() *types.CertificateInfo); ok { r0 = rf() } else { - r0 = ret.Get(0).(types.CertificateInfo) + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.CertificateInfo) + } } if rf, ok := ret.Get(1).(func() error); ok { @@ -232,12 +236,12 @@ func (_c *AggSenderStorage_GetLastSentCertificate_Call) Run(run func()) *AggSend return _c } -func (_c *AggSenderStorage_GetLastSentCertificate_Call) Return(_a0 types.CertificateInfo, _a1 error) *AggSenderStorage_GetLastSentCertificate_Call { +func (_c *AggSenderStorage_GetLastSentCertificate_Call) Return(_a0 *types.CertificateInfo, _a1 error) *AggSenderStorage_GetLastSentCertificate_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *AggSenderStorage_GetLastSentCertificate_Call) RunAndReturn(run func() (types.CertificateInfo, error)) *AggSenderStorage_GetLastSentCertificate_Call { +func (_c *AggSenderStorage_GetLastSentCertificate_Call) RunAndReturn(run func() (*types.CertificateInfo, error)) *AggSenderStorage_GetLastSentCertificate_Call { _c.Call.Return(run) return _c } @@ -289,12 +293,12 @@ func (_c *AggSenderStorage_SaveLastSentCertificate_Call) RunAndReturn(run func(c return _c } -// UpdateCertificateStatus provides a mock function with given fields: ctx, certificate -func (_m *AggSenderStorage) UpdateCertificateStatus(ctx context.Context, certificate types.CertificateInfo) error { +// UpdateCertificate provides a mock function with given fields: ctx, certificate +func (_m *AggSenderStorage) UpdateCertificate(ctx context.Context, certificate types.CertificateInfo) error { ret := _m.Called(ctx, certificate) if len(ret) == 0 { - panic("no return value specified for UpdateCertificateStatus") + panic("no return value specified for UpdateCertificate") } var r0 error @@ -307,31 +311,31 @@ func (_m *AggSenderStorage) UpdateCertificateStatus(ctx context.Context, certifi return r0 } -// AggSenderStorage_UpdateCertificateStatus_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateCertificateStatus' -type AggSenderStorage_UpdateCertificateStatus_Call struct { +// AggSenderStorage_UpdateCertificate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateCertificate' +type AggSenderStorage_UpdateCertificate_Call struct { *mock.Call } -// UpdateCertificateStatus is a helper method to define mock.On call +// UpdateCertificate is a helper method to define mock.On call // - ctx context.Context // - certificate types.CertificateInfo -func (_e *AggSenderStorage_Expecter) UpdateCertificateStatus(ctx interface{}, certificate interface{}) *AggSenderStorage_UpdateCertificateStatus_Call { - return &AggSenderStorage_UpdateCertificateStatus_Call{Call: _e.mock.On("UpdateCertificateStatus", ctx, certificate)} +func (_e *AggSenderStorage_Expecter) UpdateCertificate(ctx interface{}, certificate interface{}) *AggSenderStorage_UpdateCertificate_Call { + return &AggSenderStorage_UpdateCertificate_Call{Call: _e.mock.On("UpdateCertificate", ctx, certificate)} } -func (_c *AggSenderStorage_UpdateCertificateStatus_Call) Run(run func(ctx context.Context, certificate types.CertificateInfo)) *AggSenderStorage_UpdateCertificateStatus_Call { +func (_c *AggSenderStorage_UpdateCertificate_Call) Run(run func(ctx context.Context, certificate types.CertificateInfo)) *AggSenderStorage_UpdateCertificate_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(types.CertificateInfo)) }) return _c } -func (_c *AggSenderStorage_UpdateCertificateStatus_Call) Return(_a0 error) *AggSenderStorage_UpdateCertificateStatus_Call { +func (_c *AggSenderStorage_UpdateCertificate_Call) Return(_a0 error) *AggSenderStorage_UpdateCertificate_Call { _c.Call.Return(_a0) return _c } -func (_c *AggSenderStorage_UpdateCertificateStatus_Call) RunAndReturn(run func(context.Context, types.CertificateInfo) error) *AggSenderStorage_UpdateCertificateStatus_Call { +func (_c *AggSenderStorage_UpdateCertificate_Call) RunAndReturn(run func(context.Context, types.CertificateInfo) error) *AggSenderStorage_UpdateCertificate_Call { _c.Call.Return(run) return _c } diff --git a/aggsender/types/types.go b/aggsender/types/types.go index d9e0b2e7..d6f2650e 100644 --- a/aggsender/types/types.go +++ b/aggsender/types/types.go @@ -43,6 +43,7 @@ type EthClient interface { // Logger is an interface that defines the methods to log messages type Logger interface { + Fatalf(format string, args ...interface{}) Info(args ...interface{}) Infof(format string, args ...interface{}) Error(args ...interface{}) @@ -65,23 +66,50 @@ type CertificateInfo struct { SignedCertificate string `meddler:"signed_certificate"` } -func (c CertificateInfo) String() string { +func (c *CertificateInfo) String() string { + if c == nil { + return "nil" + } return fmt.Sprintf( - "Height: %d\n"+ - "CertificateID: %s\n"+ - "FromBlock: %d\n"+ - "ToBlock: %d\n"+ - "NewLocalExitRoot: %s\n"+ - "Status: %s\n"+ - "CreatedAt: %s\n"+ - "UpdatedAt: %s\n", + "Height: %d "+ + "CertificateID: %s "+ + "NewLocalExitRoot: %s "+ + "Status: %s "+ + "FromBlock: %d "+ + "ToBlock: %d "+ + "CreatedAt: %s "+ + "UpdatedAt: %s", c.Height, c.CertificateID.String(), - c.FromBlock, - c.ToBlock, c.NewLocalExitRoot.String(), c.Status.String(), + c.FromBlock, + c.ToBlock, time.UnixMilli(c.CreatedAt), time.UnixMilli(c.UpdatedAt), ) } + +// ID returns a string with the ident of this cert (height/certID) +func (c *CertificateInfo) ID() string { + if c == nil { + return "nil" + } + return fmt.Sprintf("%d/%s", c.Height, c.CertificateID.String()) +} + +// IsClosed returns true if the certificate is closed (settled or inError) +func (c *CertificateInfo) IsClosed() bool { + if c == nil { + return false + } + return c.Status.IsClosed() +} + +// ElapsedTimeSinceCreation returns the time elapsed since the certificate was created +func (c *CertificateInfo) ElapsedTimeSinceCreation() time.Duration { + if c == nil { + return 0 + } + return time.Now().UTC().Sub(time.UnixMilli(c.CreatedAt)) +} diff --git a/config/default.go b/config/default.go index 316cfb76..b1cfe16e 100644 --- a/config/default.go +++ b/config/default.go @@ -213,7 +213,7 @@ SyncBlockChunkSize=100 BlockFinality="LatestBlock" URLRPCL1="{{L1URL}}" WaitForNewBlocksPeriod="100ms" -InitialBlock=0 +InitialBlock={{genesisBlockNumber}} [AggOracle] TargetChainType="EVM" @@ -238,7 +238,7 @@ WaitPeriodNextGER="100ms" ForcedGas = 0 GasPriceMarginFactor = 1 MaxGasPriceLimit = 0 - StoragePath = "/{{PathRWData}}/ethtxmanager-sequencesender.sqlite" + StoragePath = "{{PathRWData}}/ethtxmanager-sequencesender.sqlite" ReadPendingL1Txs = false SafeStatusL1NumberOfBlocks = 5 FinalizedStatusL1NumberOfBlocks = 10 @@ -256,7 +256,7 @@ WriteTimeout = "2s" MaxRequestsPerIPAndSecond = 10 [ClaimSponsor] -DBPath = "/{{PathRWData}}/claimsopnsor.sqlite" +DBPath = "{{PathRWData}}/claimsopnsor.sqlite" Enabled = true SenderAddr = "0xfa3b44587990f97ba8b6ba7e230a5f0e95d14b3d" BridgeAddrL2 = "0xB7098a13a48EcE087d3DA15b2D28eCE0f89819B8" @@ -277,7 +277,7 @@ GasOffset = 0 ForcedGas = 0 GasPriceMarginFactor = 1 MaxGasPriceLimit = 0 - StoragePath = "/{{PathRWData}}/ethtxmanager-claimsponsor.sqlite" + StoragePath = "{{PathRWData}}/ethtxmanager-claimsponsor.sqlite" ReadPendingL1Txs = false SafeStatusL1NumberOfBlocks = 5 FinalizedStatusL1NumberOfBlocks = 10 @@ -337,4 +337,6 @@ CheckSettledInterval = "2s" BlockFinality = "LatestBlock" EpochNotificationPercentage = 50 SaveCertificatesToFilesPath = "" +MaxRetriesStoreCertificate = 3 +DelayBeetweenRetries = "60s" ` diff --git a/scripts/local_config b/scripts/local_config index ca25dbbb..90b5ae11 100755 --- a/scripts/local_config +++ b/scripts/local_config @@ -3,7 +3,7 @@ source $(dirname $0)/../test/scripts/env.sh ############################################################################### function log_debug() { - echo -e "\033[0;30mDebug: $*" "\033[0m" + echo -e "\033[0;90mDebug: $*" "\033[0m" } ############################################################################### function log_error() { diff --git a/test/bats/pp/e2e-pp.bats b/test/bats/pp/e2e-pp.bats index 77d7910d..f7963592 100644 --- a/test/bats/pp/e2e-pp.bats +++ b/test/bats/pp/e2e-pp.bats @@ -1,10 +1,25 @@ setup() { load '../../helpers/common-setup' _common_setup + + if [ -z "$BRIDGE_ADDRESS" ]; then + local combined_json_file="/opt/zkevm/combined.json" + echo "BRIDGE_ADDRESS env variable is not provided, resolving the bridge address from the Kurtosis CDK '$combined_json_file'" >&3 + + # Fetching the combined JSON output and filtering to get polygonZkEVMBridgeAddress + combined_json_output=$($contracts_service_wrapper "cat $combined_json_file" | tail -n +2) + bridge_default_address=$(echo "$combined_json_output" | jq -r .polygonZkEVMBridgeAddress) + BRIDGE_ADDRESS=$bridge_default_address + fi + echo "Bridge address=$BRIDGE_ADDRESS" >&3 } -@test "Verify batches" { - echo "Waiting 10 minutes to get some settle certificate...." - run $PROJECT_ROOT/../scripts/agglayer_certificates_monitor.sh 1 600 +@test "Verify certificate settlement" { + echo "Waiting 10 minutes to get some settle certificate...." >&3 + + readonly bridge_addr=$BRIDGE_ADDRESS + readonly l2_rpc_network_id=$(cast call --rpc-url $l2_rpc_url $bridge_addr 'networkID() (uint32)') + + run $PROJECT_ROOT/../scripts/agglayer_certificates_monitor.sh 1 600 $l2_rpc_network_id assert_success } diff --git a/test/combinations/fork12-pessimistic.yml b/test/combinations/fork12-pessimistic.yml index 088822c5..d92375c5 100644 --- a/test/combinations/fork12-pessimistic.yml +++ b/test/combinations/fork12-pessimistic.yml @@ -1,5 +1,5 @@ args: - agglayer_image: ghcr.io/agglayer/agglayer:0.2.0-rc.11 + agglayer_image: ghcr.io/agglayer/agglayer:0.2.0-rc.12 cdk_erigon_node_image: hermeznetwork/cdk-erigon:v2.60.0-beta8 cdk_node_image: cdk zkevm_bridge_proxy_image: haproxy:3.0-bookworm @@ -11,4 +11,5 @@ args: sequencer_type: erigon erigon_strict_mode: false zkevm_use_gas_token_contract: true - enable_normalcy: true \ No newline at end of file + agglayer_prover_sp1_key: {{.agglayer_prover_sp1_key}} + enable_normalcy: true diff --git a/test/run-e2e.sh b/test/run-e2e.sh index 08a6b2cd..adbbcbcb 100755 --- a/test/run-e2e.sh +++ b/test/run-e2e.sh @@ -9,7 +9,7 @@ fi DATA_AVAILABILITY_MODE=$2 if [ -z $DATA_AVAILABILITY_MODE ]; then - echo "Missing DATA_AVAILABILITY_MODE: ['rollup', 'cdk-validium']" + echo "Missing DATA_AVAILABILITY_MODE: ['rollup', 'cdk-validium', 'pessimistic']" exit 1 fi @@ -27,4 +27,9 @@ fi kurtosis clean --all echo "Override cdk config file" cp $BASE_FOLDER/config/kurtosis-cdk-node-config.toml.template $KURTOSIS_FOLDER/templates/trusted-node/cdk-node-config.toml -kurtosis run --enclave cdk --args-file "combinations/$FORK-$DATA_AVAILABILITY_MODE.yml" --image-download always $KURTOSIS_FOLDER +KURTOSIS_CONFIG_FILE="combinations/$FORK-$DATA_AVAILABILITY_MODE.yml" +TEMP_CONFIG_FILE=$(mktemp --suffix ".yml") +echo "rendering $KURTOSIS_CONFIG_FILE to temp file $TEMP_CONFIG_FILE" +go run ../scripts/run_template.go $KURTOSIS_CONFIG_FILE > $TEMP_CONFIG_FILE +kurtosis run --enclave cdk --args-file "$TEMP_CONFIG_FILE" --image-download always $KURTOSIS_FOLDER +rm $TEMP_CONFIG_FILE \ No newline at end of file diff --git a/test/scripts/agglayer_certificates_monitor.sh b/test/scripts/agglayer_certificates_monitor.sh index 47c116b6..c530548f 100755 --- a/test/scripts/agglayer_certificates_monitor.sh +++ b/test/scripts/agglayer_certificates_monitor.sh @@ -3,8 +3,8 @@ function parse_params(){ # Check if the required arguments are provided. - if [ "$#" -lt 2 ]; then - echo "Usage: $0 " + if [ "$#" -lt 3 ]; then + echo "Usage: $0 " exit 1 fi @@ -13,6 +13,9 @@ function parse_params(){ # The script timeout (in seconds). timeout="$2" + + # The network id of the L2 network. + l2_rpc_network_id="$3" } function check_timeout(){ @@ -25,39 +28,55 @@ function check_timeout(){ } function check_num_certificates(){ - local _cmd="echo 'select status, count(*) from certificate_info group by status;' | sqlite3 /tmp/aggsender.sqlite" - local _outcmd=$(mktemp) - kurtosis service exec cdk cdk-node-001 "$_cmd" > $_outcmd + readonly agglayer_rpc_url="$(kurtosis port print cdk agglayer agglayer)" + + cast_output=$(cast rpc --rpc-url "$agglayer_rpc_url" "interop_getLatestKnownCertificateHeader" "$l2_rpc_network_id" 2>&1) + if [ $? -ne 0 ]; then - echo "[$(date '+%Y-%m-%d %H:%M:%S')] Error executing command kurtosis service: $_cmd" - # clean temp file - rm $_outcmd - return + echo "[$(date '+%Y-%m-%d %H:%M:%S')] Error executing command cast rpc: $cast_output" + return fi - local num_certs=$(cat $_outcmd | tail -n +2 | cut -f 2 -d '|' | xargs |tr ' ' '+' | bc) - # Get the number of settled certificates "4|0" - local _num_settle_certs=$(cat $_outcmd | tail -n +2 | grep ^${aggsender_status_settled} | cut -d'|' -f2) - [ -z "$_num_settle_certs" ] && _num_settle_certs=0 - # clean temp file - rm $_outcmd - echo "[$(date '+%Y-%m-%d %H:%M:%S')] Num certificates on aggsender: $num_certs. Settled certificates : $_num_settle_certs" - if [ $num_certs -ge $settle_certificates_target ]; then - echo "[$(date '+%Y-%m-%d %H:%M:%S')] ✅ Exiting... $num_certs certificates were settled! (total certs $num_certs)" + + height=$(extract_certificate_height "$cast_output") + [[ -z "$height" ]] && { + echo "Error: Failed to extract certificate height: $height." >&3 + return + } + + status=$(extract_certificate_status "$cast_output") + [[ -z "$status" ]] && { + echo "Error: Failed to extract certificate status." >&3 + return + } + + echo "[$(date '+%Y-%m-%d %H:%M:%S')] Last known agglayer certificate height: $height, status: $status" >&3 + + if (( height > settle_certificates_target - 1 )); then + echo "[$(date '+%Y-%m-%d %H:%M:%S')] ✅ Success! The number of settled certificates has reached the target." >&3 exit 0 fi + + if (( height == settle_certificates_target - 1 )); then + if [ "$status" == "Settled" ]; then + echo "[$(date '+%Y-%m-%d %H:%M:%S')] ✅ Success! The number of settled certificates has reached the target." >&3 + exit 0 + fi + + echo "[$(date '+%Y-%m-%d %H:%M:%S')] ⚠️ Warning! The number of settled certificates is one less than the target." >&3 + fi } -# MAIN -declare -A aggsender_status_map=( - [0]="Pending" - [1]="Proven" - [2]="Candidate" - [3]="InError" - [4]="Settled" -) +function extract_certificate_height() { + local cast_output="$1" + echo "$cast_output" | jq -r '.height' +} -readonly aggsender_status_settled=4 +function extract_certificate_status() { + local cast_output="$1" + echo "$cast_output" | jq -r '.status' +} +# MAIN parse_params $* start_time=$(date +%s)