diff --git a/aggregator/agglayer_client.go b/agglayer/client.go similarity index 81% rename from aggregator/agglayer_client.go rename to agglayer/client.go index 4726ccc1..954444cf 100644 --- a/aggregator/agglayer_client.go +++ b/agglayer/client.go @@ -1,4 +1,4 @@ -package aggregator +package agglayer import ( "context" @@ -17,6 +17,7 @@ import ( type AgglayerClientInterface interface { SendTx(signedTx SignedTx) (common.Hash, error) WaitTxToBeMined(hash common.Hash, ctx context.Context) error + SendCertificate(certificate *SignedCertificate) error } // AggLayerClient is the client that will be used to interact with the AggLayer @@ -79,3 +80,17 @@ func (c *AggLayerClient) WaitTxToBeMined(hash common.Hash, ctx context.Context) } } } + +// SendCertificate sends a certificate to the AggLayer +func (c *AggLayerClient) SendCertificate(certificate *SignedCertificate) error { + response, err := rpc.JSONRPCCall(c.url, "interop_sendCertificate", certificate) + if err != nil { + return err + } + + if response.Error != nil { + return fmt.Errorf("%v %v", response.Error.Code, response.Error.Message) + } + + return nil +} diff --git a/aggregator/agglayer_tx.go b/agglayer/tx.go similarity index 98% rename from aggregator/agglayer_tx.go rename to agglayer/tx.go index 30a483ae..f024f570 100644 --- a/aggregator/agglayer_tx.go +++ b/agglayer/tx.go @@ -1,4 +1,4 @@ -package aggregator +package agglayer import ( "crypto/ecdsa" diff --git a/agglayer/types.go b/agglayer/types.go new file mode 100644 index 00000000..8b915068 --- /dev/null +++ b/agglayer/types.go @@ -0,0 +1,104 @@ +package agglayer + +import ( + "math/big" + + cdkcommon "github.com/0xPolygon/cdk/common" + "github.com/0xPolygon/cdk/tree/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +type LeafType uint8 + +func (l LeafType) Uint8() uint8 { + return uint8(l) +} + +const ( + LeafTypeAsset LeafType = 0 + LeafTypeMessage LeafType = 1 +) + +// Certificate is the data structure that will be sent to the aggLayer +type Certificate struct { + NetworkID uint32 `json:"network_id"` + Height uint64 `json:"height"` + PrevLocalExitRoot common.Hash `json:"prev_local_exit_root"` + NewLocalExitRoot common.Hash `json:"new_local_exit_root"` + BridgeExits []*BridgeExit `json:"bridge_exits"` + ImportedBridgeExits []*ImportedBridgeExit `json:"imported_bridge_exits"` +} + +// Hash returns a hash that uniquely identifies the certificate +func (c *Certificate) Hash() common.Hash { + importedBridgeHashes := make([][]byte, 0, len(c.ImportedBridgeExits)) + for _, claim := range c.ImportedBridgeExits { + importedBridgeHashes = append(importedBridgeHashes, claim.Hash().Bytes()) + } + + incomeCommitment := crypto.Keccak256Hash(importedBridgeHashes...) + outcomeCommitment := c.NewLocalExitRoot + + return crypto.Keccak256Hash(outcomeCommitment.Bytes(), incomeCommitment.Bytes()) +} + +// SignedCertificate is the struct that contains the certificate and the signature of the signer +type SignedCertificate struct { + *Certificate + Signature []byte `json:"signature"` +} + +// TokenInfo encapsulates the information to uniquely identify a token on the origin network. +type TokenInfo struct { + OriginNetwork uint32 `json:"origin_network"` + OriginTokenAddress common.Address `json:"origin_token_address"` +} + +// GlobalIndex represents the global index of an imported bridge exit +type GlobalIndex struct { + MainnetFlag bool `json:"mainnet_flag"` + RollupIndex uint32 `json:"rollup_index"` + LeafIndex uint32 `json:"leaf_index"` +} + +// BridgeExit represents a token bridge exit +type BridgeExit struct { + LeafType LeafType `json:"leaf_type"` + TokenInfo *TokenInfo `json:"token_info"` + DestinationNetwork uint32 `json:"destination_network"` + DestinationAddress common.Address `json:"destination_address"` + Amount *big.Int `json:"amount"` + Metadata []byte `json:"metadata"` +} + +// Hash returns a hash that uniquely identifies the bridge exit +func (c *BridgeExit) Hash() common.Hash { + if c.Amount == nil { + c.Amount = big.NewInt(0) + } + + return crypto.Keccak256Hash( + []byte{c.LeafType.Uint8()}, + cdkcommon.Uint32ToBytes(c.TokenInfo.OriginNetwork), + c.TokenInfo.OriginTokenAddress.Bytes(), + cdkcommon.Uint32ToBytes(c.DestinationNetwork), + c.DestinationAddress.Bytes(), + c.Amount.Bytes(), + crypto.Keccak256(c.Metadata), + ) +} + +// ImportedBridgeExit represents a token bridge exit originating on another network but claimed on the current network. +type ImportedBridgeExit struct { + BridgeExit *BridgeExit `json:"bridge_exit"` + ImportedLocalExitRoot common.Hash `json:"imported_local_exit_root"` + InclusionProof [types.DefaultHeight]common.Hash `json:"inclusion_proof"` + InclusionProofRER [types.DefaultHeight]common.Hash `json:"inclusion_proof_rer"` + GlobalIndex *GlobalIndex `json:"global_index"` +} + +// Hash returns a hash that uniquely identifies the imported bridge exit +func (c *ImportedBridgeExit) Hash() common.Hash { + return c.BridgeExit.Hash() +} diff --git a/aggregator/aggregator.go b/aggregator/aggregator.go index a97d72f9..8fddc66b 100644 --- a/aggregator/aggregator.go +++ b/aggregator/aggregator.go @@ -16,6 +16,7 @@ import ( "github.com/0xPolygon/cdk-rpc/rpc" cdkTypes "github.com/0xPolygon/cdk-rpc/types" + "github.com/0xPolygon/cdk/agglayer" ethmanTypes "github.com/0xPolygon/cdk/aggregator/ethmantypes" "github.com/0xPolygon/cdk/aggregator/prover" cdkcommon "github.com/0xPolygon/cdk/common" @@ -95,7 +96,7 @@ type Aggregator struct { exit context.CancelFunc sequencerPrivateKey *ecdsa.PrivateKey - aggLayerClient AgglayerClientInterface + aggLayerClient agglayer.AgglayerClientInterface } // New creates a new aggregator. @@ -165,14 +166,14 @@ func New( } var ( - aggLayerClient AgglayerClientInterface + aggLayerClient agglayer.AgglayerClientInterface sequencerPrivateKey *ecdsa.PrivateKey ) if !cfg.SyncModeOnlyEnabled && cfg.SettlementBackend == AggLayer { - aggLayerClient = NewAggLayerClient(cfg.AggLayerURL) + aggLayerClient = agglayer.NewAggLayerClient(cfg.AggLayerURL) - sequencerPrivateKey, err = newKeyFromKeystore(cfg.SequencerPrivateKey) + sequencerPrivateKey, err = cdkcommon.NewKeyFromKeystore(cfg.SequencerPrivateKey) if err != nil { return nil, err } @@ -919,10 +920,10 @@ func (a *Aggregator) settleWithAggLayer( inputs ethmanTypes.FinalProofInputs) bool { proofStrNo0x := strings.TrimPrefix(inputs.FinalProof.Proof, "0x") proofBytes := common.Hex2Bytes(proofStrNo0x) - tx := Tx{ + tx := agglayer.Tx{ LastVerifiedBatch: cdkTypes.ArgUint64(proof.BatchNumber - 1), NewVerifiedBatch: cdkTypes.ArgUint64(proof.BatchNumberFinal), - ZKP: ZKP{ + ZKP: agglayer.ZKP{ NewStateRoot: common.BytesToHash(inputs.NewStateRoot), NewLocalExitRoot: common.BytesToHash(inputs.NewLocalExitRoot), Proof: cdkTypes.ArgBytes(proofBytes), diff --git a/aggregator/config.go b/aggregator/config.go index 4550c637..fd407ff3 100644 --- a/aggregator/config.go +++ b/aggregator/config.go @@ -1,18 +1,14 @@ package aggregator import ( - "crypto/ecdsa" "fmt" "math/big" - "os" - "path/filepath" "github.com/0xPolygon/cdk/aggregator/db" "github.com/0xPolygon/cdk/config/types" "github.com/0xPolygon/cdk/log" "github.com/0xPolygonHermez/zkevm-ethtx-manager/ethtxmanager" syncronizerConfig "github.com/0xPolygonHermez/zkevm-synchronizer-l1/config" - "github.com/ethereum/go-ethereum/accounts/keystore" ) // SettlementBackend is the type of the settlement backend @@ -164,20 +160,3 @@ type StreamClientCfg struct { // Log is the log configuration Log log.Config `mapstructure:"Log"` } - -// newKeyFromKeystore creates a private key from a keystore file -func newKeyFromKeystore(cfg types.KeystoreFileConfig) (*ecdsa.PrivateKey, error) { - if cfg.Path == "" && cfg.Password == "" { - return nil, nil - } - keystoreEncrypted, err := os.ReadFile(filepath.Clean(cfg.Path)) - if err != nil { - return nil, err - } - key, err := keystore.DecryptKey(keystoreEncrypted, cfg.Password) - if err != nil { - return nil, err - } - - return key.PrivateKey, nil -} diff --git a/aggsender/aggsender.go b/aggsender/aggsender.go new file mode 100644 index 00000000..3238fd94 --- /dev/null +++ b/aggsender/aggsender.go @@ -0,0 +1,314 @@ +package aggsender + +import ( + "context" + "crypto/ecdsa" + "encoding/json" + "fmt" + "path/filepath" + "time" + + "github.com/0xPolygon/cdk/agglayer" + "github.com/0xPolygon/cdk/bridgesync" + cdkcommon "github.com/0xPolygon/cdk/common" + "github.com/0xPolygon/cdk/config/types" + "github.com/0xPolygon/cdk/l1infotreesync" + "github.com/0xPolygon/cdk/log" + "github.com/0xPolygon/cdk/tree" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ledgerwatch/erigon-lib/kv" + "github.com/ledgerwatch/erigon-lib/kv/mdbx" +) + +const ( + aggSenderDBFolder = "aggsender" + sentCertificatesL2Table = "sent_certificates_l2" +) + +func tableCfgFunc(defaultBuckets kv.TableCfg) kv.TableCfg { + return kv.TableCfg{ + sentCertificatesL2Table: {}, + } +} + +// AggSender is a component that will send certificates to the aggLayer +type AggSender struct { + l2Syncer *bridgesync.BridgeSync + l2Client bridgesync.EthClienter + l1infoTreeSyncer *l1infotreesync.L1InfoTreeSync + + db kv.RwDB + aggLayerClient agglayer.AgglayerClientInterface + + sendInterval types.Duration + + sequencerKey *ecdsa.PrivateKey +} + +// New returns a new AggSender +func New( + ctx context.Context, + cfg Config, + aggLayerClient agglayer.AgglayerClientInterface, + l1InfoTreeSyncer *l1infotreesync.L1InfoTreeSync, + l2Syncer *bridgesync.BridgeSync, + l2Client bridgesync.EthClienter) (*AggSender, error) { + db, err := mdbx.NewMDBX(nil). + Path(filepath.Join(cfg.DBPath, aggSenderDBFolder)). + WithTableCfg(tableCfgFunc). + Open() + if err != nil { + return nil, err + } + + sequencerPrivateKey, err := cdkcommon.NewKeyFromKeystore(cfg.SequencerPrivateKey) + if err != nil { + return nil, err + } + + return &AggSender{ + db: db, + l2Syncer: l2Syncer, + l2Client: l2Client, + aggLayerClient: aggLayerClient, + l1infoTreeSyncer: l1InfoTreeSyncer, + sequencerKey: sequencerPrivateKey, + sendInterval: cfg.CertificateSendInterval, + }, nil +} + +// Start starts the AggSender +func (a *AggSender) Start(ctx context.Context) { + go a.sendCertificates(ctx) +} + +// sendCertificates sends certificates to the aggLayer +func (a *AggSender) sendCertificates(ctx context.Context) { + ticker := time.NewTicker(a.sendInterval.Duration) + + for { + select { + case <-ticker.C: + if err := a.sendCertificatesForNetwork(ctx); err != nil { + log.Error(err) + } + case <-ctx.Done(): + log.Info("AggSender stopped") + return + } + } +} + +// buildCertificate builds a certificate from the bridge events +func (a *AggSender) buildCertificate(ctx context.Context, + bridges []bridgesync.Bridge, + claims []bridgesync.Claim, + previousExitRoot common.Hash, lastHeight uint64) (*agglayer.Certificate, error) { + bridgeExits := make([]*agglayer.BridgeExit, 0, len(bridges)) + importedBridgeExits := make([]*agglayer.ImportedBridgeExit, 0, len(claims)) + + for _, bridge := range bridges { + bridgeExit := convertBridgeToBridgeExit(bridge) + bridgeExits = append(bridgeExits, bridgeExit) + } + + for _, claim := range claims { + importedBridgeExit, err := convertClaimToImportedBridgeExit(claim) + if err != nil { + return nil, fmt.Errorf("error converting claim to imported bridge exit: %w", err) + } + + importedBridgeExits = append(importedBridgeExits, importedBridgeExit) + } + + var depositCount uint32 + if len(bridges) > 0 { + depositCount = bridges[len(bridges)-1].DepositCount + } + + exitRoot, err := a.l2Syncer.GetExitRootByIndex(ctx, depositCount) + if err != nil { + return nil, fmt.Errorf("error getting exit root by index: %d. Error: %w", depositCount, err) + } + + return &agglayer.Certificate{ + NetworkID: a.l2Syncer.OriginNetwork(), + PrevLocalExitRoot: previousExitRoot, + NewLocalExitRoot: exitRoot.Hash, + BridgeExits: bridgeExits, + ImportedBridgeExits: importedBridgeExits, + Height: lastHeight + 1, + }, nil +} + +func convertBridgeToBridgeExit(bridge bridgesync.Bridge) *agglayer.BridgeExit { + return &agglayer.BridgeExit{ + LeafType: agglayer.LeafType(bridge.LeafType), + TokenInfo: &agglayer.TokenInfo{ + OriginNetwork: bridge.OriginNetwork, + OriginTokenAddress: bridge.OriginAddress, + }, + DestinationNetwork: bridge.DestinationNetwork, + DestinationAddress: bridge.DestinationAddress, + Amount: bridge.Amount, + Metadata: bridge.Metadata, + } +} + +func convertClaimToImportedBridgeExit(claim bridgesync.Claim) (*agglayer.ImportedBridgeExit, error) { + leafType := agglayer.LeafTypeAsset + if claim.IsMessage { + leafType = agglayer.LeafTypeMessage + } + + bridgeExit := &agglayer.BridgeExit{ + LeafType: leafType, + TokenInfo: &agglayer.TokenInfo{ + OriginNetwork: claim.OriginNetwork, + OriginTokenAddress: claim.OriginAddress, + }, + DestinationNetwork: claim.DestinationNetwork, + DestinationAddress: claim.DestinationAddress, + Amount: claim.Amount, + Metadata: claim.Metadata, + } + + mainnetFlag, rollupIndex, leafIndex, err := bridgesync.DecodeGlobalIndex(claim.GlobalIndex) + if err != nil { + return nil, fmt.Errorf("error decoding global index: %w", err) + } + + return &agglayer.ImportedBridgeExit{ + BridgeExit: bridgeExit, + ImportedLocalExitRoot: tree.CalculateRoot(bridgeExit.Hash(), + claim.ProofLocalExitRoot, uint32(claim.GlobalIndex.Uint64())), + InclusionProof: claim.ProofLocalExitRoot, + InclusionProofRER: claim.ProofRollupExitRoot, + GlobalIndex: &agglayer.GlobalIndex{ + MainnetFlag: mainnetFlag, + RollupIndex: rollupIndex, + LeafIndex: leafIndex, + }, + }, nil +} + +// sendCertificatesForNetwork sends certificates for a network +func (a *AggSender) sendCertificatesForNetwork(ctx context.Context) error { + lastSentCertificateBlock, lastSentCertificate, err := a.getLastSentCertificate(ctx) + if err != nil { + return fmt.Errorf("error getting last sent certificate: %w", err) + } + + finality := a.l2Syncer.BlockFinality() + blockFinality, err := finality.ToBlockNum() + if err != nil { + return fmt.Errorf("error getting block finality: %w", err) + } + + lastFinalizedBlock, err := a.l2Client.HeaderByNumber(ctx, blockFinality) + if err != nil { + return fmt.Errorf("error getting block number: %w", err) + } + + bridges, err := a.l2Syncer.GetBridges(ctx, lastSentCertificateBlock+1, lastFinalizedBlock.Nonce.Uint64()) + if err != nil { + return fmt.Errorf("error getting bridges: %w", err) + } + + claims, err := a.l2Syncer.GetClaims(ctx, lastSentCertificateBlock+1, lastFinalizedBlock.Nonce.Uint64()) + if err != nil { + return fmt.Errorf("error getting claims: %w", err) + } + + if len(bridges) == 0 && len(claims) == 0 { + // nothing to send + return nil + } + + previousExitRoot := common.Hash{} + lastHeight := uint64(0) + if lastSentCertificate != nil { + previousExitRoot = lastSentCertificate.NewLocalExitRoot + lastHeight = lastSentCertificate.Height + } + + certificate, err := a.buildCertificate(ctx, bridges, claims, previousExitRoot, lastHeight) + if err != nil { + return fmt.Errorf("error building certificate: %w", err) + } + + signedCertificate, err := a.signCertificate(certificate) + if err != nil { + return fmt.Errorf("error signing certificate: %w", err) + } + + if err := a.aggLayerClient.SendCertificate(signedCertificate); err != nil { + return fmt.Errorf("error sending certificate: %w", err) + } + + if err := a.saveLastSentCertificate(ctx, lastFinalizedBlock.Nonce.Uint64(), certificate); err != nil { + return fmt.Errorf("error saving last sent certificate in db: %w", err) + } + + return nil +} + +// saveLastSentCertificate saves the last sent certificate +func (a *AggSender) saveLastSentCertificate(ctx context.Context, blockNum uint64, + certificate *agglayer.Certificate) error { + return a.db.Update(ctx, func(tx kv.RwTx) error { + raw, err := json.Marshal(certificate) + if err != nil { + return err + } + + return tx.Put(sentCertificatesL2Table, cdkcommon.Uint64ToBytes(blockNum), raw) + }) +} + +// getLastSentCertificate returns the last sent certificate +func (a *AggSender) getLastSentCertificate(ctx context.Context) (uint64, *agglayer.Certificate, error) { + var ( + lastSentCertificateBlock uint64 + lastCertificate *agglayer.Certificate + ) + + err := a.db.View(ctx, func(tx kv.Tx) error { + cursor, err := tx.Cursor(sentCertificatesL2Table) + if err != nil { + return err + } + + k, v, err := cursor.Last() + if err != nil { + return err + } + + if k != nil { + lastSentCertificateBlock = cdkcommon.BytesToUint64(k) + if err := json.Unmarshal(v, &lastCertificate); err != nil { + return err + } + } + + return nil + }) + + return lastSentCertificateBlock, lastCertificate, err +} + +// signCertificate signs a certificate with the sequencer key +func (a *AggSender) signCertificate(certificate *agglayer.Certificate) (*agglayer.SignedCertificate, error) { + hashToSign := certificate.Hash() + + sig, err := crypto.Sign(hashToSign.Bytes(), a.sequencerKey) + if err != nil { + return nil, err + } + + return &agglayer.SignedCertificate{ + Certificate: certificate, + Signature: sig, + }, nil +} diff --git a/aggsender/config.go b/aggsender/config.go new file mode 100644 index 00000000..f640496e --- /dev/null +++ b/aggsender/config.go @@ -0,0 +1,13 @@ +package aggsender + +import ( + "github.com/0xPolygon/cdk/config/types" +) + +// Config is the configuration for the AggSender +type Config struct { + DBPath string `mapstructure:"DBPath"` + AggLayerURL string `mapstructure:"AggLayerURL"` + CertificateSendInterval types.Duration `mapstructure:"CertificateSendInterval"` + SequencerPrivateKey types.KeystoreFileConfig `mapstructure:"SequencerPrivateKey"` +} diff --git a/bridgesync/bridgesync.go b/bridgesync/bridgesync.go index e6a61c5e..7e89f0ff 100644 --- a/bridgesync/bridgesync.go +++ b/bridgesync/bridgesync.go @@ -20,6 +20,9 @@ const ( type BridgeSync struct { processor *processor driver *sync.EVMDriver + + originNetwork uint32 + blockFinality etherman.BlockNumberFinality } // NewL1 creates a bridge syncer that synchronizes the mainnet exit tree @@ -35,6 +38,7 @@ func NewL1( waitForNewBlocksPeriod time.Duration, retryAfterErrorPeriod time.Duration, maxRetryAttemptsAfterError int, + originNetwork uint32, ) (*BridgeSync, error) { return newBridgeSync( ctx, @@ -49,6 +53,7 @@ func NewL1( waitForNewBlocksPeriod, retryAfterErrorPeriod, maxRetryAttemptsAfterError, + originNetwork, false, ) } @@ -66,6 +71,7 @@ func NewL2( waitForNewBlocksPeriod time.Duration, retryAfterErrorPeriod time.Duration, maxRetryAttemptsAfterError int, + originNetwork uint32, ) (*BridgeSync, error) { return newBridgeSync( ctx, @@ -80,6 +86,7 @@ func NewL2( waitForNewBlocksPeriod, retryAfterErrorPeriod, maxRetryAttemptsAfterError, + originNetwork, true, ) } @@ -97,6 +104,7 @@ func newBridgeSync( waitForNewBlocksPeriod time.Duration, retryAfterErrorPeriod time.Duration, maxRetryAttemptsAfterError int, + originNetwork uint32, syncFullClaims bool, ) (*BridgeSync, error) { processor, err := newProcessor(dbPath, l1OrL2ID) @@ -146,8 +154,10 @@ func newBridgeSync( } return &BridgeSync{ - processor: processor, - driver: driver, + processor: processor, + driver: driver, + originNetwork: originNetwork, + blockFinality: blockFinalityType, }, nil } @@ -191,3 +201,18 @@ func (s *BridgeSync) GetRootByLER(ctx context.Context, ler common.Hash) (*tree.R } return root, nil } + +// GetExitRootByIndex returns the root of the exit tree at the moment the leaf with the given index was added +func (s *BridgeSync) GetExitRootByIndex(ctx context.Context, index uint32) (tree.Root, error) { + return s.processor.exitTree.GetRootByIndex(ctx, index) +} + +// OriginNetwork returns the network ID of the origin chain +func (s *BridgeSync) OriginNetwork() uint32 { + return s.originNetwork +} + +// BlockFinality returns the block finality type +func (s *BridgeSync) BlockFinality() etherman.BlockNumberFinality { + return s.blockFinality +} diff --git a/bridgesync/config.go b/bridgesync/config.go index 66eb00ed..d2373b53 100644 --- a/bridgesync/config.go +++ b/bridgesync/config.go @@ -24,4 +24,6 @@ type Config struct { MaxRetryAttemptsAfterError int `mapstructure:"MaxRetryAttemptsAfterError"` // WaitForNewBlocksPeriod time that will be waited when the synchronizer has reached the latest block WaitForNewBlocksPeriod types.Duration `mapstructure:"WaitForNewBlocksPeriod"` + // OriginNetwork is the id of the network where the bridge is deployed + OriginNetwork uint32 `mapstructure:"OriginNetwork"` } diff --git a/bridgesync/e2e_test.go b/bridgesync/e2e_test.go index a19afb8d..8f7647fe 100644 --- a/bridgesync/e2e_test.go +++ b/bridgesync/e2e_test.go @@ -61,7 +61,7 @@ func TestBridgeEventE2E(t *testing.T) { go rd.Start(ctx) //nolint:errcheck testClient := helpers.TestClient{ClientRenamed: client.Client()} - syncer, err := bridgesync.NewL1(ctx, dbPathSyncer, bridgeAddr, 10, etherman.LatestBlock, rd, testClient, 0, time.Millisecond*10, 0, 0) + syncer, err := bridgesync.NewL1(ctx, dbPathSyncer, bridgeAddr, 10, etherman.LatestBlock, rd, testClient, 0, time.Millisecond*10, 0, 0, 1) require.NoError(t, err) go syncer.Start(ctx) diff --git a/bridgesync/processor.go b/bridgesync/processor.go index e4ba5423..cf279c41 100644 --- a/bridgesync/processor.go +++ b/bridgesync/processor.go @@ -20,6 +20,11 @@ import ( _ "modernc.org/sqlite" ) +const ( + globalIndexPartSize = 4 + globalIndexMaxSize = 9 +) + var ( // ErrBlockNotProcessed indicates that the given block(s) have not been processed yet. ErrBlockNotProcessed = errors.New("given block(s) have not been processed yet") @@ -300,7 +305,7 @@ func (p *processor) ProcessBlock(ctx context.Context, block sync.Block) error { func GenerateGlobalIndex(mainnetFlag bool, rollupIndex uint32, localExitRootIndex uint32) *big.Int { var ( globalIndexBytes []byte - buf [4]byte + buf [globalIndexPartSize]byte ) if mainnetFlag { globalIndexBytes = append(globalIndexBytes, big.NewInt(1).Bytes()...) @@ -313,5 +318,52 @@ func GenerateGlobalIndex(mainnetFlag bool, rollupIndex uint32, localExitRootInde leri := big.NewInt(0).SetUint64(uint64(localExitRootIndex)).FillBytes(buf[:]) globalIndexBytes = append(globalIndexBytes, leri...) - return big.NewInt(0).SetBytes(globalIndexBytes) + result := big.NewInt(0).SetBytes(globalIndexBytes) + + return result +} + +// Decodes global index to its three parts: +// 1. mainnetFlag - first byte +// 2. rollupIndex - next 4 bytes +// 3. localExitRootIndex - last 4 bytes +// NOTE - mainnet flag is not in the global index bytes if it is false +// NOTE - rollup index is 0 if mainnet flag is true +// NOTE - rollup index is not in the global index bytes if mainnet flag is false and rollup index is 0 +func DecodeGlobalIndex(globalIndex *big.Int) (mainnetFlag bool, + rollupIndex uint32, localExitRootIndex uint32, err error) { + globalIndexBytes := globalIndex.Bytes() + l := len(globalIndexBytes) + if l > globalIndexMaxSize { + return false, 0, 0, errors.New("invalid global index length") + } + + if l == 0 { + // false, 0, 0 + return + } + + if l == globalIndexMaxSize { + // true, rollupIndex, localExitRootIndex + mainnetFlag = true + } + + localExitRootFromIdx := l - globalIndexPartSize + if localExitRootFromIdx < 0 { + localExitRootFromIdx = 0 + } + + rollupIndexFromIdx := localExitRootFromIdx - globalIndexPartSize + if rollupIndexFromIdx < 0 { + rollupIndexFromIdx = 0 + } + + rollupIndex = convertBytesToUint32(globalIndexBytes[rollupIndexFromIdx:localExitRootFromIdx]) + localExitRootIndex = convertBytesToUint32(globalIndexBytes[localExitRootFromIdx:]) + + return +} + +func convertBytesToUint32(bytes []byte) uint32 { + return uint32(big.NewInt(0).SetBytes(bytes).Uint64()) } diff --git a/bridgesync/processor_test.go b/bridgesync/processor_test.go index 2ff03c76..86dd33c5 100644 --- a/bridgesync/processor_test.go +++ b/bridgesync/processor_test.go @@ -3,6 +3,7 @@ package bridgesync import ( "context" "encoding/json" + "errors" "fmt" "math/big" "os" @@ -582,3 +583,91 @@ func TestHashBridge(t *testing.T) { }) } } + +func TestDecodeGlobalIndex(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + globalIndex *big.Int + expectedMainnetFlag bool + expectedRollupIndex uint32 + expectedLocalIndex uint32 + expectedErr error + }{ + { + name: "Mainnet flag true, rollup index 0", + globalIndex: GenerateGlobalIndex(true, 0, 2), + expectedMainnetFlag: true, + expectedRollupIndex: 0, + expectedLocalIndex: 2, + expectedErr: nil, + }, + { + name: "Mainnet flag true, indexes 0", + globalIndex: GenerateGlobalIndex(true, 0, 0), + expectedMainnetFlag: true, + expectedRollupIndex: 0, + expectedLocalIndex: 0, + expectedErr: nil, + }, + { + name: "Mainnet flag false, rollup index 0", + globalIndex: GenerateGlobalIndex(false, 0, 2), + expectedMainnetFlag: false, + expectedRollupIndex: 0, + expectedLocalIndex: 2, + expectedErr: nil, + }, + { + name: "Mainnet flag false, rollup index non-zero", + globalIndex: GenerateGlobalIndex(false, 11, 0), + expectedMainnetFlag: false, + expectedRollupIndex: 11, + expectedLocalIndex: 0, + expectedErr: nil, + }, + { + name: "Mainnet flag false, indexes 0", + globalIndex: GenerateGlobalIndex(false, 0, 0), + expectedMainnetFlag: false, + expectedRollupIndex: 0, + expectedLocalIndex: 0, + expectedErr: nil, + }, + { + name: "Mainnet flag false, indexes non zero", + globalIndex: GenerateGlobalIndex(false, 1231, 111234), + expectedMainnetFlag: false, + expectedRollupIndex: 1231, + expectedLocalIndex: 111234, + expectedErr: nil, + }, + { + name: "Invalid global index length", + globalIndex: big.NewInt(0).SetBytes([]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}), + expectedMainnetFlag: false, + expectedRollupIndex: 0, + expectedLocalIndex: 0, + expectedErr: errors.New("invalid global index length"), + }, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + mainnetFlag, rollupIndex, localExitRootIndex, err := DecodeGlobalIndex(tt.globalIndex) + if tt.expectedErr != nil { + require.EqualError(t, err, tt.expectedErr.Error()) + } else { + require.NoError(t, err) + } + require.Equal(t, tt.expectedMainnetFlag, mainnetFlag) + require.Equal(t, tt.expectedRollupIndex, rollupIndex) + require.Equal(t, tt.expectedLocalIndex, localExitRootIndex) + }) + } +} diff --git a/claimsponsor/e2e_test.go b/claimsponsor/e2e_test.go index 8a037a58..d6d3675a 100644 --- a/claimsponsor/e2e_test.go +++ b/claimsponsor/e2e_test.go @@ -25,7 +25,7 @@ func TestE2EL1toEVML2(t *testing.T) { env := helpers.SetupAggoracleWithEVMChain(t) dbPathBridgeSyncL1 := path.Join(t.TempDir(), "file::memory:?cache=shared") testClient := helpers.TestClient{ClientRenamed: env.L1Client.Client()} - bridgeSyncL1, err := bridgesync.NewL1(ctx, dbPathBridgeSyncL1, env.BridgeL1Addr, 10, etherman.LatestBlock, env.ReorgDetector, testClient, 0, time.Millisecond*10, 0, 0) + bridgeSyncL1, err := bridgesync.NewL1(ctx, dbPathBridgeSyncL1, env.BridgeL1Addr, 10, etherman.LatestBlock, env.ReorgDetector, testClient, 0, time.Millisecond*10, 0, 0, 1) require.NoError(t, err) go bridgeSyncL1.Start(ctx) diff --git a/cmd/main.go b/cmd/main.go index 300851e7..5d966696 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -41,7 +41,8 @@ var ( Aliases: []string{"co"}, Usage: "List of components to run", Required: false, - Value: cli.NewStringSlice(common.SEQUENCE_SENDER, common.AGGREGATOR, common.AGGORACLE, common.RPC), + Value: cli.NewStringSlice(common.SEQUENCE_SENDER, common.AGGREGATOR, + common.AGGORACLE, common.RPC, common.AGGSENDER), } ) diff --git a/cmd/run.go b/cmd/run.go index 0b744243..e1cfe03b 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -12,10 +12,12 @@ import ( zkevm "github.com/0xPolygon/cdk" dataCommitteeClient "github.com/0xPolygon/cdk-data-availability/client" jRPC "github.com/0xPolygon/cdk-rpc/rpc" + "github.com/0xPolygon/cdk/agglayer" "github.com/0xPolygon/cdk/aggoracle" "github.com/0xPolygon/cdk/aggoracle/chaingersender" "github.com/0xPolygon/cdk/aggregator" "github.com/0xPolygon/cdk/aggregator/db" + "github.com/0xPolygon/cdk/aggsender" "github.com/0xPolygon/cdk/bridgesync" "github.com/0xPolygon/cdk/claimsponsor" cdkcommon "github.com/0xPolygon/cdk/common" @@ -119,6 +121,13 @@ func start(cliCtx *cli.Context) error { log.Fatal(err) } }() + case cdkcommon.AGGSENDER: + aggsender, err := createAggSender(cliCtx.Context, c.AggSender, l1InfoTreeSync, l2BridgeSync, l2Client) + if err != nil { + log.Fatal(err) + } + + go aggsender.Start(cliCtx.Context) } } @@ -127,6 +136,18 @@ func start(cliCtx *cli.Context) error { return nil } +func createAggSender( + ctx context.Context, + cfg aggsender.Config, + l1InfoTreeSync *l1infotreesync.L1InfoTreeSync, + l2Syncer *bridgesync.BridgeSync, + l2Client bridgesync.EthClienter, +) (*aggsender.AggSender, error) { + agglayerClient := agglayer.NewAggLayerClient(cfg.AggLayerURL) + + return aggsender.New(ctx, cfg, agglayerClient, l1InfoTreeSync, l2Syncer, l2Client) +} + func createAggregator(ctx context.Context, c config.Config, runMigrations bool) *aggregator.Aggregator { logger := log.WithFields("module", cdkcommon.AGGREGATOR) // Migrations @@ -476,7 +497,8 @@ func runL1InfoTreeSyncerIfNeeded( l1Client *ethclient.Client, reorgDetector *reorgdetector.ReorgDetector, ) *l1infotreesync.L1InfoTreeSync { - if !isNeeded([]string{cdkcommon.AGGORACLE, cdkcommon.RPC, cdkcommon.SEQUENCE_SENDER}, components) { + if !isNeeded([]string{cdkcommon.AGGORACLE, cdkcommon.RPC, + cdkcommon.SEQUENCE_SENDER, cdkcommon.AGGSENDER}, components) { return nil } l1InfoTreeSync, err := l1infotreesync.New( @@ -518,7 +540,7 @@ func runL1ClientIfNeeded(components []string, urlRPCL1 string) *ethclient.Client } func runL2ClientIfNeeded(components []string, urlRPCL2 string) *ethclient.Client { - if !isNeeded([]string{cdkcommon.AGGORACLE, cdkcommon.RPC}, components) { + if !isNeeded([]string{cdkcommon.AGGORACLE, cdkcommon.RPC, cdkcommon.AGGSENDER}, components) { return nil } log.Debugf("dialing L2 client at: %s", urlRPCL2) @@ -671,6 +693,7 @@ func runBridgeSyncL1IfNeeded( cfg.WaitForNewBlocksPeriod.Duration, cfg.RetryAfterErrorPeriod.Duration, cfg.MaxRetryAttemptsAfterError, + cfg.OriginNetwork, ) if err != nil { log.Fatalf("error creating bridgeSyncL1: %s", err) @@ -687,8 +710,7 @@ func runBridgeSyncL2IfNeeded( reorgDetectorL2 *reorgdetector.ReorgDetector, l2Client *ethclient.Client, ) *bridgesync.BridgeSync { - // TODO: will be needed by AGGSENDER - if !isNeeded([]string{cdkcommon.RPC}, components) { + if !isNeeded([]string{cdkcommon.RPC, cdkcommon.AGGSENDER}, components) { return nil } bridgeSyncL2, err := bridgesync.NewL2( @@ -703,6 +725,7 @@ func runBridgeSyncL2IfNeeded( cfg.WaitForNewBlocksPeriod.Duration, cfg.RetryAfterErrorPeriod.Duration, cfg.MaxRetryAttemptsAfterError, + cfg.OriginNetwork, ) if err != nil { log.Fatalf("error creating bridgeSyncL2: %s", err) diff --git a/common/common.go b/common/common.go index cd5b5d70..c74f56e4 100644 --- a/common/common.go +++ b/common/common.go @@ -1,10 +1,15 @@ package common import ( + "crypto/ecdsa" "encoding/binary" "math/big" + "os" + "path/filepath" + "github.com/0xPolygon/cdk/config/types" "github.com/0xPolygon/cdk/log" + "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/common" "github.com/iden3/go-iden3-crypto/keccak256" ) @@ -88,3 +93,19 @@ func CalculateAccInputHash( return common.BytesToHash(keccak256.Hash(v1, v2, v3, v4, v5, v6)) } + +// NewKeyFromKeystore creates a private key from a keystore file +func NewKeyFromKeystore(cfg types.KeystoreFileConfig) (*ecdsa.PrivateKey, error) { + if cfg.Path == "" && cfg.Password == "" { + return nil, nil + } + keystoreEncrypted, err := os.ReadFile(filepath.Clean(cfg.Path)) + if err != nil { + return nil, err + } + key, err := keystore.DecryptKey(keystoreEncrypted, cfg.Password) + if err != nil { + return nil, err + } + return key.PrivateKey, nil +} diff --git a/common/components.go b/common/components.go index 0c2df8d7..7ef9d285 100644 --- a/common/components.go +++ b/common/components.go @@ -13,4 +13,6 @@ const ( CLAIM_SPONSOR = "claim-sponsor" //nolint:stylecheck // PROVER name to identify the prover component PROVER = "prover" + // AGGSENDER name to identify the aggsender component + AGGSENDER = "aggsender" ) diff --git a/config/config.go b/config/config.go index 431d0175..ddceec22 100644 --- a/config/config.go +++ b/config/config.go @@ -9,6 +9,7 @@ import ( jRPC "github.com/0xPolygon/cdk-rpc/rpc" "github.com/0xPolygon/cdk/aggoracle" "github.com/0xPolygon/cdk/aggregator" + "github.com/0xPolygon/cdk/aggsender" "github.com/0xPolygon/cdk/bridgesync" "github.com/0xPolygon/cdk/claimsponsor" "github.com/0xPolygon/cdk/common" @@ -92,7 +93,6 @@ type Config struct { NetworkConfig NetworkConfig // Configuration of the sequence sender service SequenceSender sequencesender.Config - // Common Config that affects all the services Common common.Config // Configuration of the reorg detector service to be used for the L1 @@ -119,6 +119,9 @@ type Config struct { // LastGERSync is the config for the synchronizer in charge of syncing the last GER injected on L2. // Needed for the bridge service (RPC) LastGERSync lastgersync.Config + + // AggSender is the configuration of the agg sender service + AggSender aggsender.Config } // Default parses the default configuration values. diff --git a/config/default.go b/config/default.go index 377e9033..55e76c50 100644 --- a/config/default.go +++ b/config/default.go @@ -250,4 +250,10 @@ RetryAfterErrorPeriod = "1s" MaxRetryAttemptsAfterError = -1 WaitForNewBlocksPeriod = "1s" DownloadBufferSize = 100 + +[AggSender] +DBPath = "/tmp/aggsender" +AggLayerURL = "http://zkevm-agglayer" +SequencerPrivateKey = {Path = "/pk/sequencer.keystore", Password = "testonly"} +CertificateSendInterval = "1m" ` diff --git a/tree/tree.go b/tree/tree.go index 5d307e8a..407b46ae 100644 --- a/tree/tree.go +++ b/tree/tree.go @@ -9,6 +9,7 @@ import ( "github.com/0xPolygon/cdk/db" "github.com/0xPolygon/cdk/tree/types" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" "github.com/russross/meddler" "golang.org/x/crypto/sha3" ) @@ -247,5 +248,20 @@ func (t *Tree) Reorg(tx db.Txer, firstReorgedBlock uint64) error { firstReorgedBlock, ) return err - // NOTE: rht is not cleaned, this could be done in the future as optimization +} + +// CalculateRoot calculates the Merkle Root based on the leaf and proof of inclusion +func CalculateRoot(leafHash common.Hash, proof [types.DefaultHeight]common.Hash, index uint32) common.Hash { + node := leafHash + + // Compute the Merkle root + for height := uint8(0); height < types.DefaultHeight; height++ { + if (index>>height)&1 == 1 { + node = crypto.Keccak256Hash(proof[height].Bytes(), node.Bytes()) + } else { + node = crypto.Keccak256Hash(node.Bytes(), proof[height].Bytes()) + } + } + + return node }