Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Node/CCQ: Allow address wildcard #4062

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/query_proxy.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,11 @@ The following are the Solana call types. Both require the `chain` parameter plus

The Solana account and and program address can be expressed as either a 32 byte hex string starting with "0x" or as a base 58 value.

#### Wild Card Contract Addresses

For the eth calls, the `contractAddress` field may be set to `"*"` which means the specified call type and call may be made to any
contract address on the specified chain.

#### Creating New API Keys

Each user must have an API key. These keys only have meaning to the proxy server. They are not passed to the guardians.
Expand Down
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
}
26 changes: 24 additions & 2 deletions 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,8 +262,13 @@ 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)
w.ccqRequestBackfill(timestampForCache)
} else {
w.ccqLogger.Error("block look up failed in eth_call_by_timestamp query request, timestamp is in a gap in the cache, failing request",
zap.String("requestId", requestId),
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
Loading