node: generalised governance (#3895)
* node/admin: add generalised EVM call governance handler Handles governance requests of the form: ``` current_set_index: 4 messages: { sequence: 4513077582118919631 nonce: 2809988562 evm_call: { chain_id: 3 governance_contract: "0xD8E4C2DbDd2e2bd8F1336EA691dBFF6952B1a6eB" target_contract: "0xF890982f9310df57d00f659cf4fd87e65adEd8d7" abi_encoded_call: "6497f75a000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000f890982f9310df57d00f659cf4fd87e65aded8d70000000000000000000000000000000000000000000000000000000000000140bebebebebebebebebebebebebebebebebebebebebebebebebebebebebebebebe000000000000000000000000000000000000000000000000000000000000000268690000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004beefface00000000000000000000000000000000000000000000000000000000" } } ``` * node/admin: add admin template for evm governance call * node/admin: add generalised Solana call governance handler handles governance requests of the form ``` current_set_index: 4 messages: { sequence: 4513077582118919631 nonce: 2809988562 solana_call: { chain_id: 3 governance_contract: "3u8hJUVTA4jH1wYAyUur7FFZVQ8H635K3tSHHF4ssjQ5" encoded_instruction: "BEEFFACE" } } ``` * node/admin: check address lengths and fix typo in governance handler * node/admin: better error handling and fix comments * sdk/vaa: add constants for general purpose governance actions
This commit is contained in:
parent
35f0b343ed
commit
9620fca895
|
@ -60,6 +60,11 @@ var ibcUpdateChannelChainChainId *string
|
|||
var recoverChainIdEvmChainId *string
|
||||
var recoverChainIdNewChainId *string
|
||||
|
||||
var governanceContractAddress *string
|
||||
var governanceTargetAddress *string
|
||||
var governanceTargetChain *string
|
||||
var governanceCallData *string
|
||||
|
||||
func init() {
|
||||
governanceFlagSet := pflag.NewFlagSet("governance", pflag.ExitOnError)
|
||||
chainID = governanceFlagSet.String("chain-id", "", "Chain ID")
|
||||
|
@ -171,6 +176,19 @@ func init() {
|
|||
AdminClientRecoverChainIdCmd.Flags().AddFlagSet(recoverChainIdFlagSet)
|
||||
AdminClientRecoverChainIdCmd.Flags().AddFlagSet(moduleFlagSet)
|
||||
TemplateCmd.AddCommand(AdminClientRecoverChainIdCmd)
|
||||
|
||||
// flags for general-purpose governance call command
|
||||
generalPurposeGovernanceFlagSet := pflag.NewFlagSet("general-purpose-governance", pflag.ExitOnError)
|
||||
governanceContractAddress = generalPurposeGovernanceFlagSet.String("governance-contract", "", "Governance contract address")
|
||||
governanceTargetAddress = generalPurposeGovernanceFlagSet.String("target-address", "", "Address of the governed contract")
|
||||
governanceCallData = generalPurposeGovernanceFlagSet.String("call-data", "", "calldata")
|
||||
governanceTargetChain = generalPurposeGovernanceFlagSet.String("chain-id", "", "Chain ID")
|
||||
// evm call command
|
||||
AdminClientGeneralPurposeGovernanceEvmCallCmd.Flags().AddFlagSet(generalPurposeGovernanceFlagSet)
|
||||
TemplateCmd.AddCommand(AdminClientGeneralPurposeGovernanceEvmCallCmd)
|
||||
// solana call command
|
||||
AdminClientGeneralPurposeGovernanceSolanaCallCmd.Flags().AddFlagSet(generalPurposeGovernanceFlagSet)
|
||||
TemplateCmd.AddCommand(AdminClientGeneralPurposeGovernanceSolanaCallCmd)
|
||||
}
|
||||
|
||||
var TemplateCmd = &cobra.Command{
|
||||
|
@ -292,6 +310,18 @@ var AdminClientWormholeRelayerSetDefaultDeliveryProviderCmd = &cobra.Command{
|
|||
Run: runWormholeRelayerSetDefaultDeliveryProviderTemplate,
|
||||
}
|
||||
|
||||
var AdminClientGeneralPurposeGovernanceEvmCallCmd = &cobra.Command{
|
||||
Use: "governance-evm-call",
|
||||
Short: "Generate a 'general purpose evm governance call' template for specified chain and address",
|
||||
Run: runGeneralPurposeGovernanceEvmCallTemplate,
|
||||
}
|
||||
|
||||
var AdminClientGeneralPurposeGovernanceSolanaCallCmd = &cobra.Command{
|
||||
Use: "governance-solana-call",
|
||||
Short: "Generate a 'general purpose solana governance call' template for specified chain and address",
|
||||
Run: runGeneralPurposeGovernanceSolanaCallTemplate,
|
||||
}
|
||||
|
||||
func runGuardianSetTemplate(cmd *cobra.Command, args []string) {
|
||||
// Use deterministic devnet addresses as examples in the template, such that this doubles as a test fixture.
|
||||
guardians := make([]*nodev1.GuardianSetUpdate_Guardian, *setUpdateNumGuardians)
|
||||
|
@ -932,6 +962,100 @@ func runWormholeRelayerSetDefaultDeliveryProviderTemplate(cmd *cobra.Command, ar
|
|||
fmt.Print(string(b))
|
||||
}
|
||||
|
||||
func runGeneralPurposeGovernanceEvmCallTemplate(cmd *cobra.Command, args []string) {
|
||||
if *governanceTargetAddress == "" {
|
||||
log.Fatal("--target-address must be specified")
|
||||
}
|
||||
if !common.IsHexAddress(*governanceTargetAddress) {
|
||||
log.Fatal("invalid target address")
|
||||
}
|
||||
governanceTargetAddress := common.HexToAddress(*governanceTargetAddress).Hex()
|
||||
if *governanceCallData == "" {
|
||||
log.Fatal("--call-data must be specified")
|
||||
}
|
||||
if *governanceContractAddress == "" {
|
||||
log.Fatal("--governance-contract must be specified")
|
||||
}
|
||||
if !common.IsHexAddress(*governanceContractAddress) {
|
||||
log.Fatal("invalid governance contract address")
|
||||
}
|
||||
governanceContractAddress := common.HexToAddress(*governanceContractAddress).Hex()
|
||||
if *governanceTargetChain == "" {
|
||||
log.Fatal("--chain-id must be specified")
|
||||
}
|
||||
chainID, err := parseChainID(*governanceTargetChain)
|
||||
if err != nil {
|
||||
log.Fatal("failed to parse chain id: ", err)
|
||||
}
|
||||
|
||||
m := &nodev1.InjectGovernanceVAARequest{
|
||||
CurrentSetIndex: uint32(*templateGuardianIndex),
|
||||
Messages: []*nodev1.GovernanceMessage{
|
||||
{
|
||||
Sequence: rand.Uint64(),
|
||||
Nonce: rand.Uint32(),
|
||||
Payload: &nodev1.GovernanceMessage_EvmCall{
|
||||
EvmCall: &nodev1.EvmCall{
|
||||
ChainId: uint32(chainID),
|
||||
GovernanceContract: governanceContractAddress,
|
||||
TargetContract: governanceTargetAddress,
|
||||
AbiEncodedCall: *governanceCallData,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
b, err := prototext.MarshalOptions{Multiline: true}.Marshal(m)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Print(string(b))
|
||||
}
|
||||
|
||||
func runGeneralPurposeGovernanceSolanaCallTemplate(cmd *cobra.Command, args []string) {
|
||||
if *governanceCallData == "" {
|
||||
log.Fatal("--call-data must be specified")
|
||||
}
|
||||
if *governanceContractAddress == "" {
|
||||
log.Fatal("--governance-contract must be specified")
|
||||
}
|
||||
_, err := base58.Decode(*governanceContractAddress)
|
||||
if err != nil {
|
||||
log.Fatal("invalid base58 governance contract address")
|
||||
}
|
||||
if *governanceTargetChain == "" {
|
||||
log.Fatal("--chain-id must be specified")
|
||||
}
|
||||
chainID, err := parseChainID(*governanceTargetChain)
|
||||
if err != nil {
|
||||
log.Fatal("failed to parse chain id: ", err)
|
||||
}
|
||||
|
||||
m := &nodev1.InjectGovernanceVAARequest{
|
||||
CurrentSetIndex: uint32(*templateGuardianIndex),
|
||||
Messages: []*nodev1.GovernanceMessage{
|
||||
{
|
||||
Sequence: rand.Uint64(),
|
||||
Nonce: rand.Uint32(),
|
||||
Payload: &nodev1.GovernanceMessage_SolanaCall{
|
||||
SolanaCall: &nodev1.SolanaCall{
|
||||
ChainId: uint32(chainID),
|
||||
GovernanceContract: *governanceContractAddress,
|
||||
EncodedInstruction: *governanceCallData,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
b, err := prototext.MarshalOptions{Multiline: true}.Marshal(m)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Print(string(b))
|
||||
}
|
||||
|
||||
// parseAddress parses either a hex-encoded address and returns
|
||||
// a left-padded 32 byte hex string.
|
||||
func parseAddress(s string) (string, error) {
|
||||
|
|
|
@ -577,6 +577,58 @@ func wormholeRelayerSetDefaultDeliveryProvider(req *nodev1.WormholeRelayerSetDef
|
|||
return v, nil
|
||||
}
|
||||
|
||||
func evmCallToVaa(evmCall *nodev1.EvmCall, timestamp time.Time, guardianSetIndex, nonce uint32, sequence uint64) (*vaa.VAA, error) {
|
||||
governanceContract := ethcommon.HexToAddress(evmCall.GovernanceContract)
|
||||
targetContract := ethcommon.HexToAddress(evmCall.TargetContract)
|
||||
|
||||
payload, err := hex.DecodeString(evmCall.AbiEncodedCall)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode ABI encoded call: %w", err)
|
||||
}
|
||||
|
||||
body, err := vaa.BodyGeneralPurposeGovernanceEvm{
|
||||
ChainID: vaa.ChainID(evmCall.ChainId),
|
||||
GovernanceContract: governanceContract,
|
||||
TargetContract: targetContract,
|
||||
Payload: payload,
|
||||
}.Serialize()
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to serialize governance body: %w", err)
|
||||
}
|
||||
|
||||
v := vaa.CreateGovernanceVAA(timestamp, nonce, sequence, guardianSetIndex, body)
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func solanaCallToVaa(solanaCall *nodev1.SolanaCall, timestamp time.Time, guardianSetIndex, nonce uint32, sequence uint64) (*vaa.VAA, error) {
|
||||
address, err := base58.Decode(solanaCall.GovernanceContract)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode base58 governance contract address: %w", err)
|
||||
}
|
||||
if len(address) != 32 {
|
||||
return nil, errors.New("invalid governance contract address length (expected 32 bytes)")
|
||||
}
|
||||
|
||||
var governanceContract [32]byte
|
||||
copy(governanceContract[:], address)
|
||||
|
||||
instruction, err := hex.DecodeString(solanaCall.EncodedInstruction)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode instruction: %w", err)
|
||||
}
|
||||
|
||||
v := vaa.CreateGovernanceVAA(timestamp, nonce, sequence, guardianSetIndex,
|
||||
vaa.BodyGeneralPurposeGovernanceSolana{
|
||||
ChainID: vaa.ChainID(solanaCall.ChainId),
|
||||
GovernanceContract: governanceContract,
|
||||
Instruction: instruction,
|
||||
}.Serialize())
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func GovMsgToVaa(message *nodev1.GovernanceMessage, currentSetIndex uint32, timestamp time.Time) (*vaa.VAA, error) {
|
||||
var (
|
||||
v *vaa.VAA
|
||||
|
@ -620,6 +672,10 @@ func GovMsgToVaa(message *nodev1.GovernanceMessage, currentSetIndex uint32, time
|
|||
v, err = ibcUpdateChannelChain(payload.IbcUpdateChannelChain, timestamp, currentSetIndex, message.Nonce, message.Sequence)
|
||||
case *nodev1.GovernanceMessage_WormholeRelayerSetDefaultDeliveryProvider:
|
||||
v, err = wormholeRelayerSetDefaultDeliveryProvider(payload.WormholeRelayerSetDefaultDeliveryProvider, timestamp, currentSetIndex, message.Nonce, message.Sequence)
|
||||
case *nodev1.GovernanceMessage_EvmCall:
|
||||
v, err = evmCallToVaa(payload.EvmCall, timestamp, currentSetIndex, message.Nonce, message.Sequence)
|
||||
case *nodev1.GovernanceMessage_SolanaCall:
|
||||
v, err = solanaCallToVaa(payload.SolanaCall, timestamp, currentSetIndex, message.Nonce, message.Sequence)
|
||||
default:
|
||||
panic(fmt.Sprintf("unsupported VAA type: %T", payload))
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -40,7 +40,7 @@ service NodePrivilegedService {
|
|||
|
||||
// ChainGovernorReleasePendingVAA release a VAA from the chain governor pending list, publishing it immediately.
|
||||
rpc ChainGovernorReleasePendingVAA (ChainGovernorReleasePendingVAARequest) returns (ChainGovernorReleasePendingVAAResponse);
|
||||
|
||||
|
||||
// ChainGovernorResetReleaseTimer resets the release timer for a chain governor pending VAA to the configured maximum.
|
||||
rpc ChainGovernorResetReleaseTimer (ChainGovernorResetReleaseTimerRequest) returns (ChainGovernorResetReleaseTimerResponse);
|
||||
|
||||
|
@ -51,10 +51,10 @@ service NodePrivilegedService {
|
|||
rpc SignExistingVAA (SignExistingVAARequest) returns (SignExistingVAAResponse);
|
||||
|
||||
// DumpRPCs returns the RPCs being used by the guardian
|
||||
rpc DumpRPCs (DumpRPCsRequest) returns (DumpRPCsResponse);
|
||||
rpc DumpRPCs (DumpRPCsRequest) returns (DumpRPCsResponse);
|
||||
|
||||
// GetMissingVAAs returns the VAAs from a cloud function that need to be reobserved.
|
||||
rpc GetAndObserveMissingVAAs (GetAndObserveMissingVAAsRequest) returns (GetAndObserveMissingVAAsResponse);
|
||||
rpc GetAndObserveMissingVAAs (GetAndObserveMissingVAAsRequest) returns (GetAndObserveMissingVAAsResponse);
|
||||
}
|
||||
|
||||
message InjectGovernanceVAARequest {
|
||||
|
@ -85,7 +85,7 @@ message GovernanceMessage {
|
|||
GuardianSetUpdate guardian_set = 10;
|
||||
ContractUpgrade contract_upgrade = 11;
|
||||
|
||||
// Token bridge, NFT module, and Wormhole Relayer module (for the first two)
|
||||
// Token bridge, NFT module, and Wormhole Relayer module (for the first two)
|
||||
|
||||
BridgeRegisterChain bridge_register_chain = 12;
|
||||
BridgeUpgradeContract bridge_contract_upgrade = 13;
|
||||
|
@ -117,6 +117,10 @@ message GovernanceMessage {
|
|||
IbcUpdateChannelChain ibc_update_channel_chain = 21;
|
||||
// Wormhole Relayer module
|
||||
WormholeRelayerSetDefaultDeliveryProvider wormhole_relayer_set_default_delivery_provider = 22;
|
||||
|
||||
// Generic governance
|
||||
EvmCall evm_call = 28;
|
||||
SolanaCall solana_call = 29;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -191,7 +195,7 @@ message AccountantModifyBalance {
|
|||
|
||||
// ContractUpgrade represents a Wormhole contract update to be submitted to and signed by the node.
|
||||
message ContractUpgrade {
|
||||
// ID of the chain where the Wormhole contract should be updated (uint8).
|
||||
// ID of the chain where the Wormhole contract should be updated (uint16).
|
||||
uint32 chain_id = 1;
|
||||
|
||||
// Hex-encoded address (without leading 0x) address of the new program/contract.
|
||||
|
@ -417,3 +421,30 @@ message GetAndObserveMissingVAAsRequest {
|
|||
message GetAndObserveMissingVAAsResponse {
|
||||
string response =1;
|
||||
}
|
||||
|
||||
// EvmCall represents a generic EVM call that can be executed by the generalized governance contract.
|
||||
message EvmCall {
|
||||
// ID of the chain where the action should be executed (uint16).
|
||||
uint32 chain_id = 1;
|
||||
|
||||
// Address of the governance contract (eth address starting with 0x)
|
||||
string governance_contract = 2;
|
||||
|
||||
// Address of the governed contract (eth address starting with 0x)
|
||||
string target_contract = 3;
|
||||
|
||||
// ABI-encoded calldata to be passed on to the governed contract (hex encoded)
|
||||
string abi_encoded_call = 4;
|
||||
}
|
||||
|
||||
// SolanaCall represents a generic Solana call that can be executed by the generalized governance contract.
|
||||
message SolanaCall {
|
||||
// ID of the chain where the action should be executed (uint16).
|
||||
uint32 chain_id = 1;
|
||||
|
||||
// Address of the governance contract (solana address)
|
||||
string governance_contract = 2;
|
||||
|
||||
// Encoded instruction data to be passed on to the governed contract (hex encoded)
|
||||
string encoded_instruction = 3;
|
||||
}
|
||||
|
|
|
@ -4,8 +4,9 @@ import (
|
|||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
ethcommon "github.com/ethereum/go-ethereum/common"
|
||||
"github.com/holiman/uint256"
|
||||
)
|
||||
|
||||
|
@ -55,6 +56,13 @@ var WormholeRelayerModule = [32]byte{
|
|||
}
|
||||
var WormholeRelayerModuleStr = string(WormholeRelayerModule[:])
|
||||
|
||||
var GeneralPurposeGovernanceModule = [32]byte{
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x65, 0x6E, 0x65, 0x72, 0x61, 0x6C,
|
||||
0x50, 0x75, 0x72, 0x70, 0x6F, 0x73, 0x65, 0x47, 0x6F, 0x76, 0x65, 0x72, 0x6E, 0x61, 0x6E,
|
||||
0x63, 0x65,
|
||||
}
|
||||
var GeneralPurposeGovernanceModuleStr = string(GeneralPurposeGovernanceModule[:])
|
||||
|
||||
type GovernanceAction uint8
|
||||
|
||||
var (
|
||||
|
@ -99,6 +107,10 @@ var (
|
|||
|
||||
// Wormhole relayer governance actions
|
||||
WormholeRelayerSetDefaultDeliveryProvider GovernanceAction = 3
|
||||
|
||||
// General purpose governance
|
||||
GeneralPurposeGovernanceEvmAction GovernanceAction = 1
|
||||
GeneralPurposeGovernanceSolanaAction GovernanceAction = 2
|
||||
)
|
||||
|
||||
type (
|
||||
|
@ -110,7 +122,7 @@ type (
|
|||
|
||||
// BodyGuardianSetUpdate is a governance message to set a new guardian set
|
||||
BodyGuardianSetUpdate struct {
|
||||
Keys []common.Address
|
||||
Keys []ethcommon.Address
|
||||
NewIndex uint32
|
||||
}
|
||||
|
||||
|
@ -216,6 +228,24 @@ type (
|
|||
ChainID ChainID
|
||||
NewDefaultDeliveryProviderAddress Address
|
||||
}
|
||||
|
||||
// BodyGeneralPurposeGovernanceEvm is a general purpose governance message for EVM chains
|
||||
BodyGeneralPurposeGovernanceEvm struct {
|
||||
ChainID ChainID
|
||||
GovernanceContract ethcommon.Address
|
||||
TargetContract ethcommon.Address
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
// BodyGeneralPurposeGovernanceSolana is a general purpose governance message for Solana chains
|
||||
BodyGeneralPurposeGovernanceSolana struct {
|
||||
ChainID ChainID
|
||||
GovernanceContract Address
|
||||
// NOTE: unlike in EVM, no target contract in the schema here, the
|
||||
// instruction encodes the target contract address (unlike in EVM, where
|
||||
// an abi encoded calldata doesn't include the target contract address)
|
||||
Instruction []byte
|
||||
}
|
||||
)
|
||||
|
||||
func (b BodyContractUpgrade) Serialize() []byte {
|
||||
|
@ -402,6 +432,31 @@ func (r BodyWormholeRelayerSetDefaultDeliveryProvider) Serialize() []byte {
|
|||
return serializeBridgeGovernanceVaa(WormholeRelayerModuleStr, WormholeRelayerSetDefaultDeliveryProvider, r.ChainID, payload.Bytes())
|
||||
}
|
||||
|
||||
func (r BodyGeneralPurposeGovernanceEvm) Serialize() ([]byte, error) {
|
||||
payload := &bytes.Buffer{}
|
||||
payload.Write(r.GovernanceContract[:])
|
||||
payload.Write(r.TargetContract[:])
|
||||
|
||||
// write payload len as uint16
|
||||
if len(r.Payload) > math.MaxUint16 {
|
||||
return nil, fmt.Errorf("payload too long; expected at most %d bytes", math.MaxUint16)
|
||||
}
|
||||
MustWrite(payload, binary.BigEndian, uint16(len(r.Payload)))
|
||||
payload.Write(r.Payload)
|
||||
return serializeBridgeGovernanceVaa(GeneralPurposeGovernanceModuleStr, GeneralPurposeGovernanceEvmAction, r.ChainID, payload.Bytes()), nil
|
||||
}
|
||||
|
||||
func (r BodyGeneralPurposeGovernanceSolana) Serialize() []byte {
|
||||
payload := &bytes.Buffer{}
|
||||
payload.Write(r.GovernanceContract[:])
|
||||
// NOTE: unlike in EVM, we don't write the payload length here, because we're using
|
||||
// a custom instruction encoding (there is no standard encoding like evm ABI
|
||||
// encoding), generated by an external tool. That tool length-prefixes all
|
||||
// the relevant dynamic fields.
|
||||
payload.Write(r.Instruction)
|
||||
return serializeBridgeGovernanceVaa(GeneralPurposeGovernanceModuleStr, GeneralPurposeGovernanceSolanaAction, r.ChainID, payload.Bytes())
|
||||
}
|
||||
|
||||
func EmptyPayloadVaa(module string, actionId GovernanceAction, chainId ChainID) []byte {
|
||||
return serializeBridgeGovernanceVaa(module, actionId, chainId, []byte{})
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue