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/Solana: Add sol_pda query #3782

Merged
merged 11 commits into from
Mar 8, 2024
Merged
7 changes: 7 additions & 0 deletions node/cmd/ccq/devnet.permissions.json
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,13 @@
"chain": 1,
"account": "BVxyYhm498L79r4HMQ9sxZ5bi41DmJmeWZ7SCS7Cyvna"
}
},
{
"solPDA": {
"note:": "Core Bridge on Devnet",
"chain": 1,
"programAddress": "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o"
}
}
]
},
Expand Down
37 changes: 35 additions & 2 deletions node/cmd/ccq/parse_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ func TestParseConfigUnsupportedCallType(t *testing.T) {

_, err := parseConfig([]byte(str))
require.Error(t, err)
assert.Equal(t, `unsupported call type for user "Test User", must be "ethCall", "ethCallByTimestamp", "ethCallWithFinality" or "solAccount"`, err.Error())
assert.Equal(t, `unsupported call type for user "Test User", must be "ethCall", "ethCallByTimestamp", "ethCallWithFinality", "solAccount" or "solPDA"`, err.Error())
}

func TestParseConfigInvalidContractAddress(t *testing.T) {
Expand Down Expand Up @@ -295,7 +295,29 @@ func TestParseConfigSuccess(t *testing.T) {
"contractAddress": "B4FBF271143F4FBf7B91A5ded31805e42b2208d7",
"call": "0x06fdde03"
}
}
},
{
"ethCallWithFinality": {
"note:": "Decimals of WETH on Devnet",
"chain": 2,
"contractAddress": "0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E",
"call": "0x313ce567"
}
},
{
"solAccount": {
"note:": "Example NFT on Devnet",
"chain": 1,
"account": "BVxyYhm498L79r4HMQ9sxZ5bi41DmJmeWZ7SCS7Cyvna"
}
},
{
"solPDA": {
"note:": "Core Bridge on Devnet",
"chain": 1,
"programAddress": "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o"
}
}
]
}
]
Expand All @@ -308,9 +330,20 @@ func TestParseConfigSuccess(t *testing.T) {
perm, exists := perms["my_secret_key"]
require.True(t, exists)

assert.Equal(t, 5, len(perm.allowedCalls))

_, exists = perm.allowedCalls["ethCall:2:000000000000000000000000b4fbf271143f4fbf7b91a5ded31805e42b2208d6:06fdde03"]
assert.True(t, exists)

_, exists = perm.allowedCalls["ethCallByTimestamp:2:000000000000000000000000b4fbf271143f4fbf7b91a5ded31805e42b2208d7:06fdde03"]
assert.True(t, exists)

_, exists = perm.allowedCalls["ethCallWithFinality:2:000000000000000000000000ddb64fe46a91d46ee29420539fc25fd07c5fea3e:313ce567"]
assert.True(t, exists)

_, exists = perm.allowedCalls["solAccount:1:BVxyYhm498L79r4HMQ9sxZ5bi41DmJmeWZ7SCS7Cyvna"]
assert.True(t, exists)

_, exists = perm.allowedCalls["solPDA:1:Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o"]
assert.True(t, exists)
}
29 changes: 28 additions & 1 deletion node/cmd/ccq/permissions.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type (
EthCallByTimestamp *EthCallByTimestamp `json:"ethCallByTimestamp"`
EthCallWithFinality *EthCallWithFinality `json:"ethCallWithFinality"`
SolanaAccount *SolanaAccount `json:"solAccount"`
SolanaPda *SolanaPda `json:"solPDA"`
}

