diff --git a/client/pdao.go b/client/pdao.go index da48d6731..6e807a898 100644 --- a/client/pdao.go +++ b/client/pdao.go @@ -217,6 +217,20 @@ func (r *PDaoRequester) GetCurrentVotingDelegate() (*types.ApiResponse[api.Proto } // Get the pDAO status -func (r *PDaoRequester) GetStatus() (*types.ApiResponse[api.ProtocolDAOStatusResponse], error) { - return client.SendGetRequest[api.ProtocolDAOStatusResponse](r, "get-status", "GetStatus", nil) +func (r *PDaoRequester) GetStatus() (*types.ApiResponse[api.ProtocolDaoStatusResponse], error) { + return client.SendGetRequest[api.ProtocolDaoStatusResponse](r, "get-status", "GetStatus", nil) +} + +// Set the signalling address for the node +func (r *PDaoRequester) SetSignallingAddress(signallingAddress common.Address, signature string) (*types.ApiResponse[types.TxInfoData], error) { + args := map[string]string{ + "signallingAddress": signallingAddress.Hex(), + "signature": string(signature), + } + return client.SendGetRequest[types.TxInfoData](r, "set-signalling-address", "SetSignallingAddress", args) +} + +// Set the signalling address for the node +func (r *PDaoRequester) ClearSignallingAddress() (*types.ApiResponse[types.TxInfoData], error) { + return client.SendGetRequest[types.TxInfoData](r, "clear-signalling-address", "ClearSignallingAddress", nil) } diff --git a/rocketpool-cli/commands/node/status.go b/rocketpool-cli/commands/node/status.go index 5bd08ea44..709ba8bb8 100644 --- a/rocketpool-cli/commands/node/status.go +++ b/rocketpool-cli/commands/node/status.go @@ -67,9 +67,9 @@ func getStatus(c *cli.Context) error { return err } - // rp.NodeStatus() will fail with an error, but we can short-circuit it here. + // rp.Api.Node.Status() will fail with an error, but we can short-circuit it here. if !walletStatus.Address.HasAddress { - return errors.New("No node address is loaded.") + return errors.New("Node Wallet is not initialized.") } // Get node status diff --git a/rocketpool-cli/commands/pdao/commands.go b/rocketpool-cli/commands/pdao/commands.go index 79e8d3a2a..0d66caa55 100644 --- a/rocketpool-cli/commands/pdao/commands.go +++ b/rocketpool-cli/commands/pdao/commands.go @@ -154,6 +154,66 @@ func RegisterCommands(app *cli.App, name string, aliases []string) { }, }, + { + Name: "initialize-voting", + Aliases: []string{"iv"}, + Usage: "Unlocks a node operator's voting power (only required for node operators who registered before governance structure was in place)", + Action: func(c *cli.Context) error { + // Run + return initializeVoting(c) + }, + }, + + { + Name: "set-signalling-address", + Aliases: []string{"ssa"}, + Usage: "Set the address you want to use to represent your node on Snapshot", + ArgsUsage: "signalling-address signature", + Flags: []cli.Flag{ + cliutils.YesFlag, + }, + Action: func(c *cli.Context) error { + // Validate args + utils.ValidateArgCount(c, 2) + + signallingAddress, err := input.ValidateAddress("signalling-address", c.Args().Get(0)) + if err != nil { + return err + } + signature := c.Args().Get(1) + + // Run + return setSignallingAddress(c, signallingAddress, signature) + }, + }, + + { + Name: "clear-signalling-address", + Aliases: []string{"csa"}, + Usage: "Clear the node's signalling address", + Action: func(c *cli.Context) error { + // Run + return clearSignallingAddress(c) + }, + }, + + { + Name: "set-voting-delegate", + Aliases: []string{"svd"}, + Usage: "Set the address you want to use when voting on Rocket Pool on-chain governance proposals, or the address you want to delegate your voting power to.", + ArgsUsage: "address", + Flags: []cli.Flag{ + cliutils.YesFlag, + }, + Action: func(c *cli.Context) error { + // Validate args + utils.ValidateArgCount(c, 1) + delegate := c.Args().Get(0) + // Run + return setVotingDelegate(c, delegate) + }, + }, + { Name: "claim-bonds", Aliases: []string{"cb"}, @@ -469,33 +529,6 @@ func RegisterCommands(app *cli.App, name string, aliases []string) { }, }, }, - - { - Name: "initialize-voting", - Aliases: []string{"iv"}, - Usage: "Unlocks a node operator's voting power (only required for node operators who registered before governance structure was in place)", - Action: func(c *cli.Context) error { - // Run - return initializeVoting(c) - }, - }, - - { - Name: "set-voting-delegate", - Aliases: []string{"svd"}, - Usage: "Set the address you want to use when voting on Rocket Pool on-chain governance proposals, or the address you want to delegate your voting power to.", - ArgsUsage: "address", - Flags: []cli.Flag{ - cliutils.YesFlag, - }, - Action: func(c *cli.Context) error { - // Validate args - utils.ValidateArgCount(c, 1) - delegate := c.Args().Get(0) - // Run - return setVotingDelegate(c, delegate) - }, - }, }, }) } diff --git a/rocketpool-cli/commands/pdao/signalling-address.go b/rocketpool-cli/commands/pdao/signalling-address.go new file mode 100644 index 000000000..2093dc236 --- /dev/null +++ b/rocketpool-cli/commands/pdao/signalling-address.go @@ -0,0 +1,70 @@ +package pdao + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/rocket-pool/smartnode/v2/rocketpool-cli/client" + "github.com/rocket-pool/smartnode/v2/rocketpool-cli/utils/tx" + "github.com/urfave/cli/v2" +) + +func setSignallingAddress(c *cli.Context, signallingAddress common.Address, signature string) error { + // Get RP client + rp, err := client.NewClientFromCtx(c) + if err != nil { + return err + } + + // Build the TX + response, err := rp.Api.PDao.SetSignallingAddress(signallingAddress, signature) + if err != nil { + return fmt.Errorf("Error setting the signalling address: %w", err) + } + + validated, err := tx.HandleTx(c, rp, response.Data.TxInfo, + "Are you sure you want to set your signalling address?", + "setting signalling address", + "Setting signalling address...", + ) + if err != nil { + return err + } + if !validated { + return nil + } + + return nil + +} + +func clearSignallingAddress(c *cli.Context) error { + + // Get RP client + rp, err := client.NewClientFromCtx(c) + if err != nil { + return err + } + + // Build the TX + response, err := rp.Api.PDao.ClearSignallingAddress() + if err != nil { + return fmt.Errorf("Error clearing the signalling address: %w", err) + } + + validated, err := tx.HandleTx(c, rp, response.Data.TxInfo, + "Are you sure you want to clear the current signalling address?", + "clearing signalling address", + "Clearing signalling address...", + ) + if err != nil { + return err + } + if !validated { + return nil + } + + // Log & return + fmt.Println("The node's signalling address has been sucessfully cleared.") + return nil +} diff --git a/rocketpool-cli/commands/pdao/status.go b/rocketpool-cli/commands/pdao/status.go index 6aa966606..016d5f837 100644 --- a/rocketpool-cli/commands/pdao/status.go +++ b/rocketpool-cli/commands/pdao/status.go @@ -1,6 +1,7 @@ package pdao import ( + "errors" "fmt" "math/big" @@ -12,15 +13,10 @@ import ( "github.com/rocket-pool/rocketpool-go/v2/types" "github.com/rocket-pool/smartnode/v2/rocketpool-cli/client" "github.com/rocket-pool/smartnode/v2/rocketpool-cli/utils" + "github.com/rocket-pool/smartnode/v2/rocketpool-cli/utils/terminal" "github.com/rocket-pool/smartnode/v2/shared/types/api" ) -const ( - colorBlue string = "\033[36m" - colorReset string = "\033[0m" - colorGreen string = "\033[32m" -) - func getStatus(c *cli.Context) error { // Get RP client @@ -29,6 +25,30 @@ func getStatus(c *cli.Context) error { return err } + // Get the config + cfg, isNew, err := rp.LoadConfig() + if err != nil { + return fmt.Errorf("error loading configuration: %w", err) + } + + // Get wallet status + statusResponse, err := rp.Api.Wallet.Status() + if err != nil { + return err + } + walletStatus := statusResponse.Data.WalletStatus + + // Print what network we're on + err = utils.PrintNetwork(cfg.Network.Value, isNew) + if err != nil { + return err + } + + // rp.Api.PDao.GetStatus() will fail with an error, but we can short-circuit it here. + if !walletStatus.Address.HasAddress { + return errors.New("Node Wallet is not initialized.") + } + // Get PDAO status at the latest block response, err := rp.Api.PDao.GetStatus() if err != nil { @@ -42,88 +62,94 @@ func getStatus(c *cli.Context) error { } // Get protocol DAO proposals - claimableBondsResponse, err := rp.Api.PDao.GetClaimableBonds() - if err != nil { - return fmt.Errorf("error checking for claimable bonds: %w", err) + var claimableBonds []api.BondClaimResult + if response.Data.IsNodeRegistered { + claimableBondsResponse, err := rp.Api.PDao.GetClaimableBonds() + if err != nil { + return fmt.Errorf("error checking for claimable bonds: %w", err) + } + claimableBonds = claimableBondsResponse.Data.ClaimableBonds } - claimableBonds := claimableBondsResponse.Data.ClaimableBonds - // Snapshot voting status - fmt.Printf("%s=== Snapshot Voting ===%s\n", colorGreen, colorReset) + // Signalling Status + fmt.Printf("%s=== Signalling on Snapshot ===%s\n", terminal.ColorGreen, terminal.ColorReset) blankAddress := common.Address{} - if response.Data.SnapshotVotingDelegate == blankAddress { - fmt.Println("The node does not currently have a voting delegate set, which means it can only vote directly on Snapshot proposals (using a hardware wallet with the node mnemonic loaded).\nRun `rocketpool n sv
` to vote from a different wallet or have a delegate represent you. (See https://delegates.rocketpool.net for options)") + if response.Data.SignallingAddress == blankAddress { + fmt.Println("The node does not currently have a snapshot signalling address set.") + fmt.Println("To learn more about snapshot signalling, please visit https://docs.rocketpool.net/guides/houston/participate#setting-your-snapshot-signalling-address/") } else { - fmt.Printf("The node has a voting delegate of %s%s%s which can represent it when voting on Rocket Pool Snapshot governance proposals.\n", colorBlue, response.Data.SnapshotVotingDelegateFormatted, colorReset) + fmt.Printf("The node has a signalling address of %s%s%s which can represent it when voting on Rocket Pool Snapshot governance proposals.\n", terminal.ColorBlue, response.Data.SignallingAddressFormatted, terminal.ColorReset) } - if response.Data.SnapshotResponse.Error != "" { fmt.Printf("Unable to fetch latest voting information from snapshot.org: %s\n", response.Data.SnapshotResponse.Error) } else { voteCount := 0 + for _, activeProposal := range response.Data.SnapshotResponse.ActiveSnapshotProposals { + if len(activeProposal.DelegateVotes) > 0 || len(activeProposal.UserVotes) > 0 { + voteCount++ + break + } + } if len(response.Data.SnapshotResponse.ActiveSnapshotProposals) == 0 { - fmt.Print("Rocket Pool has no Snapshot governance proposals being voted on.\n") + fmt.Println("Rocket Pool has no Snapshot governance proposals being voted on.") } else { fmt.Printf("Rocket Pool has %d Snapshot governance proposal(s) being voted on. You have voted on %d of those. See details using 'rocketpool network dao-proposals'.\n", len(response.Data.SnapshotResponse.ActiveSnapshotProposals), voteCount) } - fmt.Println("") } + fmt.Println() // Onchain Voting Status - fmt.Printf("%s=== Onchain Voting ===%s\n", colorGreen, colorReset) + fmt.Printf("%s=== Onchain Voting ===%s\n", terminal.ColorGreen, terminal.ColorReset) if response.Data.IsVotingInitialized { - fmt.Println("The node has been initialized for onchain voting.") - + fmt.Printf("The node %s%s%s has been initialized for onchain voting.\n", terminal.ColorBlue, response.Data.AccountAddressFormatted, terminal.ColorReset) } else { - fmt.Println("The node has NOT been initialized for onchain voting. You need to run `rocketpool pdao initialize-voting` to participate in onchain votes.") + fmt.Printf("The node %s%s%s has NOT been initialized for onchain voting. You need to run `rocketpool pdao initialize-voting` to participate in onchain votes.\n", terminal.ColorBlue, response.Data.AccountAddressFormatted, terminal.ColorReset) } - if response.Data.OnchainVotingDelegate == blankAddress { fmt.Println("The node doesn't have a delegate, which means it can vote directly on onchain proposals after it initializes voting.") } else if response.Data.OnchainVotingDelegate == response.Data.AccountAddress { fmt.Println("The node doesn't have a delegate, which means it can vote directly on onchain proposals. You can have another node represent you by running `rocketpool p svd `.") } else { - fmt.Printf("The node has a voting delegate of %s%s%s which can represent it when voting on Rocket Pool onchain governance proposals.\n", colorBlue, response.Data.OnchainVotingDelegateFormatted, colorReset) + fmt.Printf("The node has a voting delegate of %s%s%s which can represent it when voting on Rocket Pool onchain governance proposals.\n", terminal.ColorBlue, response.Data.OnchainVotingDelegateFormatted, terminal.ColorReset) } fmt.Printf("The node's local voting power: %.10f\n", eth.WeiToEth(response.Data.VotingPower)) - - fmt.Printf("Total voting power delegated to the node: %.10f\n", eth.WeiToEth(response.Data.TotalDelegatedVp)) - + if response.Data.IsNodeRegistered { + fmt.Printf("Total voting power delegated to the node: %.10f\n", eth.WeiToEth(response.Data.TotalDelegatedVp)) + } else { + fmt.Println("The node must register using 'rocketpool node register' to be eligible to receive delegated voting power") + } fmt.Printf("Network total initialized voting power: %.10f\n", eth.WeiToEth(response.Data.SumVotingPower)) - - fmt.Println("") + fmt.Println() // Claimable Bonds Status: - fmt.Printf("%s=== Claimable RPL Bonds ===%s\n", colorGreen, colorReset) + fmt.Printf("%s=== Claimable RPL Bonds ===%s\n", terminal.ColorGreen, terminal.ColorReset) if response.Data.IsRPLLockingAllowed { - fmt.Print("The node is allowed to lock RPL to create governance proposals/challenges.\n") + fmt.Println("The node is allowed to lock RPL to create governance proposals/challenges.") if response.Data.NodeRPLLocked.Cmp(big.NewInt(0)) != 0 { - fmt.Printf("The node currently has %.6f RPL locked.\n", - utilsMath.RoundDown(eth.WeiToEth(response.Data.NodeRPLLocked), 6)) + fmt.Printf("The node currently has %.6f RPL locked.\n", utilsMath.RoundDown(eth.WeiToEth(response.Data.NodeRPLLocked), 6)) } - } else { - fmt.Print("The node is NOT allowed to lock RPL to create governance proposals/challenges. Use 'rocketpool node allow-rpl-locking, to allow RPL locking.\n") + fmt.Println("The node is NOT allowed to lock RPL to create governance proposals/challenges. Use 'rocketpool node allow-rpl-locking` to allow RPL locking.") } if len(claimableBonds) == 0 { - fmt.Println("You do not have any unlockable bonds or claimable rewards.") + fmt.Println("The node does not have any unlockable bonds or claimable rewards.") } else { fmt.Println("The node has unlockable bonds or claimable rewards available. Use 'rocketpool pdao claim-bonds' to view and claim.") } - fmt.Println("") + fmt.Println() // Check if PDAO proposal checking duty is enabled - fmt.Printf("%s=== PDAO Proposal Checking Duty ===%s\n", colorGreen, colorReset) + fmt.Printf("%s=== PDAO Proposal Checking Duty ===%s\n", terminal.ColorGreen, terminal.ColorReset) // Make sure the user opted into this duty if response.Data.VerifyEnabled { fmt.Println("The node has PDAO proposal checking duties enabled. It will periodically check for proposals to challenge.") } else { fmt.Println("The node does not have PDAO proposal checking duties enabled (See https://docs.rocketpool.net/guides/houston/pdao#challenge-process to learn more about this duty).") } - fmt.Println("") + fmt.Println() // Claimable Bonds Status: - fmt.Printf("%s=== Pending, Active and Succeeded Proposals ===%s\n", colorGreen, colorReset) + fmt.Printf("%s=== Pending, Active and Succeeded Proposals ===%s\n", terminal.ColorGreen, terminal.ColorReset) // Get proposals by state stateProposals := map[string][]api.ProtocolDaoProposalDetails{} for _, proposal := range allProposals.Data.Proposals { @@ -155,12 +181,12 @@ func getStatus(c *cli.Context) error { // Print message for Succeeded Proposals if stateName == "Succeeded" { succeededExists = true - fmt.Printf("%sThe following proposal(s) have succeeded and are waiting to be executed. Use `rocketpool pdao proposals execute` to execute.%s\n\n", colorBlue, colorReset) + fmt.Printf("%sThe following proposal(s) have succeeded and are waiting to be executed. Use `rocketpool pdao proposals execute` to execute.%s\n", terminal.ColorBlue, terminal.ColorReset) } // Proposal state count fmt.Printf("%d %s proposal(s):\n", len(proposals), stateName) - fmt.Println("") + fmt.Println() // Proposals for _, proposal := range proposals { diff --git a/rocketpool-daemon/api/pdao/clear-signalling-address.go b/rocketpool-daemon/api/pdao/clear-signalling-address.go new file mode 100644 index 000000000..13bc4637f --- /dev/null +++ b/rocketpool-daemon/api/pdao/clear-signalling-address.go @@ -0,0 +1,84 @@ +package pdao + +import ( + "fmt" + "net/url" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/gorilla/mux" + batch "github.com/rocket-pool/batch-query" + "github.com/rocket-pool/node-manager-core/api/server" + "github.com/rocket-pool/node-manager-core/api/types" + "github.com/rocket-pool/rocketpool-go/v2/rocketpool" +) + +// =============== +// === Factory === +// =============== + +type protocolDaoClearSignallingAddressFactory struct { + handler *ProtocolDaoHandler +} + +func (f *protocolDaoClearSignallingAddressFactory) Create(args url.Values) (*protocolDaoClearSignallingAddressContext, error) { + c := &protocolDaoClearSignallingAddressContext{ + handler: f.handler, + } + return c, nil +} + +func (f *protocolDaoClearSignallingAddressFactory) RegisterRoute(router *mux.Router) { + server.RegisterQuerylessGet[*protocolDaoClearSignallingAddressContext, types.TxInfoData]( + router, "clear-signalling-address", f, f.handler.logger.Logger, f.handler.serviceProvider.ServiceProvider, + ) +} + +// =============== +// === Context === +// =============== + +type protocolDaoClearSignallingAddressContext struct { + handler *ProtocolDaoHandler + rp *rocketpool.RocketPool + + signallingAddress common.Address +} + +func (c *protocolDaoClearSignallingAddressContext) PrepareData(data *types.TxInfoData, opts *bind.TransactOpts) (types.ResponseStatus, error) { + sp := c.handler.serviceProvider + c.rp = sp.GetRocketPool() + nodeAddress, _ := sp.GetWallet().GetAddress() + cfg := sp.GetConfig() + network := cfg.GetNetworkResources().Network + + // Requirements + err := sp.RequireNodeAddress() + if err != nil { + return types.ResponseStatus_AddressNotPresent, err + } + registry := sp.GetRocketSignerRegistry() + if registry == nil { + return types.ResponseStatus_Error, fmt.Errorf("Network [%v] does not have a signer registry contract", network) + } + + // Query registry contract + err = c.rp.Query(func(mc *batch.MultiCaller) error { + registry.NodeToSigner(mc, &c.signallingAddress, nodeAddress) + return nil + }, nil) + if err != nil { + return types.ResponseStatus_Error, fmt.Errorf("Error getting the registry contract: %w", err) + } + + // Return if there if no signalling address is set + if c.signallingAddress == (common.Address{}) { + return types.ResponseStatus_Error, fmt.Errorf("No signalling address set") + } + data.TxInfo, err = registry.ClearSigner(opts) + if err != nil { + return types.ResponseStatus_Error, fmt.Errorf("Error getting the TX info for ClearSigner: %w", err) + + } + return types.ResponseStatus_Success, nil +} diff --git a/rocketpool-daemon/api/pdao/handler.go b/rocketpool-daemon/api/pdao/handler.go index cae166597..c43536c38 100644 --- a/rocketpool-daemon/api/pdao/handler.go +++ b/rocketpool-daemon/api/pdao/handler.go @@ -47,6 +47,8 @@ func NewProtocolDaoHandler(logger *log.Logger, ctx context.Context, serviceProvi &protocolDaoSetVotingDelegateContextFactory{h}, &protocolDaoCurrentVotingDelegateContextFactory{h}, &protocolDaoGetStatusContextFactory{h}, + &protocolDaoClearSignallingAddressFactory{h}, + &protocolDaoSetSignallingAddressFactory{h}, } return h } diff --git a/rocketpool-daemon/api/pdao/set-signalling-address.go b/rocketpool-daemon/api/pdao/set-signalling-address.go new file mode 100644 index 000000000..17feeaa29 --- /dev/null +++ b/rocketpool-daemon/api/pdao/set-signalling-address.go @@ -0,0 +1,137 @@ +package pdao + +import ( + "errors" + "fmt" + "net/url" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/gorilla/mux" + batch "github.com/rocket-pool/batch-query" + "github.com/rocket-pool/node-manager-core/api/server" + "github.com/rocket-pool/node-manager-core/api/types" + "github.com/rocket-pool/node-manager-core/eth" + "github.com/rocket-pool/node-manager-core/utils/input" + "github.com/rocket-pool/rocketpool-go/v2/node" + "github.com/rocket-pool/rocketpool-go/v2/rocketpool" + "github.com/rocket-pool/smartnode/v2/rocketpool-daemon/common/contracts" + "github.com/rocket-pool/smartnode/v2/shared/eip712" +) + +// =============== +// === Factory === +// =============== + +type protocolDaoSetSignallingAddressFactory struct { + handler *ProtocolDaoHandler +} + +func (f *protocolDaoSetSignallingAddressFactory) Create(args url.Values) (*protocolDaoSetSignallingAddressContext, error) { + c := &protocolDaoSetSignallingAddressContext{ + handler: f.handler, + } + inputErrs := []error{ + server.ValidateArg("signallingAddress", args, input.ValidateAddress, &c.signallingAddress), + server.GetStringFromVars("signature", args, &c.signature), + } + return c, errors.Join(inputErrs...) +} + +func (f *protocolDaoSetSignallingAddressFactory) RegisterRoute(router *mux.Router) { + server.RegisterSingleStageRoute[*protocolDaoSetSignallingAddressContext, types.TxInfoData]( + router, "set-signalling-address", f, f.handler.logger.Logger, f.handler.serviceProvider.ServiceProvider, + ) +} + +// =============== +// === Context === +// =============== + +type protocolDaoSetSignallingAddressContext struct { + handler *ProtocolDaoHandler + rp *rocketpool.RocketPool + registry *contracts.RocketSignerRegistry + + node *node.Node + nodeAddress common.Address + signallingAddress common.Address + nodeToSigner common.Address + signature string +} + +func (c *protocolDaoSetSignallingAddressContext) Initialize() (types.ResponseStatus, error) { + sp := c.handler.serviceProvider + c.rp = sp.GetRocketPool() + c.nodeAddress, _ = sp.GetWallet().GetAddress() + cfg := sp.GetConfig() + network := cfg.GetNetworkResources().Network + + // Requirements + err := sp.RequireNodeAddress() + if err != nil { + return types.ResponseStatus_AddressNotPresent, err + } + c.registry = sp.GetRocketSignerRegistry() + if c.registry == nil { + return types.ResponseStatus_Error, fmt.Errorf("Network [%v] does not have a signer registry contract.", network) + } + + // Binding + c.node, err = node.NewNode(c.rp, c.nodeAddress) + if err != nil { + return types.ResponseStatus_Error, fmt.Errorf("Error creating node %s binding: %w", c.nodeAddress.Hex(), err) + } + + return types.ResponseStatus_Success, nil +} + +func (c *protocolDaoSetSignallingAddressContext) GetState(mc *batch.MultiCaller) { + eth.AddQueryablesToMulticall(mc, + c.node.Exists, + c.node.IsVotingInitialized, + ) + + // Check if the node already has a signer + if c.registry != nil { + c.registry.NodeToSigner(mc, &c.nodeToSigner, c.node.Address) + } +} + +func (c *protocolDaoSetSignallingAddressContext) PrepareData(data *types.TxInfoData, opts *bind.TransactOpts) (types.ResponseStatus, error) { + + if c.signallingAddress == c.nodeToSigner { + return types.ResponseStatus_Error, fmt.Errorf("Signer address already in use") + } + + if !c.node.IsVotingInitialized.Get() { + return types.ResponseStatus_Error, fmt.Errorf("Voting must be initialized to set a signalling address. Use 'rocketpool pdao initialize-voting' to initialize voting first") + } + + eip712Components := new(eip712.EIP712Components) + err := eip712Components.UnmarshalText([]byte(c.signature)) + if err != nil { + return types.ResponseStatus_Error, fmt.Errorf("Failed to unmarshal signature: %w", err) + } + + message := constructMessage(c.nodeAddress.Hex()) + err = eip712Components.Validate(message, c.signallingAddress) + if err != nil { + return types.ResponseStatus_Error, fmt.Errorf("Error validating signature: %w", err) + } + + // Get the tx + data.TxInfo, err = c.registry.SetSigner(c.signallingAddress, opts, eip712Components.V, eip712Components.R, eip712Components.S) + if err != nil { + return types.ResponseStatus_Error, fmt.Errorf("Error getting the TX info for SetSigner: %w", err) + } + + return types.ResponseStatus_Success, nil +} + +func constructMessage(nodeAddress string) []byte { + nodeAddress = strings.ToLower(nodeAddress) + message := fmt.Sprintf("%s may delegate to me for Rocket Pool governance", nodeAddress) + return []byte(message) +} diff --git a/rocketpool-daemon/api/pdao/status.go b/rocketpool-daemon/api/pdao/status.go index 6ec184ec2..5ab9d5c93 100644 --- a/rocketpool-daemon/api/pdao/status.go +++ b/rocketpool-daemon/api/pdao/status.go @@ -2,9 +2,11 @@ package pdao import ( "fmt" + "math/big" "net/url" "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" "github.com/gorilla/mux" batch "github.com/rocket-pool/batch-query" "github.com/rocket-pool/rocketpool-go/v2/node" @@ -13,8 +15,11 @@ import ( "github.com/rocket-pool/node-manager-core/api/server" "github.com/rocket-pool/node-manager-core/api/types" "github.com/rocket-pool/node-manager-core/beacon" + "github.com/rocket-pool/node-manager-core/eth" + "github.com/rocket-pool/smartnode/v2/rocketpool-daemon/common/contracts" "github.com/rocket-pool/smartnode/v2/rocketpool-daemon/common/proposals" "github.com/rocket-pool/smartnode/v2/rocketpool-daemon/common/utils" + "github.com/rocket-pool/smartnode/v2/rocketpool-daemon/common/voting" "github.com/rocket-pool/smartnode/v2/shared/config" "github.com/rocket-pool/smartnode/v2/shared/types/api" ) @@ -35,7 +40,7 @@ func (f *protocolDaoGetStatusContextFactory) Create(args url.Values) (*protocolD } func (f *protocolDaoGetStatusContextFactory) RegisterRoute(router *mux.Router) { - server.RegisterQuerylessGet[*protocolDaoGetStatusContext, api.ProtocolDAOStatusResponse]( + server.RegisterSingleStageRoute[*protocolDaoGetStatusContext, api.ProtocolDaoStatusResponse]( router, "get-status", f, f.handler.logger.Logger, f.handler.serviceProvider.ServiceProvider, ) } @@ -45,69 +50,122 @@ func (f *protocolDaoGetStatusContextFactory) RegisterRoute(router *mux.Router) { // =============== type protocolDaoGetStatusContext struct { - handler *ProtocolDaoHandler - cfg *config.SmartNodeConfig - rp *rocketpool.RocketPool - bc beacon.IBeaconClient - - propMgr *proposals.ProposalManager + handler *ProtocolDaoHandler + cfg *config.SmartNodeConfig + rp *rocketpool.RocketPool + ec eth.IExecutionClient + bc beacon.IBeaconClient + registry *contracts.RocketSignerRegistry + + node *node.Node + nodeAddress common.Address + propMgr *proposals.ProposalManager + blockNumber uint64 + signallingAddress common.Address + votingTree *proposals.NetworkVotingTree } -func (c *protocolDaoGetStatusContext) PrepareData(data *api.ProtocolDAOStatusResponse, opts *bind.TransactOpts) (types.ResponseStatus, error) { +func (c *protocolDaoGetStatusContext) Initialize() (types.ResponseStatus, error) { sp := c.handler.serviceProvider - rp := sp.GetRocketPool() - ec := sp.GetEthClient() c.cfg = sp.GetConfig() c.rp = sp.GetRocketPool() + c.ec = sp.GetEthClient() c.bc = sp.GetBeaconClient() - ctx := c.handler.ctx - - nodeAddress, _ := sp.GetWallet().GetAddress() + c.nodeAddress, _ = sp.GetWallet().GetAddress() + network := c.cfg.GetNetworkResources().Network // Requirements - status, err := sp.RequireNodeRegistered(c.handler.ctx) + err := sp.RequireNodeAddress() if err != nil { - return status, err + return types.ResponseStatus_AddressNotPresent, err + } + c.registry = sp.GetRocketSignerRegistry() + if c.registry == nil { + return types.ResponseStatus_Error, fmt.Errorf("Network [%v] does not have a signer registry contract.", network) } // Bindings - node, err := node.NewNode(rp, nodeAddress) + c.node, err = node.NewNode(c.rp, c.nodeAddress) if err != nil { - return types.ResponseStatus_Error, fmt.Errorf("error creating node %s binding: %w", nodeAddress.Hex(), err) + return types.ResponseStatus_Error, fmt.Errorf("error creating node %s binding: %w", c.nodeAddress.Hex(), err) } - c.propMgr, err = proposals.NewProposalManager(ctx, c.handler.logger.Logger, c.cfg, c.rp, c.bc) + c.propMgr, err = proposals.NewProposalManager(c.handler.ctx, c.handler.logger.Logger, c.cfg, c.rp, c.bc) if err != nil { return types.ResponseStatus_Error, fmt.Errorf("error creating proposal manager: %w", err) } - - // Get the latest block - blockNumber, err := ec.BlockNumber(c.handler.ctx) + c.blockNumber, err = c.ec.BlockNumber(c.handler.ctx) if err != nil { return types.ResponseStatus_Error, fmt.Errorf("error getting latest block number: %w", err) } - data.BlockNumber = uint32(blockNumber) - - totalDelegatedVP, _, _, err := c.propMgr.GetArtifactsForVoting(uint32(blockNumber), nodeAddress) + c.votingTree, err = c.propMgr.GetNetworkTree(uint32(c.blockNumber), nil) if err != nil { - return types.ResponseStatus_Error, fmt.Errorf("error getting voting artifacts for node %s at block %d: %w", nodeAddress.Hex(), blockNumber, err) + return types.ResponseStatus_Error, fmt.Errorf("error getting network tree") } - data.TotalDelegatedVp = totalDelegatedVP - votingTree, err := c.propMgr.GetNetworkTree(uint32(blockNumber), nil) - if err != nil { - return types.ResponseStatus_Error, fmt.Errorf("error getting network tree") + return types.ResponseStatus_Success, nil + +} + +func (c *protocolDaoGetStatusContext) GetState(mc *batch.MultiCaller) { + eth.AddQueryablesToMulticall(mc, + // Node + c.node.Exists, + c.node.IsVotingInitialized, + c.node.IsRplLockingAllowed, + c.node.RplLocked, + ) + // Snapshot Registry + c.registry.NodeToSigner(mc, &c.signallingAddress, c.node.Address) +} + +func (c *protocolDaoGetStatusContext) PrepareData(data *api.ProtocolDaoStatusResponse, opts *bind.TransactOpts) (types.ResponseStatus, error) { + + var err error + + data.IsVotingInitialized = c.node.IsVotingInitialized.Get() + + if !data.IsVotingInitialized { + data.TotalDelegatedVp = big.NewInt(0) + } else { + data.TotalDelegatedVp, _, _, err = c.propMgr.GetArtifactsForVoting(uint32(c.blockNumber), c.nodeAddress) + if err != nil { + return types.ResponseStatus_Error, fmt.Errorf("error getting voting artifacts for node %s at block %d: %w", c.nodeAddress.Hex(), c.blockNumber, err) + } } - data.SumVotingPower = votingTree.Nodes[0].Sum + + data.IsNodeRegistered = c.node.Exists.Get() + data.BlockNumber = uint32(c.blockNumber) + data.AccountAddress = c.node.Address + data.AccountAddressFormatted = utils.GetFormattedAddress(c.ec, data.AccountAddress) + data.IsVotingInitialized = c.node.IsVotingInitialized.Get() + data.SumVotingPower = c.votingTree.Nodes[0].Sum + data.IsRPLLockingAllowed = c.node.IsRplLockingAllowed.Get() + data.NodeRPLLocked = c.node.RplLocked.Get() + data.VerifyEnabled = c.cfg.VerifyProposals.Value // Get the voting power and delegate at that block - err = rp.Query(func(mc *batch.MultiCaller) error { - node.GetVotingPowerAtBlock(mc, &data.VotingPower, data.BlockNumber) - node.GetVotingDelegateAtBlock(mc, &data.OnchainVotingDelegate, data.BlockNumber) + err = c.rp.Query(func(mc *batch.MultiCaller) error { + c.node.GetVotingPowerAtBlock(mc, &data.VotingPower, data.BlockNumber) + c.node.GetVotingDelegateAtBlock(mc, &data.OnchainVotingDelegate, data.BlockNumber) return nil }, nil) if err != nil { - return types.ResponseStatus_Error, fmt.Errorf("error getting voting info for block %d: %w", blockNumber, err) + return types.ResponseStatus_Error, fmt.Errorf("error getting voting info for block %d: %w", c.blockNumber, err) + } + data.OnchainVotingDelegateFormatted = utils.GetFormattedAddress(c.ec, data.OnchainVotingDelegate) + + // Get the signalling address and active snapshot proposals + emptyAddress := common.Address{} + data.SignallingAddress = c.signallingAddress + if data.SignallingAddress != emptyAddress { + data.SignallingAddressFormatted = utils.GetFormattedAddress(c.ec, c.signallingAddress) + } + props, err := voting.GetSnapshotProposals(c.cfg, c.node.Address, c.signallingAddress, true) + if err != nil { + data.SnapshotResponse.Error = fmt.Sprintf("error getting snapshot proposals: %s", err.Error()) + } else { + data.SnapshotResponse.ActiveSnapshotProposals = props } - data.OnchainVotingDelegateFormatted = utils.GetFormattedAddress(ec, data.OnchainVotingDelegate) + return types.ResponseStatus_Success, nil } diff --git a/rocketpool-daemon/common/contracts/rocket-signer-registry.go b/rocketpool-daemon/common/contracts/rocket-signer-registry.go new file mode 100644 index 000000000..7b519b17d --- /dev/null +++ b/rocketpool-daemon/common/contracts/rocket-signer-registry.go @@ -0,0 +1,89 @@ +package contracts + +import ( + "fmt" + "strings" + "sync" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + batch "github.com/rocket-pool/batch-query" + "github.com/rocket-pool/node-manager-core/eth" +) + +const ( + rocketSignerRegistryAbiString string = "[{\"type\":\"function\",\"name\":\"clearSigner\",\"inputs\":[],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"nodeToSigner\",\"inputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"setSigner\",\"inputs\":[{\"name\":\"_signer\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"_v\",\"type\":\"uint8\",\"internalType\":\"uint8\"},{\"name\":\"_r\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"_s\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"signerToNode\",\"inputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"event\",\"name\":\"SignerSet\",\"inputs\":[{\"name\":\"nodeAddress\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"signerAddress\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}],\"anonymous\":false},{\"type\":\"error\",\"name\":\"StringsInsufficientHexLength\",\"inputs\":[{\"name\":\"value\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"length\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]}]" +) + +// ABI cache +var rocketSignerRegistryAbi abi.ABI +var rocketSignerRegistryOnce sync.Once + +// =============== +// === Structs === +// =============== + +// Binding for Rocket Signer Registry +type RocketSignerRegistry struct { + contract *eth.Contract + txMgr *eth.TransactionManager +} + +// ==================== +// === Constructors === +// ==================== + +// Creates a new Rocket Signer Registry contract binding +func NewRocketSignerRegistry(address common.Address, client eth.IExecutionClient, txMgr *eth.TransactionManager) (*RocketSignerRegistry, error) { + // Parse the ABI + var err error + rocketSignerRegistryOnce.Do(func() { + var parsedAbi abi.ABI + parsedAbi, err = abi.JSON(strings.NewReader(rocketSignerRegistryAbiString)) + if err == nil { + rocketSignerRegistryAbi = parsedAbi + } + }) + if err != nil { + return nil, fmt.Errorf("error parsing rocket signer registry ABI: %w", err) + } + + // Create the contract + contract := ð.Contract{ + ContractImpl: bind.NewBoundContract(address, rocketSignerRegistryAbi, client, client, client), + Address: address, + ABI: &rocketSignerRegistryAbi, + } + + return &RocketSignerRegistry{ + contract: contract, + txMgr: txMgr, + }, nil +} + +// ============= +// === Calls === +// ============= + +// Get the delegate for the provided address +func (c *RocketSignerRegistry) NodeToSigner(mc *batch.MultiCaller, out *common.Address, address common.Address) { + eth.AddCallToMulticaller(mc, c.contract, out, "nodeToSigner", address) +} +func (c *RocketSignerRegistry) SignerToNode(mc *batch.MultiCaller, out *common.Address, address common.Address) { + eth.AddCallToMulticaller(mc, c.contract, out, "signerToNode", address) +} + +// ==================== +// === Transactions === +// ==================== + +// Get info for setting the signalling address +func (c *RocketSignerRegistry) SetSigner(_signer common.Address, opts *bind.TransactOpts, _v uint8, _r [32]byte, _s [32]byte) (*eth.TransactionInfo, error) { + return c.txMgr.CreateTransactionInfo(c.contract, "setSigner", opts, _signer, _v, _r, _s) +} + +// Get info for clearing the signalling address +func (c *RocketSignerRegistry) ClearSigner(opts *bind.TransactOpts) (*eth.TransactionInfo, error) { + return c.txMgr.CreateTransactionInfo(c.contract, "clearSigner", opts) +} diff --git a/rocketpool-daemon/common/proposals/proposal-manager.go b/rocketpool-daemon/common/proposals/proposal-manager.go index 09c07037c..df68e4b3b 100644 --- a/rocketpool-daemon/common/proposals/proposal-manager.go +++ b/rocketpool-daemon/common/proposals/proposal-manager.go @@ -216,6 +216,9 @@ func (m *ProposalManager) GetArtifactsForVoting(blockNumber uint32, nodeAddress // Get the artifacts totalDelegatedVp := nodeTree.Nodes[0].Sum + if totalDelegatedVp == nil { + totalDelegatedVp = big.NewInt(0) + } treeIndex := getTreeNodeIndexFromRPNodeIndex(snapshot, nodeIndex) proofPtrs := networkTree.generateMerkleProof(treeIndex) diff --git a/rocketpool-daemon/common/services/service-provider.go b/rocketpool-daemon/common/services/service-provider.go index 6e91b57be..aaa83922a 100644 --- a/rocketpool-daemon/common/services/service-provider.go +++ b/rocketpool-daemon/common/services/service-provider.go @@ -22,11 +22,12 @@ type ServiceProvider struct { *services.ServiceProvider // Services - cfg *config.SmartNodeConfig - rocketPool *rocketpool.RocketPool - validatorManager *validator.ValidatorManager - snapshotDelegation *contracts.SnapshotDelegation - watchtowerLog *log.Logger + cfg *config.SmartNodeConfig + rocketPool *rocketpool.RocketPool + validatorManager *validator.ValidatorManager + snapshotDelegation *contracts.SnapshotDelegation + rocketSignerRegistry *contracts.RocketSignerRegistry + watchtowerLog *log.Logger // Internal use loadedContractVersion *version.Version @@ -109,6 +110,15 @@ func CreateServiceProviderFromComponents(cfg *config.SmartNodeConfig, sp *servic return nil, fmt.Errorf("error creating snapshot delegation binding: %w", err) } } + // Rocket Signer Registry + var rocketSignerRegistry *contracts.RocketSignerRegistry + registryAddress := resources.RocketSignerRegistryAddress + if registryAddress != nil { + rocketSignerRegistry, err = contracts.NewRocketSignerRegistry(*registryAddress, sp.GetEthClient(), sp.GetTransactionManager()) + if err != nil { + return nil, fmt.Errorf("error creating rocket signer registry binding: %w", err) + } + } // Create the provider defaultVersion, _ := version.NewSemver("0.0.0") @@ -119,6 +129,7 @@ func CreateServiceProviderFromComponents(cfg *config.SmartNodeConfig, sp *servic rocketPool: rp, validatorManager: vMgr, snapshotDelegation: snapshotDelegation, + rocketSignerRegistry: rocketSignerRegistry, watchtowerLog: watchtowerLogger, loadedContractVersion: defaultVersion, refreshLock: &sync.Mutex{}, @@ -154,6 +165,10 @@ func (p *ServiceProvider) GetSnapshotDelegation() *contracts.SnapshotDelegation return p.snapshotDelegation } +func (p *ServiceProvider) GetRocketSignerRegistry() *contracts.RocketSignerRegistry { + return p.rocketSignerRegistry +} + func (p *ServiceProvider) GetWatchtowerLogger() *log.Logger { return p.watchtowerLog } diff --git a/shared/config/resources.go b/shared/config/resources.go index 7ecd123ec..dee5520f8 100644 --- a/shared/config/resources.go +++ b/shared/config/resources.go @@ -56,6 +56,9 @@ type RocketPoolResources struct { // The contract address of rocketNetworkBalances from v1.2.0 V1_2_0_NetworkBalancesAddress *common.Address + // The contract Address for Rocket Signer Registry + RocketSignerRegistryAddress *common.Address + // The contract address for Snapshot delegation SnapshotDelegationAddress *common.Address @@ -121,6 +124,7 @@ func newRocketPoolResources(network config.Network) *RocketPoolResources { PreviousRewardsPoolAddresses: []common.Address{ common.HexToAddress("0x594Fb75D3dc2DFa0150Ad03F99F97817747dd4E1"), }, + RocketSignerRegistryAddress: hexToAddressPtr("0xc1062617d10Ae99E09D941b60746182A87eAB38F"), PreviousProtocolDaoVerifierAddresses: []common.Address{}, OptimismPriceMessengerAddress: hexToAddressPtr("0xdddcf2c25d50ec22e67218e873d46938650d03a7"), PolygonPriceMessengerAddress: hexToAddressPtr("0xb1029Ac2Be4e08516697093e2AFeC435057f3511"), @@ -156,6 +160,7 @@ func newRocketPoolResources(network config.Network) *RocketPoolResources { PreviousRewardsPoolAddresses: []common.Address{ common.HexToAddress("0x4a625C617a44E60F74E3fe3bf6d6333b63766e91"), }, + RocketSignerRegistryAddress: hexToAddressPtr("0x657FDE6B4764E26A81A323dbb79791A11B90dD91"), PreviousProtocolDaoVerifierAddresses: nil, OptimismPriceMessengerAddress: nil, PolygonPriceMessengerAddress: nil, diff --git a/shared/eip712/eip712.go b/shared/eip712/eip712.go new file mode 100644 index 000000000..5a86c1deb --- /dev/null +++ b/shared/eip712/eip712.go @@ -0,0 +1,106 @@ +package eip712 + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" +) + +type EIP712Components struct { + R [32]byte `json:"r"` + S [32]byte `json:"s"` + V uint8 `json:"v"` +} + +const EIP712Length = 65 + +// Pretty print for EIP712Components +func (e *EIP712Components) Print() { + fmt.Println("EIP712 Components:") + fmt.Printf("R: %x\n", e.R) + fmt.Printf("S: %x\n", e.S) + fmt.Printf("V: %d\n", e.V) +} + +// String returns a hexadecimal string representation of EIP712Components +func (e *EIP712Components) String() string { + out, err := e.MarshalText() + if err != nil { + // MarshalText should never return an error + panic(err) + } + return string(out) +} + +// UnmarshalText expects an EIP-712 signature as a []byte, decodes the signature, verifies the length, +// then assigns the appropriate bytes to R/S/V +func (e *EIP712Components) UnmarshalText(inp []byte) error { + // Cast to string then decode + signatureString := string(inp) + decodedSignature, err := hexutil.Decode(signatureString) + if err != nil { + return fmt.Errorf("Failed to decode hex string: %w", err) + } + + if len(decodedSignature) != EIP712Length { + return fmt.Errorf("Failed to unmarshal EIP-712 signature string: invalid length %d bytes (expected %d bytes)", len(decodedSignature), EIP712Length) + } + + copy(e.R[:], decodedSignature[0:32]) + copy(e.S[:], decodedSignature[32:64]) + e.V = decodedSignature[64] + + return nil +} + +// MarshalText initializes an empty byte slice, copies fields R/S/V into signatureBytes, +// then returns the encoded signature as a []byte +func (e *EIP712Components) MarshalText() ([]byte, error) { + signatureBytes := make([]byte, EIP712Length) + + copy(signatureBytes[0:32], e.R[:]) + copy(signatureBytes[32:64], e.S[:]) + signatureBytes[64] = e.V + + encodedSignature := hexutil.Encode(signatureBytes) + + return []byte(encodedSignature), nil +} + +// Validate recovers the address of a signer from a message and signature then +// compares the recovered signer address to the expected signer address +func (e *EIP712Components) Validate(msg []byte, expectedSigner common.Address) error { + + hash := accounts.TextHash(msg) + + // Convert the EIP712Components to a signature + sig := make([]byte, 65) + copy(sig[0:32], e.R[:]) + copy(sig[32:64], e.S[:]) + sig[64] = e.V + + // V (Recovery ID) must by 27 or 28, so we subtract 27 from 0 or 1 to get the recovery ID. + sig[crypto.RecoveryIDOffset] -= 27 + + // Recover the public key from the signature + pubKey, err := crypto.SigToPub(hash, sig) + if err != nil { + return fmt.Errorf("error recovering public key: %v", err) + } + + // Restore V to its original value + sig[crypto.RecoveryIDOffset] += 27 + + // Derive the signer address from the public key + recoveredSigner := crypto.PubkeyToAddress(*pubKey) + + // Compare the recovered signer address with the expected address + if recoveredSigner != expectedSigner { + return fmt.Errorf("signature does not match the expected signer: got %s, expected %s", recoveredSigner.Hex(), expectedSigner.Hex()) + } + + return nil +} diff --git a/shared/eip712/eip712_test.go b/shared/eip712/eip712_test.go new file mode 100644 index 000000000..55b77d069 --- /dev/null +++ b/shared/eip712/eip712_test.go @@ -0,0 +1,131 @@ +package eip712 + +import ( + "fmt" + "testing" + + "github.com/ethereum/go-ethereum/common" +) + +func TestUnmarshalAndMarshal(t *testing.T) { + signature := "0xba283b21f7168e53b082ad552d974591abe0f4db5b7032374abbcdcf09e0eadc2c0530ff4ac1d63e19c1ceca2d14b374c86b6c84f46bbd57747b48c21388c4e71c" + eip712Components := new(EIP712Components) + + err := eip712Components.UnmarshalText([]byte(signature)) + if err != nil { + t.Fatalf("Failed to unmarshal signature: %v", err) + } + + // Convert the components back to a hex string + encodedHexSig := eip712Components.String() + + if encodedHexSig != signature { + t.Fatalf("Expected %s but got %s", signature, encodedHexSig) + } +} + +func TestUnmarshalInvalid712Hex(t *testing.T) { + invalidSignature := "0xinvalidsignature" + eip712Components := new(EIP712Components) + + err := eip712Components.UnmarshalText([]byte(invalidSignature)) + if err == nil { + t.Fatal("Expected error for invalid signature but got none") + } +} + +func TestUnmarshalEmptySignature(t *testing.T) { + emptySignature := "" + eip712Components := new(EIP712Components) + + err := eip712Components.UnmarshalText([]byte(emptySignature)) + if err == nil { + t.Fatal("Expected error for empty signature but got none") + } +} + +func TestUnmarshalInvalidLength(t *testing.T) { + // Create a hex-encoded signature with an invalid length (not 65 bytes) + invalidLengthSignature := "0xba283b21f7168e53b082ad552d974591abe0f4db5b7032374abbcdcf09e0eadc" // 64 characters (32 bytes) + eip712Components := new(EIP712Components) + + err := eip712Components.UnmarshalText([]byte(invalidLengthSignature)) + if err == nil { + t.Fatal("Expected error for signature with invalid length but got none") + } + + expectedErrMsg := fmt.Sprintf("Failed to unmarshal EIP-712 signature string: invalid length %d bytes (expected %d bytes)", 32, EIP712Length) + if err.Error() != expectedErrMsg { + t.Fatalf("Expected error message: '%s' but got: '%s'", expectedErrMsg, err.Error()) + } +} + +func TestValidateSuccess(t *testing.T) { + // Create a valid EIP-712 signature + signature := "0xba283b21f7168e53b082ad552d974591abe0f4db5b7032374abbcdcf09e0eadc2c0530ff4ac1d63e19c1ceca2d14b374c86b6c84f46bbd57747b48c21388c4e71c" + + eip712Components := new(EIP712Components) + err := eip712Components.UnmarshalText([]byte(signature)) + if err != nil { + t.Fatalf("Failed to unmarshal signature: %v", err) + } + + // Message to be signed + message := "0xe8325f5f4486c2ff2ac7b522fbc9eb249d46c936 may delegate to me for Rocket Pool governance" + msg := []byte(message) + + // Expected signer + expectedSigner := common.HexToAddress("0x18eea3fBe5008d6f7a95d963a4BE403E82d35758") + + // Validate + err = eip712Components.Validate(msg, expectedSigner) + if err != nil { + t.Fatalf("Validation failed: %v", err) + } +} + +func TestValidateInvalidSignature(t *testing.T) { + // Create an invalid EIP-712 signature + invalidSignature := "0xba283b21f7168e53b082ad552d974591abe0f4db5b7032374abbcdcf09e0eadc2c0530ff4ac1d63e19c1ceca2d14b374c86b6c84f46bbd57747b48c21388c4e71f" // Last byte is invalid + eip712Components := new(EIP712Components) + err := eip712Components.UnmarshalText([]byte(invalidSignature)) + if err != nil { + t.Fatalf("Failed to unmarshal signature: %v", err) + } + + // Message to be signed + msg := []byte("0xe8325f5f4486c2ff2ac7b522fbc9eb249d46c936 may delegate to me for Rocket Pool governance") + + // Some arbitrary expected signer + expectedSigner := common.HexToAddress("0x7f0bfc4a2d057dc75a7a2d3c9dc7eae2b3881e3e") + + // Validate + err = eip712Components.Validate(msg, expectedSigner) + if err == nil { + t.Fatal("Expected error for invalid signature but got none") + } +} + +func TestValidateSignerMismatch(t *testing.T) { + // Create a valid EIP-712 signature + signature := "0xba283b21f7168e53b082ad552d974591abe0f4db5b7032374abbcdcf09e0eadc2c0530ff4ac1d63e19c1ceca2d14b374c86b6c84f46bbd57747b48c21388c4e71c" + + eip712Components := new(EIP712Components) + err := eip712Components.UnmarshalText([]byte(signature)) + if err != nil { + t.Fatalf("Failed to unmarshal signature: %v", err) + } + + // Message to be signed + message := "0xe8325f5f4486c2ff2ac7b522fbc9eb249d46c936 may delegate to me for Rocket Pool governance" + msg := []byte(message) + + // Provide a mismatched expected signer + expectedSigner := common.HexToAddress("0x0000000000000000000000000000000000000000") + + // Validate + err = eip712Components.Validate(msg, expectedSigner) + if err == nil { + t.Fatalf("Expected validation to fail, but it succeeded") + } +} diff --git a/shared/types/api/pdao.go b/shared/types/api/pdao.go index bc41acb57..7d5b28774 100644 --- a/shared/types/api/pdao.go +++ b/shared/types/api/pdao.go @@ -300,25 +300,28 @@ type ProtocolDaoCurrentVotingDelegateData struct { VotingDelegate common.Address `json:"votingDelegate"` } -type ProtocolDAOStatusResponse struct { - Status string `json:"status"` - Error string `json:"error"` - VotingPower *big.Int `json:"votingPower"` - OnchainVotingDelegate common.Address `json:"onchainVotingDelegate"` - OnchainVotingDelegateFormatted string `json:"onchainVotingDelegateFormatted"` - BlockNumber uint32 `json:"blockNumber"` - VerifyEnabled bool `json:"verifyEnabled"` - IsVotingInitialized bool `json:"isVotingInitialized"` - SnapshotVotingDelegate common.Address `json:"snapshotVotingDelegate"` - SnapshotVotingDelegateFormatted string `json:"snapshotVotingDelegateFormatted"` - SnapshotResponse struct { - Error string `json:"error"` - ActiveSnapshotProposals []*sharedtypes.SnapshotProposal `json:"activeSnapshotProposals"` - } `json:"snapshotResponse"` - IsRPLLockingAllowed bool `json:"isRPLLockingAllowed"` - NodeRPLLocked *big.Int `json:"nodeRPLLocked"` - AccountAddress common.Address `json:"accountAddress"` - AccountAddressFormatted string `json:"accountAddressFormatted"` - TotalDelegatedVp *big.Int `json:"totalDelegatedVp"` - SumVotingPower *big.Int `json:"sumVotingPower"` +type ProtocolDaoStatusResponse struct { + Status string `json:"status"` + Error string `json:"error"` + VotingPower *big.Int `json:"votingPower"` + OnchainVotingDelegate common.Address `json:"onchainVotingDelegate"` + OnchainVotingDelegateFormatted string `json:"onchainVotingDelegateFormatted"` + BlockNumber uint32 `json:"blockNumber"` + VerifyEnabled bool `json:"verifyEnabled"` + IsVotingInitialized bool `json:"isVotingInitialized"` + SignallingAddress common.Address `json:"signallingAddress"` + SignallingAddressFormatted string `json:"signallingAddressFormatted"` + SnapshotResponse SnapshotResponseData `json:"snapshotResponse"` + IsRPLLockingAllowed bool `json:"isRPLLockingAllowed"` + NodeRPLLocked *big.Int `json:"nodeRPLLocked"` + AccountAddress common.Address `json:"accountAddress"` + AccountAddressFormatted string `json:"accountAddressFormatted"` + TotalDelegatedVp *big.Int `json:"totalDelegatedVp"` + SumVotingPower *big.Int `json:"sumVotingPower"` + IsNodeRegistered bool `json:"isNodeRegistered"` +} + +type SnapshotResponseData struct { + Error string `json:"error"` + ActiveSnapshotProposals []*sharedtypes.SnapshotProposal `json:"activeSnapshotProposals"` } diff --git a/shared/types/voting.go b/shared/types/voting.go index 2578cfe98..60d613550 100644 --- a/shared/types/voting.go +++ b/shared/types/voting.go @@ -11,12 +11,17 @@ const ( ) type SnapshotProposal struct { + Id string `json:"id"` Title string `json:"title"` - State ProposalState `json:"state"` Start time.Time `json:"start"` End time.Time `json:"end"` + State ProposalState `json:"state"` + Snapshot string `json:"snapshot"` + Author string `json:"author"` Choices []string `json:"choices"` Scores []float64 `json:"scores"` + ScoresTotal float64 `json:"score_total"` + ScoresUpdated int64 `json:"scores_updated"` Quorum float64 `json:"quorum"` Link string `json:"link"` UserVotes []int `json:"userVotes"`