diff --git a/agglayer/client_test.go b/agglayer/client_test.go index 09bea14e..c4117eb8 100644 --- a/agglayer/client_test.go +++ b/agglayer/client_test.go @@ -45,7 +45,7 @@ func TestExploratoryGetEpochConfiguration(t *testing.T) { func TestExploratoryGetLatestKnownCertificateHeader(t *testing.T) { t.Skip("This test is exploratory and should be skipped") - aggLayerClient := NewAggLayerClient("http://localhost:32781") + aggLayerClient := NewAggLayerClient("http://localhost:32843") cert, err := aggLayerClient.GetLatestKnownCertificateHeader(1) require.NoError(t, err) fmt.Print(cert) @@ -116,7 +116,9 @@ func TestGetLatestKnownCertificateHeaderOkResponse(t *testing.T) { cert, err := sut.GetLatestKnownCertificateHeader(1) require.NotNil(t, cert) require.NoError(t, err) + require.Nil(t, cert.PreviousLocalExitRoot) } + func TestGetLatestKnownCertificateHeaderErrorResponse(t *testing.T) { sut := NewAggLayerClient(testURL) jSONRPCCall = func(url, method string, params ...interface{}) (rpc.Response, error) { @@ -143,3 +145,19 @@ func TestGetLatestKnownCertificateHeaderResponseBadJson(t *testing.T) { require.Nil(t, cert) require.Error(t, err) } + +func TestGetLatestKnownCertificateHeaderWithPrevLERResponse(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","prev_local_exit_root":"0x27ae5ba08d7291c96c8cbddcc148bf48a6d68c7974b94356f53754ef6171d757","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.NoError(t, err) + require.NotNil(t, cert) + + require.Equal(t, "0x27ae5ba08d7291c96c8cbddcc148bf48a6d68c7974b94356f53754ef6171d757", cert.PreviousLocalExitRoot.String()) +} diff --git a/agglayer/types.go b/agglayer/types.go index 09697d3d..c910beac 100644 --- a/agglayer/types.go +++ b/agglayer/types.go @@ -23,6 +23,8 @@ const ( Candidate InError Settled + + nilStr = "nil" ) var ( @@ -574,36 +576,41 @@ func (p *GenericPPError) String() string { // CertificateHeader is the structure returned by the interop_getCertificateHeader RPC call type CertificateHeader struct { - NetworkID uint32 `json:"network_id"` - Height uint64 `json:"height"` - EpochNumber *uint64 `json:"epoch_number"` - CertificateIndex *uint64 `json:"certificate_index"` - CertificateID common.Hash `json:"certificate_id"` - NewLocalExitRoot common.Hash `json:"new_local_exit_root"` - Status CertificateStatus `json:"status"` - Metadata common.Hash `json:"metadata"` - Error PPError `json:"-"` + NetworkID uint32 `json:"network_id"` + Height uint64 `json:"height"` + EpochNumber *uint64 `json:"epoch_number"` + CertificateIndex *uint64 `json:"certificate_index"` + CertificateID common.Hash `json:"certificate_id"` + PreviousLocalExitRoot *common.Hash `json:"prev_local_exit_root,omitempty"` + NewLocalExitRoot common.Hash `json:"new_local_exit_root"` + Status CertificateStatus `json:"status"` + Metadata common.Hash `json:"metadata"` + Error PPError `json:"-"` } // ID returns a string with the ident of this cert (height/certID) func (c *CertificateHeader) ID() string { if c == nil { - return "nil" + return nilStr } return fmt.Sprintf("%d/%s", c.Height, c.CertificateID.String()) } func (c *CertificateHeader) String() string { if c == nil { - return "nil" + return nilStr } errors := "" if c.Error != nil { errors = c.Error.String() } - - return fmt.Sprintf("Height: %d, CertificateID: %s, NewLocalExitRoot: %s. Status: %s. Errors: [%s]", - c.Height, c.CertificateID.String(), c.NewLocalExitRoot.String(), c.Status.String(), errors) + previousLocalExitRoot := "nil" + if c.PreviousLocalExitRoot != nil { + previousLocalExitRoot = c.PreviousLocalExitRoot.String() + } + return fmt.Sprintf("Height: %d, CertificateID: %s, previousLocalExitRoot:%s, NewLocalExitRoot: %s. Status: %s."+ + " Errors: [%s]", + c.Height, c.CertificateID.String(), previousLocalExitRoot, c.NewLocalExitRoot.String(), c.Status.String(), errors) } func (c *CertificateHeader) UnmarshalJSON(data []byte) error { diff --git a/agglayer/types_test.go b/agglayer/types_test.go index ecef4bca..c19abf72 100644 --- a/agglayer/types_test.go +++ b/agglayer/types_test.go @@ -20,6 +20,29 @@ func TestMGenericPPError(t *testing.T) { require.Equal(t, "Generic error: test: value", err.String()) } +func TestCertificateHeaderID(t *testing.T) { + certificate := CertificateHeader{ + Height: 1, + CertificateID: common.HexToHash("0x123"), + } + require.Equal(t, "1/0x0000000000000000000000000000000000000000000000000000000000000123", certificate.ID()) + + var certNil *CertificateHeader + require.Equal(t, "nil", certNil.ID()) +} + +func TestCertificateHeaderString(t *testing.T) { + certificate := CertificateHeader{ + Height: 1, + CertificateID: common.HexToHash("0x123"), + } + require.Equal(t, "Height: 1, CertificateID: 0x0000000000000000000000000000000000000000000000000000000000000123, previousLocalExitRoot:nil, NewLocalExitRoot: 0x0000000000000000000000000000000000000000000000000000000000000000. Status: Pending. Errors: []", + certificate.String()) + + var certNil *CertificateHeader + require.Equal(t, "nil", certNil.String()) +} + func TestMarshalJSON(t *testing.T) { cert := SignedCertificate{ Certificate: &Certificate{ diff --git a/aggsender/aggsender.go b/aggsender/aggsender.go index eea9f125..9856a3bf 100644 --- a/aggsender/aggsender.go +++ b/aggsender/aggsender.go @@ -152,16 +152,14 @@ 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 { - // if the last certificate was in error, we need to resend it - // from the block before the error - previousToBlock = lastSentCertificateInfo.FromBlock - 1 + previousToBlock := uint64(0) + if lastSentCertificateInfo != nil { + previousToBlock = lastSentCertificateInfo.ToBlock + if lastSentCertificateInfo.Status == agglayer.InError { + // if the last certificate was in error, we need to resend it + // from the block before the error + previousToBlock = lastSentCertificateInfo.FromBlock - 1 + } } if previousToBlock >= lasL2BlockSynced { @@ -190,7 +188,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) } @@ -215,15 +213,17 @@ func (a *AggSender) sendCertificate(ctx context.Context) (*agglayer.SignedCertif } createdTime := time.Now().UTC().UnixMilli() + prevLER := common.BytesToHash(certificate.PrevLocalExitRoot[:]) certInfo := types.CertificateInfo{ - Height: certificate.Height, - CertificateID: certificateHash, - NewLocalExitRoot: certificate.NewLocalExitRoot, - FromBlock: fromBlock, - ToBlock: toBlock, - CreatedAt: createdTime, - UpdatedAt: createdTime, - SignedCertificate: string(raw), + Height: certificate.Height, + CertificateID: certificateHash, + NewLocalExitRoot: certificate.NewLocalExitRoot, + PreviousLocalExitRoot: &prevLER, + FromBlock: fromBlock, + ToBlock: toBlock, + CreatedAt: createdTime, + UpdatedAt: createdTime, + SignedCertificate: string(raw), } // 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) @@ -278,31 +278,53 @@ func (a *AggSender) saveCertificateToFile(signedCertificate *agglayer.SignedCert // getNextHeightAndPreviousLER returns the height and previous LER for the new certificate func (a *AggSender) getNextHeightAndPreviousLER( - lastSentCertificateInfo *types.CertificateInfo) (uint64, common.Hash) { - height := lastSentCertificateInfo.Height + 1 - 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) - height = lastSentCertificateInfo.Height + lastSentCertificateInfo *types.CertificateInfo) (uint64, common.Hash, error) { + if lastSentCertificateInfo == nil { + return 0, zeroLER, nil } - - previousLER := lastSentCertificateInfo.NewLocalExitRoot - if lastSentCertificateInfo.NewLocalExitRoot == (common.Hash{}) || - lastSentCertificateInfo.Height == 0 && lastSentCertificateInfo.Status.IsInError() { - // meaning this is the first certificate - height = 0 - previousLER = zeroLER + if !lastSentCertificateInfo.Status.IsClosed() { + return 0, zeroLER, fmt.Errorf("last certificate %s is not closed (status: %s)", + lastSentCertificateInfo.ID(), lastSentCertificateInfo.Status.String()) + } + if lastSentCertificateInfo.Status.IsSettled() { + return lastSentCertificateInfo.Height + 1, lastSentCertificateInfo.NewLocalExitRoot, nil } - return height, previousLER + if lastSentCertificateInfo.Status.IsInError() { + // We can reuse last one of lastCert? + if lastSentCertificateInfo.PreviousLocalExitRoot != nil { + return lastSentCertificateInfo.Height, *lastSentCertificateInfo.PreviousLocalExitRoot, nil + } + // Is the first one, so we can set the zeroLER + if lastSentCertificateInfo.Height == 0 { + return 0, zeroLER, nil + } + // We get previous certificate that must be settled + a.log.Debugf("last certificate %s is in error, getting previous settled certificate height:%d", + lastSentCertificateInfo.Height-1) + lastSettleCert, err := a.storage.GetCertificateByHeight(lastSentCertificateInfo.Height - 1) + if err != nil { + return 0, common.Hash{}, fmt.Errorf("error getting last settled certificate: %w", err) + } + if lastSettleCert == nil { + return 0, common.Hash{}, fmt.Errorf("none settled certificate: %w", err) + } + if !lastSettleCert.Status.IsSettled() { + return 0, common.Hash{}, fmt.Errorf("last settled certificate %s is not settled (status: %s)", + lastSettleCert.ID(), lastSettleCert.Status.String()) + } + + return lastSentCertificateInfo.Height, lastSettleCert.NewLocalExitRoot, nil + } + return 0, zeroLER, fmt.Errorf("last certificate %s has an unknown status: %s", + lastSentCertificateInfo.ID(), lastSentCertificateInfo.Status.String()) } // buildCertificate builds a certificate from the bridge events func (a *AggSender) buildCertificate(ctx context.Context, bridges []bridgesync.Bridge, claims []bridgesync.Claim, - lastSentCertificateInfo types.CertificateInfo, + lastSentCertificateInfo *types.CertificateInfo, toBlock uint64) (*agglayer.Certificate, error) { if len(bridges) == 0 && len(claims) == 0 { return nil, errNoBridgesAndClaims @@ -325,7 +347,10 @@ func (a *AggSender) buildCertificate(ctx context.Context, return nil, fmt.Errorf("error getting exit root by index: %d. Error: %w", depositCount, err) } - height, previousLER := a.getNextHeightAndPreviousLER(&lastSentCertificateInfo) + height, previousLER, err := a.getNextHeightAndPreviousLER(lastSentCertificateInfo) + if err != nil { + return nil, fmt.Errorf("error getting next height and previous LER: %w", err) + } return &agglayer.Certificate{ NetworkID: a.l2Syncer.OriginNetwork(), @@ -709,7 +734,7 @@ func NewCertificateInfoFromAgglayerCertHeader(c *agglayer.CertificateHeader) *ty return nil } now := time.Now().UTC().UnixMilli() - return &types.CertificateInfo{ + res := &types.CertificateInfo{ Height: c.Height, CertificateID: c.CertificateID, NewLocalExitRoot: c.NewLocalExitRoot, @@ -720,4 +745,8 @@ func NewCertificateInfoFromAgglayerCertHeader(c *agglayer.CertificateHeader) *ty UpdatedAt: now, SignedCertificate: "na/agglayer header", } + if c.PreviousLocalExitRoot != nil { + res.PreviousLocalExitRoot = c.PreviousLocalExitRoot + } + return res } diff --git a/aggsender/aggsender_test.go b/aggsender/aggsender_test.go index 9185050f..79504f6a 100644 --- a/aggsender/aggsender_test.go +++ b/aggsender/aggsender_test.go @@ -33,6 +33,8 @@ const ( var ( errTest = errors.New("unitest error") + ler1 = common.HexToHash("0x123") + ler2 = common.HexToHash("0x12345") ) func TestConfigString(t *testing.T) { @@ -283,7 +285,7 @@ func TestAggSenderStart(t *testing.T) { ctx, log.WithFields("test", "unittest"), Config{ - StoragePath: "file::memory:?cache=shared", + StoragePath: "file:TestAggSenderStart?mode=memory&cache=shared", DelayBeetweenRetries: types.Duration{Duration: 1 * time.Microsecond}, }, aggLayerMock, @@ -627,6 +629,7 @@ func TestBuildCertificate(t *testing.T) { lastSentCertificateInfo: aggsendertypes.CertificateInfo{ NewLocalExitRoot: common.HexToHash("0x123"), Height: 1, + Status: agglayer.Settled, }, toBlock: 10, expectedCert: &agglayer.Certificate{ @@ -784,7 +787,7 @@ func TestBuildCertificate(t *testing.T) { l1infoTreeSyncer: mockL1InfoTreeSyncer, log: log.WithFields("test", "unittest"), } - cert, err := aggSender.buildCertificate(context.Background(), tt.bridges, tt.claims, tt.lastSentCertificateInfo, tt.toBlock) + cert, err := aggSender.buildCertificate(context.Background(), tt.bridges, tt.claims, &tt.lastSentCertificateInfo, tt.toBlock) if tt.expectedError { require.Error(t, err) @@ -957,7 +960,7 @@ func TestSendCertificate(t *testing.T) { var ( aggsender = &AggSender{ log: log.WithFields("aggsender", 1), - cfg: Config{MaxRetriesStoreCertificate: 3}, + cfg: Config{MaxRetriesStoreCertificate: 1}, sequencerKey: cfg.sequencerKey, } mockStorage *mocks.AggSenderStorage @@ -1223,11 +1226,13 @@ func TestSendCertificate(t *testing.T) { shouldSendCertificate: []interface{}{[]*aggsendertypes.CertificateInfo{}, nil}, lastL2BlockProcessed: []interface{}{uint64(99), nil}, getLastSentCertificate: []interface{}{&aggsendertypes.CertificateInfo{ - Height: 90, - CertificateID: common.HexToHash("0x1121111"), - NewLocalExitRoot: common.HexToHash("0x111122211"), - FromBlock: 80, - ToBlock: 81, + Height: 90, + CertificateID: common.HexToHash("0x1121111"), + NewLocalExitRoot: common.HexToHash("0x111122211"), + PreviousLocalExitRoot: &ler1, + FromBlock: 80, + ToBlock: 81, + Status: agglayer.Settled, }, nil}, getBridges: []interface{}{[]bridgesync.Bridge{ { @@ -1255,6 +1260,7 @@ func TestSendCertificate(t *testing.T) { NewLocalExitRoot: common.HexToHash("0x1211122211"), FromBlock: 90, ToBlock: 91, + Status: agglayer.Settled, }, nil}, getBridges: []interface{}{[]bridgesync.Bridge{ { @@ -1283,6 +1289,7 @@ func TestSendCertificate(t *testing.T) { NewLocalExitRoot: common.HexToHash("0x1221122211"), FromBlock: 100, ToBlock: 101, + Status: agglayer.Settled, }, nil}, getBridges: []interface{}{[]bridgesync.Bridge{ { @@ -1489,14 +1496,18 @@ func TestGetNextHeightAndPreviousLER(t *testing.T) { t.Parallel() tests := []struct { - name string - lastSentCertificateInfo aggsendertypes.CertificateInfo - expectedHeight uint64 - expectedPreviousLER common.Hash + name string + lastSentCertificateInfo *aggsendertypes.CertificateInfo + lastSettleCertificateInfoCall bool + lastSettleCertificateInfo *aggsendertypes.CertificateInfo + lastSettleCertificateInfoError error + expectedHeight uint64 + expectedPreviousLER common.Hash + expectedError bool }{ { name: "Normal case", - lastSentCertificateInfo: aggsendertypes.CertificateInfo{ + lastSentCertificateInfo: &aggsendertypes.CertificateInfo{ Height: 10, NewLocalExitRoot: common.HexToHash("0x123"), Status: agglayer.Settled, @@ -1505,24 +1516,107 @@ func TestGetNextHeightAndPreviousLER(t *testing.T) { expectedPreviousLER: common.HexToHash("0x123"), }, { - name: "Previous certificate in error", - lastSentCertificateInfo: aggsendertypes.CertificateInfo{ - Height: 10, + name: "First certificate", + lastSentCertificateInfo: nil, + expectedHeight: 0, + expectedPreviousLER: zeroLER, + }, + { + name: "First certificate error, with prevLER", + lastSentCertificateInfo: &aggsendertypes.CertificateInfo{ + Height: 0, + NewLocalExitRoot: common.HexToHash("0x123"), + Status: agglayer.InError, + PreviousLocalExitRoot: &ler1, + }, + expectedHeight: 0, + expectedPreviousLER: ler1, + }, + { + name: "First certificate error, no prevLER", + lastSentCertificateInfo: &aggsendertypes.CertificateInfo{ + Height: 0, NewLocalExitRoot: common.HexToHash("0x123"), Status: agglayer.InError, }, + expectedHeight: 0, + expectedPreviousLER: zeroLER, + }, + { + name: "n certificate error, prevLER", + lastSentCertificateInfo: &aggsendertypes.CertificateInfo{ + Height: 10, + NewLocalExitRoot: common.HexToHash("0x123"), + PreviousLocalExitRoot: &ler1, + Status: agglayer.InError, + }, expectedHeight: 10, - expectedPreviousLER: common.HexToHash("0x123"), + expectedPreviousLER: ler1, }, { - name: "First certificate", - lastSentCertificateInfo: aggsendertypes.CertificateInfo{ - Height: 0, - NewLocalExitRoot: common.Hash{}, + name: "last cert not closed, error", + lastSentCertificateInfo: &aggsendertypes.CertificateInfo{ + Height: 10, + NewLocalExitRoot: common.HexToHash("0x123"), + PreviousLocalExitRoot: &ler1, + Status: agglayer.Pending, + }, + expectedHeight: 10, + expectedPreviousLER: ler1, + expectedError: true, + }, + { + name: "Previous certificate in error, no prevLER", + lastSentCertificateInfo: &aggsendertypes.CertificateInfo{ + Height: 10, + NewLocalExitRoot: common.HexToHash("0x123"), + Status: agglayer.InError, + }, + lastSettleCertificateInfo: &aggsendertypes.CertificateInfo{ + Height: 9, + NewLocalExitRoot: common.HexToHash("0x3456"), Status: agglayer.Settled, }, - expectedHeight: 0, - expectedPreviousLER: zeroLER, + expectedHeight: 10, + expectedPreviousLER: common.HexToHash("0x3456"), + }, + { + name: "Previous certificate in error, no prevLER. Error getting previous cert", + lastSentCertificateInfo: &aggsendertypes.CertificateInfo{ + Height: 10, + NewLocalExitRoot: common.HexToHash("0x123"), + Status: agglayer.InError, + }, + lastSettleCertificateInfo: nil, + lastSettleCertificateInfoError: errors.New("error getting last settle certificate"), + expectedError: true, + }, + { + name: "Previous certificate in error, no prevLER. prev cert not available on storage", + lastSentCertificateInfo: &aggsendertypes.CertificateInfo{ + Height: 10, + NewLocalExitRoot: common.HexToHash("0x123"), + Status: agglayer.InError, + }, + lastSettleCertificateInfoCall: true, + lastSettleCertificateInfo: nil, + lastSettleCertificateInfoError: nil, + expectedError: true, + }, + { + name: "Previous certificate in error, no prevLER. prev cert not available on storage", + lastSentCertificateInfo: &aggsendertypes.CertificateInfo{ + Height: 10, + NewLocalExitRoot: common.HexToHash("0x123"), + Status: agglayer.InError, + }, + lastSettleCertificateInfo: &aggsendertypes.CertificateInfo{ + Height: 9, + NewLocalExitRoot: common.HexToHash("0x3456"), + Status: agglayer.InError, + }, + lastSettleCertificateInfoError: nil, + expectedError: true, }, } @@ -1531,12 +1625,19 @@ func TestGetNextHeightAndPreviousLER(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - - aggSender := &AggSender{log: log.WithFields("aggsender-test", "getNextHeightAndPreviousLER")} - height, previousLER := aggSender.getNextHeightAndPreviousLER(&tt.lastSentCertificateInfo) - - require.Equal(t, tt.expectedHeight, height) - require.Equal(t, tt.expectedPreviousLER, previousLER) + storageMock := mocks.NewAggSenderStorage(t) + aggSender := &AggSender{log: log.WithFields("aggsender-test", "getNextHeightAndPreviousLER"), storage: storageMock} + if tt.lastSettleCertificateInfoCall || tt.lastSettleCertificateInfo != nil || tt.lastSettleCertificateInfoError != nil { + storageMock.EXPECT().GetCertificateByHeight(mock.Anything).Return(tt.lastSettleCertificateInfo, tt.lastSettleCertificateInfoError).Once() + } + height, previousLER, err := aggSender.getNextHeightAndPreviousLER(tt.lastSentCertificateInfo) + if tt.expectedError { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.expectedHeight, height) + require.Equal(t, tt.expectedPreviousLER, previousLER) + } }) } } @@ -1572,6 +1673,7 @@ func TestSendCertificate_NoClaims(t *testing.T) { Height: 1, FromBlock: 0, ToBlock: 10, + Status: agglayer.Settled, }, nil).Once() mockStorage.On("SaveLastSentCertificate", mock.Anything, mock.Anything).Return(nil).Once() mockL2Syncer.On("GetLastProcessedBlock", mock.Anything).Return(uint64(50), nil) diff --git a/aggsender/db/aggsender_db_storage_test.go b/aggsender/db/aggsender_db_storage_test.go index 642c8ae7..15c017bc 100644 --- a/aggsender/db/aggsender_db_storage_test.go +++ b/aggsender/db/aggsender_db_storage_test.go @@ -368,3 +368,40 @@ func Test_SaveLastSentCertificate(t *testing.T) { require.NoError(t, storage.clean()) }) } + +func Test_StoragePreviousLER(t *testing.T) { + ctx := context.TODO() + dbPath := path.Join(t.TempDir(), "Test_StoragePreviousLER.sqlite") + storage, err := NewAggSenderSQLStorage(log.WithFields("aggsender-db"), dbPath) + require.NoError(t, err) + require.NotNil(t, storage) + + certNoLER := types.CertificateInfo{ + Height: 0, + CertificateID: common.HexToHash("0x1"), + Status: agglayer.InError, + NewLocalExitRoot: common.HexToHash("0x2"), + } + err = storage.SaveLastSentCertificate(ctx, certNoLER) + require.NoError(t, err) + + readCertNoLER, err := storage.GetCertificateByHeight(0) + require.NoError(t, err) + require.NotNil(t, readCertNoLER) + require.Equal(t, certNoLER, *readCertNoLER) + + certLER := types.CertificateInfo{ + Height: 1, + CertificateID: common.HexToHash("0x2"), + Status: agglayer.InError, + NewLocalExitRoot: common.HexToHash("0x2"), + PreviousLocalExitRoot: &common.Hash{}, + } + err = storage.SaveLastSentCertificate(ctx, certLER) + require.NoError(t, err) + + readCertWithLER, err := storage.GetCertificateByHeight(1) + require.NoError(t, err) + require.NotNil(t, readCertWithLER) + require.Equal(t, certLER, *readCertWithLER) +} diff --git a/aggsender/db/migrations/0001.sql b/aggsender/db/migrations/0001.sql index b2d600b8..bdc2b3d3 100644 --- a/aggsender/db/migrations/0001.sql +++ b/aggsender/db/migrations/0001.sql @@ -6,6 +6,7 @@ CREATE TABLE certificate_info ( height INTEGER NOT NULL, certificate_id VARCHAR NOT NULL PRIMARY KEY, status INTEGER NOT NULL, + previous_local_exit_root VARCHAR , new_local_exit_root VARCHAR NOT NULL, from_block INTEGER NOT NULL, to_block INTEGER NOT NULL, diff --git a/aggsender/mocks/logger.go b/aggsender/mocks/logger.go index bb26739e..54be6942 100644 --- a/aggsender/mocks/logger.go +++ b/aggsender/mocks/logger.go @@ -189,6 +189,50 @@ func (_c *Logger_Errorf_Call) RunAndReturn(run func(string, ...interface{})) *Lo return _c } +// Fatalf provides a mock function with given fields: format, args +func (_m *Logger) Fatalf(format string, args ...interface{}) { + var _ca []interface{} + _ca = append(_ca, format) + _ca = append(_ca, args...) + _m.Called(_ca...) +} + +// Logger_Fatalf_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Fatalf' +type Logger_Fatalf_Call struct { + *mock.Call +} + +// Fatalf is a helper method to define mock.On call +// - format string +// - args ...interface{} +func (_e *Logger_Expecter) Fatalf(format interface{}, args ...interface{}) *Logger_Fatalf_Call { + return &Logger_Fatalf_Call{Call: _e.mock.On("Fatalf", + append([]interface{}{format}, args...)...)} +} + +func (_c *Logger_Fatalf_Call) Run(run func(format string, args ...interface{})) *Logger_Fatalf_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), variadicArgs...) + }) + return _c +} + +func (_c *Logger_Fatalf_Call) Return() *Logger_Fatalf_Call { + _c.Call.Return() + return _c +} + +func (_c *Logger_Fatalf_Call) RunAndReturn(run func(string, ...interface{})) *Logger_Fatalf_Call { + _c.Call.Return(run) + return _c +} + // Info provides a mock function with given fields: args func (_m *Logger) Info(args ...interface{}) { var _ca []interface{} diff --git a/aggsender/types/types.go b/aggsender/types/types.go index d6f2650e..3199932b 100644 --- a/aggsender/types/types.go +++ b/aggsender/types/types.go @@ -55,24 +55,32 @@ type Logger interface { } type CertificateInfo struct { - Height uint64 `meddler:"height"` - CertificateID common.Hash `meddler:"certificate_id,hash"` - NewLocalExitRoot common.Hash `meddler:"new_local_exit_root,hash"` - FromBlock uint64 `meddler:"from_block"` - ToBlock uint64 `meddler:"to_block"` - Status agglayer.CertificateStatus `meddler:"status"` - CreatedAt int64 `meddler:"created_at"` - UpdatedAt int64 `meddler:"updated_at"` - SignedCertificate string `meddler:"signed_certificate"` + Height uint64 `meddler:"height"` + CertificateID common.Hash `meddler:"certificate_id,hash"` + // PreviousLocalExitRoot if it's nil means no reported + PreviousLocalExitRoot *common.Hash `meddler:"previous_local_exit_root,hash"` + NewLocalExitRoot common.Hash `meddler:"new_local_exit_root,hash"` + FromBlock uint64 `meddler:"from_block"` + ToBlock uint64 `meddler:"to_block"` + Status agglayer.CertificateStatus `meddler:"status"` + CreatedAt int64 `meddler:"created_at"` + UpdatedAt int64 `meddler:"updated_at"` + SignedCertificate string `meddler:"signed_certificate"` } func (c *CertificateInfo) String() string { if c == nil { + //nolint:all return "nil" } + previousLocalExitRoot := "nil" + if c.PreviousLocalExitRoot != nil { + previousLocalExitRoot = c.PreviousLocalExitRoot.String() + } return fmt.Sprintf( "Height: %d "+ "CertificateID: %s "+ + "PreviousLocalExitRoot: %s "+ "NewLocalExitRoot: %s "+ "Status: %s "+ "FromBlock: %d "+ @@ -81,6 +89,7 @@ func (c *CertificateInfo) String() string { "UpdatedAt: %s", c.Height, c.CertificateID.String(), + previousLocalExitRoot, c.NewLocalExitRoot.String(), c.Status.String(), c.FromBlock, diff --git a/db/meddler.go b/db/meddler.go index 8dd17fe8..6eaa5994 100644 --- a/db/meddler.go +++ b/db/meddler.go @@ -163,6 +163,16 @@ func (b HashMeddler) PostRead(fieldPtr, scanTarget interface{}) error { } field, ok := fieldPtr.(*common.Hash) if !ok { + hashPtr, ok := fieldPtr.(**common.Hash) + if ok { + if ptr == nil || len(*ptr) == 0 { + *hashPtr = nil + } else { + tmp := common.HexToHash(*ptr) + *hashPtr = &tmp + } + return nil + } return errors.New("fieldPtr is not common.Hash") } *field = common.HexToHash(*ptr) @@ -173,7 +183,14 @@ func (b HashMeddler) PostRead(fieldPtr, scanTarget interface{}) error { func (b HashMeddler) PreWrite(fieldPtr interface{}) (saveValue interface{}, err error) { field, ok := fieldPtr.(common.Hash) if !ok { - return nil, errors.New("fieldPtr is not common.Hash") + hashPtr, ok := fieldPtr.(*common.Hash) + if !ok { + return nil, errors.New("fieldPtr is not common.Hash") + } + if hashPtr == nil { + return []byte{}, nil + } + return hashPtr.Hex(), nil } return field.Hex(), nil }