| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721 |
- package txverifier
- import (
- "encoding/json"
- "fmt"
- "math/big"
- "reflect"
- "slices"
- "testing"
- "github.com/ethereum/go-ethereum/common"
- "github.com/wormhole-foundation/wormhole/sdk/vaa"
- )
- func TestExtractFromJsonPath(t *testing.T) {
- testcases := []struct {
- name string
- data json.RawMessage
- path string
- expected interface{}
- wantErr bool
- typ string
- }{
- {
- name: "ValidPathString",
- data: json.RawMessage(`{"key1": {"key2": "value"}}`),
- path: "key1.key2",
- expected: "value",
- wantErr: false,
- typ: "string",
- },
- {
- name: "ValidPathFloat",
- data: json.RawMessage(`{"key1": {"key2": 123.45}}`),
- path: "key1.key2",
- expected: 123.45,
- wantErr: false,
- typ: "float64",
- },
- {
- name: "InvalidPath",
- data: json.RawMessage(`{"key1": {"key2": "value"}}`),
- path: "key1.key3",
- expected: nil,
- wantErr: true,
- typ: "string",
- },
- {
- name: "NestedPath",
- data: json.RawMessage(`{"key1": {"key2": {"key3": "value"}}}`),
- path: "key1.key2.key3",
- expected: "value",
- wantErr: false,
- typ: "string",
- },
- {
- name: "EmptyPath",
- data: json.RawMessage(`{"key1": {"key2": "value"}}`),
- path: "",
- expected: nil,
- wantErr: true,
- typ: "string",
- },
- {
- name: "NonExistentPath",
- data: json.RawMessage(`{"key1": {"key2": "value"}}`),
- path: "key3.key4",
- expected: nil,
- wantErr: true,
- typ: "string",
- },
- {
- name: "MalformedJson",
- data: json.RawMessage(`{"key1": {"key2": "value"`),
- path: "key1.key2",
- expected: nil,
- wantErr: true,
- typ: "string",
- },
- {
- name: "DataIsNil",
- data: nil,
- path: "test",
- expected: nil,
- wantErr: true,
- typ: "string",
- },
- }
- for _, tt := range testcases {
- t.Run(tt.name, func(t *testing.T) {
- var result interface{}
- var err error
- switch tt.typ {
- case "string":
- var res string
- res, err = extractFromJsonPath[string](tt.data, tt.path)
- result = res
- case "float64":
- var res float64
- res, err = extractFromJsonPath[float64](tt.data, tt.path)
- result = res
- default:
- t.Fatalf("Unsupported type: %v", tt.typ)
- }
- if (err != nil) != tt.wantErr {
- t.Errorf("Expected error: %v, got: %v", tt.wantErr, err)
- }
- if !tt.wantErr && result != tt.expected {
- t.Errorf("Expected %v, got %v", tt.expected, result)
- }
- })
- }
- }
- func TestNormalize(t *testing.T) {
- testcases := []struct {
- name string
- amount *big.Int
- decimals uint8
- expected *big.Int
- }{
- {
- name: "AmountWithMoreThan8Decimals",
- amount: big.NewInt(1000000000000000000),
- decimals: 18,
- expected: big.NewInt(100000000),
- },
- {
- name: "AmountWithExactly8Decimals",
- amount: big.NewInt(12345678),
- decimals: 8,
- expected: big.NewInt(12345678),
- },
- {
- name: "AmountWithLessThan8Decimals",
- amount: big.NewInt(12345),
- decimals: 5,
- expected: big.NewInt(12345),
- },
- {
- name: "AmountWithZeroDecimals",
- amount: big.NewInt(12345678),
- decimals: 0,
- expected: big.NewInt(12345678),
- },
- {
- name: "AmountWith9Decimals",
- amount: big.NewInt(123456789),
- decimals: 9,
- expected: big.NewInt(12345678),
- },
- {
- name: "AmountWith10Decimals",
- amount: big.NewInt(1234567890),
- decimals: 10,
- expected: big.NewInt(12345678),
- },
- {
- name: "AmountEqualsNil",
- amount: nil,
- decimals: 18,
- expected: nil,
- },
- }
- for _, tt := range testcases {
- t.Run(tt.name, func(t *testing.T) {
- result := normalize(tt.amount, tt.decimals)
- if result.Cmp(tt.expected) != 0 {
- t.Errorf("Expected %v, got %v", tt.expected, result)
- }
- })
- }
- }
- func TestDenormalize(t *testing.T) {
- t.Parallel() // marks TLog as capable of running in parallel with other tests
- tests := map[string]struct {
- amount *big.Int
- decimals uint8
- expected *big.Int
- }{
- "noop: decimals less than 8": {
- amount: big.NewInt(123000),
- decimals: 1,
- expected: big.NewInt(123000),
- },
- "noop: decimals equal to 8": {
- amount: big.NewInt(123000),
- decimals: 8,
- expected: big.NewInt(123000),
- },
- "denormalize: decimals greater than 8": {
- amount: big.NewInt(123000),
- decimals: 12,
- expected: big.NewInt(1230000000),
- },
- // NOTE: some tokens on NEAR have as many as 24 decimals so this isn't a strict limit for Wormhole
- // overall, but should be true for EVM chains.
- "denormalize: decimals at maximum expected size": {
- amount: big.NewInt(123_000_000),
- decimals: 18,
- expected: big.NewInt(1_230_000_000_000_000_000),
- },
- // https://github.com/wormhole-foundation/wormhole/blob/main/whitepapers/0003_token_bridge.md#handling-of-token-amounts-and-decimals
- "denormalize: whitepaper example 1": {
- amount: big.NewInt(100000000),
- decimals: 18,
- expected: big.NewInt(1000000000000000000),
- },
- "denormalize: whitepaper example 2": {
- amount: big.NewInt(20000),
- decimals: 4,
- expected: big.NewInt(20000),
- },
- }
- for name, test := range tests {
- test := test // NOTE: uncomment for Go < 1.22, see /doc/faq#closures_and_goroutines
- t.Run(name, func(t *testing.T) {
- t.Parallel() // marks each test case as capable of running in parallel with each other
- if got := denormalize(test.amount, test.decimals); got.Cmp(test.expected) != 0 {
- t.Fatalf("denormalize(%s, %d) returned %s; expected %s",
- test.amount.String(),
- test.decimals,
- got,
- test.expected.String(),
- )
- }
- })
- }
- }
- func TestValidateChains(t *testing.T) {
- type args struct {
- input []uint
- }
- tests := []struct {
- name string
- args args
- want []vaa.ChainID
- wantErr bool
- }{
- {
- name: "invalid chainId",
- args: args{
- input: []uint{65535},
- },
- want: nil,
- wantErr: true,
- },
- {
- name: "unsupported chainId",
- args: args{
- input: []uint{22},
- },
- want: nil,
- wantErr: true,
- },
- {
- name: "empty input",
- args: args{
- input: []uint{},
- },
- want: nil,
- wantErr: true,
- },
- {
- name: "happy path",
- args: args{
- input: []uint{2},
- },
- want: []vaa.ChainID{vaa.ChainIDEthereum},
- wantErr: false,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- got, err := ValidateChains(tt.args.input)
- if (err != nil) != tt.wantErr {
- t.Errorf("ValidateChains() error = %v, wantErr %v", err, tt.wantErr)
- return
- }
- if !reflect.DeepEqual(got, tt.want) {
- t.Errorf("ValidateChains() = %v, want %v", got, tt.want)
- }
- })
- }
- }
- func Test_deleteEntries_StringKeys(t *testing.T) {
- tests := []struct {
- name string
- setupCache func() *map[string]vaa.Address
- wantNumPruned int
- wantErr bool
- wantFinalLen int
- }{
- {
- name: "nil pointer",
- setupCache: func() *map[string]vaa.Address {
- return nil
- },
- wantNumPruned: 0,
- wantErr: true,
- },
- {
- name: "pointer to nil map",
- setupCache: func() *map[string]vaa.Address {
- var m map[string]vaa.Address = nil
- return &m
- },
- wantNumPruned: 0,
- wantErr: true,
- },
- {
- name: "cache within limits - no deletion needed",
- setupCache: func() *map[string]vaa.Address {
- m := make(map[string]vaa.Address)
- // Add entries below CacheMaxSize
- for i := range CacheMaxSize - 10 {
- m[fmt.Sprintf("key%d", i)] = vaa.Address{}
- }
- return &m
- },
- wantNumPruned: 0,
- wantErr: false,
- wantFinalLen: CacheMaxSize - 10,
- },
- {
- name: "cache exactly at limit - no deletion needed",
- setupCache: func() *map[string]vaa.Address {
- m := make(map[string]vaa.Address)
- for i := range CacheMaxSize {
- m[fmt.Sprintf("key%d", i)] = vaa.Address{}
- }
- return &m
- },
- wantNumPruned: 0,
- wantErr: false,
- wantFinalLen: CacheMaxSize,
- },
- {
- name: "cache way over limit - delete enough to reach CacheMaxSize",
- setupCache: func() *map[string]vaa.Address {
- m := make(map[string]vaa.Address)
- for i := range CacheMaxSize + 50 {
- m[fmt.Sprintf("key%d", i)] = vaa.Address{}
- }
- return &m
- },
- wantNumPruned: 50, // CacheMaxSize+50-CacheMaxSize = 50 (more than CacheDeleteCount)
- wantErr: false,
- wantFinalLen: CacheMaxSize,
- },
- {
- name: "small cache over limit",
- setupCache: func() *map[string]vaa.Address {
- m := make(map[string]vaa.Address)
- for i := range CacheMaxSize + 3 {
- m[fmt.Sprintf("key%d", i)] = vaa.Address{}
- }
- return &m
- },
- wantNumPruned: CacheDeleteCount, // max(10, 3) = 10
- wantErr: false,
- wantFinalLen: CacheMaxSize + 3 - CacheDeleteCount,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- cachePtr := tt.setupCache()
- // Store original length for verification
- var originalLen int
- if cachePtr != nil && *cachePtr != nil {
- originalLen = len(*cachePtr)
- }
- got, err := deleteEntries(cachePtr)
- // Check error expectation
- if (err != nil) != tt.wantErr {
- t.Errorf("deleteEntries() error = %v, wantErr %v", err, tt.wantErr)
- return
- }
- // Check return value
- if got != tt.wantNumPruned {
- t.Errorf("deleteEntries() returned %v, want %v", got, tt.wantNumPruned)
- return
- }
- // If no error expected, verify the cache state
- if !tt.wantErr && cachePtr != nil && *cachePtr != nil {
- finalLen := len(*cachePtr)
- if finalLen != tt.wantFinalLen {
- t.Errorf("deleteEntries() final cache length = %v, want %v (original: %v, deleted: %v)",
- finalLen, tt.wantFinalLen, originalLen, got)
- }
- // Verify that the returned count matches actual deletions
- expectedDeletions := originalLen - finalLen
- if got != expectedDeletions {
- t.Errorf("deleteEntries() returned %v deletions, but actual deletions = %v",
- got, expectedDeletions)
- }
- }
- })
- }
- }
- //nolint:gosec // Testing on the uint8 value types, but ranging over a size gives int. The truncation issues don't matter here.
- func Test_deleteEntries_AddressKeys(t *testing.T) {
- tests := []struct {
- name string
- setupCache func() *map[common.Address]uint8
- want int
- wantErr bool
- wantFinalLen int
- }{
- {
- name: "nil pointer",
- setupCache: func() *map[common.Address]uint8 {
- return nil
- },
- want: 0,
- wantErr: true,
- },
- {
- name: "pointer to nil map",
- setupCache: func() *map[common.Address]uint8 {
- var m map[common.Address]uint8 = nil
- return &m
- },
- want: 0,
- wantErr: true,
- },
- {
- name: "cache within limits - no deletion needed",
- setupCache: func() *map[common.Address]uint8 {
- m := make(map[common.Address]uint8)
- // Add entries below CacheMaxSize
- for i := range CacheMaxSize - 10 {
- // TODO needs to be common.Address
- m[common.BytesToAddress([]byte{byte(i)})] = uint8(i)
- }
- return &m
- },
- want: 0,
- wantErr: false,
- wantFinalLen: CacheMaxSize - 10,
- },
- {
- name: "cache exactly at limit - no deletion needed",
- setupCache: func() *map[common.Address]uint8 {
- m := make(map[common.Address]uint8)
- for i := range CacheMaxSize {
- m[common.BytesToAddress([]byte{byte(i)})] = uint8(i)
- }
- return &m
- },
- want: 0,
- wantErr: false,
- wantFinalLen: CacheMaxSize,
- },
- {
- name: "cache way over limit - delete enough to reach CacheMaxSize",
- setupCache: func() *map[common.Address]uint8 {
- m := make(map[common.Address]uint8)
- for i := range CacheMaxSize + 50 {
- m[common.BytesToAddress([]byte{byte(i)})] = uint8(i)
- }
- return &m
- },
- want: 50, // CacheMaxSize+50-CacheMaxSize = 50 (more than CacheDeleteCount)
- wantErr: false,
- wantFinalLen: CacheMaxSize,
- },
- {
- name: "small cache over limit",
- setupCache: func() *map[common.Address]uint8 {
- m := make(map[common.Address]uint8)
- for i := range CacheMaxSize + 3 {
- m[common.BytesToAddress([]byte{byte(i)})] = uint8(i)
- }
- return &m
- },
- want: CacheDeleteCount, // max(10, 3) = 10
- wantErr: false,
- wantFinalLen: CacheMaxSize + 3 - CacheDeleteCount,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- cachePtr := tt.setupCache()
- // Store original length for verification
- var originalLen int
- if cachePtr != nil && *cachePtr != nil {
- originalLen = len(*cachePtr)
- }
- got, err := deleteEntries(cachePtr)
- // Check error expectation
- if (err != nil) != tt.wantErr {
- t.Errorf("deleteEntries() error = %v, wantErr %v", err, tt.wantErr)
- return
- }
- // Check return value
- if got != tt.want {
- t.Errorf("deleteEntries() returned %v, want %v", got, tt.want)
- return
- }
- // If no error expected, verify the cache state
- if !tt.wantErr && cachePtr != nil && *cachePtr != nil {
- finalLen := len(*cachePtr)
- if finalLen != tt.wantFinalLen {
- t.Errorf("deleteEntries() final cache length = %v, want %v (original: %v, deleted: %v)",
- finalLen, tt.wantFinalLen, originalLen, got)
- }
- // Verify that the returned count matches actual deletions
- expectedDeletions := originalLen - finalLen
- if got != expectedDeletions {
- t.Errorf("deleteEntries() returned %v deletions, but actual deletions = %v",
- got, expectedDeletions)
- }
- }
- })
- }
- }
- func Test_validateSolvency(t *testing.T) {
- tests := []struct {
- name string
- requests MsgIdToRequestOutOfBridge
- transfers AssetKeyToTransferIntoBridge
- wantErr bool
- invalidTransfers []string
- }{
- {
- name: "SingleRequest_SingleAsset_Solvent",
- requests: MsgIdToRequestOutOfBridge{
- "msg1": {
- AssetKey: "asset1",
- Amount: big.NewInt(100),
- DepositMade: false,
- DepositSolvent: false, // will be updated by validateSolvency
- },
- },
- transfers: AssetKeyToTransferIntoBridge{
- "asset1": {
- Amount: big.NewInt(200),
- },
- },
- wantErr: false,
- invalidTransfers: []string{},
- },
- {
- name: "SingleRequest_SingleAsset_Insolvent",
- requests: MsgIdToRequestOutOfBridge{
- "msg1": {
- AssetKey: "asset1",
- Amount: big.NewInt(300),
- DepositMade: false,
- DepositSolvent: false, // will be updated by validateSolvency
- },
- },
- transfers: AssetKeyToTransferIntoBridge{
- "asset1": {
- Amount: big.NewInt(200),
- },
- },
- wantErr: false,
- invalidTransfers: []string{"msg1"},
- },
- {
- name: "MultipleRequests_MultipleAssets_MixedSolvency",
- requests: MsgIdToRequestOutOfBridge{
- "msg1": {
- AssetKey: "asset1",
- Amount: big.NewInt(100),
- DepositMade: false,
- DepositSolvent: false, // will be updated by validateSolvency
- },
- "msg2": {
- AssetKey: "asset2",
- Amount: big.NewInt(150),
- DepositMade: false,
- DepositSolvent: false, // will be updated by validateSolvency
- },
- "msg3": {
- AssetKey: "asset1",
- Amount: big.NewInt(50),
- DepositMade: false,
- DepositSolvent: false, // will be updated by validateSolvency
- },
- },
- transfers: AssetKeyToTransferIntoBridge{
- "asset1": {
- Amount: big.NewInt(120),
- },
- "asset2": {
- Amount: big.NewInt(200),
- },
- },
- wantErr: false,
- // asset1 is insolvent because of msg1 and msg3
- invalidTransfers: []string{"msg1", "msg3"},
- },
- {
- name: "RequestWithNoMatchingTransfer",
- requests: MsgIdToRequestOutOfBridge{
- "msg1": {
- AssetKey: "asset1",
- Amount: big.NewInt(100),
- DepositMade: false,
- DepositSolvent: false, // will be updated by validateSolvency
- },
- },
- transfers: AssetKeyToTransferIntoBridge{},
- wantErr: false,
- invalidTransfers: []string{"msg1"}, // no transfer for asset1
- },
- {
- name: "EmptyRequests",
- requests: MsgIdToRequestOutOfBridge{},
- transfers: AssetKeyToTransferIntoBridge{},
- wantErr: false,
- invalidTransfers: []string{},
- },
- {
- name: "EmptyTransfers",
- requests: MsgIdToRequestOutOfBridge{
- "msg1": {
- AssetKey: "asset1",
- Amount: big.NewInt(100),
- DepositMade: false,
- DepositSolvent: false, // will be updated by validateSolvency
- },
- },
- transfers: AssetKeyToTransferIntoBridge{},
- wantErr: false,
- invalidTransfers: []string{"msg1"}, // no transfer for asset1
- },
- {
- name: "EmptyRequests",
- requests: MsgIdToRequestOutOfBridge{},
- transfers: AssetKeyToTransferIntoBridge{},
- wantErr: false,
- invalidTransfers: []string{},
- },
- {
- name: "RequestWithNilAmount",
- requests: MsgIdToRequestOutOfBridge{
- "msg1": {
- AssetKey: "asset1",
- Amount: nil,
- DepositMade: false,
- DepositSolvent: false, // will be updated by validateSolvency
- },
- },
- transfers: AssetKeyToTransferIntoBridge{},
- wantErr: true,
- invalidTransfers: []string{},
- },
- {
- name: "TransferWithNilAmount",
- requests: MsgIdToRequestOutOfBridge{
- "msg1": {
- AssetKey: "asset1",
- Amount: big.NewInt(100),
- DepositMade: false,
- DepositSolvent: false, // will be updated by validateSolvency
- },
- },
- transfers: AssetKeyToTransferIntoBridge{
- "asset1": {
- Amount: nil,
- },
- },
- wantErr: true,
- invalidTransfers: []string{},
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- resolved, err := validateSolvency(tt.requests, tt.transfers)
- if (err != nil) != tt.wantErr {
- t.Errorf("validateSolvency() error = %v, wantErr %v", err, tt.wantErr)
- return
- }
- for msgIdStr, req := range resolved {
- reqIsValid := req.DepositSolvent && req.DepositMade
- shouldBeInvalid := slices.Contains(tt.invalidTransfers, msgIdStr)
- if reqIsValid && !shouldBeInvalid {
- // Valid and should be valid, all good.
- } else if reqIsValid && shouldBeInvalid {
- // Request was marked as valid, but should be invalid
- t.Errorf("Expected message ID %s to be marked as invalid, but it was marked as valid", msgIdStr)
- } else if !reqIsValid && !shouldBeInvalid {
- // Request was marked as invalid, but should be valid
- t.Errorf("Expected message ID %s to be marked as valid, but it was marked as invalid", msgIdStr)
- }
- }
- })
- }
- }
|