wormhole/sdk/vaa/structs_test.go

1065 lines
35 KiB
Go

package vaa
import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/binary"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"math/big"
"reflect"
"testing"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestChainIDFromString(t *testing.T) {
type test struct {
input string
output ChainID
}
// Positive Test Cases
p_tests := []test{
{input: "solana", output: ChainIDSolana},
{input: "ethereum", output: ChainIDEthereum},
{input: "terra", output: ChainIDTerra},
{input: "bsc", output: ChainIDBSC},
{input: "polygon", output: ChainIDPolygon},
{input: "avalanche", output: ChainIDAvalanche},
{input: "oasis", output: ChainIDOasis},
{input: "algorand", output: ChainIDAlgorand},
{input: "aptos", output: ChainIDAptos},
{input: "sui", output: ChainIDSui},
{input: "near", output: ChainIDNear},
{input: "aurora", output: ChainIDAurora},
{input: "fantom", output: ChainIDFantom},
{input: "karura", output: ChainIDKarura},
{input: "acala", output: ChainIDAcala},
{input: "klaytn", output: ChainIDKlaytn},
{input: "celo", output: ChainIDCelo},
{input: "moonbeam", output: ChainIDMoonbeam},
{input: "terra2", output: ChainIDTerra2},
{input: "injective", output: ChainIDInjective},
{input: "osmosis", output: ChainIDOsmosis},
{input: "arbitrum", output: ChainIDArbitrum},
{input: "pythnet", output: ChainIDPythNet},
{input: "optimism", output: ChainIDOptimism},
{input: "gnosis", output: ChainIDGnosis},
{input: "xpla", output: ChainIDXpla},
{input: "btc", output: ChainIDBtc},
{input: "base", output: ChainIDBase},
{input: "sei", output: ChainIDSei},
{input: "rootstock", output: ChainIDRootstock},
{input: "wormchain", output: ChainIDWormchain},
{input: "cosmoshub", output: ChainIDCosmoshub},
{input: "evmos", output: ChainIDEvmos},
{input: "kujira", output: ChainIDKujira},
{input: "neutron", output: ChainIDNeutron},
{input: "celestia", output: ChainIDCelestia},
{input: "stargaze", output: ChainIDStargaze},
{input: "seda", output: ChainIDSeda},
{input: "dymension", output: ChainIDDymension},
{input: "sepolia", output: ChainIDSepolia},
{input: "arbitrum_sepolia", output: ChainIDArbitrumSepolia},
{input: "base_sepolia", output: ChainIDBaseSepolia},
{input: "optimism_sepolia", output: ChainIDOptimismSepolia},
{input: "holesky", output: ChainIDHolesky},
{input: "polygon_sepolia", output: ChainIDPolygonSepolia},
{input: "Solana", output: ChainIDSolana},
{input: "Ethereum", output: ChainIDEthereum},
{input: "Terra", output: ChainIDTerra},
{input: "Bsc", output: ChainIDBSC},
{input: "Polygon", output: ChainIDPolygon},
{input: "Avalanche", output: ChainIDAvalanche},
{input: "Oasis", output: ChainIDOasis},
{input: "Algorand", output: ChainIDAlgorand},
{input: "Aptos", output: ChainIDAptos},
{input: "Sui", output: ChainIDSui},
{input: "Near", output: ChainIDNear},
{input: "Aurora", output: ChainIDAurora},
{input: "Fantom", output: ChainIDFantom},
{input: "Karura", output: ChainIDKarura},
{input: "Acala", output: ChainIDAcala},
{input: "Klaytn", output: ChainIDKlaytn},
{input: "Celo", output: ChainIDCelo},
{input: "Moonbeam", output: ChainIDMoonbeam},
{input: "Terra2", output: ChainIDTerra2},
{input: "Injective", output: ChainIDInjective},
{input: "Osmosis", output: ChainIDOsmosis},
{input: "Arbitrum", output: ChainIDArbitrum},
{input: "Optimism", output: ChainIDOptimism},
{input: "Gnosis", output: ChainIDGnosis},
{input: "Pythnet", output: ChainIDPythNet},
{input: "XPLA", output: ChainIDXpla},
{input: "BTC", output: ChainIDBtc},
{input: "Base", output: ChainIDBase},
{input: "Sei", output: ChainIDSei},
{input: "Rootstock", output: ChainIDRootstock},
{input: "Wormchain", output: ChainIDWormchain},
{input: "Cosmoshub", output: ChainIDCosmoshub},
{input: "Evmos", output: ChainIDEvmos},
{input: "Kujira", output: ChainIDKujira},
{input: "Neutron", output: ChainIDNeutron},
{input: "Celestia", output: ChainIDCelestia},
{input: "Stargaze", output: ChainIDStargaze},
{input: "Seda", output: ChainIDSeda},
{input: "Sepolia", output: ChainIDSepolia},
{input: "Arbitrum_Sepolia", output: ChainIDArbitrumSepolia},
{input: "Base_Sepolia", output: ChainIDBaseSepolia},
{input: "Optimism_Sepolia", output: ChainIDOptimismSepolia},
{input: "Holesky", output: ChainIDHolesky},
{input: "Polygon_Sepolia", output: ChainIDPolygonSepolia},
}
// Negative Test Cases
n_tests := []test{
{input: "Unknown", output: ChainIDUnset},
}
for _, tc := range p_tests {
t.Run(tc.input, func(t *testing.T) {
chainId, err := ChainIDFromString(tc.input)
assert.Equal(t, tc.output, chainId)
assert.NoError(t, err)
})
}
for _, tc := range n_tests {
t.Run(tc.input, func(t *testing.T) {
chainId, err := ChainIDFromString(tc.input)
assert.Equal(t, tc.output, chainId)
assert.NotNil(t, err)
})
}
}
func TestAddress_MarshalJSON(t *testing.T) {
addr := 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}
expected := "223030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303422"
marshalJsonAddress, err := addr.MarshalJSON()
assert.Equal(t, hex.EncodeToString(marshalJsonAddress), expected)
assert.NoError(t, err)
}
func TestAddress_UnmarshalJSON(t *testing.T) {
tests := []struct {
name string
address Address
addressJSON string
err error
}{
{
name: "working",
addressJSON: "0000000000000000000000000290fb167208af455bb137780163b7b7a9a10c16",
address: Address{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x90, 0xfb, 0x16, 0x72, 0x8, 0xaf, 0x45, 0x5b, 0xb1, 0x37, 0x78, 0x1, 0x63, 0xb7, 0xb7, 0xa9, 0xa1, 0xc, 0x16},
err: nil,
},
{
name: "failure",
addressJSON: "derp",
address: Address{},
err: hex.InvalidByteError(0x72),
},
}
for _, testCase := range tests {
t.Run(testCase.name, func(t *testing.T) {
var unmarshalAddr Address
err := unmarshalAddr.UnmarshalJSON([]byte(testCase.addressJSON))
require.Equal(t, testCase.err, err)
assert.Equal(t, testCase.address, unmarshalAddr)
})
}
}
func TestAddress_Unmarshal(t *testing.T) {
addr, _ := StringToAddress("0x0290fb167208af455bb137780163b7b7a9a10c16")
b, err := json.Marshal(addr)
require.NoError(t, err)
var unmarshalAddr Address
err = json.Unmarshal(b, &unmarshalAddr)
require.NoError(t, err)
assert.Equal(t, addr, unmarshalAddr)
}
func TestAddress_UnmarshalEmptyBuffer(t *testing.T) {
b := []byte{}
var unmarshalAddr Address
err := json.Unmarshal(b, &unmarshalAddr)
require.Error(t, err)
}
func TestAddress_UnmarshalBufferTooShort(t *testing.T) {
addr, _ := StringToAddress("0x0290fb167208af455bb137780163b7b7a9a10c16")
b, err := json.Marshal(addr)
require.NoError(t, err)
var unmarshalAddr Address
// Lop off the first byte, and it should fail.
b1 := b[1:]
err = json.Unmarshal(b1, &unmarshalAddr)
assert.Error(t, err)
// Lop off the last byte, and it should fail.
b2 := b[0 : len(b)-1]
err = json.Unmarshal(b2, &unmarshalAddr)
assert.Error(t, err)
}
func TestAddress_String(t *testing.T) {
addr := 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}
expected := "0000000000000000000000000000000000000000000000000000000000000004"
assert.Equal(t, addr.String(), expected)
}
func TestAddress_Bytes(t *testing.T) {
addr := 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}
expected := []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4}
assert.Equal(t, addr.Bytes(), expected)
}
func TestSignatureData_MarshalJSON(t *testing.T) {
sigData := SignatureData{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, 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, 0}
marshalJsonSigData, err := sigData.MarshalJSON()
require.Nil(t, err)
expected := "223030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303430303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303034303022"
assert.Equal(t, hex.EncodeToString(marshalJsonSigData), expected)
}
func TestSignature_DataString(t *testing.T) {
sigData := SignatureData{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, 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, 0}
expected := "0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000400"
assert.Equal(t, sigData.String(), expected)
}
func TestMinVAALength(t *testing.T) {
assert.Equal(t, minVAALength, 57)
}
func TestChainId_String(t *testing.T) {
type test struct {
input ChainID
output string
}
tests := []test{
{input: 0, output: "unset"},
{input: 1, output: "solana"},
{input: 2, output: "ethereum"},
{input: 3, output: "terra"},
{input: 4, output: "bsc"},
{input: 5, output: "polygon"},
{input: 6, output: "avalanche"},
{input: 7, output: "oasis"},
{input: 8, output: "algorand"},
{input: 9, output: "aurora"},
{input: 10, output: "fantom"},
{input: 11, output: "karura"},
{input: 12, output: "acala"},
{input: 13, output: "klaytn"},
{input: 14, output: "celo"},
{input: 15, output: "near"},
{input: 16, output: "moonbeam"},
{input: 18, output: "terra2"},
{input: 19, output: "injective"},
{input: 20, output: "osmosis"},
{input: 21, output: "sui"},
{input: 22, output: "aptos"},
{input: 23, output: "arbitrum"},
{input: 24, output: "optimism"},
{input: 25, output: "gnosis"},
{input: 26, output: "pythnet"},
{input: 28, output: "xpla"},
{input: 29, output: "btc"},
{input: 30, output: "base"},
{input: 32, output: "sei"},
{input: 33, output: "rootstock"},
{input: 3104, output: "wormchain"},
{input: 4000, output: "cosmoshub"},
{input: 4001, output: "evmos"},
{input: 4002, output: "kujira"},
{input: 4003, output: "neutron"},
{input: 4004, output: "celestia"},
{input: 4005, output: "stargaze"},
{input: 4006, output: "seda"},
{input: 10002, output: "sepolia"},
{input: 10003, output: "arbitrum_sepolia"},
{input: 10004, output: "base_sepolia"},
{input: 10005, output: "optimism_sepolia"},
{input: 10006, output: "holesky"},
{input: 10007, output: "polygon_sepolia"},
{input: 10000, output: "unknown chain ID: 10000"},
}
for _, tc := range tests {
t.Run(tc.output, func(t *testing.T) {
assert.Equal(t, ChainID(tc.input).String(), tc.output)
})
}
}
func getVaa() VAA {
var payload = []byte{97, 97, 97, 97, 97, 97}
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}
return VAA{
Version: uint8(1),
GuardianSetIndex: uint32(1),
Signatures: []*Signature{},
Timestamp: time.Unix(0, 0),
Nonce: uint32(1),
Sequence: uint64(1),
ConsistencyLevel: uint8(32),
EmitterChain: ChainIDSolana,
EmitterAddress: governanceEmitter,
Payload: payload,
}
}
func getEmptyPayloadVaa() VAA {
var payload = []byte{}
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}
return VAA{
Version: uint8(1),
GuardianSetIndex: uint32(1),
Signatures: []*Signature{},
Timestamp: time.Unix(0, 0),
Nonce: uint32(1),
Sequence: uint64(1),
ConsistencyLevel: uint8(32),
EmitterChain: ChainIDSolana,
EmitterAddress: governanceEmitter,
Payload: payload,
}
}
func TestAddSignature(t *testing.T) {
vaa := getVaa()
// Generate a random private key to sign with
key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
assert.Equal(t, []*Signature{}, vaa.Signatures)
// Add a signature and make sure it's added
vaa.AddSignature(key, 0)
assert.Equal(t, len(vaa.Signatures), 1)
}
func TestSerializeBody(t *testing.T) {
vaa := getVaa()
expected := []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x20, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61}
assert.Equal(t, vaa.serializeBody(), expected)
}
func TestSigningBody(t *testing.T) {
vaa := getVaa()
expected := []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x20, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61}
assert.Equal(t, vaa.signingBody(), expected)
}
func TestSigningMsg(t *testing.T) {
vaa := getVaa()
expected := common.HexToHash("4fae136bb1fd782fe1b5180ba735cdc83bcece3f9b7fd0e5e35300a61c8acd8f")
assert.Equal(t, vaa.SigningDigest(), expected)
}
func TestMessageID(t *testing.T) {
vaa := getVaa()
expected := "1/0000000000000000000000000000000000000000000000000000000000000004/1"
assert.Equal(t, vaa.MessageID(), expected)
}
func TestHexDigest(t *testing.T) {
vaa := getVaa()
expected := "4fae136bb1fd782fe1b5180ba735cdc83bcece3f9b7fd0e5e35300a61c8acd8f"
assert.Equal(t, vaa.HexDigest(), expected)
}
func TestMarshal(t *testing.T) {
expectedBytes := []byte{0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x20, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61}
vaa := getVaa()
marshalBytes, err := vaa.Marshal()
assert.Nil(t, err)
assert.Equal(t, expectedBytes, marshalBytes)
}
func TestUnmarshal(t *testing.T) {
vaaBytes := []byte{0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x20, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61}
vaa1 := getVaa()
vaa2, err := Unmarshal(vaaBytes)
assert.Nil(t, err)
assert.Equal(t, &vaa1, vaa2)
}
func TestUnmarshalNoPayload(t *testing.T) {
vaaBytes := []byte{0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x20}
vaa1 := getEmptyPayloadVaa()
vaa2, err := Unmarshal(vaaBytes)
assert.Nil(t, err)
assert.Equal(t, &vaa1, vaa2)
}
func TestUnmarshalBigPayload(t *testing.T) {
vaa := getVaa()
// Create a payload of more than 1000 bytes.
var payload []byte
for i := 0; i < 2000; i++ {
ch := i % 255
payload = append(payload, byte(ch))
}
vaa.Payload = payload
marshalBytes, err := vaa.Marshal()
require.NoError(t, err)
vaa2, err := Unmarshal(marshalBytes)
require.NoError(t, err)
assert.Equal(t, vaa, *vaa2)
}
func FuzzUnmarshalBigPayload(f *testing.F) {
f.Add([]byte{})
f.Add([]byte{0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x20})
f.Fuzz(func(t *testing.T, payload []byte) {
vaa := getVaa()
vaa.Payload = payload
// It should always marshal
marshalBytes, err := vaa.Marshal()
require.NoError(t, err)
// It should aways unmarshal
vaa2, err := Unmarshal(marshalBytes)
require.NoError(t, err)
// The payload should not be lossy
assert.Equal(t, vaa.Payload, payload)
assert.Equal(t, vaa2.Payload, payload)
// The marshal and unmarshal should always be the same
assert.Equal(t, vaa, *vaa2)
})
}
func TestVerifySignatures(t *testing.T) {
// Generate some random private keys to sign with
privKey1, _ := ecdsa.GenerateKey(crypto.S256(), rand.Reader)
privKey2, _ := ecdsa.GenerateKey(crypto.S256(), rand.Reader)
privKey3, _ := ecdsa.GenerateKey(crypto.S256(), rand.Reader)
privKey4, _ := ecdsa.GenerateKey(crypto.S256(), rand.Reader)
// Give a fixed order of trusted addresses
addrs := []common.Address{}
addrs = append(addrs, crypto.PubkeyToAddress(privKey1.PublicKey))
addrs = append(addrs, crypto.PubkeyToAddress(privKey2.PublicKey))
addrs = append(addrs, crypto.PubkeyToAddress(privKey3.PublicKey))
type test struct {
label string
keyOrder []*ecdsa.PrivateKey
addrs []common.Address
indexOrder []uint8
result bool
}
tests := []test{
{label: "NoSignerZero",
keyOrder: []*ecdsa.PrivateKey{},
addrs: addrs,
indexOrder: []uint8{0},
result: true},
{label: "NoSignerOne",
keyOrder: []*ecdsa.PrivateKey{},
addrs: addrs,
indexOrder: []uint8{1},
result: true},
{label: "SingleZero",
keyOrder: []*ecdsa.PrivateKey{privKey1},
addrs: addrs,
indexOrder: []uint8{0},
result: true},
{label: "RogueSingleOne",
keyOrder: []*ecdsa.PrivateKey{privKey4},
addrs: addrs,
indexOrder: []uint8{0},
result: false},
{label: "RogueSingleZero",
keyOrder: []*ecdsa.PrivateKey{privKey4},
addrs: addrs,
indexOrder: []uint8{0},
result: false},
{label: "SingleOne",
keyOrder: []*ecdsa.PrivateKey{privKey1},
addrs: addrs,
indexOrder: []uint8{0},
result: true},
{label: "MultiUniqSignerMonotonicIndex",
keyOrder: []*ecdsa.PrivateKey{privKey1, privKey2, privKey3},
addrs: addrs,
indexOrder: []uint8{0, 1, 2},
result: true},
{label: "MultiMisOrderedSignerMonotonicIndex",
keyOrder: []*ecdsa.PrivateKey{privKey3, privKey2, privKey1},
addrs: addrs,
indexOrder: []uint8{0, 1, 2}, result: false},
{label: "MultiUniqSignerNonMonotonic",
keyOrder: []*ecdsa.PrivateKey{privKey1, privKey2, privKey3},
addrs: addrs,
indexOrder: []uint8{0, 2, 1},
result: false},
{label: "MultiUniqSignerFullSameIndex0",
keyOrder: []*ecdsa.PrivateKey{privKey1, privKey2, privKey3},
addrs: addrs,
indexOrder: []uint8{0, 0, 0},
result: false},
{label: "MultiUniqSignerFullSameIndex1",
keyOrder: []*ecdsa.PrivateKey{privKey1, privKey2, privKey3},
addrs: addrs,
indexOrder: []uint8{0, 0, 0},
result: false},
{label: "MultiUniqSignerPartialSameIndex",
keyOrder: []*ecdsa.PrivateKey{privKey1, privKey2, privKey3},
addrs: addrs,
indexOrder: []uint8{0, 1, 1},
result: false},
{label: "MultiSameSignerPartialSameIndex",
keyOrder: []*ecdsa.PrivateKey{privKey1, privKey2, privKey2},
addrs: addrs,
indexOrder: []uint8{0, 1, 1},
result: false},
{label: "MultiSameSignerNonMonotonic",
keyOrder: []*ecdsa.PrivateKey{privKey1, privKey2, privKey2},
addrs: addrs,
indexOrder: []uint8{0, 2, 1},
result: false},
{label: "MultiSameSignerFullSameIndex",
keyOrder: []*ecdsa.PrivateKey{privKey1, privKey1, privKey1},
addrs: addrs,
indexOrder: []uint8{0, 0, 0},
result: false},
{label: "MultiSameSignerMonotonic",
keyOrder: []*ecdsa.PrivateKey{privKey1, privKey1, privKey1},
addrs: addrs,
indexOrder: []uint8{0, 1, 2},
result: false},
}
for _, tc := range tests {
t.Run(tc.label, func(t *testing.T) {
vaa := getVaa()
for i, key := range tc.keyOrder {
vaa.AddSignature(key, tc.indexOrder[i])
}
assert.Equal(t, tc.result, vaa.VerifySignatures(tc.addrs))
})
}
}
func TestVerifySignaturesFuzz(t *testing.T) {
// Generate some random trusted private keys to sign with
privKey1, _ := ecdsa.GenerateKey(crypto.S256(), rand.Reader)
privKey2, _ := ecdsa.GenerateKey(crypto.S256(), rand.Reader)
privKey3, _ := ecdsa.GenerateKey(crypto.S256(), rand.Reader)
// Generate some random untrusted private keys to sign with
privKey4, _ := ecdsa.GenerateKey(crypto.S256(), rand.Reader)
privKey5, _ := ecdsa.GenerateKey(crypto.S256(), rand.Reader)
privKey6, _ := ecdsa.GenerateKey(crypto.S256(), rand.Reader)
// Give a fixed order of trusted addresses (we intentionally omit privKey4, privKey5, privKey6)
addrs := []common.Address{}
addrs = append(addrs, crypto.PubkeyToAddress(privKey1.PublicKey))
addrs = append(addrs, crypto.PubkeyToAddress(privKey2.PublicKey))
addrs = append(addrs, crypto.PubkeyToAddress(privKey3.PublicKey))
// key space for fuzz tests
keys := []*ecdsa.PrivateKey{}
keys = append(keys, privKey1)
keys = append(keys, privKey2)
keys = append(keys, privKey3)
keys = append(keys, privKey4)
keys = append(keys, privKey5)
keys = append(keys, privKey6)
// index space for fuzz tests
indexes := []uint8{0, 1, 2, 3, 4, 5}
type test struct {
label string
keyOrder []*ecdsa.PrivateKey
addrs []common.Address
indexOrder []uint8
result bool
}
type allow struct {
keyPair []*ecdsa.PrivateKey
indexPair []uint8
}
// Known good cases where we should have a verified result for
allows := []allow{
{keyPair: []*ecdsa.PrivateKey{}, indexPair: []uint8{}},
{keyPair: []*ecdsa.PrivateKey{privKey1}, indexPair: []uint8{0}},
{keyPair: []*ecdsa.PrivateKey{privKey2}, indexPair: []uint8{1}},
{keyPair: []*ecdsa.PrivateKey{privKey3}, indexPair: []uint8{2}},
{keyPair: []*ecdsa.PrivateKey{privKey1, privKey2}, indexPair: []uint8{0, 1}},
{keyPair: []*ecdsa.PrivateKey{privKey1, privKey3}, indexPair: []uint8{0, 2}},
{keyPair: []*ecdsa.PrivateKey{privKey2, privKey3}, indexPair: []uint8{1, 2}},
{keyPair: []*ecdsa.PrivateKey{privKey1, privKey2, privKey3}, indexPair: []uint8{0, 1, 2}},
}
tests := []test{}
keyPairs := [][]*ecdsa.PrivateKey{}
indexPairs := [][]uint8{}
// Build empty keyPair
keyPairs = append(keyPairs, []*ecdsa.PrivateKey{})
// Build single keyPairs
for _, key := range keys {
keyPairs = append(keyPairs, []*ecdsa.PrivateKey{key})
}
// Build double keyPairs
for _, key_i := range keys {
for _, key_j := range keys {
keyPairs = append(keyPairs, []*ecdsa.PrivateKey{key_i, key_j})
}
}
// Build triple keyPairs
for _, key_i := range keys {
for _, key_j := range keys {
for _, key_k := range keys {
keyPairs = append(keyPairs, []*ecdsa.PrivateKey{key_i, key_j, key_k})
}
}
}
// Build empty indexPairs
indexPairs = append(indexPairs, []uint8{})
// Build single indexPairs
for _, ind := range indexes {
indexPairs = append(indexPairs, []uint8{ind})
}
// Build double indexPairs
for _, ind_i := range indexes {
for _, ind_j := range indexes {
indexPairs = append(indexPairs, []uint8{ind_i, ind_j})
}
}
// Build triple keyPairs
for _, ind_i := range indexes {
for _, ind_j := range indexes {
for _, ind_k := range indexes {
indexPairs = append(indexPairs, []uint8{ind_i, ind_j, ind_k})
}
}
}
// Build out the fuzzTest cases
for _, keyPair := range keyPairs {
for _, indexPair := range indexPairs {
if len(keyPair) == len(indexPair) {
result := false
for _, allow := range allows {
if reflect.DeepEqual(allow.indexPair, indexPair) && reflect.DeepEqual(allow.keyPair, keyPair) {
result = true
break
}
}
test := test{label: "A", keyOrder: keyPair, addrs: addrs, indexOrder: indexPair, result: result}
tests = append(tests, test)
}
}
}
// Run the fuzzTest cases
for _, tc := range tests {
t.Run(tc.label, func(t *testing.T) {
vaa := getVaa()
for i, key := range tc.keyOrder {
vaa.AddSignature(key, tc.indexOrder[i])
}
/* Fuzz Debugging
* Tell us what keys and indexes were used (for debug when/if we have a failure case)
*/
if vaa.VerifySignatures(tc.addrs) != tc.result {
if len(tc.keyOrder) == 0 {
t.Logf("Key Order %v\n", tc.keyOrder)
} else {
keyIndex := []uint8{}
for i, key_i := range keys {
for _, key_k := range tc.keyOrder {
if key_i == key_k {
keyIndex = append(keyIndex, uint8(i))
}
}
}
t.Logf("Key Order %v\n", keyIndex)
}
t.Logf("Index Order %v\n", tc.indexOrder)
}
assert.Equal(t, tc.result, vaa.VerifySignatures(tc.addrs))
})
}
}
func TestStringToAddress(t *testing.T) {
type Test struct {
label string
rawAddr string
addr Address
errString string
}
tests := []Test{
{label: "simple",
rawAddr: "0000000000000000000000000000000000000000000000000000000000000004",
addr: 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},
errString: ""},
{label: "zero-padding",
rawAddr: "04",
addr: 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},
errString: ""},
{label: "trim-0x", rawAddr: "0x04",
addr: 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},
errString: ""},
{label: "20byte eth-style address", rawAddr: "0x0290FB167208Af455bB137780163b7B7a9a10C16",
addr: Address{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x90, 0xfb, 0x16, 0x72, 0x8, 0xaf, 0x45, 0x5b, 0xb1, 0x37, 0x78, 0x1, 0x63, 0xb7, 0xb7, 0xa9, 0xa1, 0xc, 0x16},
errString: ""},
{label: "too long",
rawAddr: "0x0000000000000000000000000000000000000000000000000000000000000000000004",
errString: "value must be no more than 32 bytes"},
{label: "too short",
rawAddr: "4",
errString: "value must be at least 1 byte"},
{label: "empty string",
rawAddr: "",
errString: "value must be at least 1 byte"},
}
for _, tc := range tests {
t.Run(string(tc.label), func(t *testing.T) {
addr, err := StringToAddress(tc.rawAddr)
if len(tc.errString) == 0 {
assert.NoError(t, err)
assert.Equal(t, tc.addr, addr)
} else {
assert.Equal(t, tc.errString, err.Error())
}
})
}
}
func TestBytesToAddress(t *testing.T) {
addrStr := "0000000000000000000000003ee18b2214aff97000d974cf647e7c347e8fa585"
expectedAddr, err := StringToAddress(addrStr)
assert.NoError(t, err)
addrBytes, err := hex.DecodeString(addrStr)
assert.NoError(t, err)
addr, err := BytesToAddress(addrBytes)
assert.NoError(t, err)
assert.Equal(t, expectedAddr, addr)
// More than 32 bytes should generate an error.
tooLongAddrBytes, err := hex.DecodeString("0000" + addrStr)
assert.NoError(t, err)
_, err = BytesToAddress(tooLongAddrBytes)
assert.NotNil(t, err)
assert.Equal(t, "value must be no more than 32 bytes", err.Error())
// Less than 32 bytes should get left padded with zeros.
shortAddr, err := BytesToAddress(addrBytes[4:])
assert.NoError(t, err)
assert.Equal(t, expectedAddr, shortAddr)
}
func TestDecodeTransferPayloadHdr(t *testing.T) {
type Test struct {
label string
vaa string
payloadType uint8
emitterChainId ChainID
emitterAddr string
originChain ChainID
originAddress string
targetChain ChainID
targetAddress string
amount int64
errString string
}
tests := []Test{
{label: "valid vaa",
vaa: "01000000000100e424aef95296cb0f2185f351086c7c0b9cd031d1288f0537d04ab20d5fc709416224b2bd9a8010a81988aa9cb38b378eb915f88b67e32a765928d948dc02077e00000102584a8d000000020000000000000000000000000290fb167208af455bb137780163b7b7a9a10c16000000000000000f0f01000000000000000000000000000000000000000000000000000000002b369f40000000000000000000000000ddb64fe46a91d46ee29420539fc25fd07c5fea3e000221c175fcd8e3a19fe2e0deae96534f0f4e6a896f4df0e3ec5345fe27ac3f63f000010000000000000000000000000000000000000000000000000000000000000000",
payloadType: 1,
emitterChainId: ChainIDEthereum,
emitterAddr: "0000000000000000000000000290FB167208Af455bB137780163b7B7a9a10C16",
originChain: ChainIDEthereum,
originAddress: "000000000000000000000000DDb64fE46a91D46ee29420539FC25FD07c5FEa3E",
targetChain: ChainIDSolana,
targetAddress: "21c175fcd8e3a19fe2e0deae96534f0f4e6a896f4df0e3ec5345fe27ac3f63f0",
amount: 725000000,
errString: "",
},
{label: "unsupported payload type",
vaa: "01000000000100e424aef95296cb0f2185f351086c7c0b9cd031d1288f0537d04ab20d5fc709416224b2bd9a8010a81988aa9cb38b378eb915f88b67e32a765928d948dc02077e00000102584a8d000000020000000000000000000000000290fb167208af455bb137780163b7b7a9a10c16000000000000000f0f02000000000000000000000000000000000000000000000000000000002b369f40000000000000000000000000ddb64fe46a91d46ee29420539fc25fd07c5fea3e000221c175fcd8e3a19fe2e0deae96534f0f4e6a896f4df0e3ec5345fe27ac3f63f000010000000000000000000000000000000000000000000000000000000000000000",
errString: "unsupported payload type",
},
{label: "buffer too short",
vaa: "01000000000100e424aef95296cb0f2185f351086c7c0b9cd031d1288f0537d04ab20d5fc709416224b2bd9a8010a81988aa9cb38b378eb915f88b67e32a765928d948dc02077e00000102584a8d000000020000000000000000000000000290fb167208af455bb137780163b7b7a9a10c16000000000000000f0f01",
errString: "buffer too short",
},
{label: "empty string",
vaa: "",
errString: "VAA is too short",
},
}
for _, tc := range tests {
t.Run(string(tc.label), func(t *testing.T) {
data, err := hex.DecodeString(tc.vaa)
assert.NoError(t, err)
vaa, err := Unmarshal(data)
if err != nil {
assert.Equal(t, tc.errString, err.Error())
} else {
assert.NoError(t, err)
assert.NotNil(t, vaa)
if len(tc.errString) == 0 {
expectedEmitterAddr, err := StringToAddress(tc.emitterAddr)
assert.NoError(t, err)
expectedOriginAddress, err := StringToAddress(tc.originAddress)
assert.NoError(t, err)
expectedTargetAddress, err := StringToAddress(tc.targetAddress)
assert.NoError(t, err)
expectedAmount := big.NewInt(tc.amount)
assert.Equal(t, tc.emitterChainId, vaa.EmitterChain)
assert.Equal(t, expectedEmitterAddr, vaa.EmitterAddress)
assert.Equal(t, 133, len(vaa.Payload))
payload, err := DecodeTransferPayloadHdr(vaa.Payload)
assert.NoError(t, err)
assert.Equal(t, tc.payloadType, payload.Type)
assert.Equal(t, tc.originChain, payload.OriginChain)
assert.Equal(t, expectedOriginAddress, payload.OriginAddress)
assert.Equal(t, tc.targetChain, payload.TargetChain)
assert.Equal(t, expectedTargetAddress, payload.TargetAddress)
assert.Equal(t, expectedAmount.Cmp(payload.Amount), 0)
} else {
_, err = DecodeTransferPayloadHdr(vaa.Payload)
assert.NotNil(t, err)
assert.Equal(t, tc.errString, err.Error())
}
}
})
}
}
func TestIsTransfer(t *testing.T) {
type Test struct {
label string
payload []byte
result bool
}
tests := []Test{
{label: "empty", payload: []byte{}, result: false},
{label: "non-valid payload", payload: []byte{0x0}, result: false},
{label: "payload 1", payload: []byte{0x1}, result: true},
{label: "payload 2", payload: []byte{0x2}, result: false},
{label: "payload 3", payload: []byte{0x3}, result: true},
{label: "payload 4", payload: []byte{0x4}, result: false},
}
for _, tc := range tests {
t.Run(string(tc.label), func(t *testing.T) {
assert.Equal(t, tc.result, IsTransfer(tc.payload))
})
}
}
func TestUnmarshalBody(t *testing.T) {
addr, _ := StringToAddress("0x0290fb167208af455bb137780163b7b7a9a10c16")
testPayload := []byte("Hi")
tests := []struct {
name string
data []byte
vaa *VAA
err error
expectedVAA *VAA
dataFunc func() []byte
}{
{
name: "invalid_timestamp",
dataFunc: func() []byte {
return []byte("Hi")
},
err: fmt.Errorf("failed to read timestamp: %w", errors.New("unexpected EOF")),
},
{
name: "invalid_nonce",
err: fmt.Errorf("failed to read nonce: %w", errors.New("EOF")),
vaa: &VAA{},
dataFunc: func() []byte {
buf := new(bytes.Buffer)
MustWrite(buf, binary.BigEndian, uint32(time.Now().Unix()))
return buf.Bytes()
},
},
{
name: "invalid_emmitter_chain",
err: fmt.Errorf("failed to read emitter chain: %w", errors.New("EOF")),
vaa: &VAA{},
dataFunc: func() []byte {
buf := new(bytes.Buffer)
MustWrite(buf, binary.BigEndian, uint32(time.Now().Unix()))
MustWrite(buf, binary.BigEndian, uint32(123))
return buf.Bytes()
},
},
{
name: "invalid_emmitter_address",
err: fmt.Errorf("failed to read emitter address [0]: %w", errors.New("EOF")),
vaa: &VAA{},
dataFunc: func() []byte {
buf := new(bytes.Buffer)
MustWrite(buf, binary.BigEndian, uint32(time.Now().Unix()))
MustWrite(buf, binary.BigEndian, uint32(123))
MustWrite(buf, binary.BigEndian, ChainIDPythNet)
return buf.Bytes()
},
},
{
name: "invalid_sequence_number",
err: fmt.Errorf("failed to read sequence: %w", errors.New("EOF")),
vaa: &VAA{},
dataFunc: func() []byte {
buf := new(bytes.Buffer)
MustWrite(buf, binary.BigEndian, uint32(time.Now().Unix()))
MustWrite(buf, binary.BigEndian, uint32(123))
MustWrite(buf, binary.BigEndian, ChainIDBSC)
buf.Write(addr[:])
return buf.Bytes()
},
},
{
name: "invalid_consistency_level",
err: fmt.Errorf("failed to read commitment: %w", errors.New("EOF")),
vaa: &VAA{},
dataFunc: func() []byte {
buf := new(bytes.Buffer)
MustWrite(buf, binary.BigEndian, uint32(time.Now().Unix()))
MustWrite(buf, binary.BigEndian, uint32(123))
MustWrite(buf, binary.BigEndian, ChainIDBSC)
buf.Write(addr[:])
MustWrite(buf, binary.BigEndian, uint64(42))
return buf.Bytes()
},
},
{
name: "has_payload",
err: nil,
vaa: &VAA{},
expectedVAA: &VAA{
Nonce: uint32(123),
Sequence: uint64(42),
ConsistencyLevel: uint8(1),
EmitterChain: ChainIDBSC,
Timestamp: time.Unix(0, 0),
EmitterAddress: addr,
Payload: testPayload,
},
dataFunc: func() []byte {
buf := new(bytes.Buffer)
MustWrite(buf, binary.BigEndian, uint32(0))
MustWrite(buf, binary.BigEndian, uint32(123))
MustWrite(buf, binary.BigEndian, ChainIDBSC)
buf.Write(addr[:])
MustWrite(buf, binary.BigEndian, uint64(42))
MustWrite(buf, binary.BigEndian, uint8(1))
buf.Write(testPayload)
return buf.Bytes()
},
},
{
name: "has_empty_payload",
err: nil,
vaa: &VAA{},
expectedVAA: &VAA{
Nonce: uint32(123),
Sequence: uint64(42),
ConsistencyLevel: uint8(1),
EmitterChain: ChainIDBSC,
Timestamp: time.Unix(0, 0),
EmitterAddress: addr,
Payload: []byte{},
},
dataFunc: func() []byte {
buf := new(bytes.Buffer)
MustWrite(buf, binary.BigEndian, uint32(0))
MustWrite(buf, binary.BigEndian, uint32(123))
MustWrite(buf, binary.BigEndian, ChainIDBSC)
buf.Write(addr[:])
MustWrite(buf, binary.BigEndian, uint64(42))
MustWrite(buf, binary.BigEndian, uint8(1))
buf.Write([]byte{})
return buf.Bytes()
},
},
}
for _, testCase := range tests {
t.Run(testCase.name, func(t *testing.T) {
testBytes := testCase.dataFunc()
body, err := UnmarshalBody(testCase.data, bytes.NewReader(testBytes), testCase.vaa)
require.Equal(t, testCase.err, err)
if err == nil {
assert.Equal(t, testCase.expectedVAA, body)
}
})
}
}