EthCall struct {
Expand All @@ -62,6 +63,12 @@ type (
Account string `json:"account"`
}

SolanaPda struct {
Chain int `json:"chain"`
ProgramAddress string `json:"programAddress"`
// As a future enhancement, we may want to specify the allowed seeds.
}

PermissionsMap map[string]*permissionEntry

permissionEntry struct {
Expand Down Expand Up @@ -234,8 +241,28 @@ func parseConfig(byteValue []byte) (PermissionsMap, error) {
}
}
callKey = fmt.Sprintf("solAccount:%d:%s", ac.SolanaAccount.Chain, account)
} else if ac.SolanaPda != nil {
// We assume the account is base58, but if it starts with "0x" it should be 32 bytes of hex.
pa := ac.SolanaPda.ProgramAddress
if strings.HasPrefix(pa, "0x") {
buf, err := hex.DecodeString(pa[2:])
if err != nil {
return nil, fmt.Errorf(`invalid solana program address hex string "%s" for user "%s": %w`, pa, user.UserName, err)
}
if len(buf) != query.SolanaPublicKeyLength {
return nil, fmt.Errorf(`invalid solana program address hex string "%s" for user "%s, must be %d bytes`, pa, user.UserName, query.SolanaPublicKeyLength)
}
pa = solana.PublicKey(buf).String()
} else {
// Make sure it is valid base58.
_, err := solana.PublicKeyFromBase58(pa)
if err != nil {
return nil, fmt.Errorf(`solana program address string "%s" for user "%s" is not valid base58: %w`, pa, user.UserName, err)
}
}
callKey = fmt.Sprintf("solPDA:%d:%s", ac.SolanaPda.Chain, pa)
} else {
return nil, fmt.Errorf(`unsupported call type for user "%s", must be "ethCall", "ethCallByTimestamp", "ethCallWithFinality" or "solAccount"`, user.UserName)
return nil, fmt.Errorf(`unsupported call type for user "%s", must be "ethCall", "ethCallByTimestamp", "ethCallWithFinality", "solAccount" or "solPDA"`, user.UserName)
}

