diff --git a/cli/core/beaconClient.go b/cli/core/beaconClient.go index da85ac69..f47dce98 100644 --- a/cli/core/beaconClient.go +++ b/cli/core/beaconClient.go @@ -27,10 +27,11 @@ type BeaconClient interface { type beaconClient struct { eth2client eth2client.Service + verbose bool } -func NewBeaconClient(endpoint string) (BeaconClient, context.CancelFunc, error) { - beaconClient := beaconClient{} +func NewBeaconClient(endpoint string, verbose bool) (BeaconClient, context.CancelFunc, error) { + beaconClient := beaconClient{verbose: verbose} ctx, cancel := context.WithCancel(context.Background()) client, err := http.New(ctx, @@ -42,7 +43,10 @@ func NewBeaconClient(endpoint string) (BeaconClient, context.CancelFunc, error) if err != nil { return nil, cancel, err } - log.Info().Msgf("Connected to %s\n", client.Name()) + + if verbose { + log.Info().Msgf("Connected to %s\n", client.Name()) + } beaconClient.eth2client = client return &beaconClient, cancel, nil @@ -64,7 +68,9 @@ func (b *beaconClient) GetBeaconHeader(ctx context.Context, blockId string) (*v1 func (b *beaconClient) GetBeaconState(ctx context.Context, stateId string) (*spec.VersionedBeaconState, error) { timeout, _ := time.ParseDuration("200s") if provider, ok := b.eth2client.(eth2client.BeaconStateProvider); ok { - log.Info().Msgf("downloading beacon state %s", stateId) + if b.verbose { + log.Info().Msgf("downloading beacon state %s", stateId) + } opts := &api.BeaconStateOpts{State: stateId, Common: api.CommonOpts{ Timeout: timeout, }} @@ -77,7 +83,9 @@ func (b *beaconClient) GetBeaconState(ctx context.Context, stateId string) (*spe return nil, errors.New("beacon state is nil") } - log.Info().Msg("finished download") + if b.verbose { + log.Info().Msg("finished download") + } return beaconState.Data, nil } diff --git a/cli/core/checkpoint.go b/cli/core/checkpoint.go index 0866e6be..94891172 100644 --- a/cli/core/checkpoint.go +++ b/cli/core/checkpoint.go @@ -20,7 +20,7 @@ import ( "github.com/fatih/color" ) -func SubmitCheckpointProof(ctx context.Context, owner, eigenpodAddress string, chainId *big.Int, proof *eigenpodproofs.VerifyCheckpointProofsCallParams, eth *ethclient.Client, batchSize uint64, noPrompt bool) ([]*types.Transaction, error) { +func SubmitCheckpointProof(ctx context.Context, owner, eigenpodAddress string, chainId *big.Int, proof *eigenpodproofs.VerifyCheckpointProofsCallParams, eth *ethclient.Client, batchSize uint64, noPrompt bool, noSend bool) ([]*types.Transaction, error) { tracing := GetContextTracingCallbacks(ctx) allProofChunks := chunk(proof.BalanceProofs, batchSize) @@ -32,7 +32,7 @@ func SubmitCheckpointProof(ctx context.Context, owner, eigenpodAddress string, c tracing.OnStartSection("pepe::proof::checkpoint::batch::submit", map[string]string{ "chunk": fmt.Sprintf("%d", i), }) - txn, err := SubmitCheckpointProofBatch(ctx, owner, eigenpodAddress, chainId, proof.ValidatorBalancesRootProof, balanceProofs, eth) + txn, err := SubmitCheckpointProofBatch(ctx, owner, eigenpodAddress, chainId, proof.ValidatorBalancesRootProof, balanceProofs, eth, noSend) tracing.OnEndSection() if err != nil { // failed to submit batch. @@ -43,19 +43,26 @@ func SubmitCheckpointProof(ctx context.Context, owner, eigenpodAddress string, c tracing.OnStartSection("pepe::proof::checkpoint::batch::wait", map[string]string{ "chunk": fmt.Sprintf("%d", i), }) - bind.WaitMined(ctx, eth, txn) + + if !noSend { + bind.WaitMined(ctx, eth, txn) + } tracing.OnEndSection() color.Green("OK") } - color.Green("Complete! re-run with `status` to see the updated Eigenpod state.") + if !noSend { + color.Green("Complete! re-run with `status` to see the updated Eigenpod state.") + } else { + color.Yellow("Submit these proofs to network and re-run with `status` to see the updated Eigenpod state.") + } return transactions, nil } -func SubmitCheckpointProofBatch(ctx context.Context, owner, eigenpodAddress string, chainId *big.Int, proof *eigenpodproofs.ValidatorBalancesRootProof, balanceProofs []*eigenpodproofs.BalanceProof, eth *ethclient.Client) (*types.Transaction, error) { +func SubmitCheckpointProofBatch(ctx context.Context, owner, eigenpodAddress string, chainId *big.Int, proof *eigenpodproofs.ValidatorBalancesRootProof, balanceProofs []*eigenpodproofs.BalanceProof, eth *ethclient.Client, noSend bool) (*types.Transaction, error) { tracing := GetContextTracingCallbacks(ctx) - ownerAccount, err := PrepareAccount(&owner, chainId) + ownerAccount, err := PrepareAccount(&owner, chainId, noSend) if err != nil { return nil, err } diff --git a/cli/core/utils.go b/cli/core/utils.go index c51385eb..ebab2a47 100644 --- a/cli/core/utils.go +++ b/cli/core/utils.go @@ -20,6 +20,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" gethCommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/params" @@ -113,17 +114,18 @@ type Owner = struct { FromAddress gethCommon.Address PublicKey *ecdsa.PublicKey TransactionOptions *bind.TransactOpts + IsDryRun bool } -func StartCheckpoint(ctx context.Context, eigenpodAddress string, ownerPrivateKey string, chainId *big.Int, eth *ethclient.Client, forceCheckpoint bool) (uint64, error) { - ownerAccount, err := PrepareAccount(&ownerPrivateKey, chainId) +func StartCheckpoint(ctx context.Context, eigenpodAddress string, ownerPrivateKey string, chainId *big.Int, eth *ethclient.Client, forceCheckpoint bool, noSend bool) (*types.Transaction, error) { + ownerAccount, err := PrepareAccount(&ownerPrivateKey, chainId, noSend) if err != nil { - return 0, fmt.Errorf("failed to parse private key: %w", err) + return nil, fmt.Errorf("failed to parse private key: %w", err) } eigenPod, err := onchain.NewEigenPod(gethCommon.HexToAddress(eigenpodAddress), eth) if err != nil { - return 0, fmt.Errorf("failed to reach eigenpod: %w", err) + return nil, fmt.Errorf("failed to reach eigenpod: %w", err) } revertIfNoBalance := !forceCheckpoint @@ -131,28 +133,17 @@ func StartCheckpoint(ctx context.Context, eigenpodAddress string, ownerPrivateKe txn, err := eigenPod.StartCheckpoint(ownerAccount.TransactionOptions, revertIfNoBalance) if err != nil { if !forceCheckpoint { - return 0, fmt.Errorf("failed to start checkpoint (try running again with `--force`): %w", err) + return nil, fmt.Errorf("failed to start checkpoint (try running again with `--force`): %w", err) } - return 0, fmt.Errorf("failed to start checkpoint: %w", err) + return nil, fmt.Errorf("failed to start checkpoint: %w", err) } - color.Green("starting checkpoint: %s.. (waiting for txn to be mined)...", txn.Hash().Hex()) - - bind.WaitMined(ctx, eth, txn) - - color.Green("started checkpoint! txn: %s", txn.Hash().Hex()) - - currentCheckpoint, err := GetCurrentCheckpoint(eigenpodAddress, eth) - if err != nil { - return 0, fmt.Errorf("failed to fetch current checkpoint: %w", err) - } - - return currentCheckpoint, nil + return txn, nil } -func GetBeaconClient(beaconUri string) (BeaconClient, error) { - beaconClient, _, err := NewBeaconClient(beaconUri) +func GetBeaconClient(beaconUri string, verbose bool) (BeaconClient, error) { + beaconClient, _, err := NewBeaconClient(beaconUri, verbose) return beaconClient, err } @@ -278,7 +269,7 @@ func GetCurrentCheckpointBlockRoot(eigenpodAddress string, eth *ethclient.Client return &checkpoint.BeaconBlockRoot, nil } -func GetClients(ctx context.Context, node, beaconNodeUri string) (*ethclient.Client, BeaconClient, *big.Int, error) { +func GetClients(ctx context.Context, node, beaconNodeUri string, enableLogs bool) (*ethclient.Client, BeaconClient, *big.Int, error) { eth, err := ethclient.Dial(node) if err != nil { return nil, nil, nil, fmt.Errorf("failed to reach eth --node: %w", err) @@ -290,10 +281,10 @@ func GetClients(ctx context.Context, node, beaconNodeUri string) (*ethclient.Cli } if chainId == nil || chainId.Int64() != 17000 { - return nil, nil, nil, fmt.Errorf("This tool only supports the Holesky network.") + return nil, nil, nil, errors.New("this tool only supports the Holesky network") } - beaconClient, err := GetBeaconClient(beaconNodeUri) + beaconClient, err := GetBeaconClient(beaconNodeUri, enableLogs) if err != nil { return nil, nil, nil, fmt.Errorf("failed to reach beacon client: %w", err) } @@ -328,7 +319,38 @@ func PanicIfNoConsent(prompt string) { } } -func PrepareAccount(owner *string, chainID *big.Int) (*Owner, error) { +func PrepareAccount(owner *string, chainID *big.Int, noSend bool) (*Owner, error) { + if noSend { + privateKey, err := crypto.HexToECDSA("372d94b8645091147a5dfc10a454d0d539773d2431293bf0a195b44fa5ddbb33") // this is a RANDOM private key. Do not use this for anything. + if err != nil { + return nil, err + } + + publicKey := privateKey.Public() + publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) + if !ok { + log.Fatal("error casting public key to ECDSA") + } + fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA) + auth, err := bind.NewKeyedTransactorWithChainID(privateKey, chainID) + if err != nil { + return nil, err + } + + auth.GasPrice = nil // big.NewInt(10) // Gas price to use for the transaction execution (nil = gas price oracle) + auth.GasFeeCap = big.NewInt(10) // big.NewInt(10) // Gas fee cap to use for the 1559 transaction execution (nil = gas price oracle) + auth.GasTipCap = big.NewInt(2) // big.NewInt(2) // Gas priority fee cap to use for the 1559 transaction execution (nil = gas price oracle) + auth.GasLimit = 21000 + auth.NoSend = true + + return &Owner{ + FromAddress: fromAddress, + PublicKey: nil, + TransactionOptions: auth, + IsDryRun: true, + }, nil + } + if owner == nil { return nil, errors.New("no owner") } @@ -353,6 +375,7 @@ func PrepareAccount(owner *string, chainID *big.Int) (*Owner, error) { FromAddress: fromAddress, PublicKey: publicKeyECDSA, TransactionOptions: auth, + IsDryRun: noSend, }, nil } diff --git a/cli/core/validator.go b/cli/core/validator.go index 616b98a7..5cfd233a 100644 --- a/cli/core/validator.go +++ b/cli/core/validator.go @@ -38,31 +38,39 @@ func LoadValidatorProofFromFile(path string) (*SerializableCredentialProof, erro return &res, nil } -func SubmitValidatorProof(ctx context.Context, owner, eigenpodAddress string, chainId *big.Int, eth *ethclient.Client, batchSize uint64, proofs *eigenpodproofs.VerifyValidatorFieldsCallParams, oracleBeaconTimesetamp uint64, noPrompt bool) ([]*types.Transaction, error) { - ownerAccount, err := PrepareAccount(&owner, chainId) +func SubmitValidatorProof(ctx context.Context, owner, eigenpodAddress string, chainId *big.Int, eth *ethclient.Client, batchSize uint64, proofs *eigenpodproofs.VerifyValidatorFieldsCallParams, oracleBeaconTimesetamp uint64, noPrompt bool, noSend bool, verbose bool) ([]*types.Transaction, [][]*big.Int, error) { + ownerAccount, err := PrepareAccount(&owner, chainId, noSend) if err != nil { - return nil, err + return nil, [][]*big.Int{}, err } PanicOnError("failed to parse private key", err) eigenPod, err := onchain.NewEigenPod(common.HexToAddress(eigenpodAddress), eth) if err != nil { - return nil, err + return nil, [][]*big.Int{}, err } indices := Uint64ArrayToBigIntArray(proofs.ValidatorIndices) validatorIndicesChunks := chunk(indices, batchSize) validatorProofsChunks := chunk(proofs.ValidatorFieldsProofs, batchSize) validatorFieldsChunks := chunk(proofs.ValidatorFields, batchSize) - if !noPrompt { + if !noPrompt && !noSend { PanicIfNoConsent(SubmitCredentialsProofConsent(len(validatorFieldsChunks))) } transactions := []*types.Transaction{} numChunks := len(validatorIndicesChunks) - color.Green("calling EigenPod.VerifyWithdrawalCredentials() (using %d txn(s), max(%d) proofs per txn)", numChunks, batchSize) - color.Green("Submitting proofs with %d transactions", numChunks) + if verbose { + color.Green("calling EigenPod.VerifyWithdrawalCredentials() (using %d txn(s), max(%d) proofs per txn [%s])", numChunks, batchSize, func() string { + if ownerAccount.TransactionOptions.NoSend { + return "simulated" + } else { + return "live" + } + }()) + color.Green("Submitting proofs with %d transactions", numChunks) + } for i := 0; i < numChunks; i++ { curValidatorIndices := validatorIndicesChunks[i] @@ -75,20 +83,24 @@ func SubmitValidatorProof(ctx context.Context, owner, eigenpodAddress string, ch } var curValidatorFields [][][32]byte = CastValidatorFields(validatorFieldsChunks[i]) - fmt.Printf("Submitted chunk %d/%d -- waiting for transaction...: ", i+1, numChunks) - txn, err := SubmitValidatorProofChunk(ctx, ownerAccount, eigenPod, chainId, eth, curValidatorIndices, curValidatorFields, proofs.StateRootProof, validatorFieldsProofs, oracleBeaconTimesetamp) + if verbose { + fmt.Printf("Submitted chunk %d/%d -- waiting for transaction...: ", i+1, numChunks) + } + txn, err := SubmitValidatorProofChunk(ctx, ownerAccount, eigenPod, chainId, eth, curValidatorIndices, curValidatorFields, proofs.StateRootProof, validatorFieldsProofs, oracleBeaconTimesetamp, verbose) if err != nil { - return transactions, err + return transactions, validatorIndicesChunks, err } transactions = append(transactions, txn) } - return transactions, err + return transactions, validatorIndicesChunks, err } -func SubmitValidatorProofChunk(ctx context.Context, ownerAccount *Owner, eigenPod *onchain.EigenPod, chainId *big.Int, eth *ethclient.Client, indices []*big.Int, validatorFields [][][32]byte, stateRootProofs *eigenpodproofs.StateRootProof, validatorFieldsProofs [][]byte, oracleBeaconTimesetamp uint64) (*types.Transaction, error) { - color.Green("submitting onchain...") +func SubmitValidatorProofChunk(ctx context.Context, ownerAccount *Owner, eigenPod *onchain.EigenPod, chainId *big.Int, eth *ethclient.Client, indices []*big.Int, validatorFields [][][32]byte, stateRootProofs *eigenpodproofs.StateRootProof, validatorFieldsProofs [][]byte, oracleBeaconTimesetamp uint64, verbose bool) (*types.Transaction, error) { + if verbose { + color.Green("submitting onchain...") + } txn, err := eigenPod.VerifyWithdrawalCredentials( ownerAccount.TransactionOptions, oracleBeaconTimesetamp, @@ -108,7 +120,7 @@ func SubmitValidatorProofChunk(ctx context.Context, ownerAccount *Owner, eigenPo * Generates a .ProveValidatorContainers() proof for all eligible validators on the pod. If `validatorIndex` is set, it will only generate a proof * against that validator, regardless of the validator's state. */ -func GenerateValidatorProof(ctx context.Context, eigenpodAddress string, eth *ethclient.Client, chainId *big.Int, beaconClient BeaconClient, validatorIndex *big.Int) (*eigenpodproofs.VerifyValidatorFieldsCallParams, uint64, error) { +func GenerateValidatorProof(ctx context.Context, eigenpodAddress string, eth *ethclient.Client, chainId *big.Int, beaconClient BeaconClient, validatorIndex *big.Int, verbose bool) (*eigenpodproofs.VerifyValidatorFieldsCallParams, uint64, error) { latestBlock, err := eth.BlockByNumber(ctx, nil) if err != nil { return nil, 0, fmt.Errorf("failed to load latest block: %w", err) @@ -139,11 +151,11 @@ func GenerateValidatorProof(ctx context.Context, eigenpodAddress string, eth *et return nil, 0, fmt.Errorf("failed to initialize provider: %w", err) } - proofs, err := GenerateValidatorProofAtState(proofExecutor, eigenpodAddress, beaconState, eth, chainId, header, latestBlock.Time(), validatorIndex) + proofs, err := GenerateValidatorProofAtState(proofExecutor, eigenpodAddress, beaconState, eth, chainId, header, latestBlock.Time(), validatorIndex, verbose) return proofs, latestBlock.Time(), err } -func GenerateValidatorProofAtState(proofs *eigenpodproofs.EigenPodProofs, eigenpodAddress string, beaconState *spec.VersionedBeaconState, eth *ethclient.Client, chainId *big.Int, header *v1.BeaconBlockHeader, blockTimestamp uint64, forSpecificValidatorIndex *big.Int) (*eigenpodproofs.VerifyValidatorFieldsCallParams, error) { +func GenerateValidatorProofAtState(proofs *eigenpodproofs.EigenPodProofs, eigenpodAddress string, beaconState *spec.VersionedBeaconState, eth *ethclient.Client, chainId *big.Int, header *v1.BeaconBlockHeader, blockTimestamp uint64, forSpecificValidatorIndex *big.Int, verbose bool) (*eigenpodproofs.VerifyValidatorFieldsCallParams, error) { allValidators, err := FindAllValidatorsForEigenpod(eigenpodAddress, beaconState) if err != nil { return nil, fmt.Errorf("failed to find validators: %w", err) @@ -172,10 +184,14 @@ func GenerateValidatorProofAtState(proofs *eigenpodproofs.EigenPodProofs, eigenp } if len(awaitingCredentialValidators) == 0 { - color.Red("You have no inactive validators to verify. Everything up-to-date.") + if verbose { + color.Red("You have no inactive validators to verify. Everything up-to-date.") + } return nil, nil } else { - color.Blue("Verifying %d inactive validators", len(awaitingCredentialValidators)) + if verbose { + color.Blue("Verifying %d inactive validators", len(awaitingCredentialValidators)) + } } validatorIndices := make([]uint64, len(awaitingCredentialValidators)) diff --git a/cli/flags.go b/cli/flags.go new file mode 100644 index 00000000..48c5990e --- /dev/null +++ b/cli/flags.go @@ -0,0 +1,67 @@ +package main + +import cli "github.com/urfave/cli/v2" + +// Required for commands that need an EigenPod's address +var POD_ADDRESS_FLAG = &cli.StringFlag{ + Name: "podAddress", + Aliases: []string{"p", "pod"}, + Value: "", + Usage: "[required] The onchain `address` of your eigenpod contract (0x123123123123)", + Required: true, + Destination: &eigenpodAddress, +} + +// Required for commands that need a beacon chain RPC +var BEACON_NODE_FLAG = &cli.StringFlag{ + Name: "beaconNode", + Aliases: []string{"b"}, + Value: "", + Usage: "[required] `URL` to a functioning beacon node RPC (https://)", + Required: true, + Destination: &beacon, +} + +// Required for commands that need an execution layer RPC +var EXEC_NODE_FLAG = &cli.StringFlag{ + Name: "execNode", + Aliases: []string{"e"}, + Value: "", + Usage: "[required] `URL` to a functioning execution-layer RPC (https://)", + Required: true, + Destination: &node, +} +var PRINT_CALLDATA_BUT_DO_NOT_EXECUTE_FLAG = &cli.BoolFlag{ + Name: "print-calldata", + Value: false, + Usage: "Print the calldata for all associated transactions, but do not execute them. Note that some transactions have an order dependency (you cannot submit checkpoint proofs if you haven't started a checkpoint) so this may require you to get your pod into the correct state before usage.", + Required: false, + Destination: &simulateTransaction, +} + +// Optional commands: + +// Optional use for commands that want direct tx submission from a specific private key +var SENDER_PK_FLAG = &cli.StringFlag{ + Name: "sender", + Aliases: []string{"s"}, + Value: "", + Usage: "`Private key` of the account that will send any transactions. If set, this will automatically submit the proofs to their corresponding onchain functions after generation. If using checkpoint mode, it will also begin a checkpoint if one hasn't been started already.", + Destination: &sender, +} + +// Optional use for commands that support JSON output +var PRINT_JSON_FLAG = &cli.BoolFlag{ + Name: "json", + Value: false, + Usage: "print only plain JSON", + Required: false, + Destination: &useJson, +} + +var PROOF_PATH_FLAG = &cli.StringFlag{ + Name: "proof", + Value: "", + Usage: "the `path` to a previous proof generated from this step (via -o proof.json). If provided, this proof will submitted to network via the --sender flag.", + Destination: &proofPath, +} diff --git a/cli/main.go b/cli/main.go index 20471c47..7c2c63e9 100644 --- a/cli/main.go +++ b/cli/main.go @@ -12,105 +12,21 @@ import ( "github.com/Layr-Labs/eigenpod-proofs-generation/cli/core" "github.com/Layr-Labs/eigenpod-proofs-generation/cli/core/onchain" - gethCommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" "github.com/fatih/color" "github.com/pkg/errors" cli "github.com/urfave/cli/v2" ) -func shortenHex(publicKey string) string { - return publicKey[0:6] + ".." + publicKey[len(publicKey)-4:] -} - -// shared flag --batch -func BatchBySize(destination *uint64, defaultValue uint64) *cli.Uint64Flag { - return &cli.Uint64Flag{ - Name: "batch", - Value: defaultValue, - Usage: "Submit proofs in groups of size `batchSize`, to avoid gas limit.", - Required: false, - Destination: destination, - } -} - -// Hack to make a copy of a flag that sets `Required` to true -func Require(flag *cli.StringFlag) *cli.StringFlag { - return &cli.StringFlag{ - Name: flag.Name, - Aliases: flag.Aliases, - Value: flag.Value, - Usage: flag.Usage, - Destination: flag.Destination, - Required: true, - } -} - // Destinations for values set by various flags var eigenpodAddress, beacon, node, sender, output string -var useJson bool = false +var useJson, simulateTransaction bool = false, false var specificValidator uint64 = math.MaxUint64 var proofPath string -// Required flags: - -// Required for commands that need an EigenPod's address -var POD_ADDRESS_FLAG = &cli.StringFlag{ - Name: "podAddress", - Aliases: []string{"p", "pod"}, - Value: "", - Usage: "[required] The onchain `address` of your eigenpod contract (0x123123123123)", - Required: true, - Destination: &eigenpodAddress, -} - -// Required for commands that need a beacon chain RPC -var BEACON_NODE_FLAG = &cli.StringFlag{ - Name: "beaconNode", - Aliases: []string{"b"}, - Value: "", - Usage: "[required] `URL` to a functioning beacon node RPC (https://)", - Required: true, - Destination: &beacon, -} - -// Required for commands that need an execution layer RPC -var EXEC_NODE_FLAG = &cli.StringFlag{ - Name: "execNode", - Aliases: []string{"e"}, - Value: "", - Usage: "[required] `URL` to a functioning execution-layer RPC (https://)", - Required: true, - Destination: &node, -} - -// Optional commands: - -// Optional use for commands that want direct tx submission from a specific private key -var SENDER_PK_FLAG = &cli.StringFlag{ - Name: "sender", - Aliases: []string{"s"}, - Value: "", - Usage: "`Private key` of the account that will send any transactions. If set, this will automatically submit the proofs to their corresponding onchain functions after generation. If using checkpoint mode, it will also begin a checkpoint if one hasn't been started already.", - Destination: &sender, -} - -// Optional use for commands that support JSON output -var PRINT_JSON_FLAG = &cli.BoolFlag{ - Name: "json", - Value: false, - Usage: "print only plain JSON", - Required: false, - Destination: &useJson, -} - -var PROOF_PATH_FLAG = &cli.StringFlag{ - Name: "proof", - Value: "", - Usage: "the `path` to a previous proof generated from this step (via -o proof.json). If provided, this proof will submitted to network via the --sender flag.", - Destination: &proofPath, -} - // maximum number of proofs per txn for each of the following proof types: const DEFAULT_BATCH_CREDENTIALS = 60 const DEFAULT_BATCH_CHECKPOINT = 80 @@ -142,7 +58,7 @@ func main() { targetAddress := cctx.Args().First() if len(targetAddress) == 0 { return fmt.Errorf("usage: `assign-submitter <0xsubmitter>`") - } else if !gethCommon.IsHexAddress(targetAddress) { + } else if !common.IsHexAddress(targetAddress) { return fmt.Errorf("invalid address for 0xsubmitter: %s", targetAddress) } @@ -156,18 +72,18 @@ func main() { return fmt.Errorf("failed to reach eth node for chain id: %w", err) } - ownerAccount, err := core.PrepareAccount(&sender, chainId) + ownerAccount, err := core.PrepareAccount(&sender, chainId, false /* noSend */) if err != nil { return fmt.Errorf("failed to parse --sender: %w", err) } - pod, err := onchain.NewEigenPod(gethCommon.HexToAddress(eigenpodAddress), eth) + pod, err := onchain.NewEigenPod(common.HexToAddress(eigenpodAddress), eth) if err != nil { return fmt.Errorf("error contacting eigenpod: %w", err) } // Check that the existing submitter is not the current submitter - newSubmitter := gethCommon.HexToAddress(targetAddress) + newSubmitter := common.HexToAddress(targetAddress) currentSubmitter, err := pod.ProofSubmitter(nil) if err != nil { return fmt.Errorf("error fetching current proof submitter: %w", err) @@ -175,7 +91,7 @@ func main() { return fmt.Errorf("error: new proof submitter is existing proof submitter (%s)", currentSubmitter) } - if !noPrompt { + if !noPrompt && !simulateTransaction { fmt.Printf("Your pod's current proof submitter is %s.\n", currentSubmitter) core.PanicIfNoConsent(fmt.Sprintf("This will update your EigenPod to allow %s to submit proofs on its behalf. As the EigenPod's owner, you can always change this later.", newSubmitter)) } @@ -205,7 +121,9 @@ func main() { color.NoColor = true } - eth, beaconClient, _, err := core.GetClients(ctx, node, beacon) + isVerbose := !useJson + + eth, beaconClient, _, err := core.GetClients(ctx, node, beacon, isVerbose) core.PanicOnError("failed to load ethereum clients", err) status := core.GetStatus(ctx, eigenpodAddress, eth, beaconClient) @@ -215,6 +133,7 @@ func main() { core.PanicOnError("failed to get status", err) statusStr := string(bytes) fmt.Println(statusStr) + return nil } else { bold := color.New(color.Bold, color.FgBlue) ital := color.New(color.Italic, color.FgBlue) @@ -373,6 +292,7 @@ func main() { BEACON_NODE_FLAG, EXEC_NODE_FLAG, SENDER_PK_FLAG, + PRINT_CALLDATA_BUT_DO_NOT_EXECUTE_FLAG, BatchBySize(&batchSize, DEFAULT_BATCH_CHECKPOINT), PROOF_PATH_FLAG, &cli.BoolFlag{ @@ -395,13 +315,20 @@ func main() { color.NoColor = true } + isVerbose := !useJson && !simulateTransaction + var out *string = nil if len(cctx.String("out")) > 0 { outProp := cctx.String("out") out = &outProp } - eth, beaconClient, chainId, err := core.GetClients(ctx, node, beacon) + if simulateTransaction && len(sender) > 0 { + core.Panic("if using `--print-calldata`, please do not specify a sender.") + return nil + } + + eth, beaconClient, chainId, err := core.GetClients(ctx, node, beacon, isVerbose) core.PanicOnError("failed to reach ethereum clients", err) if len(proofPath) > 0 { @@ -414,7 +341,7 @@ func main() { proof, err := core.LoadCheckpointProofFromFile(proofPath) core.PanicOnError("failed to parse checkpoint proof from file", err) - txns, err := core.SubmitCheckpointProof(ctx, sender, eigenpodAddress, chainId, proof, eth, batchSize, noPrompt) + txns, err := core.SubmitCheckpointProof(ctx, sender, eigenpodAddress, chainId, proof, eth, batchSize, noPrompt, simulateTransaction) for _, txn := range txns { color.Green("submitted txn: %s", txn.Hash()) } @@ -425,20 +352,46 @@ func main() { currentCheckpoint, err := core.GetCurrentCheckpoint(eigenpodAddress, eth) core.PanicOnError("failed to load checkpoint", err) + eigenpod, err := onchain.NewEigenPod(common.HexToAddress(eigenpodAddress), eth) + core.PanicOnError("failed to connect to eigenpod", err) + if currentCheckpoint == 0 { - if len(sender) != 0 { - if !noPrompt { + if len(sender) > 0 || simulateTransaction { + if !noPrompt && !simulateTransaction { core.PanicIfNoConsent(core.StartCheckpointProofConsent()) } - newCheckpoint, err := core.StartCheckpoint(ctx, eigenpodAddress, sender, chainId, eth, forceCheckpoint) + txn, err := core.StartCheckpoint(ctx, eigenpodAddress, sender, chainId, eth, forceCheckpoint, simulateTransaction) core.PanicOnError("failed to start checkpoint", err) + + if !simulateTransaction { + color.Green("starting checkpoint: %s.. (waiting for txn to be mined)", txn.Hash().Hex()) + bind.WaitMined(ctx, eth, txn) + color.Green("started checkpoint! txn: %s", txn.Hash().Hex()) + } else { + printProofs([]Transaction{ + { + Type: "checkpoint_start", + To: txn.To().Hex(), + CallData: common.Bytes2Hex(txn.Data()), + }, + }) + + return nil + } + + newCheckpoint, err := eigenpod.CurrentCheckpointTimestamp(nil) + core.PanicOnError("failed to fetch current checkpoint", err) + currentCheckpoint = newCheckpoint } else { core.PanicOnError("no checkpoint active and no private key provided to start one", errors.New("no checkpoint")) } } - color.Green("pod has active checkpoint! checkpoint timestamp: %d", currentCheckpoint) + + if isVerbose { + color.Green("pod has active checkpoint! checkpoint timestamp: %d", currentCheckpoint) + } proof, err := core.GenerateCheckpointProof(ctx, eigenpodAddress, eth, chainId, beaconClient) core.PanicOnError("failed to generate checkpoint proof", err) @@ -448,10 +401,21 @@ func main() { if out != nil { core.WriteOutputToFileOrStdout(jsonString, out) - } else if len(sender) != 0 { - txns, err := core.SubmitCheckpointProof(ctx, sender, eigenpodAddress, chainId, proof, eth, batchSize, noPrompt) - for _, txn := range txns { - color.Green("submitted txn: %s", txn.Hash()) + } else if len(sender) != 0 || simulateTransaction { + txns, err := core.SubmitCheckpointProof(ctx, sender, eigenpodAddress, chainId, proof, eth, batchSize, noPrompt, simulateTransaction) + if simulateTransaction { + printableTxns := aMap(txns, func(txn *types.Transaction) Transaction { + return Transaction{ + To: txn.To().Hex(), + CallData: common.Bytes2Hex(txn.Data()), + Type: "checkpoint_proof", + } + }) + printProofs(printableTxns) + } else { + for i, txn := range txns { + color.Green("transaction(%d): %s", i, txn.Hash().Hex()) + } } core.PanicOnError("an error occurred while submitting your checkpoint proofs", err) } @@ -468,6 +432,7 @@ func main() { BEACON_NODE_FLAG, EXEC_NODE_FLAG, SENDER_PK_FLAG, + PRINT_CALLDATA_BUT_DO_NOT_EXECUTE_FLAG, BatchBySize(&batchSize, DEFAULT_BATCH_CREDENTIALS), &cli.Uint64Flag{ Name: "validatorIndex", @@ -488,13 +453,22 @@ func main() { color.NoColor = true } - eth, beaconClient, chainId, err := core.GetClients(ctx, node, beacon) + isVerbose := !useJson && !simulateTransaction + + eth, beaconClient, chainId, err := core.GetClients(ctx, node, beacon, isVerbose) core.PanicOnError("failed to reach ethereum clients", err) + if simulateTransaction && len(sender) > 0 { + core.Panic("if using --print-calldata, please do not specify a --sender.") + return nil + } + var specificValidatorIndex *big.Int = nil if specificValidator != math.MaxUint64 && specificValidator != 0 { specificValidatorIndex = new(big.Int).SetUint64(specificValidator) - fmt.Printf("Using specific validator: %d", specificValidator) + if verbose { + fmt.Printf("Using specific validator: %d", specificValidator) + } } if len(proofPath) > 0 { @@ -505,26 +479,53 @@ func main() { proof, err := core.LoadValidatorProofFromFile(proofPath) core.PanicOnError("failed to parse checkpoint proof from file", err) - txns, err := core.SubmitValidatorProof(ctx, sender, eigenpodAddress, chainId, eth, batchSize, proof.ValidatorProofs, proof.OracleBeaconTimestamp, noPrompt) - for _, txn := range txns { - color.Green("submitted txn: %s", txn.Hash()) + txns, _, err := core.SubmitValidatorProof(ctx, sender, eigenpodAddress, chainId, eth, batchSize, proof.ValidatorProofs, proof.OracleBeaconTimestamp, noPrompt, simulateTransaction, verbose) + if verbose { + for _, txn := range txns { + color.Green("submitted txn: %s", txn.Hash()) + } } core.PanicOnError("an error occurred while submitting your credential proofs", err) return nil } - validatorProofs, oracleBeaconTimestamp, err := core.GenerateValidatorProof(ctx, eigenpodAddress, eth, chainId, beaconClient, specificValidatorIndex) + validatorProofs, oracleBeaconTimestamp, err := core.GenerateValidatorProof(ctx, eigenpodAddress, eth, chainId, beaconClient, specificValidatorIndex, verbose) if err != nil || validatorProofs == nil { core.PanicOnError("Failed to generate validator proof", err) core.Panic("no inactive validators") } - if len(sender) != 0 { - txns, err := core.SubmitValidatorProof(ctx, sender, eigenpodAddress, chainId, eth, batchSize, validatorProofs, oracleBeaconTimestamp, noPrompt) - for i, txn := range txns { - color.Green("transaction(%d): %s", i, txn.Hash().Hex()) + if len(sender) != 0 || simulateTransaction { + txns, indices, err := core.SubmitValidatorProof(ctx, sender, eigenpodAddress, chainId, eth, batchSize, validatorProofs, oracleBeaconTimestamp, noPrompt, simulateTransaction, verbose) + core.PanicOnError(fmt.Sprintf("failed to %s validator proof", func() string { + if simulateTransaction { + return "simulate" + } else { + return "submit" + } + }()), err) + + if simulateTransaction { + out := aMap(txns, func(txn *types.Transaction) CredentialProofTransaction { + return CredentialProofTransaction{ + Transaction: Transaction{ + Type: "credential_proof", + To: txn.To().Hex(), + CallData: common.Bytes2Hex(txn.Data()), + }, + ValidatorIndices: aMap(aFlatten(indices), func(index *big.Int) uint64 { + return index.Uint64() + }), + } + }) + printProofs(out) + } else { + for i, txn := range txns { + color.Green("transaction(%d): %s", i, txn.Hash().Hex()) + } } + core.PanicOnError("failed to invoke verifyWithdrawalCredentials", err) } else { proof := core.SerializableCredentialProof{ diff --git a/cli/utils.go b/cli/utils.go new file mode 100644 index 00000000..48635194 --- /dev/null +++ b/cli/utils.go @@ -0,0 +1,73 @@ +package main + +import ( + "encoding/json" + "fmt" + + "github.com/Layr-Labs/eigenpod-proofs-generation/cli/core" + cli "github.com/urfave/cli/v2" +) + +type Transaction struct { + Type string `json:"type"` + To string `json:"to"` + CallData string `json:"calldata"` +} +type TransactionList = []Transaction + +type CredentialProofTransaction struct { + Transaction + ValidatorIndices []uint64 `json:"validator_indices"` +} + +func printProofs(txns any) { + out, err := json.Marshal(txns) + core.PanicOnError("failed to serialize proofs", err) + fmt.Println(string(out)) +} + +// imagine if golang had a standard library +func aMap[A any, B any](coll []A, mapper func(i A) B) []B { + out := make([]B, len(coll)) + for i, item := range coll { + out[i] = mapper(item) + } + return out +} + +func aFlatten[A any](coll [][]A) []A { + out := []A{} + for _, arr := range coll { + for _, item := range arr { + out = append(out, item) + } + } + return out +} + +func shortenHex(publicKey string) string { + return publicKey[0:6] + ".." + publicKey[len(publicKey)-4:] +} + +// shared flag --batch +func BatchBySize(destination *uint64, defaultValue uint64) *cli.Uint64Flag { + return &cli.Uint64Flag{ + Name: "batch", + Value: defaultValue, + Usage: "Submit proofs in groups of size `batchSize`, to avoid gas limit.", + Required: false, + Destination: destination, + } +} + +// Hack to make a copy of a flag that sets `Required` to true +func Require(flag *cli.StringFlag) *cli.StringFlag { + return &cli.StringFlag{ + Name: flag.Name, + Aliases: flag.Aliases, + Value: flag.Value, + Usage: flag.Usage, + Destination: flag.Destination, + Required: true, + } +}