From 4c85ece861433084274d5c1ec2b69950f4f9d03a Mon Sep 17 00:00:00 2001 From: Goran Rojovic Date: Wed, 30 Oct 2024 17:10:38 +0100 Subject: [PATCH 1/3] feat: use metadata field on certificate --- agglayer/types.go | 21 ++++++++++++- aggsender/aggsender.go | 20 +++++++++++-- aggsender/aggsender_test.go | 5 +++- common/common.go | 16 ++++++++++ common/common_test.go | 59 +++++++++++++++++++++++++++++++++++++ 5 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 common/common_test.go diff --git a/agglayer/types.go b/agglayer/types.go index e8bdb254..387c2d79 100644 --- a/agglayer/types.go +++ b/agglayer/types.go @@ -83,6 +83,7 @@ type Certificate struct { NewLocalExitRoot [32]byte `json:"new_local_exit_root"` BridgeExits []*BridgeExit `json:"bridge_exits"` ImportedBridgeExits []*ImportedBridgeExit `json:"imported_bridge_exits"` + Metadata common.Hash `json:"metadata"` } // Hash returns a hash that uniquely identifies the certificate @@ -110,6 +111,20 @@ func (c *Certificate) Hash() common.Hash { ) } +// HashToSign is the actual hash that needs to be signed by the aggsender +// as expected by the agglayer +func (c *Certificate) HashToSign() common.Hash { + globalIndexHashes := make([][]byte, len(c.ImportedBridgeExits)) + for i, importedBridgeExit := range c.ImportedBridgeExits { + globalIndexHashes[i] = importedBridgeExit.GlobalIndex.Hash().Bytes() + } + + return crypto.Keccak256Hash( + c.NewLocalExitRoot[:], + crypto.Keccak256Hash(globalIndexHashes...).Bytes(), + ) +} + // SignedCertificate is the struct that contains the certificate and the signature of the signer type SignedCertificate struct { *Certificate @@ -138,7 +153,10 @@ type GlobalIndex struct { func (g *GlobalIndex) Hash() common.Hash { return crypto.Keccak256Hash( - bridgesync.GenerateGlobalIndex(g.MainnetFlag, g.RollupIndex, g.LeafIndex).Bytes()) + cdkcommon.AsLittleEndianSlice( + bridgesync.GenerateGlobalIndex(g.MainnetFlag, g.RollupIndex, g.LeafIndex), + ), + ) } // BridgeExit represents a token bridge exit @@ -379,6 +397,7 @@ type CertificateHeader struct { CertificateID common.Hash `json:"certificate_id"` NewLocalExitRoot common.Hash `json:"new_local_exit_root"` Status CertificateStatus `json:"status"` + Metadata common.Hash `json:"metadata"` } func (c CertificateHeader) String() string { diff --git a/aggsender/aggsender.go b/aggsender/aggsender.go index a228e1a9..f1df20ff 100644 --- a/aggsender/aggsender.go +++ b/aggsender/aggsender.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "math/big" "os" "time" @@ -153,7 +154,7 @@ func (a *AggSender) sendCertificate(ctx context.Context) error { a.log.Infof("building certificate for block: %d to block: %d", fromBlock, toBlock) - certificate, err := a.buildCertificate(ctx, bridges, claims, lastSentCertificateInfo) + certificate, err := a.buildCertificate(ctx, bridges, claims, lastSentCertificateInfo, toBlock) if err != nil { return fmt.Errorf("error building certificate: %w", err) } @@ -209,7 +210,8 @@ func (a *AggSender) saveCertificateToFile(signedCertificate *agglayer.SignedCert func (a *AggSender) buildCertificate(ctx context.Context, bridges []bridgesync.Bridge, claims []bridgesync.Claim, - lastSentCertificateInfo aggsendertypes.CertificateInfo) (*agglayer.Certificate, error) { + lastSentCertificateInfo aggsendertypes.CertificateInfo, + toBlock uint64) (*agglayer.Certificate, error) { if len(bridges) == 0 && len(claims) == 0 { return nil, errNoBridgesAndClaims } @@ -245,6 +247,7 @@ func (a *AggSender) buildCertificate(ctx context.Context, BridgeExits: bridgeExits, ImportedBridgeExits: importedBridgeExits, Height: height, + Metadata: createCertificateMetadata(toBlock), }, nil } @@ -412,13 +415,19 @@ func (a *AggSender) getImportedBridgeExits( // signCertificate signs a certificate with the sequencer key func (a *AggSender) signCertificate(certificate *agglayer.Certificate) (*agglayer.SignedCertificate, error) { - hashToSign := certificate.Hash() + hashToSign := certificate.HashToSign() sig, err := crypto.Sign(hashToSign.Bytes(), a.sequencerKey) if err != nil { return nil, err } + a.log.Infof("Signed certificate. sequencer address: %s. New local exit root: %s Hash signed: %s", + crypto.PubkeyToAddress(a.sequencerKey.PublicKey).String(), + common.BytesToHash(certificate.NewLocalExitRoot[:]).String(), + hashToSign.String(), + ) + r, s, isOddParity, err := extractSignatureData(sig) if err != nil { return nil, err @@ -500,3 +509,8 @@ func extractSignatureData(signature []byte) (r, s common.Hash, isOddParity bool, return } + +// createCertificateMetadata creates a certificate metadata from given input +func createCertificateMetadata(toBlock uint64) common.Hash { + return common.BigToHash(new(big.Int).SetUint64(toBlock)) +} diff --git a/aggsender/aggsender_test.go b/aggsender/aggsender_test.go index 69dc6ed1..71878679 100644 --- a/aggsender/aggsender_test.go +++ b/aggsender/aggsender_test.go @@ -493,6 +493,7 @@ func TestBuildCertificate(t *testing.T) { bridges []bridgesync.Bridge claims []bridgesync.Claim lastSentCertificateInfo aggsendertypes.CertificateInfo + toBlock uint64 mockFn func() expectedCert *agglayer.Certificate expectedError bool @@ -532,10 +533,12 @@ func TestBuildCertificate(t *testing.T) { NewLocalExitRoot: common.HexToHash("0x123"), Height: 1, }, + toBlock: 10, expectedCert: &agglayer.Certificate{ NetworkID: 1, PrevLocalExitRoot: common.HexToHash("0x123"), NewLocalExitRoot: common.HexToHash("0x789"), + Metadata: createCertificateMetadata(10), BridgeExits: []*agglayer.BridgeExit{ { LeafType: agglayer.LeafTypeAsset, @@ -686,7 +689,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) + cert, err := aggSender.buildCertificate(context.Background(), tt.bridges, tt.claims, tt.lastSentCertificateInfo, tt.toBlock) if tt.expectedError { require.Error(t, err) diff --git a/common/common.go b/common/common.go index c74f56e4..bc6befa0 100644 --- a/common/common.go +++ b/common/common.go @@ -109,3 +109,19 @@ func NewKeyFromKeystore(cfg types.KeystoreFileConfig) (*ecdsa.PrivateKey, error) } return key.PrivateKey, nil } + +// AsLittleEndianSlice converts a big.Int to a 32-byte little-endian representation. +func AsLittleEndianSlice(n *big.Int) []byte { + // Get the absolute value in big-endian byte slice + beBytes := n.Bytes() + + // Initialize a 32-byte array for the result + leBytes := make([]byte, 32) + + // Fill the array in reverse order to convert to little-endian + for i := 0; i < len(beBytes) && i < 32; i++ { + leBytes[i] = beBytes[len(beBytes)-1-i] + } + + return leBytes +} diff --git a/common/common_test.go b/common/common_test.go new file mode 100644 index 00000000..09cd1b0a --- /dev/null +++ b/common/common_test.go @@ -0,0 +1,59 @@ +package common + +import ( + "math/big" + "testing" +) + +func TestAsLittleEndianSlice(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input *big.Int + expected []byte + }{ + { + name: "Zero value", + input: big.NewInt(0), + expected: make([]byte, 32), + }, + { + name: "Positive value", + input: big.NewInt(123456789), + expected: append([]byte{21, 205, 91, 7}, make([]byte, 28)...), + }, + { + name: "Negative value", + input: big.NewInt(-123456789), + expected: append([]byte{21, 205, 91, 7}, make([]byte, 28)...), + }, + { + name: "Large positive value", + input: new(big.Int).SetBytes([]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}), + expected: []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, + }, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + result := AsLittleEndianSlice(tt.input) + if len(result) != 32 { + t.Errorf("expected length 32, got %d", len(result)) + } + for i := range result { + if result[i] != tt.expected[i] { + t.Errorf("expected byte at index %d to be %x, got %x", i, tt.expected[i], result[i]) + } + } + }) + } +} From 1fc06b4451ffbf82e5b90312a7ebd28b81038f18 Mon Sep 17 00:00:00 2001 From: Goran Rojovic Date: Thu, 31 Oct 2024 11:53:19 +0100 Subject: [PATCH 2/3] fix: lint and UT --- agglayer/types_test.go | 4 ++-- common/common.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/agglayer/types_test.go b/agglayer/types_test.go index 1df1f20f..325c0b88 100644 --- a/agglayer/types_test.go +++ b/agglayer/types_test.go @@ -11,8 +11,8 @@ import ( ) const ( - expectedSignedCertificateEmptyMetadataJSON = `{"network_id":1,"height":1,"prev_local_exit_root":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"new_local_exit_root":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"bridge_exits":[{"leaf_type":"Transfer","token_info":null,"dest_network":0,"dest_address":"0x0000000000000000000000000000000000000000","amount":"1","metadata":[]}],"imported_bridge_exits":[{"bridge_exit":{"leaf_type":"Transfer","token_info":null,"dest_network":0,"dest_address":"0x0000000000000000000000000000000000000000","amount":"1","metadata":[]},"claim_data":null,"global_index":{"mainnet_flag":false,"rollup_index":1,"leaf_index":1}}],"signature":{"r":"0x0000000000000000000000000000000000000000000000000000000000000000","s":"0x0000000000000000000000000000000000000000000000000000000000000000","odd_y_parity":false}}` - expectedSignedCertificateyMetadataJSON = `{"network_id":1,"height":1,"prev_local_exit_root":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"new_local_exit_root":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"bridge_exits":[{"leaf_type":"Transfer","token_info":null,"dest_network":0,"dest_address":"0x0000000000000000000000000000000000000000","amount":"1","metadata":[1,2,3]}],"imported_bridge_exits":[{"bridge_exit":{"leaf_type":"Transfer","token_info":null,"dest_network":0,"dest_address":"0x0000000000000000000000000000000000000000","amount":"1","metadata":[]},"claim_data":null,"global_index":{"mainnet_flag":false,"rollup_index":1,"leaf_index":1}}],"signature":{"r":"0x0000000000000000000000000000000000000000000000000000000000000000","s":"0x0000000000000000000000000000000000000000000000000000000000000000","odd_y_parity":false}}` + expectedSignedCertificateEmptyMetadataJSON = `{"network_id":1,"height":1,"prev_local_exit_root":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"new_local_exit_root":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"bridge_exits":[{"leaf_type":"Transfer","token_info":null,"dest_network":0,"dest_address":"0x0000000000000000000000000000000000000000","amount":"1","metadata":[]}],"imported_bridge_exits":[{"bridge_exit":{"leaf_type":"Transfer","token_info":null,"dest_network":0,"dest_address":"0x0000000000000000000000000000000000000000","amount":"1","metadata":[]},"claim_data":null,"global_index":{"mainnet_flag":false,"rollup_index":1,"leaf_index":1}}],"metadata":"0x0000000000000000000000000000000000000000000000000000000000000000","signature":{"r":"0x0000000000000000000000000000000000000000000000000000000000000000","s":"0x0000000000000000000000000000000000000000000000000000000000000000","odd_y_parity":false}}` + expectedSignedCertificateyMetadataJSON = `{"network_id":1,"height":1,"prev_local_exit_root":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"new_local_exit_root":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"bridge_exits":[{"leaf_type":"Transfer","token_info":null,"dest_network":0,"dest_address":"0x0000000000000000000000000000000000000000","amount":"1","metadata":[1,2,3]}],"imported_bridge_exits":[{"bridge_exit":{"leaf_type":"Transfer","token_info":null,"dest_network":0,"dest_address":"0x0000000000000000000000000000000000000000","amount":"1","metadata":[]},"claim_data":null,"global_index":{"mainnet_flag":false,"rollup_index":1,"leaf_index":1}}],"metadata":"0x0000000000000000000000000000000000000000000000000000000000000000","signature":{"r":"0x0000000000000000000000000000000000000000000000000000000000000000","s":"0x0000000000000000000000000000000000000000000000000000000000000000","odd_y_parity":false}}` ) func TestMarshalJSON(t *testing.T) { diff --git a/common/common.go b/common/common.go index bc6befa0..b1a1b384 100644 --- a/common/common.go +++ b/common/common.go @@ -116,10 +116,10 @@ func AsLittleEndianSlice(n *big.Int) []byte { beBytes := n.Bytes() // Initialize a 32-byte array for the result - leBytes := make([]byte, 32) + leBytes := make([]byte, common.HashLength) // Fill the array in reverse order to convert to little-endian - for i := 0; i < len(beBytes) && i < 32; i++ { + for i := 0; i < len(beBytes) && i < common.HashLength; i++ { leBytes[i] = beBytes[len(beBytes)-1-i] } From 4e61a9d540c71ae6f9d1bdbe77b62201bd621148 Mon Sep 17 00:00:00 2001 From: Goran Rojovic Date: Thu, 31 Oct 2024 14:18:32 +0100 Subject: [PATCH 3/3] fix: comments --- agglayer/types.go | 2 +- common/common.go | 5 +++-- common/common_test.go | 16 +++++++++------- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/agglayer/types.go b/agglayer/types.go index 387c2d79..825c9db2 100644 --- a/agglayer/types.go +++ b/agglayer/types.go @@ -153,7 +153,7 @@ type GlobalIndex struct { func (g *GlobalIndex) Hash() common.Hash { return crypto.Keccak256Hash( - cdkcommon.AsLittleEndianSlice( + cdkcommon.BigIntToLittleEndianBytes( bridgesync.GenerateGlobalIndex(g.MainnetFlag, g.RollupIndex, g.LeafIndex), ), ) diff --git a/common/common.go b/common/common.go index b1a1b384..f8b92d16 100644 --- a/common/common.go +++ b/common/common.go @@ -110,8 +110,9 @@ func NewKeyFromKeystore(cfg types.KeystoreFileConfig) (*ecdsa.PrivateKey, error) return key.PrivateKey, nil } -// AsLittleEndianSlice converts a big.Int to a 32-byte little-endian representation. -func AsLittleEndianSlice(n *big.Int) []byte { +// BigIntToLittleEndianBytes converts a big.Int to a 32-byte little-endian representation. +// big.Int is capped to 32 bytes +func BigIntToLittleEndianBytes(n *big.Int) []byte { // Get the absolute value in big-endian byte slice beBytes := n.Bytes() diff --git a/common/common_test.go b/common/common_test.go index 09cd1b0a..b6b99c5f 100644 --- a/common/common_test.go +++ b/common/common_test.go @@ -1,8 +1,12 @@ package common import ( + "fmt" "math/big" "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" ) func TestAsLittleEndianSlice(t *testing.T) { @@ -45,14 +49,12 @@ func TestAsLittleEndianSlice(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - result := AsLittleEndianSlice(tt.input) - if len(result) != 32 { - t.Errorf("expected length 32, got %d", len(result)) - } + result := BigIntToLittleEndianBytes(tt.input) + require.Len(t, result, common.HashLength) + for i := range result { - if result[i] != tt.expected[i] { - t.Errorf("expected byte at index %d to be %x, got %x", i, tt.expected[i], result[i]) - } + require.Equal(t, tt.expected[i], result[i], + fmt.Sprintf("expected byte at index %d to be %x, got %x", i, tt.expected[i], result[i])) } }) }