Skip to content

Commit

Permalink
Implement upgradeability (wormhole-foundation#151)
Browse files Browse the repository at this point in the history
* Implement contract upgrade VAA action

* naming and (╯°□°)╯︵ ┻━┻

* Carefully unflip table and replace broken cutlery ┬─┬ノ( ◕◡◕ ノ)

* fix and automate upgradeability

* document contract upgrade call

* Update comments

* Exhaustiveness check in VAA payload switch

* Fix typo

Co-authored-by: Leo <[email protected]>
  • Loading branch information
hendrikhofstadt and Leo authored Jan 19, 2021
1 parent ad9e8cc commit efa03ef
Show file tree
Hide file tree
Showing 22 changed files with 578 additions and 192 deletions.
17 changes: 8 additions & 9 deletions bridge/cmd/guardiand/adminclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ func init() {
}

AdminCmd.AddCommand(AdminClientInjectGuardianSetUpdateCmd)
AdminCmd.AddCommand(AdminClientGuardianSetTemplateCmd)
AdminCmd.AddCommand(AdminClientGuardianSetVerifyCmd)
AdminCmd.AddCommand(AdminClientGovernanceVAAVerifyCmd)
}

var AdminCmd = &cobra.Command{
Expand All @@ -36,9 +35,9 @@ var AdminCmd = &cobra.Command{
}

var AdminClientInjectGuardianSetUpdateCmd = &cobra.Command{
Use: "guardian-set-update-inject",
Short: "Inject and sign a guardian set update from a prototxt file (see docs!)",
Run: runInjectGuardianSetUpdate,
Use: "governance-vaa-inject",
Short: "Inject and sign a governance VAA from a prototxt file (see docs!)",
Run: runInjectGovernanceVAA,
Args: cobra.ExactArgs(1),
}

Expand All @@ -53,7 +52,7 @@ func getAdminClient(ctx context.Context, addr string) (*grpc.ClientConn, error,
return conn, err, c
}

func runInjectGuardianSetUpdate(cmd *cobra.Command, args []string) {
func runInjectGovernanceVAA(cmd *cobra.Command, args []string) {
path := args[0]
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
Expand All @@ -66,15 +65,15 @@ func runInjectGuardianSetUpdate(cmd *cobra.Command, args []string) {
log.Fatalf("failed to read file: %v", err)
}

var msg nodev1.GuardianSetUpdate
var msg nodev1.InjectGovernanceVAARequest
err = prototext.Unmarshal(b, &msg)
if err != nil {
log.Fatalf("failed to deserialize: %v", err)
}

resp, err := c.SubmitGuardianSetVAA(ctx, &nodev1.SubmitGuardianSetVAARequest{GuardianSet: &msg})
resp, err := c.InjectGovernanceVAA(ctx, &msg)
if err != nil {
log.Fatalf("failed to submit guardian set update: %v", err)
log.Fatalf("failed to submit governance VAA: %v", err)
}

log.Printf("VAA successfully injected with digest %s", hexutils.BytesToHex(resp.Digest))
Expand Down
57 changes: 48 additions & 9 deletions bridge/cmd/guardiand/adminserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"math"
"net"
"os"
"time"
Expand All @@ -28,7 +29,7 @@ type nodePrivilegedService struct {

// adminGuardianSetUpdateToVAA converts a nodev1.GuardianSetUpdate message to its canonical VAA representation.
// Returns an error if the data is invalid.
func adminGuardianSetUpdateToVAA(req *nodev1.GuardianSetUpdate) (*vaa.VAA, error) {
func adminGuardianSetUpdateToVAA(req *nodev1.GuardianSetUpdate, guardianSetIndex uint32, timestamp uint32) (*vaa.VAA, error) {
if len(req.Guardians) == 0 {
return nil, errors.New("empty guardian set specified")
}
Expand All @@ -48,21 +49,59 @@ func adminGuardianSetUpdateToVAA(req *nodev1.GuardianSetUpdate) (*vaa.VAA, error

v := &vaa.VAA{
Version: vaa.SupportedVAAVersion,
GuardianSetIndex: req.CurrentSetIndex,
Timestamp: time.Unix(int64(req.Timestamp), 0),
GuardianSetIndex: guardianSetIndex,
Timestamp: time.Unix(int64(timestamp), 0),
Payload: &vaa.BodyGuardianSetUpdate{
Keys: addrs,
NewIndex: req.CurrentSetIndex + 1,
NewIndex: guardianSetIndex + 1,
},
}

return v, nil
}

func (s *nodePrivilegedService) SubmitGuardianSetVAA(ctx context.Context, req *nodev1.SubmitGuardianSetVAARequest) (*nodev1.SubmitGuardianSetVAAResponse, error) {
s.logger.Info("guardian set injected via admin socket", zap.String("request", req.String()))
// adminContractUpgradeToVAA converts a nodev1.ContractUpgrade message to its canonical VAA representation.
// Returns an error if the data is invalid.
func adminContractUpgradeToVAA(req *nodev1.ContractUpgrade, guardianSetIndex uint32, timestamp uint32) (*vaa.VAA, error) {
if len(req.NewContract) != 32 {
return nil, errors.New("invalid new_contract address")
}

if req.ChainId > math.MaxUint8 {
return nil, errors.New("invalid chain_id")
}

newContractAddress := vaa.Address{}
copy(newContractAddress[:], req.NewContract)

v := &vaa.VAA{
Version: vaa.SupportedVAAVersion,
GuardianSetIndex: guardianSetIndex,
Timestamp: time.Unix(int64(timestamp), 0),
Payload: &vaa.BodyContractUpgrade{
ChainID: uint8(req.ChainId),
NewContract: newContractAddress,
},
}

return v, nil
}

func (s *nodePrivilegedService) InjectGovernanceVAA(ctx context.Context, req *nodev1.InjectGovernanceVAARequest) (*nodev1.InjectGovernanceVAAResponse, error) {
s.logger.Info("governance VAA injected via admin socket", zap.String("request", req.String()))

v, err := adminGuardianSetUpdateToVAA(req.GuardianSet)
var (
v *vaa.VAA
err error
)
switch payload := req.Payload.(type) {
case *nodev1.InjectGovernanceVAARequest_GuardianSet:
v, err = adminGuardianSetUpdateToVAA(payload.GuardianSet, req.CurrentSetIndex, req.Timestamp)
case *nodev1.InjectGovernanceVAARequest_ContractUpgrade:
v, err = adminContractUpgradeToVAA(payload.ContractUpgrade, req.CurrentSetIndex, req.Timestamp)
default:
panic(fmt.Sprintf("unsupported VAA type: %T", payload))
}
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
Expand All @@ -73,14 +112,14 @@ func (s *nodePrivilegedService) SubmitGuardianSetVAA(ctx context.Context, req *n
panic(err)
}

s.logger.Info("guardian set VAA constructed",
s.logger.Info("governance VAA constructed",
zap.Any("vaa", v),
zap.String("digest", digest.String()),
)

s.injectC <- v

return &nodev1.SubmitGuardianSetVAAResponse{Digest: digest.Bytes()}, nil
return &nodev1.InjectGovernanceVAAResponse{Digest: digest.Bytes()}, nil
}

func adminServiceRunnable(logger *zap.Logger, socketPath string, injectC chan<- *vaa.VAA) (supervisor.Runnable, error) {
Expand Down
60 changes: 52 additions & 8 deletions bridge/cmd/guardiand/admintemplate.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,40 +13,84 @@ import (
nodev1 "github.com/certusone/wormhole/bridge/pkg/proto/node/v1"
)

var templateNumGuardians *int
var setUpdateNumGuardians *int
var templateGuardianIndex *int

func init() {
templateNumGuardians = AdminClientGuardianSetTemplateCmd.Flags().Int("num", 1, "Number of devnet guardians in example file")
templateGuardianIndex = AdminClientGuardianSetTemplateCmd.Flags().Int("idx", 0, "Default current guardian set index")
templateGuardianIndex = TemplateCmd.PersistentFlags().Int("idx", 0, "Default current guardian set index")
setUpdateNumGuardians = AdminClientGuardianSetTemplateCmd.Flags().Int("num", 1, "Number of devnet guardians in example file")

TemplateCmd.AddCommand(AdminClientGuardianSetTemplateCmd)
TemplateCmd.AddCommand(AdminClientContractUpgradeTemplateCmd)
}

var TemplateCmd = &cobra.Command{
Use: "template",
Short: "Guardian governance VAA template commands ",
}

var AdminClientGuardianSetTemplateCmd = &cobra.Command{
Use: "guardian-set-update-template",
Use: "guardian-set-update",
Short: "Generate an empty guardian set template at specified path (offline)",
Run: runGuardianSetTemplate,
Args: cobra.ExactArgs(1),
}

var AdminClientContractUpgradeTemplateCmd = &cobra.Command{
Use: "contract-upgrade",
Short: "Generate an empty contract upgrade template at specified path (offline)",
Run: runContractUpgradeTemplate,
Args: cobra.ExactArgs(1),
}

func runGuardianSetTemplate(cmd *cobra.Command, args []string) {
path := args[0]

// Use deterministic devnet addresses as examples in the template, such that this doubles as a test fixture.
guardians := make([]*nodev1.GuardianSetUpdate_Guardian, *templateNumGuardians)
for i := 0; i < *templateNumGuardians; i++ {
guardians := make([]*nodev1.GuardianSetUpdate_Guardian, *setUpdateNumGuardians)
for i := 0; i < *setUpdateNumGuardians; i++ {
k := devnet.DeterministicEcdsaKeyByIndex(crypto.S256(), uint64(i))
guardians[i] = &nodev1.GuardianSetUpdate_Guardian{
Pubkey: crypto.PubkeyToAddress(k.PublicKey).Hex(),
Name: fmt.Sprintf("Example validator %d", i),
}
}

m := &nodev1.GuardianSetUpdate{
m := &nodev1.InjectGovernanceVAARequest{
CurrentSetIndex: uint32(*templateGuardianIndex),
// Timestamp is hardcoded to make it reproducible on different devnet nodes.
// In production, a real UNIX timestamp should be used (see node.proto).
Timestamp: 1605744545,
Payload: &nodev1.InjectGovernanceVAARequest_GuardianSet{
GuardianSet: &nodev1.GuardianSetUpdate{Guardians: guardians},
},
}

b, err := prototext.MarshalOptions{Multiline: true}.Marshal(m)
if err != nil {
panic(err)
}

err = ioutil.WriteFile(path, b, 0640)
if err != nil {
log.Fatal(err)
}
}

func runContractUpgradeTemplate(cmd *cobra.Command, args []string) {
path := args[0]

m := &nodev1.InjectGovernanceVAARequest{
CurrentSetIndex: uint32(*templateGuardianIndex),
// Timestamp is hardcoded to make it reproducible on different devnet nodes.
// In production, a real UNIX timestamp should be used (see node.proto).
Timestamp: 1605744545,
Guardians: guardians,
Payload: &nodev1.InjectGovernanceVAARequest_ContractUpgrade{
ContractUpgrade: &nodev1.ContractUpgrade{
ChainId: 1,
NewContract: make([]byte, 32),
},
},
}

b, err := prototext.MarshalOptions{Multiline: true}.Marshal(m)
Expand Down
23 changes: 16 additions & 7 deletions bridge/cmd/guardiand/adminverify.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package guardiand

import (
"github.com/certusone/wormhole/bridge/pkg/vaa"
"io/ioutil"
"log"

Expand All @@ -11,28 +12,36 @@ import (
nodev1 "github.com/certusone/wormhole/bridge/pkg/proto/node/v1"
)

var AdminClientGuardianSetVerifyCmd = &cobra.Command{
Use: "guardian-set-update-verify",
Short: "Verify guardian set update in prototxt format (offline)",
Run: runGuardianSetVerify,
var AdminClientGovernanceVAAVerifyCmd = &cobra.Command{
Use: "governance-vaa-verify",
Short: "Verify governance vaa in prototxt format (offline)",
Run: runGovernanceVAAVerify,
Args: cobra.ExactArgs(1),
}

func runGuardianSetVerify(cmd *cobra.Command, args []string) {
func runGovernanceVAAVerify(cmd *cobra.Command, args []string) {
path := args[0]

b, err := ioutil.ReadFile(path)
if err != nil {
log.Fatalf("failed to read file: %v", err)
}

var msg nodev1.GuardianSetUpdate
var msg nodev1.InjectGovernanceVAARequest
err = prototext.Unmarshal(b, &msg)
if err != nil {
log.Fatalf("failed to deserialize: %v", err)
}

v, err := adminGuardianSetUpdateToVAA(&msg)
var (
v *vaa.VAA
)
switch payload := msg.Payload.(type) {
case *nodev1.InjectGovernanceVAARequest_GuardianSet:
v, err = adminGuardianSetUpdateToVAA(payload.GuardianSet, msg.CurrentSetIndex, msg.Timestamp)
case *nodev1.InjectGovernanceVAARequest_ContractUpgrade:
v, err = adminContractUpgradeToVAA(payload.ContractUpgrade, msg.CurrentSetIndex, msg.Timestamp)
}
if err != nil {
log.Fatalf("invalid update: %v", err)
}
Expand Down
1 change: 1 addition & 0 deletions bridge/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func init() {
rootCmd.AddCommand(guardiand.BridgeCmd)
rootCmd.AddCommand(guardiand.KeygenCmd)
rootCmd.AddCommand(guardiand.AdminCmd)
rootCmd.AddCommand(guardiand.TemplateCmd)
}

// initConfig reads in config file and ENV variables if set.
Expand Down
11 changes: 11 additions & 0 deletions bridge/pkg/processor/observation.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,17 @@ func (p *Processor) handleObservation(ctx context.Context, m *gossipv1.SignedObs
// A guardian set update is broadcast to every chain that we talk to.
p.devnetVAASubmission(ctx, signed, hash)
p.terraVAASubmission(ctx, signed, hash)
case *vaa.BodyContractUpgrade:
switch t.ChainID {
case vaa.ChainIDSolana:
// Already submitted to Solana.
default:
p.logger.Error("unsupported target chain for contract upgrade",
zap.String("digest", hash),
zap.Any("vaa", signed),
zap.String("bytes", hex.EncodeToString(vaaBytes)),
zap.Uint8("target_chain", t.ChainID))
}
default:
panic(fmt.Sprintf("unknown VAA payload type: %+v", v))
}
Expand Down
37 changes: 37 additions & 0 deletions bridge/pkg/vaa/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ type (
// NewIndex is the index of the new guardian set
NewIndex uint32
}

BodyContractUpgrade struct {
// ChainID is the chain on which the contract should be upgraded
ChainID uint8
// NewContract is the address of the account containing the new contract.
NewContract Address
}
)

func (a Address) String() string {
Expand All @@ -106,6 +113,7 @@ func (c ChainID) String() string {

const (
ActionGuardianSetUpdate Action = 0x01
ActionContractUpgrade Action = 0x02
ActionTransfer Action = 0x10

// ChainIDSolana is the ChainID of Solana
Expand Down Expand Up @@ -182,6 +190,8 @@ func Unmarshal(data []byte) (*VAA, error) {
v.Payload, err = parseBodyGuardianSetUpdate(payloadReader)
case ActionTransfer:
v.Payload, err = parseBodyTransfer(payloadReader)
case ActionContractUpgrade:
v.Payload, err = parseBodyContractUpgrade(payloadReader)
default:
return nil, fmt.Errorf("unknown action: %d", action)
}
Expand Down Expand Up @@ -403,6 +413,33 @@ func (v *BodyGuardianSetUpdate) serialize() ([]byte, error) {
return buf.Bytes(), nil
}

func parseBodyContractUpgrade(r io.Reader) (*BodyContractUpgrade, error) {
b := &BodyContractUpgrade{}

if err := binary.Read(r, binary.BigEndian, &b.ChainID); err != nil {
return nil, fmt.Errorf("failed to read chain id: %w", err)
}

if n, err := r.Read(b.NewContract[:]); err != nil || n != 32 {
return nil, fmt.Errorf("failed to read new contract address: %w", err)
}

return b, nil
}

func (v *BodyContractUpgrade) getActionID() Action {
return ActionContractUpgrade
}

func (v *BodyContractUpgrade) serialize() ([]byte, error) {
buf := new(bytes.Buffer)

MustWrite(buf, binary.BigEndian, v.ChainID)
buf.Write(v.NewContract[:])

return buf.Bytes(), nil
}

// MustWrite calls binary.Write and panics on errors
func MustWrite(w io.Writer, order binary.ByteOrder, data interface{}) {
if err := binary.Write(w, order, data); err != nil {
Expand Down
Loading

0 comments on commit efa03ef

Please sign in to comment.