if callKey == "" {
Expand Down
18 changes: 18 additions & 0 deletions node/cmd/ccq/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ func validateRequest(logger *zap.Logger, env common.Environment, perms *Permissi
status, err = validateCallData(logger, permsForUser, "ethCallWithFinality", pcq.ChainId, q.CallData)
case *query.SolanaAccountQueryRequest:
status, err = validateSolanaAccountQuery(logger, permsForUser, "solAccount", pcq.ChainId, q)
case *query.SolanaPdaQueryRequest:
status, err = validateSolanaPdaQuery(logger, permsForUser, "solPDA", pcq.ChainId, q)
default:
logger.Debug("unsupported query type", zap.String("userName", permsForUser.userName), zap.Any("type", pcq.Query))
invalidQueryRequestReceived.WithLabelValues("unsupported_query_type").Inc()
Expand Down Expand Up @@ -171,3 +173,19 @@ func validateSolanaAccountQuery(logger *zap.Logger, permsForUser *permissionEntr

return http.StatusOK, nil
}

// validateSolanaPdaQuery performs verification on a Solana sol_account query.
func validateSolanaPdaQuery(logger *zap.Logger, permsForUser *permissionEntry, callTag string, chainId vaa.ChainID, q *query.SolanaPdaQueryRequest) (int, error) {
for _, acct := range q.PDAs {
callKey := fmt.Sprintf("%s:%d:%s", callTag, chainId, solana.PublicKey(acct.ProgramAddress).String())
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.StatusForbidden, fmt.Errorf(`call "%s" not authorized`, callKey)
}

totalRequestedCallsByChain.WithLabelValues(chainId.String()).Inc()
}

return http.StatusOK, nil
}
63 changes: 46 additions & 17 deletions node/hack/query/send_req.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1"
"github.com/certusone/wormhole/node/pkg/query"
"github.com/ethereum/go-ethereum/accounts/abi"
ethCommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
ethCrypto "github.com/ethereum/go-ethereum/crypto"
pubsub "github.com/libp2p/go-libp2p-pubsub"
Expand Down Expand Up @@ -124,7 +125,7 @@ func main() {
//

{
logger.Info("Running Solana tests")
logger.Info("Running Solana account test")

// Start of query creation...
account1, err := solana.PublicKeyFromBase58("Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o")
Expand All @@ -142,11 +143,50 @@ func main() {
Accounts: [][query.SolanaPublicKeyLength]byte{account1, account2},
}

queryRequest := createSolanaQueryRequest(callRequest)
queryRequest := &query.QueryRequest{
Nonce: rand.Uint32(),
PerChainQueries: []*query.PerChainQueryRequest{
{
ChainId: 1,
Query: callRequest,
},
},
}
sendSolanaQueryAndGetRsp(queryRequest, sk, th_req, ctx, logger, sub)
}

{
logger.Info("Running Solana PDA test")

// Start of query creation...
callRequest := &query.SolanaPdaQueryRequest{
Commitment: "finalized",
DataSliceOffset: 0,
DataSliceLength: 100,
PDAs: []query.SolanaPDAEntry{
query.SolanaPDAEntry{
ProgramAddress: ethCommon.HexToHash("0x02c806312cbe5b79ef8aa6c17e3f423d8fdfe1d46909fb1f6cdf65ee8e2e6faa"), // Devnet core bridge
Seeds: [][]byte{
[]byte("GuardianSet"),
make([]byte, 4),
},
},
},
}

logger.Info("Solana tests complete!")
queryRequest := &query.QueryRequest{
Nonce: rand.Uint32(),
PerChainQueries: []*query.PerChainQueryRequest{
{
ChainId: 1,
Query: callRequest,
},
},
}
sendSolanaQueryAndGetRsp(queryRequest, sk, th_req, ctx, logger, sub)
}

logger.Info("Solana tests complete!")
// return

//
Expand Down Expand Up @@ -392,19 +432,6 @@ func sendQueryAndGetRsp(queryRequest *query.QueryRequest, sk *ecdsa.PrivateKey,
}
}

func createSolanaQueryRequest(callRequest *query.SolanaAccountQueryRequest) *query.QueryRequest {
queryRequest := &query.QueryRequest{
Nonce: rand.Uint32(),
PerChainQueries: []*query.PerChainQueryRequest{
{
ChainId: 1,
Query: callRequest,
},
},
}
return queryRequest
}

func sendSolanaQueryAndGetRsp(queryRequest *query.QueryRequest, sk *ecdsa.PrivateKey, th *pubsub.Topic, ctx context.Context, logger *zap.Logger, sub *pubsub.Subscription) {
queryRequestBytes, err := queryRequest.Marshal()
if err != nil {
Expand Down Expand Up @@ -482,7 +509,9 @@ func sendSolanaQueryAndGetRsp(queryRequest *query.QueryRequest, sk *ecdsa.Privat
for index := range response.PerChainResponses {
switch r := response.PerChainResponses[index].Response.(type) {
case *query.SolanaAccountQueryResponse:
logger.Info("solana query per chain response", zap.Int("index", index), zap.Any("pcr", r))
logger.Info("solana account query per chain response", zap.Int("index", index), zap.Any("pcr", r))
case *query.SolanaPdaQueryResponse:
logger.Info("solana pda query per chain response", zap.Int("index", index), zap.Any("pcr", r))
default:
panic(fmt.Sprintf("unsupported query type, should be solana, index: %d", index))
}
Expand Down
3 changes: 1 addition & 2 deletions node/pkg/query/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -433,11 +433,10 @@ func (pcq *perChainQuery) ccqForwardToWatcher(qLogger *zap.Logger, receiveTime t
case pcq.channel <- pcq.req:
qLogger.Debug("forwarded query request to watcher", zap.String("requestID", pcq.req.RequestID), zap.Stringer("chainID", pcq.req.Request.ChainId))
totalRequestsByChain.WithLabelValues(pcq.req.Request.ChainId.String()).Inc()
pcq.lastUpdateTime = receiveTime
default:
// By leaving lastUpdateTime unset, we will retry next interval.
qLogger.Warn("failed to send query request to watcher, will retry next interval", zap.String("requestID", pcq.req.RequestID), zap.Stringer("chain_id", pcq.req.Request.ChainId))
}
pcq.lastUpdateTime = receiveTime
}

// numPendingRequests returns the number of per chain queries in a request that are still awaiting responses. Zero means the request can now be published.
Expand Down
Loading
Loading