From 4af04b1ee7a4209ef3718151877343d33a8ba40d Mon Sep 17 00:00:00 2001 From: Aivaras Ko Date: Thu, 24 Oct 2024 17:18:45 +0300 Subject: [PATCH] Add operator delegate-to command (#233) --- pkg/internal/common/flags/general.go | 8 ++ pkg/internal/common/helper.go | 54 +++++++++ pkg/operator.go | 1 + pkg/operator/get_approval.go | 167 +++++++++++++++++++++++++++ 4 files changed, 230 insertions(+) create mode 100644 pkg/operator/get_approval.go diff --git a/pkg/internal/common/flags/general.go b/pkg/internal/common/flags/general.go index 7401abbd..9375b1c6 100644 --- a/pkg/internal/common/flags/general.go +++ b/pkg/internal/common/flags/general.go @@ -82,4 +82,12 @@ var ( Usage: "Suppress unnecessary output", EnvVars: []string{"SILENT"}, } + + ExpiryFlag = cli.Int64Flag{ + Name: "expiry", + Aliases: []string{"e"}, + Usage: "expiry in seconds", + EnvVars: []string{"EXPIRY"}, + Value: 3600, + } ) diff --git a/pkg/internal/common/helper.go b/pkg/internal/common/helper.go index 53c0c34f..6cfe066a 100644 --- a/pkg/internal/common/helper.go +++ b/pkg/internal/common/helper.go @@ -2,6 +2,7 @@ package common import ( "context" + "crypto/ecdsa" "encoding/json" "errors" "fmt" @@ -28,6 +29,7 @@ import ( eigenSdkUtils "github.com/Layr-Labs/eigensdk-go/utils" "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/common" gethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" @@ -124,6 +126,7 @@ func getWallet( if err != nil { return nil, common.Address{}, err } + return keyWallet, sender, nil } else if cfg.SignerType == types.FireBlocksSigner { var secretKey string @@ -479,3 +482,54 @@ func GetNoSendTxOpts(from common.Address) *bind.TransactOpts { func Trim0x(s string) string { return strings.TrimPrefix(s, "0x") } + +func Sign(digest []byte, cfg types.SignerConfig, p utils.Prompter) ([]byte, error) { + var privateKey *ecdsa.PrivateKey + + if cfg.SignerType == types.LocalKeystoreSigner { + ecdsaPassword, readFromPipe := utils.GetStdInPassword() + var err error + if !readFromPipe { + ecdsaPassword, err = p.InputHiddenString("Enter password to decrypt the ecdsa private key:", "", + func(password string) error { + return nil + }, + ) + if err != nil { + fmt.Println("Error while reading ecdsa key password") + return nil, err + } + } + + jsonContent, err := os.ReadFile(cfg.PrivateKeyStorePath) + if err != nil { + return nil, err + } + key, err := keystore.DecryptKey(jsonContent, ecdsaPassword) + if err != nil { + return nil, err + } + + privateKey = key.PrivateKey + } else if cfg.SignerType == types.FireBlocksSigner { + return nil, errors.New("FireBlocksSigner is not implemented") + } else if cfg.SignerType == types.Web3Signer { + return nil, errors.New("Web3Signer is not implemented") + } else if cfg.SignerType == types.PrivateKeySigner { + privateKey = cfg.PrivateKey + } else { + return nil, errors.New("signer is not implemented") + } + + signed, err := crypto.Sign(digest, privateKey) + if err != nil { + return nil, err + } + + // account for EIP-155 by incrementing V if necessary + if signed[crypto.RecoveryIDOffset] < 27 { + signed[crypto.RecoveryIDOffset] += 27 + } + + return signed, nil +} diff --git a/pkg/operator.go b/pkg/operator.go index 4fafe4f4..f3ca2ce4 100644 --- a/pkg/operator.go +++ b/pkg/operator.go @@ -17,6 +17,7 @@ func OperatorCmd(p utils.Prompter) *cli.Command { operator.StatusCmd(p), operator.UpdateCmd(p), operator.UpdateMetadataURICmd(p), + operator.GetApprovalCmd(p), }, } diff --git a/pkg/operator/get_approval.go b/pkg/operator/get_approval.go new file mode 100644 index 00000000..ccbc2eec --- /dev/null +++ b/pkg/operator/get_approval.go @@ -0,0 +1,167 @@ +package operator + +import ( + "context" + "crypto/rand" + "encoding/hex" + "fmt" + "math/big" + "time" + + "github.com/Layr-Labs/eigenlayer-cli/pkg/operator/keys" + eigenSdkUtils "github.com/Layr-Labs/eigensdk-go/utils" + + "github.com/Layr-Labs/eigenlayer-cli/pkg/internal/common" + "github.com/Layr-Labs/eigenlayer-cli/pkg/internal/common/flags" + "github.com/Layr-Labs/eigenlayer-cli/pkg/telemetry" + "github.com/Layr-Labs/eigenlayer-cli/pkg/utils" + + "github.com/Layr-Labs/eigensdk-go/chainio/clients/elcontracts" + elContracts "github.com/Layr-Labs/eigensdk-go/chainio/clients/elcontracts" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + + "github.com/urfave/cli/v2" +) + +func GetApprovalCmd(p utils.Prompter) *cli.Command { + getApprovalCmd := &cli.Command{ + Name: "get-approval", + Usage: "Generate the smart contract approval details for the delegateTo method", + UsageText: "get-approval ", + Description: ` + Generate the smart contract approval details for the delegateTo method. + + It expects the same configuration yaml file as an argument to the register command, along with the staker address. + + Use the --expiry flag to override the default expiration of 3600 seconds. + `, + After: telemetry.AfterRunAction(), + Flags: []cli.Flag{ + &flags.VerboseFlag, + &flags.ExpiryFlag, + }, + Action: func(cCtx *cli.Context) error { + logger := common.GetLogger(cCtx) + args := cCtx.Args() + if args.Len() != 2 { + return fmt.Errorf("%w: accepts 2 arg, received %d", keys.ErrInvalidNumberOfArgs, args.Len()) + } + + expirySeconds := cCtx.Int64(flags.ExpiryFlag.Name) + + configurationFilePath := args.Get(0) + stakerAddress := args.Get(1) + + if !eigenSdkUtils.IsValidEthereumAddress(stakerAddress) { + return fmt.Errorf("staker address %s is not valid address", stakerAddress) + } + + operatorCfg, err := common.ReadConfigFile(configurationFilePath) + if err != nil { + return err + } + cCtx.App.Metadata["network"] = operatorCfg.ChainId.String() + + logger.Infof( + "%s Operator configuration file read successfully %s", + utils.EmojiCheckMark, + operatorCfg.Operator.Address, + ) + logger.Info("%s validating operator config: %s", utils.EmojiWait, operatorCfg.Operator.Address) + + ethClient, err := ethclient.Dial(operatorCfg.EthRPCUrl) + if err != nil { + return err + } + id, err := ethClient.ChainID(context.Background()) + if err != nil { + return err + } + + if id.Cmp(&operatorCfg.ChainId) != 0 { + return fmt.Errorf( + "%w: chain ID in config file %d does not match the chain ID of the network %d", + ErrInvalidYamlFile, + &operatorCfg.ChainId, + id, + ) + } + + logger.Infof( + "%s Operator configuration file validated successfully %s", + utils.EmojiCheckMark, + operatorCfg.Operator.Address, + ) + + contractCfg := elcontracts.Config{ + DelegationManagerAddress: gethcommon.HexToAddress(operatorCfg.ELDelegationManagerAddress), + AvsDirectoryAddress: gethcommon.HexToAddress(operatorCfg.ELAVSDirectoryAddress), + } + reader, err := elContracts.NewReaderFromConfig( + contractCfg, + ethClient, + logger, + ) + if err != nil { + return err + } + + var staker = gethcommon.HexToAddress(stakerAddress) + var operator = gethcommon.HexToAddress(operatorCfg.Operator.Address) + var delegationApprover = gethcommon.HexToAddress(operatorCfg.Operator.DelegationApproverAddress) + salt := make([]byte, 32) + + if _, err := rand.Read(salt); err != nil { + return err + } + var expiry = new(big.Int).SetInt64(time.Now().Unix() + expirySeconds) + + callOpts := &bind.CallOpts{Context: context.Background()} + + hash, err := reader.CalculateDelegationApprovalDigestHash( + callOpts, + staker, + operator, + delegationApprover, + [32]byte(salt), + expiry, + ) + if err != nil { + return err + } + + signed, err := common.Sign(hash[:], operatorCfg.SignerConfig, p) + if err != nil { + return err + } + + fmt.Println() + fmt.Println("--------------------------- delegateTo for the staker ---------------------------") + fmt.Println() + fmt.Printf("operator: %s\n", operator) + fmt.Printf("approverSignatureAndExpiry.signature: %s\n", eigenSdkUtils.Add0x(hex.EncodeToString(signed))) + fmt.Printf("approverSignatureAndExpiry.expiry: %d\n", expiry) + fmt.Printf("approverSalt: %s\n", eigenSdkUtils.Add0x(hex.EncodeToString(salt))) + fmt.Println() + fmt.Println( + "--------------------------- CalculateDelegationApprovalDigestHash details ---------------------------", + ) + fmt.Println() + fmt.Printf("staker: %s\n", staker) + fmt.Printf("operator: %s\n", operator) + fmt.Printf("_delegationApprover: %s\n", delegationApprover) + fmt.Printf("approverSalt: %s\n", eigenSdkUtils.Add0x(hex.EncodeToString(salt))) + fmt.Printf("expiry: %d\n", expiry) + fmt.Println() + fmt.Printf("result: %s\n", eigenSdkUtils.Add0x(hex.EncodeToString(hash[:]))) + fmt.Println() + fmt.Println("------------------------------------------------------------------------") + fmt.Println() + + return nil + }, + } + return getApprovalCmd +}