Skip to content

Commit

Permalink
Node/CCQ: Allow address wildcard
Browse files Browse the repository at this point in the history
  • Loading branch information
bruce-riley committed Aug 6, 2024
1 parent e351236 commit c85d6c0
Show file tree
Hide file tree
Showing 6 changed files with 209 additions and 8 deletions.
2 changes: 1 addition & 1 deletion node/cmd/ccq/devnet.permissions.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"ethCall": {
"note:": "Name of WETH on Goerli",
"chain": 2,
"contractAddress": "B4FBF271143F4FBf7B91A5ded31805e42b2208d6",
"contractAddress": "*",
"call": "0x06fdde03"
}
},
Expand Down
157 changes: 157 additions & 0 deletions node/cmd/ccq/parse_config_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package ccq

import (
"encoding/hex"
"strings"
"testing"

"github.com/certusone/wormhole/node/pkg/query"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/wormhole-foundation/wormhole/sdk/vaa"
"go.uber.org/zap"
)

func TestParseConfigFileDoesntExist(t *testing.T) {
Expand Down Expand Up @@ -461,3 +466,155 @@ func TestParseConfigAllowAnythingSuccess(t *testing.T) {
require.True(t, ok)
assert.True(t, perm.allowAnything)
}

func TestParseConfigContractWildcard(t *testing.T) {
str := `
{
"permissions": [
{
"userName": "Test User",
"apiKey": "my_secret_key",
"allowedCalls": [
{
"ethCall": {
"note:": "Name of anything on Goerli",
"chain": 2,
"contractAddress": "*",
"call": "0x06fdde03"
}
},
{
"ethCallByTimestamp": {
"note:": "Total supply of WETH on Goerli",
"chain": 2,
"contractAddress": "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6",
"call": "0x18160ddd"
}
}
]
}
]
}`

perms, err := parseConfig([]byte(str), true)
require.NoError(t, err)
assert.Equal(t, 1, len(perms))

permsForUser, ok := perms["my_secret_key"]
require.True(t, ok)
assert.Equal(t, 2, len(permsForUser.allowedCalls))

logger := zap.NewNop()

type testCase struct {
label string
callType string
chainID vaa.ChainID
contractAddress string
data string
errText string // empty string means success
}

var testCases = []testCase{
{
label: "Wild card, success",
callType: "ethCall",
chainID: vaa.ChainIDEthereum,
contractAddress: "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6",
data: "0x06fdde03",
errText: "",
},
{
label: "Wild card, success, different address",
callType: "ethCall",
chainID: vaa.ChainIDEthereum,
contractAddress: "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d7",
data: "0x06fdde03",
errText: "",
},
{
label: "Wild card, wrong call type",
callType: "ethCallByTimestamp",
chainID: vaa.ChainIDEthereum,
contractAddress: "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6",
data: "0x06fdde03",
errText: "not authorized",
},
{
label: "Wild card, wrong chain",
callType: "ethCall",
chainID: vaa.ChainIDBase,
contractAddress: "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6",
data: "0x06fdde03",
errText: "not authorized",
},
{
label: "Specific, success",
callType: "ethCallByTimestamp",
chainID: vaa.ChainIDEthereum,
contractAddress: "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6",
data: "0x18160ddd",
errText: "",
},
{
label: "Specific, wrong call type",
callType: "ethCall",
chainID: vaa.ChainIDEthereum,
contractAddress: "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6",
data: "0x18160ddd",
errText: "not authorized",
},
{
label: "Specific, wrong chain",
callType: "ethCallByTimestamp",
chainID: vaa.ChainIDBase,
contractAddress: "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6",
data: "0x18160ddd",
errText: "not authorized",
},
{
label: "Specific, wrong address",
callType: "ethCallByTimestamp",
chainID: vaa.ChainIDEthereum,
contractAddress: "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d7",
data: "0x18160ddd",
errText: "not authorized",
},
{
label: "Specific, wrong data",
callType: "ethCallByTimestamp",
chainID: vaa.ChainIDEthereum,
contractAddress: "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6",
data: "0x18160dde",
errText: "not authorized",
},
}

for _, tst := range testCases {
t.Run(tst.label, func(t *testing.T) {
status, err := validateCallData(logger, permsForUser, tst.callType, tst.chainID, createCallData(t, tst.contractAddress, tst.data))
if tst.errText == "" {
require.NoError(t, err)
assert.Equal(t, 200, status)
} else {
require.ErrorContains(t, err, tst.errText)
}
})
}
}

func createCallData(t *testing.T, toStr string, dataStr string) []*query.EthCallData {
t.Helper()
to, err := vaa.StringToAddress(strings.TrimPrefix(toStr, "0x"))
require.NoError(t, err)

data, err := hex.DecodeString(strings.TrimPrefix(dataStr, "0x"))
require.NoError(t, err)

return []*query.EthCallData{
{
To: to.Bytes(),
Data: data,
},
}
}
10 changes: 7 additions & 3 deletions node/cmd/ccq/permissions.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,9 +280,13 @@ func parseConfig(byteValue []byte, allowAnything bool) (PermissionsMap, error) {

if callKey == "" {
// Convert the contract address into a standard format like "000000000000000000000000b4fbf271143f4fbf7b91a5ded31805e42b2208d6".
contractAddress, err := vaa.StringToAddress(contractAddressStr)
if err != nil {
return nil, fmt.Errorf(`invalid contract address "%s" for user "%s"`, contractAddressStr, user.UserName)
contractAddress := contractAddressStr
if contractAddressStr != "*" {
contractAddr, err := vaa.StringToAddress(contractAddressStr)
if err != nil {
return nil, fmt.Errorf(`invalid contract address "%s" for user "%s"`, contractAddressStr, user.UserName)
}
contractAddress = contractAddr.String()
}

// The call should be the ABI four byte hex hash of the function signature. Parse it into a standard form of "06fdde03".
Expand Down
10 changes: 7 additions & 3 deletions node/cmd/ccq/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,13 @@ func validateCallData(logger *zap.Logger, permsForUser *permissionEntry, callTag
call := hex.EncodeToString(cd.Data[0:ETH_CALL_SIG_LENGTH])
callKey := fmt.Sprintf("%s:%d:%s:%s", callTag, chainId, contractAddress, call)
if _, exists := permsForUser.allowedCalls[callKey]; !exists {
logger.Debug("requested call not authorized", zap.String("userName", permsForUser.userName), zap.String("callKey", callKey))
invalidQueryRequestReceived.WithLabelValues("call_not_authorized").Inc()
return http.StatusBadRequest, fmt.Errorf(`call "%s" not authorized`, callKey)
// The call data doesn't exist including the contract address. See if it's covered by a wildcard.
wildCardCallKey := fmt.Sprintf("%s:%d:*:%s", callTag, chainId, call)
if _, exists := permsForUser.allowedCalls[wildCardCallKey]; !exists {
logger.Debug("requested call not authorized", zap.String("userName", permsForUser.userName), zap.String("callKey", callKey))
invalidQueryRequestReceived.WithLabelValues("call_not_authorized").Inc()
return http.StatusBadRequest, fmt.Errorf(`call "%s" not authorized`, callKey)
}
}
}

Expand Down
14 changes: 14 additions & 0 deletions node/pkg/watchers/evm/blocks_by_timestamp.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,17 @@ func (lhs Block) Cmp(rhs Block) int {

return 0
}

// GetRange returns the range covered by the cache for debugging purposes.
func (bts *BlocksByTimestamp) GetRange() (firstBlockNum, firstBlockTime, lastBlockNum, lastBlockTime uint64) {
bts.mutex.Lock()
defer bts.mutex.Unlock()

l := len(bts.cache)
if l <= 0 {
return 0, 0, 0, 0
}

l = l - 1
return bts.cache[0].BlockNum, bts.cache[0].Timestamp, bts.cache[l].BlockNum, bts.cache[l].Timestamp
}
24 changes: 23 additions & 1 deletion node/pkg/watchers/evm/ccq.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,9 +220,11 @@ func (w *Watcher) ccqHandleEthCallByTimestampQueryRequest(ctx context.Context, q
}

// Look the timestamp up in the cache. Note that the cache uses native EVM time, which is seconds, but CCQ uses microseconds, so we have to convert.
blockNum, nextBlockNum, found := w.ccqTimestampCache.LookUp(req.TargetTimestamp / 1000000)
timestampForCache := req.TargetTimestamp / 1000000
blockNum, nextBlockNum, found := w.ccqTimestampCache.LookUp(timestampForCache)
if !found {
status := query.QueryRetryNeeded
firstBlockNum, firstBlockTime, lastBlockNum, lastBlockTime := w.ccqTimestampCache.GetRange()
if nextBlockNum == 0 {
w.ccqLogger.Warn("block look up failed in eth_call_by_timestamp query request, timestamp beyond the end of the cache, will wait and retry",
zap.String("requestId", requestId),
Expand All @@ -231,6 +233,11 @@ func (w *Watcher) ccqHandleEthCallByTimestampQueryRequest(ctx context.Context, q
zap.String("nextBlock", nextBlock),
zap.Uint64("blockNum", blockNum),
zap.Uint64("nextBlockNum", nextBlockNum),
zap.Uint64("timestampForCache", timestampForCache),
zap.Uint64("firstBlockNum", firstBlockNum),
zap.Uint64("firstBlockTime", firstBlockTime),
zap.Uint64("lastBlockNum", lastBlockNum),
zap.Uint64("lastBlockTime", lastBlockTime),
)
} else if blockNum == 0 {
w.ccqLogger.Error("block look up failed in eth_call_by_timestamp query request, timestamp too old, failing request",
Expand All @@ -240,6 +247,11 @@ func (w *Watcher) ccqHandleEthCallByTimestampQueryRequest(ctx context.Context, q
zap.String("nextBlock", nextBlock),
zap.Uint64("blockNum", blockNum),
zap.Uint64("nextBlockNum", nextBlockNum),
zap.Uint64("timestampForCache", timestampForCache),
zap.Uint64("firstBlockNum", firstBlockNum),
zap.Uint64("firstBlockTime", firstBlockTime),
zap.Uint64("lastBlockNum", lastBlockNum),
zap.Uint64("lastBlockTime", lastBlockTime),
)
status = query.QueryFatalError
} else if w.ccqBackfillCache {
Expand All @@ -250,6 +262,11 @@ func (w *Watcher) ccqHandleEthCallByTimestampQueryRequest(ctx context.Context, q
zap.String("nextBlock", nextBlock),
zap.Uint64("blockNum", blockNum),
zap.Uint64("nextBlockNum", nextBlockNum),
zap.Uint64("timestampForCache", timestampForCache),
zap.Uint64("firstBlockNum", firstBlockNum),
zap.Uint64("firstBlockTime", firstBlockTime),
zap.Uint64("lastBlockNum", lastBlockNum),
zap.Uint64("lastBlockTime", lastBlockTime),
)
w.ccqRequestBackfill(req.TargetTimestamp / 1000000)
} else {
Expand All @@ -260,6 +277,11 @@ func (w *Watcher) ccqHandleEthCallByTimestampQueryRequest(ctx context.Context, q
zap.String("nextBlock", nextBlock),
zap.Uint64("blockNum", blockNum),
zap.Uint64("nextBlockNum", nextBlockNum),
zap.Uint64("timestampForCache", timestampForCache),
zap.Uint64("firstBlockNum", firstBlockNum),
zap.Uint64("firstBlockTime", firstBlockTime),
zap.Uint64("lastBlockNum", lastBlockNum),
zap.Uint64("lastBlockTime", lastBlockTime),
)
status = query.QueryFatalError
}
Expand Down

0 comments on commit c85d6c0

Please sign in to comment.