wormchain: add admin-sign command to continue wormchain validator enrollment

This commit is contained in:
Conor Patrick 2022-11-28 16:46:19 +00:00
parent 2bc47889f3
commit 63a5654c02
5 changed files with 336 additions and 7 deletions

View File

@ -11,6 +11,7 @@ import (
"time"
"github.com/davecgh/go-spew/spew"
"github.com/ethereum/go-ethereum/crypto"
"github.com/mr-tron/base58"
"github.com/spf13/pflag"
@ -21,6 +22,7 @@ import (
"github.com/spf13/cobra"
"github.com/status-im/keycard-go/hexutils"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/protobuf/encoding/prototext"
@ -61,6 +63,7 @@ func init() {
AdminCmd.AddCommand(AdminClientFindMissingMessagesCmd)
AdminCmd.AddCommand(AdminClientGovernanceVAAVerifyCmd)
AdminCmd.AddCommand(AdminClientListNodes)
AdminCmd.AddCommand(AdminClientSignWormchainAddress)
AdminCmd.AddCommand(DumpVAAByMessageID)
AdminCmd.AddCommand(SendObservationRequest)
AdminCmd.AddCommand(ClientChainGovernorStatusCmd)
@ -76,6 +79,13 @@ var AdminCmd = &cobra.Command{
Short: "Guardian node admin commands",
}
var AdminClientSignWormchainAddress = &cobra.Command{
Use: "sign-wormchain-address [/path/to/guardianKey] [wormchain-validator-address]",
Short: "Sign a wormchain validator address. Only sign the address that you control the key for and will be for your validator.",
RunE: runSignWormchainValidatorAddress,
Args: cobra.ExactArgs(2),
}
var AdminClientInjectGuardianSetUpdateCmd = &cobra.Command{
Use: "governance-vaa-inject [FILENAME]",
Short: "Inject and sign a governance VAA from a prototxt file (see docs!)",
@ -168,6 +178,30 @@ func getPublicRPCServiceClient(ctx context.Context, addr string) (*grpc.ClientCo
return conn, c, err
}
func runSignWormchainValidatorAddress(cmd *cobra.Command, args []string) error {
guardianKeyPath := args[0]
wormchainAddress := args[1]
if !strings.HasPrefix(wormchainAddress, "wormhole") || strings.HasPrefix(wormchainAddress, "wormholeval") {
return fmt.Errorf("must provide a bech32 address that has 'wormhole' prefix")
}
gk, err := loadGuardianKey(guardianKeyPath)
if err != nil {
return fmt.Errorf("failed to load guardian key: %w", err)
}
addr, err := getFromBech32(wormchainAddress, "wormhole")
if err != nil {
return fmt.Errorf("failed to decode wormchain address: %w", err)
}
// Hash and sign address
addrHash := crypto.Keccak256Hash(sdk.SignedWormchainAddressPrefix, addr)
sig, err := crypto.Sign(addrHash[:], gk)
if err != nil {
return fmt.Errorf("failed to sign wormchain address: %w", err)
}
fmt.Println(hex.EncodeToString(sig))
return nil
}
func runInjectGovernanceVAA(cmd *cobra.Command, args []string) {
path := args[0]
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)

View File

@ -0,0 +1,285 @@
package guardiand
import (
"fmt"
"strings"
)
// TODO(cpatrick): replace this whole file with just "github.com/cosmos/cosmos-sdk/types"
// This is added to avoid changing go.mod for guardiand for now.
// The code in this file was faithfully copied from: https://github.com/scrtlabs/btcutil/blob/master/bech32/bech32.go
// The code in this "bech32.go" file, and only this file, has the following license:
/*
ISC License
Copyright (c) 2013-2017 The btcsuite developers
Copyright (c) 2016-2017 The Lightning Network Developers
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
// toChars converts the byte slice 'data' to a string where each byte in 'data'
// encodes the index of a character in 'charset'.
func toChars(data []byte) (string, error) {
result := make([]byte, 0, len(data))
for _, b := range data {
if int(b) >= len(charset) {
return "", fmt.Errorf("invalid data byte: %v", b)
}
result = append(result, charset[b])
}
return string(result), nil
}
// For more details on the checksum calculation, please refer to BIP 173.
func bech32Checksum(hrp string, data []byte) []byte {
// Convert the bytes to list of integers, as this is needed for the
// checksum calculation.
integers := make([]int, len(data))
for i, b := range data {
integers[i] = int(b)
}
values := append(bech32HrpExpand(hrp), integers...)
values = append(values, []int{0, 0, 0, 0, 0, 0}...)
polymod := bech32Polymod(values) ^ 1
var res []byte
for i := 0; i < 6; i++ {
res = append(res, byte((polymod>>uint(5*(5-i)))&31))
}
return res
}
// For more details on the polymod calculation, please refer to BIP 173.
func bech32Polymod(values []int) int {
chk := 1
for _, v := range values {
b := chk >> 25
chk = (chk&0x1ffffff)<<5 ^ v
for i := 0; i < 5; i++ {
if (b>>uint(i))&1 == 1 {
chk ^= gen[i]
}
}
}
return chk
}
// For more details on HRP expansion, please refer to BIP 173.
func bech32HrpExpand(hrp string) []int {
v := make([]int, 0, len(hrp)*2+1)
for i := 0; i < len(hrp); i++ {
v = append(v, int(hrp[i]>>5))
}
v = append(v, 0)
for i := 0; i < len(hrp); i++ {
v = append(v, int(hrp[i]&31))
}
return v
}
// For more details on the checksum verification, please refer to BIP 173.
func bech32VerifyChecksum(hrp string, data []byte) bool {
integers := make([]int, len(data))
for i, b := range data {
integers[i] = int(b)
}
concat := append(bech32HrpExpand(hrp), integers...)
return bech32Polymod(concat) == 1
}
// toBytes converts each character in the string 'chars' to the value of the
// index of the correspoding character in 'charset'.
func toBytes(chars string) ([]byte, error) {
decoded := make([]byte, 0, len(chars))
for i := 0; i < len(chars); i++ {
index := strings.IndexByte(charset, chars[i])
if index < 0 {
return nil, fmt.Errorf("invalid character not part of "+
"charset: %v", chars[i])
}
decoded = append(decoded, byte(index))
}
return decoded, nil
}
const charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
var gen = []int{0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3}
// Decode decodes a bech32 encoded string, returning the human-readable
// part and the data part excluding the checksum.
func decode(bech string, limit int) (string, []byte, error) {
// The maximum allowed length for a bech32 string is 90. It must also
// be at least 8 characters, since it needs a non-empty HRP, a
// separator, and a 6 character checksum.
if len(bech) < 8 || len(bech) > limit {
return "", nil, fmt.Errorf("invalid bech32 string length %d",
len(bech))
}
// Only ASCII characters between 33 and 126 are allowed.
for i := 0; i < len(bech); i++ {
if bech[i] < 33 || bech[i] > 126 {
return "", nil, fmt.Errorf("invalid character in "+
"string: '%c'", bech[i])
}
}
// The characters must be either all lowercase or all uppercase.
lower := strings.ToLower(bech)
upper := strings.ToUpper(bech)
if bech != lower && bech != upper {
return "", nil, fmt.Errorf("string not all lowercase or all " +
"uppercase")
}
// We'll work with the lowercase string from now on.
bech = lower
// The string is invalid if the last '1' is non-existent, it is the
// first character of the string (no human-readable part) or one of the
// last 6 characters of the string (since checksum cannot contain '1'),
// or if the string is more than 90 characters in total.
one := strings.LastIndexByte(bech, '1')
if one < 1 || one+7 > len(bech) {
return "", nil, fmt.Errorf("invalid index of 1")
}
// The human-readable part is everything before the last '1'.
hrp := bech[:one]
data := bech[one+1:]
// Each character corresponds to the byte with value of the index in
// 'charset'.
decoded, err := toBytes(data)
if err != nil {
return "", nil, fmt.Errorf("failed converting data to bytes: "+
"%v", err)
}
if !bech32VerifyChecksum(hrp, decoded) {
moreInfo := ""
checksum := bech[len(bech)-6:]
expected, err := toChars(bech32Checksum(hrp,
decoded[:len(decoded)-6]))
if err == nil {
moreInfo = fmt.Sprintf("Expected %v, got %v.",
expected, checksum)
}
return "", nil, fmt.Errorf("checksum failed. " + moreInfo)
}
// We exclude the last 6 bytes, which is the checksum.
return hrp, decoded[:len(decoded)-6], nil
}
func decodeAndConvert(bech string) (string, []byte, error) {
hrp, data, err := decode(bech, 1023)
if err != nil {
return "", nil, fmt.Errorf("decoding bech32 failed: %w", err)
}
converted, err := convertBits(data, 5, 8, false)
if err != nil {
return "", nil, fmt.Errorf("decoding bech32 failed: %w", err)
}
return hrp, converted, nil
}
// ConvertBits converts a byte slice where each byte is encoding fromBits bits,
// to a byte slice where each byte is encoding toBits bits.
func convertBits(data []byte, fromBits, toBits uint8, pad bool) ([]byte, error) {
if fromBits < 1 || fromBits > 8 || toBits < 1 || toBits > 8 {
return nil, fmt.Errorf("only bit groups between 1 and 8 allowed")
}
// The final bytes, each byte encoding toBits bits.
var regrouped []byte
// Keep track of the next byte we create and how many bits we have
// added to it out of the toBits goal.
nextByte := byte(0)
filledBits := uint8(0)
for _, b := range data {
// Discard unused bits.
b = b << (8 - fromBits)
// How many bits remaining to extract from the input data.
remFromBits := fromBits
for remFromBits > 0 {
// How many bits remaining to be added to the next byte.
remToBits := toBits - filledBits
// The number of bytes to next extract is the minimum of
// remFromBits and remToBits.
toExtract := remFromBits
if remToBits < toExtract {
toExtract = remToBits
}
// Add the next bits to nextByte, shifting the already
// added bits to the left.
nextByte = (nextByte << toExtract) | (b >> (8 - toExtract))
// Discard the bits we just extracted and get ready for
// next iteration.
b = b << toExtract
remFromBits -= toExtract
filledBits += toExtract
// If the nextByte is completely filled, we add it to
// our regrouped bytes and start on the next byte.
if filledBits == toBits {
regrouped = append(regrouped, nextByte)
filledBits = 0
nextByte = 0
}
}
}
// We pad any unfinished group if specified.
if pad && filledBits > 0 {
nextByte = nextByte << (toBits - filledBits)
regrouped = append(regrouped, nextByte)
filledBits = 0
nextByte = 0
}
// Any incomplete group must be <= 4 bits, and all zeroes.
if filledBits > 0 && (filledBits > 4 || nextByte != 0) {
return nil, fmt.Errorf("invalid incomplete group")
}
return regrouped, nil
}
// GetFromBech32 decodes a bytestring from a Bech32 encoded string.
func getFromBech32(bech32str, prefix string) ([]byte, error) {
if len(bech32str) == 0 {
return nil, fmt.Errorf("zero length bech32 address")
}
hrp, bz, err := decodeAndConvert(bech32str)
if err != nil {
return nil, err
}
if hrp != prefix {
return nil, fmt.Errorf("invalid Bech32 prefix; expected %s, got %s", prefix, hrp)
}
return bz, nil
}

9
sdk/p2p_consts.go Normal file
View File

@ -0,0 +1,9 @@
package sdk
var (
HeartbeatMessagePrefix = []byte("heartbeat|")
SignedObservationRequestPrefix_old = []byte("signed_observation_request|")
SignedObservationRequestPrefix = []byte("signed_observation_request_000000|")
SignedWormchainAddressPrefix = []byte("signed_wormchain_address_00000000|")
)

View File

@ -22,6 +22,7 @@ import (
"github.com/ethereum/go-ethereum/crypto"
"github.com/wormhole-foundation/wormchain/x/wormhole/keeper"
"github.com/wormhole-foundation/wormchain/x/wormhole/types"
wormholesdk "github.com/wormhole-foundation/wormhole/sdk"
"github.com/wormhole-foundation/wormhole/sdk/vaa"
)
@ -55,7 +56,7 @@ func GetGenesisCmd() *cobra.Command {
cmd.AddCommand(CmdGenerateVaa())
cmd.AddCommand(CmdGenerateGovernanceVaa())
cmd.AddCommand(CmdGenerateGuardianSetUpdatea())
cmd.AddCommand(CmdSignAddress())
cmd.AddCommand(CmdTestSignAddress())
return cmd
}
@ -406,10 +407,10 @@ func CmdGenerateGuardianSetUpdatea() *cobra.Command {
return cmd
}
func CmdSignAddress() *cobra.Command {
func CmdTestSignAddress() *cobra.Command {
cmd := &cobra.Command{
Use: "sign-address",
Short: "sign the validator address to use for registering as a guardian. read guardian key as hex in $GUARDIAN_KEY env variable. use --from to indicate address to sign.",
Use: "test-sign-address",
Short: "Test method sign the validator address to use for registering as a guardian. Use guardiand for production, not this method. Read guardian key as hex in $GUARDIAN_KEY env variable. use --from to indicate address to sign.",
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) (err error) {
clientCtx, err := client.GetClientTxContext(cmd)
@ -427,7 +428,7 @@ func CmdSignAddress() *cobra.Command {
return err
}
addr := info.GetAddress()
addrHash := crypto.Keccak256Hash(addr)
addrHash := crypto.Keccak256Hash(wormholesdk.SignedWormchainAddressPrefix, addr)
sig, err := crypto.Sign(addrHash[:], key)
if err != nil {
return err

View File

@ -9,6 +9,7 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/wormhole-foundation/wormchain/x/wormhole/types"
wormholesdk "github.com/wormhole-foundation/wormhole/sdk"
)
// TODO(csongor): high-level overview of what this does
@ -19,9 +20,8 @@ func (k msgServer) RegisterAccountAsGuardian(goCtx context.Context, msg *types.M
if err != nil {
return nil, err
}
// recover guardian key from signature
signerHash := crypto.Keccak256Hash(signer)
signerHash := crypto.Keccak256Hash(wormholesdk.SignedWormchainAddressPrefix, signer)
guardianKey, err := crypto.Ecrecover(signerHash.Bytes(), msg.Signature)
if err != nil {