node: inject wormchain cosmwasm governance messages (#2057)

* node: inject wormchain cosmwasm governance messages

* Use nested hash for defense in depth

* Use keccak.Reset() instead of creating new hash objects

* fix msg_server_wasmd_test

* Updated based on jynnantonix comments

* Check return value of binary.Write

* Include actual error in binary.Write panic case
This commit is contained in:
Nikhil Suri 2022-12-06 11:10:32 -08:00 committed by GitHub
parent 8ed35ddac1
commit 0e7c085d71
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 575 additions and 293 deletions

View File

@ -162,6 +162,43 @@ func tokenBridgeUpgradeContract(req *nodev1.BridgeUpgradeContract, timestamp tim
return v, nil
}
// wormchainStoreCode converts a nodev1.WormchainStoreCode to its canonical VAA representation
// Returns an error if the data is invalid
func wormchainStoreCode(req *nodev1.WormchainStoreCode, timestamp time.Time, guardianSetIndex uint32, nonce uint32, sequence uint64) (*vaa.VAA, error) {
// validate the length of the hex passed in
b, err := hex.DecodeString(req.WasmHash)
if err != nil {
return nil, fmt.Errorf("invalid cosmwasm bytecode hash (expected hex): %w", err)
}
if len(b) != 32 {
return nil, fmt.Errorf("invalid cosmwasm bytecode hash (expected 32 bytes but received %d bytes)", len(b))
}
wasmHash := [32]byte{}
copy(wasmHash[:], b)
v := vaa.CreateGovernanceVAA(timestamp, nonce, sequence, guardianSetIndex,
vaa.BodyWormchainStoreCode{
WasmHash: wasmHash,
}.Serialize())
return v, nil
}
// wormchainInstantiateContract converts a nodev1.WormchainInstantiateContract to its canonical VAA representation
// Returns an error if the data is invalid
func wormchainInstantiateContract(req *nodev1.WormchainInstantiateContract, timestamp time.Time, guardianSetIndex uint32, nonce uint32, sequence uint64) (*vaa.VAA, error) {
instantiationParams_hash := vaa.CreateInstatiateCosmwasmContractHash(req.CodeId, req.Label, []byte(req.InstantiationMsg))
v := vaa.CreateGovernanceVAA(timestamp, nonce, sequence, guardianSetIndex,
vaa.BodyWormchainInstantiateContract{
InstantiationParamsHash: instantiationParams_hash,
}.Serialize())
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()))
@ -184,6 +221,10 @@ func (s *nodePrivilegedService) InjectGovernanceVAA(ctx context.Context, req *no
v, err = tokenBridgeRegisterChain(payload.BridgeRegisterChain, timestamp, req.CurrentSetIndex, message.Nonce, message.Sequence)
case *nodev1.GovernanceMessage_BridgeContractUpgrade:
v, err = tokenBridgeUpgradeContract(payload.BridgeContractUpgrade, timestamp, req.CurrentSetIndex, message.Nonce, message.Sequence)
case *nodev1.GovernanceMessage_WormchainStoreCode:
v, err = wormchainStoreCode(payload.WormchainStoreCode, timestamp, req.CurrentSetIndex, message.Nonce, message.Sequence)
case *nodev1.GovernanceMessage_WormchainInstantiateContract:
v, err = wormchainInstantiateContract(payload.WormchainInstantiateContract, timestamp, req.CurrentSetIndex, message.Nonce, message.Sequence)
default:
panic(fmt.Sprintf("unsupported VAA type: %T", payload))
}

File diff suppressed because it is too large Load Diff

View File

