diff --git a/autoclaimservice/README.md b/autoclaimservice/README.md new file mode 100644 index 00000000..31a81e4e --- /dev/null +++ b/autoclaimservice/README.md @@ -0,0 +1,41 @@ +# AutoClaim service +This service allows to claim deposits whose destination is the network specified by the bridgeAddress and RPC network. + +## Configuration file: +This is an example of the configuration file. +``` +[Log] +Level = "debug" +Outputs = ["stdout"] + +[AutoClaim] +AuthorizedClaimMessageAddresses = [] +AutoClaimInterval = "10m" +MaxNumberOfClaimsPerGroup = 10 +BridgeURL = "http://localhost:8080" + +[BlockchainManager] +PrivateKey = {Path = "./test.keystore.autoclaim", Password = "testonly"} +L2RPC = "http://localhost:8123" +PolygonBridgeAddress = "0xFe12ABaa190Ef0c8638Ee0ba9F828BF41368Ca0E" +ClaimCompressorAddress = "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" +``` +- The log seccion allows to modify the log level. By default: debug mode +- The autoclaim seccion allows to configure the next parameters: + - `AuthorizedClaimMessageAddresses` These are the addresses that can use the autoclaim feature for bridge messages. By default: none + - `AutoClaimInterval` This is the param that controls the interval to check new bridges. By default: 10m + - `MaxNumberOfClaimsPerGroup` This param allow to control the maximum numbre of claims that can be grouped in a single tx. By default: 10 + - `BridgeURL` This is the bridge service URL to get the bridges that can be claimed. By default: localhost:8080 +- The BlockchainManager seccion allows to modify network parameters. + - `PrivateKey` is the wallet used to send the claim txs. By default: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 + - `L2RPC` is the URL of the L2 node to send claim txs. By default: localhost:8123 + - `PolygonBridgeAddress` is the L2 bridge address. By default: 0xFe12ABaa190Ef0c8638Ee0ba9F828BF41368Ca0E + - `ClaimCompressorAddress` is the compressor smc address. By default: 0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6 + +***Note: If ClaimCompressorAddress is not defined or MaxNumberOfClaimsPerGroup is 0, then the claim compressor feature is disabled and claim txs will be sent one by one. + +## How to run: +This is a command example of how to run this service: +``` +go run main.go run --cfg ./config/config.local.toml +``` \ No newline at end of file diff --git a/autoclaimservice/autoclaim/autoclaim.go b/autoclaimservice/autoclaim/autoclaim.go new file mode 100644 index 00000000..0b21e72c --- /dev/null +++ b/autoclaimservice/autoclaim/autoclaim.go @@ -0,0 +1,359 @@ +package autoclaim + +import ( + "context" + "fmt" + "io" + "math/big" + "net/http" + "time" + + "github.com/0xPolygonHermez/zkevm-bridge-service/autoclaimservice/blockchainmanager" + "github.com/0xPolygonHermez/zkevm-bridge-service/bridgectrl/pb" + "github.com/0xPolygonHermez/zkevm-bridge-service/etherman/smartcontracts/claimcompressor" + "github.com/0xPolygonHermez/zkevm-bridge-service/log" + "github.com/ethereum/go-ethereum/common" + "golang.org/x/crypto/sha3" + "google.golang.org/protobuf/encoding/protojson" +) + +type autoclaim struct { + ctx context.Context + bm *blockchainmanager.Client + cfg *Config +} + +func NewAutoClaim(ctx context.Context, c *Config, blockchainManager *blockchainmanager.Client) (autoclaim, error) { + if c.MaxNumberOfClaimsPerGroup == 0 || blockchainManager.ClaimCompressor == nil { //ClaimCompressor disabled + log.Info("ClaimCompressor disabled") + } else { + log.Info("ClaimCompressor enabled") + } + return autoclaim{ + bm: blockchainManager, + cfg: c, + ctx: ctx, + }, nil +} + +func (ac *autoclaim) Start() { + ticker := time.NewTicker(ac.cfg.AutoClaimInterval.Duration) + for { + select { + case <-ac.ctx.Done(): + ticker.Stop() + return + case <-ticker.C: + if ac.cfg.MaxNumberOfClaimsPerGroup == 0 || ac.bm.ClaimCompressor == nil { //ClaimCompressor disabled + err := ac.claimIndividually() + if err != nil { + log.Error("error claiming individually. Error: ", err) + } + } else { //ClaimCompressor enabled + err := ac.claimGrouped() + if err != nil { + log.Error("error claiming grouped. Error: ", err) + } + } + } + } +} + +func (ac *autoclaim) claimIndividually() error { + bridges, err := ac.getBridgesToClaim() + if err != nil { + log.Errorf("error getBridgesToClaim. Error: %v", err) + return err + } + if len(bridges) == 0 { + log.Info("No bridges to claim were found") + return nil + } + var proofs []*pb.GetProofResponse + for _, b := range bridges { + log.Debugf("Getting proof for bridge (depositCount: %d, networkID: %d)", b.DepositCnt, b.NetworkId) + proof, err := ac.getProof(b) + if err != nil { + log.Errorf("error getting proof for deposit counter: %d and networkID: %d", b.DepositCnt, b.NetworkId) + return err + } + proofs = append(proofs, proof) + } + if len(bridges) != len(proofs) { + log.Error("error: bridges length and proofs length don't match in claimIndividually()") + return fmt.Errorf("error: bridges length and proofs length don't match in claimIndividually()") + } + for i := range bridges { + b := bridges[i] + p := proofs[i].Proof + globalIndex, _ := big.NewInt(0).SetString(b.GlobalIndex, 0) + amount, _ := new(big.Int).SetString(b.Amount, 0) + var smtProof, smtRollupProof [blockchainmanager.MtHeight][blockchainmanager.KeyLen]byte + for i := 0; i < len(p.MerkleProof); i++ { + smtProof[i] = common.HexToHash(p.MerkleProof[i]) + smtRollupProof[i] = common.HexToHash(p.RollupMerkleProof[i]) + } + log.Debugf("Sending claim for bridge depositCount: %d, networkID: %d", b.DepositCnt, b.NetworkId) + tx, err := ac.bm.SendClaim(b.LeafType, b.OrigNet, common.HexToAddress(b.OrigAddr), amount, b.DestNet, common.HexToAddress(b.DestAddr), b.NetworkId, common.FromHex(b.Metadata), globalIndex, smtProof, smtRollupProof, common.HexToHash(p.MainExitRoot), common.HexToHash(p.RollupExitRoot)) + if err != nil { + log.Errorf("error sending claim for deposit counter: %d and networkID: %d, Error: %v", b.DepositCnt, b.NetworkId, err) + // Here the error is not returned to avoid block the service. An specific claim can fail but others can work properly. + } else { + log.Infof("Claim tx (%s) sent for deposit counter %d from network %d", tx.Hash(), b.DepositCnt, b.NetworkId) + } + } + return nil +} + +func (ac *autoclaim) claimGrouped() error { + bridges, err := ac.getBridgesToClaim() + if err != nil { + log.Errorf("error getBridgesToClaim. Error: %v", err) + return err + } + if len(bridges) == 0 { + log.Info("No bridges to claim were found") + return nil + } + var proofs []*pb.GetProofResponse + var ger, mainnetExitRoot, rollupExitRoot common.Hash + for i, b := range bridges { + log.Debugf("Getting proof for bridge (depositCount: %d, networkID: %d)", b.DepositCnt, b.NetworkId) + if i == 0 { + proof, err := ac.getProof(b) + if err != nil { + log.Errorf("error getting proof for deposit counter: %d and networkID: %d", b.DepositCnt, b.NetworkId) + return err + } + mainnetExitRoot = common.HexToHash(proof.Proof.MainExitRoot) + rollupExitRoot = common.HexToHash(proof.Proof.RollupExitRoot) + ger = hash(mainnetExitRoot, rollupExitRoot) + proofs = append(proofs, proof) + } else { + proof, err := ac.getProofByGER(b, ger) + if err != nil { + log.Errorf("error getting proof for deposit counter: %d and networkID: %d", b.DepositCnt, b.NetworkId) + return err + } + proofs = append(proofs, proof) + } + } + if len(bridges) != len(proofs) { + log.Error("error: bridges length and proofs length don't match in claimGrouped()") + return fmt.Errorf("error: bridges length and proofs length don't match in claimGrouped()") + } + splittedBridges := arraySplitter(bridges, ac.cfg.MaxNumberOfClaimsPerGroup) + splittedProofs := arraySplitter(proofs, ac.cfg.MaxNumberOfClaimsPerGroup) + for j, sb := range splittedBridges { + var allClaimData []claimcompressor.ClaimCompressorCompressClaimCallData + for i := range sb { + b := sb[i] + p := splittedProofs[j][i].Proof + globalIndex, _ := big.NewInt(0).SetString(b.GlobalIndex, 0) + amount, _ := new(big.Int).SetString(b.Amount, 0) + var smtProof, smtRollupProof [blockchainmanager.MtHeight][blockchainmanager.KeyLen]byte + for i := 0; i < len(p.MerkleProof); i++ { + smtProof[i] = common.HexToHash(p.MerkleProof[i]) + smtRollupProof[i] = common.HexToHash(p.RollupMerkleProof[i]) + } + metadata := common.FromHex(b.Metadata) + originalAddress := common.HexToAddress(b.OrigAddr) + destinationAddress := common.HexToAddress(b.DestAddr) + + log.Debugf("Estimating gas for bridge (depositCount: %d, networkID: %d)", b.DepositCnt, b.NetworkId) + // Estimate gas to see if it is claimable + _, err := ac.bm.EstimateGasClaim(metadata, amount, originalAddress, destinationAddress, b.OrigNet, b.DestNet, mainnetExitRoot, rollupExitRoot, b.LeafType, globalIndex, smtProof, smtRollupProof) + if err != nil { + log.Infof("error estimating gas for deposit counter: %d from networkID: %d, Skipping... Error: %v", b.DepositCnt, b.NetworkId, err) + continue + } + + claimData := claimcompressor.ClaimCompressorCompressClaimCallData{ + SmtProofLocalExitRoot: smtProof, + SmtProofRollupExitRoot: smtRollupProof, + GlobalIndex: globalIndex, + OriginNetwork: b.OrigNet, + OriginAddress: originalAddress, + DestinationAddress: destinationAddress, + Amount: amount, + Metadata: metadata, + IsMessage: b.LeafType == blockchainmanager.LeafTypeMessage, + } + allClaimData = append(allClaimData, claimData) + } + if len(allClaimData) == 0 { + log.Info("Nothing to compress and send") + return nil + } + log.Debug("Compressing data") + compressedTxData, err := ac.bm.CompressClaimCall(mainnetExitRoot, rollupExitRoot, allClaimData) + if err != nil { + log.Errorf("error compressing claim data, Error: %v", err) + return err + } + log.Debug("Sending compressed claim tx") + tx, err := ac.bm.SendCompressedClaims(compressedTxData) + if err != nil { + log.Errorf("error sending compressed claims, Error: %v", err) + return err + } + log.Info("compressed claim sent. TxHash: ", tx.Hash()) + } + return nil +} + +func arraySplitter[T *pb.Deposit | *pb.GetProofResponse](arr []T, size int) [][]T { + var chunks [][]T + for { + if len(arr) == 0 { + break + } + if len(arr) < size { + size = len(arr) + } + + chunks = append(chunks, arr[0:size]) + arr = arr[size:] + } + + return chunks +} + +func hash(data ...[32]byte) [32]byte { + var res [32]byte + hash := sha3.NewLegacyKeccak256() + for _, d := range data { + hash.Write(d[:]) //nolint:errcheck,gosec + } + copy(res[:], hash.Sum(nil)) + return res +} + +func (ac *autoclaim) getProof(bridge *pb.Deposit) (*pb.GetProofResponse, error) { + requestURL := fmt.Sprintf("%s/merkle-proof?net_id=%d&deposit_cnt=%d", ac.cfg.BridgeURL, bridge.NetworkId, bridge.DepositCnt) + req, err := http.NewRequest(http.MethodGet, requestURL, nil) + if err != nil { + log.Errorf("error creating newRequest. merkle-proof endpoint. Error: %v", err) + return nil, err + } + res, err := http.DefaultClient.Do(req) + if err != nil { + log.Errorf("error doing the request. merkle-proof endpoint. Error: %v", err) + return nil, err + } + defer func() { + err := res.Body.Close() + if err != nil { + log.Error("error closing response body in merkle-proof endpoint call") + } + }() + + var bodyBytes []byte + if res.StatusCode == http.StatusOK { + bodyBytes, err = io.ReadAll(res.Body) + if err != nil { + log.Errorf("error reading response body of merkle-proof endpoint. Error: %v", err) + return nil, err + } + } else { + return nil, fmt.Errorf("error calling /merkle-proof endpoint. Status Code: %d", res.StatusCode) + } + var proofResp pb.GetProofResponse + err = protojson.Unmarshal(bodyBytes, &proofResp) + if err != nil { + return nil, err + } + return &proofResp, nil +} + +func (ac *autoclaim) getProofByGER(bridge *pb.Deposit, ger common.Hash) (*pb.GetProofResponse, error) { + requestURL := fmt.Sprintf("%s/merkle-proof-by-ger?net_id=%d&deposit_cnt=%d&ger=%s", ac.cfg.BridgeURL, bridge.NetworkId, bridge.DepositCnt, ger) + req, err := http.NewRequest(http.MethodGet, requestURL, nil) + if err != nil { + log.Errorf("error creating newRequest. merkle-proof-by-ger endpoint. Error: %v", err) + return nil, err + } + res, err := http.DefaultClient.Do(req) + if err != nil { + log.Errorf("error doing the request. merkle-proof-by-ger endpoint. Error: %v", err) + return nil, err + } + defer func() { + err := res.Body.Close() + if err != nil { + log.Error("error closing response body in merkle-proof-by-ger endpoint call") + } + }() + + var bodyBytes []byte + if res.StatusCode == http.StatusOK { + bodyBytes, err = io.ReadAll(res.Body) + if err != nil { + log.Errorf("error reading response body of merkle-proof-by-ger endpoint. Error: %v", err) + return nil, err + } + } else { + return nil, fmt.Errorf("error calling merkle-proof-by-ger endpoint. Status Code: %d", res.StatusCode) + } + var proofResp pb.GetProofResponse + err = protojson.Unmarshal(bodyBytes, &proofResp) + if err != nil { + return nil, err + } + return &proofResp, nil +} + +func (ac *autoclaim) getBridgesToClaim() ([]*pb.Deposit, error) { + log.Debug("Getting Bridges to claim (assets)") + bridges, _, err := ac.getPendingBridges(blockchainmanager.LeafTypeAsset, common.Address{}) + if err != nil { + log.Errorf("error getPendingBridges. Error: %v", err) + return nil, err + } + for _, addr := range ac.cfg.AuthorizedClaimMessageAddresses { + log.Debug("Getting Bridges to claim (messages) from address: ", addr.String()) + authorizedBridges, _, err := ac.getPendingBridges(blockchainmanager.LeafTypeMessage, addr) + if err != nil { + log.Errorf("error getPendingBridges. Error: %v", err) + return nil, err + } + bridges = append(bridges, authorizedBridges...) + } + return bridges, nil +} + +func (ac *autoclaim) getPendingBridges(leafType uint32, destAddress common.Address) ([]*pb.Deposit, uint64, error) { + destNetwork := ac.bm.NetworkID + requestURL := fmt.Sprintf("%s/pending-bridges?dest_net=%d&leaf_type=%d&dest_addr=%s", ac.cfg.BridgeURL, destNetwork, leafType, destAddress) + req, err := http.NewRequest(http.MethodGet, requestURL, nil) + if err != nil { + log.Errorf("error creating newRequest. pending-bridges endpoint. Error: %v", err) + return nil, 0, err + } + res, err := http.DefaultClient.Do(req) + if err != nil { + log.Errorf("error doing the request. pending-bridges endpoint. Error: %v", err) + return nil, 0, err + } + defer func() { + err := res.Body.Close() + if err != nil { + log.Error("error closing response body in pending-bridges endpoint call") + } + }() + + var bodyBytes []byte + if res.StatusCode == http.StatusOK { + bodyBytes, err = io.ReadAll(res.Body) + if err != nil { + log.Errorf("error reading response body of pending-bridges endpoint. Error: %v", err) + return nil, 0, err + } + } + var bridgeResp pb.GetBridgesResponse + err = protojson.Unmarshal(bodyBytes, &bridgeResp) + if err != nil { + return nil, 0, err + } + return bridgeResp.Deposits, bridgeResp.TotalCnt, nil +} diff --git a/autoclaimservice/autoclaim/config.go b/autoclaimservice/autoclaim/config.go new file mode 100644 index 00000000..d7d9de6e --- /dev/null +++ b/autoclaimservice/autoclaim/config.go @@ -0,0 +1,18 @@ +package autoclaim + +import ( + "github.com/0xPolygonHermez/zkevm-node/config/types" + "github.com/ethereum/go-ethereum/common" +) + +// Config represents the configuration of the AutoClaim package +type Config struct { + // AuthorizedClaimMessageAddresses are the allowed address to bridge message with autoClaim + AuthorizedClaimMessageAddresses []common.Address `mapstructure:"AuthorizedClaimMessageAddresses"` + // AutoClaimInterval is time between each iteration + AutoClaimInterval types.Duration `mapstructure:"AutoClaimInterval"` + // MaxNumberOfClaimsPerGroup is the maximum number of claims per group. 0 means group claims is disabled + MaxNumberOfClaimsPerGroup int `mapstructure:"MaxNumberOfClaimsPerGroup"` + // BridgeURL is the URL of the bridge service + BridgeURL string `mapstructure:"BridgeURL"` +} diff --git a/autoclaimservice/blockchainmanager/config.go b/autoclaimservice/blockchainmanager/config.go new file mode 100644 index 00000000..2546cd06 --- /dev/null +++ b/autoclaimservice/blockchainmanager/config.go @@ -0,0 +1,19 @@ +package blockchainmanager + +import ( + "github.com/0xPolygonHermez/zkevm-node/config/types" + "github.com/ethereum/go-ethereum/common" +) + +// Config is the configuration struct for the different environments. +type Config struct { + // L2RPC is the URL of the L2 node + L2RPC string `mapstructure:"L2RPC"` + // PrivateKey defines the key store file that is going + // to be read in order to provide the private key to sign the claim txs + PrivateKey types.KeystoreFileConfig `mapstructure:"PrivateKey"` + // PolygonBridgeAddress is the l2 bridge smc address + PolygonBridgeAddress common.Address `mapstructure:"PolygonBridgeAddress"` + // ClaimCompressorAddress is the l2 claim compressor smc address. If it's not set, then group claims is disabled + ClaimCompressorAddress common.Address `mapstructure:"ClaimCompressorAddress"` +} diff --git a/autoclaimservice/blockchainmanager/manager.go b/autoclaimservice/blockchainmanager/manager.go new file mode 100644 index 00000000..3034310a --- /dev/null +++ b/autoclaimservice/blockchainmanager/manager.go @@ -0,0 +1,186 @@ +package blockchainmanager + +import ( + "context" + "fmt" + "math/big" + "os" + "path/filepath" + + "github.com/0xPolygonHermez/zkevm-bridge-service/etherman/smartcontracts/claimcompressor" + "github.com/0xPolygonHermez/zkevm-bridge-service/log" + zkevmtypes "github.com/0xPolygonHermez/zkevm-node/config/types" + "github.com/0xPolygonHermez/zkevm-node/etherman/smartcontracts/polygonzkevmbridge" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/accounts/keystore" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" +) + +const ( + // LeafTypeAsset represents a bridge asset + LeafTypeAsset uint32 = 0 + // LeafTypeMessage represents a bridge message + LeafTypeMessage uint32 = 1 + + MtHeight = 32 + KeyLen = 32 +) + +// Client is a simple implementation of EtherMan. +type Client struct { + EtherClient *ethclient.Client + PolygonBridge *polygonzkevmbridge.Polygonzkevmbridge + ClaimCompressor *claimcompressor.Claimcompressor + NetworkID uint32 + cfg *Config + logger *log.Logger + auth *bind.TransactOpts + ctx context.Context +} + +// NewClient creates a new etherman for L2. +func NewClient(ctx context.Context, cfg *Config) (*Client, error) { + // Connect to ethereum node + ethClient, err := ethclient.Dial(cfg.L2RPC) + if err != nil { + log.Errorf("error connecting to %s: %+v", cfg.L2RPC, err) + return nil, err + } + // Create smc clients + bridge, err := polygonzkevmbridge.NewPolygonzkevmbridge(cfg.PolygonBridgeAddress, ethClient) + if err != nil { + return nil, err + } + var claimCompressor *claimcompressor.Claimcompressor + if cfg.ClaimCompressorAddress == (common.Address{}) { + log.Info("Claim compressor Address not configured") + } else { + log.Infof("Grouping claims configured, claimCompressor=%s", cfg.ClaimCompressorAddress.String()) + claimCompressor, err = claimcompressor.NewClaimcompressor(cfg.ClaimCompressorAddress, ethClient) + if err != nil { + log.Errorf("error creating claimCompressor: %+v", err) + return nil, err + } + } + networkID, err := bridge.NetworkID(&bind.CallOpts{Pending: false}) + if err != nil { + return nil, err + } + auth, err := GetSignerFromKeystore(ctx, ethClient, cfg.PrivateKey) + if err != nil { + log.Errorf("error creating signer. URL: %s. Error: %v", cfg.L2RPC, err) + return nil, err + } + logger := log.WithFields("networkID", networkID) + + return &Client{ + ctx: ctx, + logger: logger, + EtherClient: ethClient, + PolygonBridge: bridge, + ClaimCompressor: claimCompressor, + cfg: cfg, + NetworkID: networkID, + auth: auth, + }, nil +} + +// GetSignerFromKeystore returns a transaction signer from the keystore file. +func GetSignerFromKeystore(ctx context.Context, ethClient *ethclient.Client, ks zkevmtypes.KeystoreFileConfig) (*bind.TransactOpts, error) { + keystoreEncrypted, err := os.ReadFile(filepath.Clean(ks.Path)) + if err != nil { + return nil, err + } + key, err := keystore.DecryptKey(keystoreEncrypted, ks.Password) + if err != nil { + return nil, err + } + chainID, err := ethClient.ChainID(ctx) + if err != nil { + return nil, err + } + return bind.NewKeyedTransactorWithChainID(key.PrivateKey, chainID) +} + +func (bm *Client) SendCompressedClaims(compressedTxData []byte) (*types.Transaction, error) { + claimTx, err := bm.ClaimCompressor.SendCompressedClaims(bm.auth, compressedTxData) + if err != nil { + bm.logger.Error("failed to call SMC SendCompressedClaims: %v", err) + return nil, err + } + return claimTx, err +} + +func (bm *Client) CompressClaimCall(mainnetExitRoot, rollupExitRoot common.Hash, claimData []claimcompressor.ClaimCompressorCompressClaimCallData) ([]byte, error) { + compressedData, err := bm.ClaimCompressor.CompressClaimCall(&bind.CallOpts{Pending: false}, mainnetExitRoot, rollupExitRoot, claimData) + if err != nil { + bm.logger.Errorf("fails call to claimCompressorSMC. Error: %v", err) + return []byte{}, nil + } + return compressedData, nil +} + +// SendClaim sends a claim transaction. +func (bm *Client) SendClaim(leafType, origNet uint32, + origAddr common.Address, + amount *big.Int, + destNet uint32, + destAddr common.Address, + networkId uint32, + metadata []byte, + globalIndex *big.Int, + smtProof [MtHeight][KeyLen]byte, + smtRollupProof [MtHeight][KeyLen]byte, + mainnetExitRoot, rollupExitRoot common.Hash, +) (*types.Transaction, error) { + var ( + tx *types.Transaction + err error + ) + if leafType == LeafTypeAsset { + tx, err = bm.PolygonBridge.ClaimAsset(bm.auth, smtProof, smtRollupProof, globalIndex, mainnetExitRoot, rollupExitRoot, origNet, origAddr, destNet, destAddr, amount, metadata) + if err != nil { + a, _ := polygonzkevmbridge.PolygonzkevmbridgeMetaData.GetAbi() + input, err3 := a.Pack("claimAsset", smtProof, smtRollupProof, globalIndex, mainnetExitRoot, rollupExitRoot, origNet, origAddr, destNet, destAddr, amount, metadata) + if err3 != nil { + bm.logger.Error("error packing call. Error: ", err3) + } + bm.logger.Warnf(`Use the next command to debug it manually. + curl --location --request POST 'http://localhost:8123' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "jsonrpc": "2.0", + "method": "eth_call", + "params": [{"from": "%s","to":"%s","data":"0x%s"},"latest"], + "id": 1 + }'`, bm.auth.From, bm.cfg.PolygonBridgeAddress.String(), common.Bytes2Hex(input)) + } + } else if leafType == LeafTypeMessage { + tx, err = bm.PolygonBridge.ClaimMessage(bm.auth, smtProof, smtRollupProof, globalIndex, mainnetExitRoot, rollupExitRoot, origNet, origAddr, destNet, destAddr, amount, metadata) + } + if err != nil { + return nil, err + } + return tx, nil +} + +func (bm *Client) EstimateGasClaim(metadata []byte, amount *big.Int, originalAddress, destinationAddress common.Address, originalNetwork, destinationNetwork uint32, mainnetExitRoot, rollupExitRoot common.Hash, leafType uint32, globalIndex *big.Int, smtProof [MtHeight][KeyLen]byte, smtRollupProof [MtHeight][KeyLen]byte) (*types.Transaction, error) { + opts := *bm.auth + opts.NoSend = true + var ( + tx *types.Transaction + err error + ) + if leafType == LeafTypeAsset { + tx, err = bm.PolygonBridge.ClaimAsset(&opts, smtProof, smtRollupProof, + globalIndex, mainnetExitRoot, rollupExitRoot, originalNetwork, originalAddress, destinationNetwork, destinationAddress, amount, metadata) + } else if leafType == LeafTypeMessage { + tx, err = bm.PolygonBridge.ClaimMessage(&opts, smtProof, smtRollupProof, globalIndex, mainnetExitRoot, rollupExitRoot, originalNetwork, originalAddress, destinationNetwork, destinationAddress, amount, metadata) + } + if err != nil { + return nil, fmt.Errorf("failed to estimate gas tx, err: %v", err) + } + return tx, nil +} diff --git a/autoclaimservice/config/config.go b/autoclaimservice/config/config.go new file mode 100644 index 00000000..ff75b145 --- /dev/null +++ b/autoclaimservice/config/config.go @@ -0,0 +1,63 @@ +package config + +import ( + "path/filepath" + "strings" + + "github.com/0xPolygonHermez/zkevm-bridge-service/autoclaimservice/autoclaim" + "github.com/0xPolygonHermez/zkevm-bridge-service/autoclaimservice/blockchainmanager" + "github.com/0xPolygonHermez/zkevm-bridge-service/log" + "github.com/mitchellh/mapstructure" + "github.com/spf13/viper" +) + +// Config struct +type Config struct { + Log log.Config + AutoClaim autoclaim.Config + BlockchainManager blockchainmanager.Config +} + +// Load loads the configuration +func Load(configFilePath string) (*Config, error) { + cfg, err := Default() + if err != nil { + return nil, err + } + + if configFilePath != "" { + dirName, fileName := filepath.Split(configFilePath) + + fileExtension := strings.TrimPrefix(filepath.Ext(fileName), ".") + fileNameWithoutExtension := strings.TrimSuffix(fileName, "."+fileExtension) + + viper.AddConfigPath(dirName) + viper.SetConfigName(fileNameWithoutExtension) + viper.SetConfigType(fileExtension) + } + + viper.AutomaticEnv() + replacer := strings.NewReplacer(".", "_") + viper.SetEnvKeyReplacer(replacer) + viper.SetEnvPrefix("ZKEVM_AUTOCLAIM") + + if err = viper.ReadInConfig(); err != nil { + if _, ok := err.(viper.ConfigFileNotFoundError); ok { + log.Infof("config file not found") + } else { + log.Infof("error reading config file: ", err) + return nil, err + } + } + + decodeHooks := []viper.DecoderConfigOption{ + // this allows arrays to be decoded from env var separated by ",", example: MY_VAR="value1,value2,value3" + viper.DecodeHook(mapstructure.ComposeDecodeHookFunc(mapstructure.TextUnmarshallerHookFunc(), mapstructure.StringToSliceHookFunc(","))), + } + err = viper.Unmarshal(&cfg, decodeHooks...) + if err != nil { + return nil, err + } + + return cfg, nil +} diff --git a/autoclaimservice/config/config.local.toml b/autoclaimservice/config/config.local.toml new file mode 100644 index 00000000..98581a7c --- /dev/null +++ b/autoclaimservice/config/config.local.toml @@ -0,0 +1,15 @@ +[Log] +Level = "debug" +Outputs = ["stdout"] + +[AutoClaim] +AuthorizedClaimMessageAddresses = [] +AutoClaimInterval = "10m" +MaxNumberOfClaimsPerGroup = 10 +BridgeURL = "http://localhost:8080" + +[BlockchainManager] +PrivateKey = {Path = "./test.keystore.autoclaim", Password = "testonly"} +L2RPC = "http://localhost:8123" +PolygonBridgeAddress = "0xFe12ABaa190Ef0c8638Ee0ba9F828BF41368Ca0E" +ClaimCompressorAddress = "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" diff --git a/autoclaimservice/config/default.go b/autoclaimservice/config/default.go new file mode 100644 index 00000000..0f093beb --- /dev/null +++ b/autoclaimservice/config/default.go @@ -0,0 +1,44 @@ +package config + +import ( + "bytes" + + "github.com/mitchellh/mapstructure" + "github.com/spf13/viper" +) + +// DefaultValues is the default configuration +const DefaultValues = ` +[Log] +Level = "debug" +Outputs = ["stdout"] + +[AutoClaim] +AuthorizedClaimMessageAddresses = [] +AutoClaimInterval = "10m" +MaxNumberOfClaimsPerGroup = 10 +BridgeURL = "http://localhost:8080" + +[BlockchainManager] +PrivateKey = {Path = "./test/test.keystore", Password = "testonly"} +L2RPC = "http://localhost:8123" +PolygonBridgeAddress = "0xFe12ABaa190Ef0c8638Ee0ba9F828BF41368Ca0E" +ClaimCompressorAddress = "0x0000000000000000000000000000000000000000" + +` + +// Default parses the default configuration values. +func Default() (*Config, error) { + var cfg Config + viper.SetConfigType("toml") + + err := viper.ReadConfig(bytes.NewBuffer([]byte(DefaultValues))) + if err != nil { + return nil, err + } + err = viper.Unmarshal(&cfg, viper.DecodeHook(mapstructure.TextUnmarshallerHookFunc())) + if err != nil { + return nil, err + } + return &cfg, nil +} diff --git a/autoclaimservice/main.go b/autoclaimservice/main.go new file mode 100644 index 00000000..5bfc9c08 --- /dev/null +++ b/autoclaimservice/main.go @@ -0,0 +1,90 @@ +package main + +import ( + "fmt" + "os" + "os/signal" + + zkevmbridgeservice "github.com/0xPolygonHermez/zkevm-bridge-service" + "github.com/0xPolygonHermez/zkevm-bridge-service/autoclaimservice/autoclaim" + "github.com/0xPolygonHermez/zkevm-bridge-service/autoclaimservice/blockchainmanager" + "github.com/0xPolygonHermez/zkevm-bridge-service/autoclaimservice/config" + "github.com/0xPolygonHermez/zkevm-bridge-service/log" + "github.com/urfave/cli/v2" +) + +const ( + flagCfg = "cfg" + flagNetwork = "network" +) + +const ( + // App name + appName = "zkevm-bridge" +) + +func main() { + app := cli.NewApp() + app.Name = appName + app.Version = zkevmbridgeservice.Version + flags := []cli.Flag{ + &cli.StringFlag{ + Name: flagCfg, + Aliases: []string{"c"}, + Usage: "Configuration `FILE`", + Required: false, + }, + } + + app.Commands = []*cli.Command{ + { + Name: "version", + Aliases: []string{}, + Usage: "Application version and build", + Action: version, + }, + { + Name: "run", + Aliases: []string{}, + Usage: "Run the zkevm bridge", + Action: run, + Flags: flags, + }, + } + + err := app.Run(os.Args) + if err != nil { + fmt.Printf("\nError: %v\n", err) + os.Exit(1) + } +} + +func version(*cli.Context) error { + zkevmbridgeservice.PrintVersion(os.Stdout) + return nil +} + +func run(ctx *cli.Context) error { + configFilePath := ctx.String(flagCfg) + c, err := config.Load(configFilePath) + if err != nil { + return err + } + log.Init(c.Log) + bm, err := blockchainmanager.NewClient(ctx.Context, &c.BlockchainManager) + if err != nil { + return err + } + ac, err := autoclaim.NewAutoClaim(ctx.Context, &c.AutoClaim, bm) + if err != nil { + return err + } + go ac.Start() + + // Wait for an in interrupt. + ch := make(chan os.Signal, 1) + signal.Notify(ch, os.Interrupt) + <-ch + + return nil +} diff --git a/autoclaimservice/test.keystore.autoclaim b/autoclaimservice/test.keystore.autoclaim new file mode 100644 index 00000000..96b662b7 --- /dev/null +++ b/autoclaimservice/test.keystore.autoclaim @@ -0,0 +1 @@ +{"address":"f39fd6e51aad88f6f4ce6ab8827279cfffb92266","crypto":{"cipher":"aes-128-ctr","ciphertext":"d005030a7684f3adad2447cbb27f63039eec2224c451eaa445de0d90502b9f3d","cipherparams":{"iv":"dc07a54bc7e388efa89c34d42f2ebdb4"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"cf2ec55ecae11171de575112cfb16963570533a9c46fb774473ceb11519eb24a"},"mac":"3eb180d405a5da6e462b2adc00091c14856c91d574bf27348714506357d6e177"},"id":"035454db-6b6d-477f-8a79-ce24c10b185f","version":3} \ No newline at end of file diff --git a/cmd/run.go b/cmd/run.go index a340fdf4..ae1b3682 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -42,6 +42,7 @@ func start(ctx *cli.Context) error { return err } setupLog(c.Log) + logVersion() err = db.RunMigrations(c.SyncDB) if err != nil { log.Error(err) @@ -97,7 +98,6 @@ func start(ctx *cli.Context) error { var chsExitRootEvent []chan *etherman.GlobalExitRoot var chsSyncedL2 []chan uint - logVersion() for i, l2EthermanClient := range l2Ethermans { log.Debug("trusted sequencer URL ", c.Etherman.L2URLs[i]) zkEVMClient := client.NewClient(c.Etherman.L2URLs[i])