@ -80,6 +80,11 @@ message GovernanceMessage {
BridgeRegisterChain bridge_register_chain = 12;
BridgeUpgradeContract bridge_contract_upgrade = 13;
// Wormchain
WormchainStoreCode wormchain_store_code = 14;
WormchainInstantiateContract wormchain_instantiate_contract = 15;
}
}
@ -142,6 +147,22 @@ message BridgeUpgradeContract {
string new_contract = 3;
}
message WormchainStoreCode {
// payload is the hex string of the sha3 256 hash of the wasm binary being uploaded
string wasm_hash = 1;
}
message WormchainInstantiateContract {
// CodeID is the reference to the stored WASM code
uint64 code_id = 1;
// Label is optional metadata to be stored with a contract instance.
string label = 2;
// Json encoded message to be passed to the contract on instantiation
string instantiation_msg = 3;
}
message FindMissingMessagesRequest {
// Emitter chain ID to iterate.
uint32 emitter_chain = 1;

View File

@ -1,7 +1,11 @@
package vaa
import (
"encoding/binary"
"fmt"
"time"
"golang.org/x/crypto/sha3"
)
var GovernanceEmitter = Address{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4}
@ -23,3 +27,33 @@ func CreateGovernanceVAA(timestamp time.Time, nonce uint32, sequence uint64, gua
return vaa
}
// Compute the hash for cosmwasm contract instatiation params.
// The hash is keccak256 hash(hash(hash(BigEndian(CodeID)), Label), Msg).
// We compute the nested hash so there is no chance of bytes leaking between CodeID, Label, and Msg.
func CreateInstatiateCosmwasmContractHash(codeId uint64, label string, msg []byte) [32]byte {
var expected_hash [32]byte
// hash(BigEndian(CodeID))
var codeId_hash [32]byte
keccak := sha3.NewLegacyKeccak256()
if err := binary.Write(keccak, binary.BigEndian, codeId); err != nil {
panic(fmt.Sprintf("failed to write binary data (%d): %v", codeId, err))
}
keccak.Sum(codeId_hash[:0])
keccak.Reset()
// hash(hash(BigEndian(CodeID)), Label)
var codeIdLabel_hash [32]byte
keccak.Write(codeId_hash[:])
keccak.Write([]byte(label))
keccak.Sum(codeIdLabel_hash[:0])
keccak.Reset()
// hash(hash(hash(BigEndian(CodeID)), Label), Msg)
keccak.Write(codeIdLabel_hash[:])
keccak.Write(msg)
keccak.Sum(expected_hash[:0])
return expected_hash
}

View File

@ -36,6 +36,16 @@ type (
TargetChainID ChainID
NewContract Address
}
// BodyWormchainStoreCode is a governance message to upload a new cosmwasm contract to wormchain
BodyWormchainStoreCode struct {
WasmHash [32]byte
}
// BodyWormchainInstantiateContract is a governance message to instantiate a cosmwasm contract on wormchain
BodyWormchainInstantiateContract struct {
InstantiationParamsHash [32]byte
}
)
func (b BodyContractUpgrade) Serialize() []byte {
@ -73,47 +83,39 @@ func (b BodyGuardianSetUpdate) Serialize() []byte {
}
func (r BodyTokenBridgeRegisterChain) Serialize() []byte {
if len(r.Module) > 32 {
panic("module longer than 32 byte")
}
buf := &bytes.Buffer{}
// Write token bridge header
for i := 0; i < (32 - len(r.Module)); i++ {
buf.WriteByte(0x00)
}
buf.Write([]byte(r.Module))
// Write action ID
MustWrite(buf, binary.BigEndian, uint8(1))
// Write target chain (0 = universal)
MustWrite(buf, binary.BigEndian, uint16(0))
// Write chain to be registered
MustWrite(buf, binary.BigEndian, r.ChainID)
// Write emitter address of chain to be registered
buf.Write(r.EmitterAddress[:])
return buf.Bytes()
return serializeBridgeGovernanceVaa(r.Module, 1, r.ChainID, r.EmitterAddress)
}
func (r BodyTokenBridgeUpgradeContract) Serialize() []byte {
if len(r.Module) > 32 {
return serializeBridgeGovernanceVaa(r.Module, 2, r.TargetChainID, r.NewContract)
}
func (r BodyWormchainStoreCode) Serialize() []byte {
return serializeBridgeGovernanceVaa("WasmdModule", 1, ChainIDWormchain, r.WasmHash)
}
func (r BodyWormchainInstantiateContract) Serialize() []byte {
return serializeBridgeGovernanceVaa("WasmdModule", 2, ChainIDWormchain, r.InstantiationParamsHash)
}
func serializeBridgeGovernanceVaa(module string, actionId uint8, chainId ChainID, payload [32]byte) []byte {
if len(module) > 32 {
panic("module longer than 32 byte")
}
buf := &bytes.Buffer{}
// Write token bridge header
for i := 0; i < (32 - len(r.Module)); i++ {
for i := 0; i < (32 - len(module)); i++ {
buf.WriteByte(0x00)
}
buf.Write([]byte(r.Module))
buf.Write([]byte(module))
// Write action ID
MustWrite(buf, binary.BigEndian, uint8(2))
MustWrite(buf, binary.BigEndian, actionId)
// Write target chain
MustWrite(buf, binary.BigEndian, r.TargetChainID)
MustWrite(buf, binary.BigEndian, chainId)
// Write emitter address of chain to be registered
buf.Write(r.NewContract[:])
buf.Write(payload[:])
return buf.Bytes()
}

View File

@ -3,12 +3,12 @@ package keeper
import (
"bytes"
"context"
"encoding/binary"
wasmdtypes "github.com/CosmWasm/wasmd/x/wasm/types"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/wormhole-foundation/wormchain/x/wormhole/types"
"github.com/wormhole-foundation/wormhole/sdk/vaa"
"golang.org/x/crypto/sha3"
)
@ -92,16 +92,10 @@ func (k msgServer) InstantiateContract(goCtx context.Context, msg *types.MsgInst
return nil, types.ErrUnknownGovernanceAction
}
// Need to verify the msg contents by checking sha3.Sum(BigEndian(CodeID) || Label || Msg)
// The vaa governance payload must contain this hash.
var expected_hash [32]byte
keccak := sha3.NewLegacyKeccak256()
binary.Write(keccak, binary.BigEndian, msg.CodeID)
keccak.Write([]byte(msg.Label))
keccak.Write([]byte(msg.Msg))
keccak.Sum(expected_hash[:0])
// Need to verify the instatiation arguments
// The vaa governance payload must contain the hash of the expected args.
expected_hash := vaa.CreateInstatiateCosmwasmContractHash(msg.CodeID, msg.Label, msg.Msg)
if !bytes.Equal(payload, expected_hash[:]) {
return nil, types.ErrInvalidHash
}

View File

@ -31,12 +31,7 @@ func createWasmInstantiatePayload(code_id uint64, label string, json_msg string)
// - code_id (big endian)
// - label
// - json_msg
var expected_hash [32]byte
keccak := sha3.NewLegacyKeccak256()
binary.Write(keccak, binary.BigEndian, code_id)
keccak.Write([]byte(label))
keccak.Write([]byte(json_msg))
keccak.Sum(expected_hash[:0])
expected_hash := vaa.CreateInstatiateCosmwasmContractHash(code_id, label, []byte(json_msg))
var payload bytes.Buffer
payload.Write(keeper.WasmdModule[:])