CCQ: eth_call_with_finality (#3460)

* CCQ: eth_call_with_finality

* Attempt to fix tilt error
This commit is contained in:
bruce-riley 2023-10-30 11:13:03 -05:00 committed by GitHub
parent 50f51b41f1
commit b708f5ac5b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1171 additions and 77 deletions

View File

@ -67,6 +67,38 @@
"contractAddress": "0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E",
"call": "0x18160ddd"
}
},
{
"ethCallWithFinality": {
"note:": "Name of WETH on Goerli",
"chain": 2,
"contractAddress": "B4FBF271143F4FBf7B91A5ded31805e42b2208d6",
"call": "0x06fdde03"
}
},
{
"ethCallWithFinality": {
"note:": "Total supply of WETH on Goerli",
"chain": 2,
"contractAddress": "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6",
"call": "0x18160ddd"
}
},
{
"ethCallWithFinality": {
"note:": "Name of WETH on Devnet",
"chain": 2,
"contractAddress": "0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E",
"call": "0x06fdde03"
}
},
{
"ethCallWithFinality": {
"note:": "Total supply of WETH on Devnet",
"chain": 2,
"contractAddress": "0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E",
"call": "0x18160ddd"
}
}
]
},

View File

@ -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" or "ethCallByTimestamp"`, err.Error())
assert.Equal(t, `unsupported call type for user "Test User", must be "ethCall", "ethCallByTimestamp" or "ethCallWithFinality"`, err.Error())
}
func TestParseConfigInvalidContractAddress(t *testing.T) {

View File

@ -65,8 +65,9 @@ type User struct {
}
type AllowedCall struct {
EthCall *EthCall `json:"ethCall"`
EthCallByTimestamp *EthCallByTimestamp `json:"ethCallByTimestamp"`
EthCall *EthCall `json:"ethCall"`
EthCallByTimestamp *EthCallByTimestamp `json:"ethCallByTimestamp"`
EthCallWithFinality *EthCallWithFinality `json:"ethCallWithFinality"`
}
type EthCall struct {
@ -81,6 +82,12 @@ type EthCallByTimestamp struct {
Call string `json:"call"`
}
type EthCallWithFinality struct {
Chain int `json:"chain"`
ContractAddress string `json:"contractAddress"`
Call string `json:"call"`
}
type Permissions map[string]*permissionEntry
type permissionEntry struct {
@ -152,8 +159,13 @@ func parseConfig(byteValue []byte) (Permissions, error) {
chain = ac.EthCallByTimestamp.Chain
contractAddressStr = ac.EthCallByTimestamp.ContractAddress
callStr = ac.EthCallByTimestamp.Call
} else if ac.EthCallWithFinality != nil {
callType = "ethCallWithFinality"
chain = ac.EthCallWithFinality.Chain
contractAddressStr = ac.EthCallWithFinality.ContractAddress
callStr = ac.EthCallWithFinality.Call
} else {
return nil, fmt.Errorf(`unsupported call type for user "%s", must be "ethCall" or "ethCallByTimestamp"`, user.UserName)
return nil, fmt.Errorf(`unsupported call type for user "%s", must be "ethCall", "ethCallByTimestamp" or "ethCallWithFinality"`, user.UserName)
}
// Convert the contract address into a standard format like "000000000000000000000000b4fbf271143f4fbf7b91a5ded31805e42b2208d6".
@ -260,19 +272,6 @@ func validateRequest(logger *zap.Logger, env common.Environment, perms Permissio
}
case *query.EthCallByTimestampQueryRequest:
for _, callData := range q.CallData {
if q.TargetTimestamp == 0 {
logger.Debug("eth call by timestamp must have a non-zero timestamp", zap.String("userName", permsForUser.userName))
return http.StatusBadRequest, fmt.Errorf("eth call by timestamp must have a non-zero timestamp")
}
// TODO: For now, the block hints are required!
if q.TargetBlockIdHint == "" {
logger.Debug("eth call by timestamp must have the target block hint", zap.String("userName", permsForUser.userName))
return http.StatusBadRequest, fmt.Errorf("eth call by timestamp must have the target block hint")
}
if q.FollowingBlockIdHint == "" {
logger.Debug("eth call by timestamp must have the following block hint", zap.String("userName", permsForUser.userName))
return http.StatusBadRequest, fmt.Errorf("eth call by timestamp must have the following block hint")
}
contractAddress, err := vaa.BytesToAddress(callData.To)
if err != nil {
logger.Debug("failed to parse contract address", zap.String("userName", permsForUser.userName), zap.String("contract", hex.EncodeToString(callData.To)), zap.Error(err))
@ -289,6 +288,24 @@ func validateRequest(logger *zap.Logger, env common.Environment, perms Permissio
return http.StatusBadRequest, fmt.Errorf(`call "%s" not authorized`, callKey)
}
}
case *query.EthCallWithFinalityQueryRequest:
for _, callData := range q.CallData {
contractAddress, err := vaa.BytesToAddress(callData.To)
if err != nil {
logger.Debug("failed to parse contract address", zap.String("userName", permsForUser.userName), zap.String("contract", hex.EncodeToString(callData.To)), zap.Error(err))
return http.StatusBadRequest, fmt.Errorf("failed to parse contract address: %w", err)
}
if len(callData.Data) < ETH_CALL_SIG_LENGTH {
logger.Debug("eth call data must be at least four bytes", zap.String("userName", permsForUser.userName), zap.String("data", hex.EncodeToString(callData.Data)))
return http.StatusBadRequest, fmt.Errorf("eth call data must be at least four bytes")
}
call := hex.EncodeToString(callData.Data[0:ETH_CALL_SIG_LENGTH])
callKey := fmt.Sprintf("ethCallWithFinality:%d:%s:%s", pcq.ChainId, contractAddress, call)
if _, exists := permsForUser.allowedCalls[callKey]; !exists {
logger.Debug("requested call not authorized", zap.String("userName", permsForUser.userName), zap.String("callKey", callKey))
return http.StatusBadRequest, fmt.Errorf(`call "%s" not authorized`, callKey)
}
}
default:
logger.Debug("unsupported query type", zap.String("userName", permsForUser.userName), zap.Any("type", pcq.Query))
return http.StatusBadRequest, fmt.Errorf("unsupported query type")

View File

@ -42,7 +42,7 @@ const (
var (
nonce = uint32(0)
watcherChainsForTest = []vaa.ChainID{vaa.ChainIDPolygon, vaa.ChainIDBSC}
watcherChainsForTest = []vaa.ChainID{vaa.ChainIDPolygon, vaa.ChainIDBSC, vaa.ChainIDArbitrum}
)
// createPerChainQueryForEthCall creates a per chain query for an eth_call for use in tests. The To and Data fields are meaningless gibberish, not ABI.
@ -102,6 +102,35 @@ func createPerChainQueryForEthCallByTimestamp(
}
}
// createPerChainQueryForEthCallWithFinality creates a per chain query for an eth_call_with_finality for use in tests. The To and Data fields are meaningless gibberish, not ABI.
func createPerChainQueryForEthCallWithFinality(
t *testing.T,
chainId vaa.ChainID,
blockId string,
finality string,
numCalls int,
) *PerChainQueryRequest {
t.Helper()
ethCallData := []*EthCallData{}
for count := 0; count < numCalls; count++ {
ethCallData = append(ethCallData, &EthCallData{
To: []byte(fmt.Sprintf("%-20s", fmt.Sprintf("To for %d:%d", chainId, count))),
Data: []byte(fmt.Sprintf("CallData for %d:%d", chainId, count)),
})
}
callRequest := &EthCallWithFinalityQueryRequest{
BlockId: blockId,
Finality: finality,
CallData: ethCallData,
}
return &PerChainQueryRequest{
ChainId: chainId,
Query: callRequest,
}
}
// createSignedQueryRequestForTesting creates a query request object and signs it using the specified key.
func createSignedQueryRequestForTesting(
t *testing.T,
@ -181,7 +210,25 @@ func createExpectedResultsForTest(t *testing.T, perChainQueries []*PerChainQuery
ChainId: pcq.ChainId,
Response: resp,
})
case *EthCallWithFinalityQueryRequest:
now := time.Now()
blockNum, err := strconv.ParseUint(strings.TrimPrefix(req.BlockId, "0x"), 16, 64)
if err != nil {
panic("invalid blockNum!")
}
resp := &EthCallQueryResponse{
BlockNumber: blockNum,
Hash: ethCommon.HexToHash("0x9999bac44d09a7f69ee7941819b0a19c59ccb1969640cc513be09ef95ed2d8e2"),
Time: timeForTest(t, now),
Results: [][]byte{},
}
for _, cd := range req.CallData {
resp.Results = append(resp.Results, []byte(hex.EncodeToString(cd.To)+":"+hex.EncodeToString(cd.Data)))
}
expectedResults = append(expectedResults, PerChainQueryResponse{
ChainId: pcq.ChainId,
Response: resp,
})
default:
panic("Invalid call data type!")
}
@ -531,7 +578,30 @@ func TestSingleEthCallByTimestampQueryShouldSucceed(t *testing.T) {
assert.True(t, validateResponseForTest(t, queryResponsePublication, signedQueryRequest, queryRequest, expectedResults))
}
func TestBatchOfTwoQueriesShouldSucceed(t *testing.T) {
func TestSingleEthCallWithFinalityQueryShouldSucceed(t *testing.T) {
ctx := context.Background()
logger := zap.NewNop()
md := createQueryHandlerForTest(t, ctx, logger, watcherChainsForTest)
// Create the request and the expected results. Give the expected results to the mock.
perChainQueries := []*PerChainQueryRequest{createPerChainQueryForEthCallWithFinality(t, vaa.ChainIDPolygon, "0x28d9630", "safe", 2)}
signedQueryRequest, queryRequest := createSignedQueryRequestForTesting(t, md.sk, perChainQueries)
expectedResults := createExpectedResultsForTest(t, queryRequest.PerChainQueries)
md.setExpectedResults(expectedResults)
// Submit the query request to the handler.
md.signedQueryReqWriteC <- signedQueryRequest
// Wait until we receive a response or timeout.
queryResponsePublication := md.waitForResponse()
require.NotNil(t, queryResponsePublication)
assert.Equal(t, 1, md.getRequestsPerChain(vaa.ChainIDPolygon))
assert.True(t, validateResponseForTest(t, queryResponsePublication, signedQueryRequest, queryRequest, expectedResults))
}
func TestBatchOfMultipleQueryTypesShouldSucceed(t *testing.T) {
ctx := context.Background()
logger := zap.NewNop()
@ -541,6 +611,7 @@ func TestBatchOfTwoQueriesShouldSucceed(t *testing.T) {
perChainQueries := []*PerChainQueryRequest{
createPerChainQueryForEthCall(t, vaa.ChainIDPolygon, "0x28d9630", 2),
createPerChainQueryForEthCallByTimestamp(t, vaa.ChainIDBSC, "0x28d9123", "0x28d9124", 3),
createPerChainQueryForEthCallWithFinality(t, vaa.ChainIDArbitrum, "0x28d9123", "finalized", 3),
}
signedQueryRequest, queryRequest := createSignedQueryRequestForTesting(t, md.sk, perChainQueries)
expectedResults := createExpectedResultsForTest(t, queryRequest.PerChainQueries)
@ -555,6 +626,7 @@ func TestBatchOfTwoQueriesShouldSucceed(t *testing.T) {
assert.Equal(t, 1, md.getRequestsPerChain(vaa.ChainIDPolygon))
assert.Equal(t, 1, md.getRequestsPerChain(vaa.ChainIDBSC))
assert.Equal(t, 1, md.getRequestsPerChain(vaa.ChainIDArbitrum))
assert.True(t, validateResponseForTest(t, queryResponsePublication, signedQueryRequest, queryRequest, expectedResults))
}

View File

@ -84,6 +84,25 @@ func (ecr *EthCallByTimestampQueryRequest) CallDataList() []*EthCallData {
return ecr.CallData
}
// EthCallWithFinalityQueryRequestType is the type of an EVM eth_call_with_finality query request.
const EthCallWithFinalityQueryRequestType ChainSpecificQueryType = 3
// EthCallWithFinalityQueryRequest implements ChainSpecificQuery for an EVM eth_call_with_finality query request.
type EthCallWithFinalityQueryRequest struct {
// BlockId identifies the block to be queried. It must be a hex string starting with 0x. It may be a block number or a block hash.
BlockId string
// Finality is required. It identifies the level of finality the block must reach before the query is performed. Valid values are "finalized" and "safe".
Finality string
// CallData is an array of specific queries to be performed on the specified block, in a single RPC call.
CallData []*EthCallData
}
func (ecr *EthCallWithFinalityQueryRequest) CallDataList() []*EthCallData {
return ecr.CallData
}
// EthCallData specifies the parameters to a single EVM eth_call request.
type EthCallData struct {
// To specifies the contract address to be queried.
@ -296,6 +315,12 @@ func (perChainQuery *PerChainQueryRequest) UnmarshalFromReader(reader *bytes.Rea
return fmt.Errorf("failed to unmarshal eth call by timestamp request: %w", err)
}
perChainQuery.Query = &q
case EthCallWithFinalityQueryRequestType:
q := EthCallWithFinalityQueryRequest{}
if err := q.UnmarshalFromReader(reader); err != nil {
return fmt.Errorf("failed to unmarshal eth call with finality request: %w", err)
}
perChainQuery.Query = &q
default:
return fmt.Errorf("unsupported query type: %d", queryType)
}
@ -358,6 +383,13 @@ func (left *PerChainQueryRequest) Equal(right *PerChainQueryRequest) bool {
default:
panic("unsupported query type on right, must be eth_call_by_timestamp")
}
case *EthCallWithFinalityQueryRequest:
switch rightEcd := right.Query.(type) {
case *EthCallWithFinalityQueryRequest:
return leftEcq.Equal(rightEcd)
default:
panic("unsupported query type on right, must be eth_call_with_finality")
}
default:
panic("unsupported query type on left")
}
@ -599,12 +631,18 @@ func (ecd *EthCallByTimestampQueryRequest) Validate() error {
if len(ecd.TargetBlockIdHint) > math.MaxUint32 {
return fmt.Errorf("target block id hint too long")
}
if ecd.TargetBlockIdHint == "" {
return fmt.Errorf("target block id is required")
}
if !strings.HasPrefix(ecd.TargetBlockIdHint, "0x") {
return fmt.Errorf("target block id must be a hex number or hash starting with 0x")
}
if len(ecd.FollowingBlockIdHint) > math.MaxUint32 {
return fmt.Errorf("following block id hint too long")
}
if ecd.FollowingBlockIdHint == "" {
return fmt.Errorf("following block id is required")
}
if !strings.HasPrefix(ecd.FollowingBlockIdHint, "0x") {
return fmt.Errorf("following block id must be a hex number or hash starting with 0x")
}
@ -658,8 +696,167 @@ func (left *EthCallByTimestampQueryRequest) Equal(right *EthCallByTimestampQuery
return true
}
//
// Implementation of EthCallWithFinalityQueryRequest, which implements the ChainSpecificQuery interface.
//
func (e *EthCallWithFinalityQueryRequest) Type() ChainSpecificQueryType {
return EthCallWithFinalityQueryRequestType
}
// Marshal serializes the binary representation of an EVM eth_call_with_finality request.
// This method calls Validate() and relies on it to range checks lengths, etc.
func (ecd *EthCallWithFinalityQueryRequest) Marshal() ([]byte, error) {
if err := ecd.Validate(); err != nil {
return nil, err
}
buf := new(bytes.Buffer)
vaa.MustWrite(buf, binary.BigEndian, uint32(len(ecd.BlockId)))
buf.Write([]byte(ecd.BlockId))
vaa.MustWrite(buf, binary.BigEndian, uint32(len(ecd.Finality)))
buf.Write([]byte(ecd.Finality))
vaa.MustWrite(buf, binary.BigEndian, uint8(len(ecd.CallData)))
for _, callData := range ecd.CallData {
buf.Write(callData.To)
vaa.MustWrite(buf, binary.BigEndian, uint32(len(callData.Data)))
buf.Write(callData.Data)
}
return buf.Bytes(), nil
}
// Unmarshal deserializes an EVM eth_call_with_finality query from a byte array
func (ecd *EthCallWithFinalityQueryRequest) Unmarshal(data []byte) error {
reader := bytes.NewReader(data[:])
return ecd.UnmarshalFromReader(reader)
}
// UnmarshalFromReader deserializes an EVM eth_call_with_finality query from a byte array
func (ecd *EthCallWithFinalityQueryRequest) UnmarshalFromReader(reader *bytes.Reader) error {
blockIdLen := uint32(0)
if err := binary.Read(reader, binary.BigEndian, &blockIdLen); err != nil {
return fmt.Errorf("failed to read target block id len: %w", err)
}
blockId := make([]byte, blockIdLen)
if n, err := reader.Read(blockId[:]); err != nil || n != int(blockIdLen) {
return fmt.Errorf("failed to read target block id [%d]: %w", n, err)
}
ecd.BlockId = string(blockId[:])
finalityLen := uint32(0)
if err := binary.Read(reader, binary.BigEndian, &finalityLen); err != nil {
return fmt.Errorf("failed to read finality len: %w", err)
}
finality := make([]byte, finalityLen)
if n, err := reader.Read(finality[:]); err != nil || n != int(finalityLen) {
return fmt.Errorf("failed to read finality [%d]: %w", n, err)
}
ecd.Finality = string(finality[:])
numCallData := uint8(0)
if err := binary.Read(reader, binary.BigEndian, &numCallData); err != nil {
return fmt.Errorf("failed to read number of call data entries: %w", err)
}
for count := 0; count < int(numCallData); count++ {
to := [EvmContractAddressLength]byte{}
if n, err := reader.Read(to[:]); err != nil || n != EvmContractAddressLength {
return fmt.Errorf("failed to read call To [%d]: %w", n, err)
}
dataLen := uint32(0)
if err := binary.Read(reader, binary.BigEndian, &dataLen); err != nil {
return fmt.Errorf("failed to read call Data len: %w", err)
}
data := make([]byte, dataLen)
if n, err := reader.Read(data[:]); err != nil || n != int(dataLen) {
return fmt.Errorf("failed to read call data [%d]: %w", n, err)
}
callData := &EthCallData{
To: to[:],
Data: data[:],
}
ecd.CallData = append(ecd.CallData, callData)
}
return nil
}
// Validate does basic validation on an EVM eth_call_with_finality query.
func (ecd *EthCallWithFinalityQueryRequest) Validate() error {
if len(ecd.BlockId) > math.MaxUint32 {
return fmt.Errorf("block id too long")
}
if ecd.BlockId == "" {
return fmt.Errorf("block id is required")
}
if !strings.HasPrefix(ecd.BlockId, "0x") {
return fmt.Errorf("block id must be a hex number or hash starting with 0x")
}
if len(ecd.Finality) > math.MaxUint32 {
return fmt.Errorf("finality too long")
}
if ecd.Finality == "" {
return fmt.Errorf("finality is required")
}
if ecd.Finality != "finalized" && ecd.Finality != "safe" {
return fmt.Errorf(`finality must be "finalized" or "safe", is "%s"`, ecd.Finality)
}
if len(ecd.CallData) <= 0 {
return fmt.Errorf("does not contain any call data")
}
if len(ecd.CallData) > math.MaxUint8 {
return fmt.Errorf("too many call data entries")
}
for _, callData := range ecd.CallData {
if callData.To == nil || len(callData.To) <= 0 {
return fmt.Errorf("no call data to")
}
if len(callData.To) != EvmContractAddressLength {
return fmt.Errorf("invalid length for To contract")
}
if callData.Data == nil || len(callData.Data) <= 0 {
return fmt.Errorf("no call data data")
}
if len(callData.Data) > math.MaxUint32 {
return fmt.Errorf("call data data too long")
}
}
return nil
}
// Equal verifies that two EVM eth_call_with_finality queries are equal.
func (left *EthCallWithFinalityQueryRequest) Equal(right *EthCallWithFinalityQueryRequest) bool {
if left.BlockId != right.BlockId {
return false
}
if left.Finality != right.Finality {
return false
}
if len(left.CallData) != len(right.CallData) {
return false
}
for idx := range left.CallData {
if !bytes.Equal(left.CallData[idx].To, right.CallData[idx].To) {
return false
}
if !bytes.Equal(left.CallData[idx].Data, right.CallData[idx].Data) {
return false
}
}
return true
}
func ValidatePerChainQueryRequestType(qt ChainSpecificQueryType) error {
if qt != EthCallQueryRequestType && qt != EthCallByTimestampQueryRequestType {
if qt != EthCallQueryRequestType && qt != EthCallByTimestampQueryRequestType && qt != EthCallWithFinalityQueryRequestType {
return fmt.Errorf("invalid query request type: %d", qt)
}
return nil

View File

@ -67,9 +67,20 @@ func createQueryRequestForTesting(t *testing.T, chainId vaa.ChainID) *QueryReque
Query: callRequest2,
}
callRequest3 := &EthCallWithFinalityQueryRequest{
BlockId: "0x28d9630",
Finality: "finalized",
CallData: callData,
}
perChainQuery3 := &PerChainQueryRequest{
ChainId: chainId,
Query: callRequest3,
}
queryRequest := &QueryRequest{
Nonce: 1,
PerChainQueries: []*PerChainQueryRequest{perChainQuery1, perChainQuery2},
PerChainQueries: []*PerChainQueryRequest{perChainQuery1, perChainQuery2, perChainQuery3},
}
return queryRequest
@ -488,7 +499,222 @@ func TestMarshalOfEthCallByTimestampQueryWithWrongToLengthShouldFail(t *testing.
require.Error(t, err)
}
///////////// End of EthCallByTimestamp tests /////////////////////////////////
///////////// EthCallWithFinality tests ////////////////////////////////////////
func TestMarshalOfEthCallWithFinalityQueryWithNilToShouldFail(t *testing.T) {
perChainQuery := &PerChainQueryRequest{
ChainId: vaa.ChainIDPolygon,
Query: &EthCallWithFinalityQueryRequest{
BlockId: "0x28d9630",
Finality: "finalized",
CallData: []*EthCallData{
{
To: nil,
Data: []byte("This can't be zero length"),
},
},
},
}
queryRequest := &QueryRequest{
Nonce: 1,
PerChainQueries: []*PerChainQueryRequest{perChainQuery},
}
_, err := queryRequest.Marshal()
require.Error(t, err)
}
func TestMarshalOfEthCallWithFinalityQueryWithEmptyToShouldFail(t *testing.T) {
perChainQuery := &PerChainQueryRequest{
ChainId: vaa.ChainIDPolygon,
Query: &EthCallWithFinalityQueryRequest{
BlockId: "0x28d9630",
Finality: "safe",
CallData: []*EthCallData{
{
To: []byte{},
Data: []byte("This can't be zero length"),
},
},
},
}
queryRequest := &QueryRequest{
Nonce: 1,
PerChainQueries: []*PerChainQueryRequest{perChainQuery},
}
_, err := queryRequest.Marshal()
require.Error(t, err)
}
func TestMarshalOfEthCallWithFinalityQueryWithWrongLengthToShouldFail(t *testing.T) {
perChainQuery := &PerChainQueryRequest{
ChainId: vaa.ChainIDPolygon,
Query: &EthCallWithFinalityQueryRequest{
BlockId: "0x28d9630",
Finality: "finalized",
CallData: []*EthCallData{
{
To: []byte("TooShort"),
Data: []byte("This can't be zero length"),
},
},
},
}
queryRequest := &QueryRequest{
Nonce: 1,
PerChainQueries: []*PerChainQueryRequest{perChainQuery},
}
_, err := queryRequest.Marshal()
require.Error(t, err)
}
func TestMarshalOfEthCallWithFinalityQueryWithNilDataShouldFail(t *testing.T) {
perChainQuery := &PerChainQueryRequest{
ChainId: vaa.ChainIDPolygon,
Query: &EthCallWithFinalityQueryRequest{
BlockId: "0x28d9630",
Finality: "safe",
CallData: []*EthCallData{
{
To: []byte(fmt.Sprintf("%-20s", fmt.Sprintf("To for %d", 0))),
Data: nil,
},
},
},
}
queryRequest := &QueryRequest{
Nonce: 1,
PerChainQueries: []*PerChainQueryRequest{perChainQuery},
}
_, err := queryRequest.Marshal()
require.Error(t, err)
}
func TestMarshalOfEthCallWithFinalityQueryWithEmptyDataShouldFail(t *testing.T) {
perChainQuery := &PerChainQueryRequest{
ChainId: vaa.ChainIDPolygon,
Query: &EthCallWithFinalityQueryRequest{
BlockId: "0x28d9630",
Finality: "finalized",
CallData: []*EthCallData{
{
To: []byte(fmt.Sprintf("%-20s", fmt.Sprintf("To for %d", 0))),
Data: []byte{},
},
},
},
}
queryRequest := &QueryRequest{
Nonce: 1,
PerChainQueries: []*PerChainQueryRequest{perChainQuery},
}
_, err := queryRequest.Marshal()
require.Error(t, err)
}
func TestMarshalOfEthCallWithFinalityQueryWithWrongToLengthShouldFail(t *testing.T) {
perChainQuery := &PerChainQueryRequest{
ChainId: vaa.ChainIDPolygon,
Query: &EthCallWithFinalityQueryRequest{
BlockId: "0x28d9630",
Finality: "safe",
CallData: []*EthCallData{
{
To: []byte("This is too short!"),
Data: []byte("This can't be zero length"),
},
},
},
}
queryRequest := &QueryRequest{
Nonce: 1,
PerChainQueries: []*PerChainQueryRequest{perChainQuery},
}
_, err := queryRequest.Marshal()
require.Error(t, err)
}
func TestMarshalOfEthCallWithFinalityQueryWithBadFinality(t *testing.T) {
to, err := hex.DecodeString("0d500b1d8e8ef31e21c99d1db9a6444d3adf1270")
require.NoError(t, err)
perChainQuery := &PerChainQueryRequest{
ChainId: vaa.ChainIDPolygon,
Query: &EthCallWithFinalityQueryRequest{
BlockId: "0x28d9630",
Finality: "HelloWorld",
CallData: []*EthCallData{
{
To: to,
Data: []byte("This can't be zero length"),
},
},
},
}
queryRequest := &QueryRequest{
Nonce: 1,
PerChainQueries: []*PerChainQueryRequest{perChainQuery},
}
_, err = queryRequest.Marshal()
require.Error(t, err)
}
func TestMarshalOfEthCallWithFinalityQueryWithFinalizedShouldSucceed(t *testing.T) {
to, err := hex.DecodeString("0d500b1d8e8ef31e21c99d1db9a6444d3adf1270")
require.NoError(t, err)
perChainQuery := &PerChainQueryRequest{
ChainId: vaa.ChainIDPolygon,
Query: &EthCallWithFinalityQueryRequest{
BlockId: "0x28d9630",
Finality: "finalized",
CallData: []*EthCallData{
{
To: to,
Data: []byte("This can't be zero length"),
},
},
},
}
queryRequest := &QueryRequest{
Nonce: 1,
PerChainQueries: []*PerChainQueryRequest{perChainQuery},
}
_, err = queryRequest.Marshal()
require.NoError(t, err)
}
func TestMarshalOfEthCallWithFinalityQueryWithSafeShouldSucceed(t *testing.T) {
to, err := hex.DecodeString("0d500b1d8e8ef31e21c99d1db9a6444d3adf1270")
require.NoError(t, err)
perChainQuery := &PerChainQueryRequest{
ChainId: vaa.ChainIDPolygon,
Query: &EthCallWithFinalityQueryRequest{
BlockId: "0x28d9630",
Finality: "safe",
CallData: []*EthCallData{
{
To: to,
Data: []byte("This can't be zero length"),
},
},
},
}
queryRequest := &QueryRequest{
Nonce: 1,
PerChainQueries: []*PerChainQueryRequest{perChainQuery},
}
_, err = queryRequest.Marshal()
require.NoError(t, err)
}
///////////// End of EthCallWithFinality tests /////////////////////////////////
func TestPostSignedQueryRequestShouldFailIfNoOneIsListening(t *testing.T) {
queryRequest := createQueryRequestForTesting(t, vaa.ChainIDPolygon)

View File

@ -97,6 +97,16 @@ type EthCallByTimestampQueryResponse struct {
Results [][]byte
}
// EthCallWithFinalityQueryResponse implements ChainSpecificResponse for an EVM eth_call_with_finality query response.
type EthCallWithFinalityQueryResponse struct {
BlockNumber uint64
Hash common.Hash
Time time.Time
// Results is the array of responses matching CallData in EthCallQueryRequest
Results [][]byte
}
//
// Implementation of QueryResponsePublication.
//
@ -347,6 +357,12 @@ func (perChainResponse *PerChainQueryResponse) UnmarshalFromReader(reader *bytes
return fmt.Errorf("failed to unmarshal eth call by timestamp response: %w", err)
}
perChainResponse.Response = &r
case EthCallWithFinalityQueryRequestType:
r := EthCallWithFinalityQueryResponse{}
if err := r.UnmarshalFromReader(reader); err != nil {
return fmt.Errorf("failed to unmarshal eth call with finality response: %w", err)
}
perChainResponse.Response = &r
default:
return fmt.Errorf("unsupported query type: %d", queryType)
}
@ -409,6 +425,13 @@ func (left *PerChainQueryResponse) Equal(right *PerChainQueryResponse) bool {
default:
panic("unsupported query type on right") // We checked this above!
}
case *EthCallWithFinalityQueryResponse:
switch rightEcd := right.Response.(type) {
case *EthCallWithFinalityQueryResponse:
return leftEcq.Equal(rightEcd)
default:
panic("unsupported query type on right") // We checked this above!
}
default:
panic("unsupported query type on left") // We checked this above!
}
@ -693,3 +716,125 @@ func (left *EthCallByTimestampQueryResponse) Equal(right *EthCallByTimestampQuer
return true
}
//
// Implementation of EthCallWithFinalityQueryResponse, which implements the ChainSpecificResponse for an EVM eth_call_with_finality query response.
//
func (e *EthCallWithFinalityQueryResponse) Type() ChainSpecificQueryType {
return EthCallWithFinalityQueryRequestType
}
// Marshal serializes the binary representation of an EVM eth_call response.
// This method calls Validate() and relies on it to range checks lengths, etc.
func (ecr *EthCallWithFinalityQueryResponse) Marshal() ([]byte, error) {
if err := ecr.Validate(); err != nil {
return nil, err
}
buf := new(bytes.Buffer)
vaa.MustWrite(buf, binary.BigEndian, ecr.BlockNumber)
buf.Write(ecr.Hash[:])
vaa.MustWrite(buf, binary.BigEndian, ecr.Time.UnixMicro())
vaa.MustWrite(buf, binary.BigEndian, uint8(len(ecr.Results)))
for idx := range ecr.Results {
vaa.MustWrite(buf, binary.BigEndian, uint32(len(ecr.Results[idx])))
buf.Write(ecr.Results[idx])
}
return buf.Bytes(), nil
}
// Unmarshal deserializes an EVM eth_call response from a byte array
func (ecr *EthCallWithFinalityQueryResponse) Unmarshal(data []byte) error {
reader := bytes.NewReader(data[:])
return ecr.UnmarshalFromReader(reader)
}
// UnmarshalFromReader deserializes an EVM eth_call response from a byte array
func (ecr *EthCallWithFinalityQueryResponse) UnmarshalFromReader(reader *bytes.Reader) error {
if err := binary.Read(reader, binary.BigEndian, &ecr.BlockNumber); err != nil {
return fmt.Errorf("failed to read response number: %w", err)
}
responseHash := common.Hash{}
if n, err := reader.Read(responseHash[:]); err != nil || n != 32 {
return fmt.Errorf("failed to read response hash [%d]: %w", n, err)
}
ecr.Hash = responseHash
unixMicros := int64(0)
if err := binary.Read(reader, binary.BigEndian, &unixMicros); err != nil {
return fmt.Errorf("failed to read response timestamp: %w", err)
}
ecr.Time = time.UnixMicro(unixMicros)
numResults := uint8(0)
if err := binary.Read(reader, binary.BigEndian, &numResults); err != nil {
return fmt.Errorf("failed to read number of results: %w", err)
}
for count := 0; count < int(numResults); count++ {
resultLen := uint32(0)
if err := binary.Read(reader, binary.BigEndian, &resultLen); err != nil {
return fmt.Errorf("failed to read result len: %w", err)
}
result := make([]byte, resultLen)
if n, err := reader.Read(result[:]); err != nil || n != int(resultLen) {
return fmt.Errorf("failed to read result [%d]: %w", n, err)
}
ecr.Results = append(ecr.Results, result)
}
return nil
}
// Validate does basic validation on an EVM eth_call response.
func (ecr *EthCallWithFinalityQueryResponse) Validate() error {
// Not checking for BlockNumber == 0, because maybe that could happen??
if len(ecr.Hash) != 32 {
return fmt.Errorf("invalid length for block hash")
}
if len(ecr.Results) <= 0 {
return fmt.Errorf("does not contain any results")
}
if len(ecr.Results) > math.MaxUint8 {
return fmt.Errorf("too many results")
}
for _, result := range ecr.Results {
if len(result) > math.MaxUint32 {
return fmt.Errorf("result too long")
}
}
return nil
}
// Equal verifies that two EVM eth_call responses are equal.
func (left *EthCallWithFinalityQueryResponse) Equal(right *EthCallWithFinalityQueryResponse) bool {
if left.BlockNumber != right.BlockNumber {
return false
}
if !bytes.Equal(left.Hash.Bytes(), right.Hash.Bytes()) {
return false
}
if left.Time != right.Time {
return false
}
if len(left.Results) != len(right.Results) {
return false
}
for idx := range left.Results {
if !bytes.Equal(left.Results[idx], right.Results[idx]) {
return false
}
}
return true
}

View File

@ -60,6 +60,21 @@ func createQueryResponseFromRequest(t *testing.T, queryRequest *QueryRequest) *Q
Results: results,
},
})
case *EthCallWithFinalityQueryRequest:
results := [][]byte{}
for idx := range req.CallData {
result := []byte([]byte(fmt.Sprintf("Result %d", idx)))
results = append(results, result[:])
}
perChainResponses = append(perChainResponses, &PerChainQueryResponse{
ChainId: pcr.ChainId,
Response: &EthCallWithFinalityQueryResponse{
BlockNumber: uint64(1000 + idx),
Hash: ethCommon.HexToHash("0x9999bac44d09a7f69ee7941819b0a19c59ccb1969640cc513be09ef95ed2d8e2"),
Time: timeForTest(t, time.Now()),
Results: results,
},
})
default:
panic("invalid query type!")
}

View File

@ -18,28 +18,6 @@ import (
"github.com/certusone/wormhole/node/pkg/query"
)
// ccqSendQueryResponseForEthCall sends an error response back to the query handler.
func (w *Watcher) ccqSendQueryResponseForEthCall(logger *zap.Logger, req *query.PerChainQueryInternal, status query.QueryStatus, resp *query.EthCallQueryResponse) {
queryResponse := query.CreatePerChainQueryResponseInternal(req.RequestID, req.RequestIdx, req.Request.ChainId, status, resp)
select {
case w.queryResponseC <- queryResponse:
logger.Debug("published query response error to handler", zap.String("component", "ccqevm"))
default:
logger.Error("failed to published query response error to handler", zap.String("component", "ccqevm"))
}
}
// ccqSendQueryResponseForEthCallByTimestamp sends an error response back to the query handler.
func (w *Watcher) ccqSendQueryResponseForEthCallByTimestamp(logger *zap.Logger, req *query.PerChainQueryInternal, status query.QueryStatus, resp *query.EthCallByTimestampQueryResponse) {
queryResponse := query.CreatePerChainQueryResponseInternal(req.RequestID, req.RequestIdx, req.Request.ChainId, status, resp)
select {
case w.queryResponseC <- queryResponse:
logger.Debug("published query response error to handler", zap.String("component", "ccqevm"))
default:
logger.Error("failed to published query response error to handler", zap.String("component", "ccqevm"))
}
}
// ccqSendQueryResponseForError sends an error response back to the query handler.
func (w *Watcher) ccqSendQueryResponseForError(logger *zap.Logger, req *query.PerChainQueryInternal, status query.QueryStatus) {
queryResponse := query.CreatePerChainQueryResponseInternal(req.RequestID, req.RequestIdx, req.Request.ChainId, status, nil)
@ -64,6 +42,8 @@ func (w *Watcher) ccqHandleQuery(logger *zap.Logger, ctx context.Context, queryR
w.ccqHandleEthCallQueryRequest(logger, ctx, queryRequest, req)
case *query.EthCallByTimestampQueryRequest:
w.ccqHandleEthCallByTimestampQueryRequest(logger, ctx, queryRequest, req)
case *query.EthCallWithFinalityQueryRequest:
w.ccqHandleEthCallWithFinalityQueryRequest(logger, ctx, queryRequest, req)
default:
logger.Warn("received unsupported request type",
zap.Uint8("payload", uint8(queryRequest.Request.Query.Type())),
@ -211,7 +191,13 @@ func (w *Watcher) ccqHandleEthCallQueryRequest(logger *zap.Logger, ctx context.C
}
if !errFound {
w.ccqSendQueryResponseForEthCall(logger, queryRequest, query.QuerySuccess, &resp)
queryResponse := query.CreatePerChainQueryResponseInternal(queryRequest.RequestID, queryRequest.RequestIdx, queryRequest.Request.ChainId, query.QuerySuccess, &resp)
select {
case w.queryResponseC <- queryResponse:
logger.Debug("published query response error to handler", zap.String("component", "ccqevm"))
default:
logger.Error("failed to published query response error to handler", zap.String("component", "ccqevm"))
}
}
}
@ -469,7 +455,190 @@ func (w *Watcher) ccqHandleEthCallByTimestampQueryRequest(logger *zap.Logger, ct
}
if !errFound {
w.ccqSendQueryResponseForEthCallByTimestamp(logger, queryRequest, query.QuerySuccess, &resp)
queryResponse := query.CreatePerChainQueryResponseInternal(queryRequest.RequestID, queryRequest.RequestIdx, queryRequest.Request.ChainId, query.QuerySuccess, &resp)
select {
case w.queryResponseC <- queryResponse:
logger.Debug("published query response error to handler", zap.String("component", "ccqevm"))
default:
logger.Error("failed to published query response error to handler", zap.String("component", "ccqevm"))
}
}
}
func (w *Watcher) ccqHandleEthCallWithFinalityQueryRequest(logger *zap.Logger, ctx context.Context, queryRequest *query.PerChainQueryInternal, req *query.EthCallWithFinalityQueryRequest) {
block := req.BlockId
logger.Info("received eth_call_with_finality query request",
zap.String("block", block),
zap.String("finality", req.Finality),
zap.Int("numRequests", len(req.CallData)),
)
safeMode := req.Finality == "safe"
if safeMode {
// If a chain does not support safe mode, treat this as finalized.
if !w.safeBlocksSupported {
logger.Debug(`eth_call_with_finality query request for safe mode, but the chain does not support it, treating as "finalized"`, zap.String("block", block))
safeMode = false
}
} else if req.Finality != "finalized" {
logger.Error("invalid finality in eth_call_with_finality query request", zap.String("block", block), zap.String("finality", req.Finality), zap.String("block", block))
w.ccqSendQueryResponseForError(logger, queryRequest, query.QueryFatalError)
return
}
blockMethod, callBlockArg, err := ccqCreateBlockRequest(block)
if err != nil {
logger.Error("invalid block id in eth_call_with_finality query request",
zap.Error(err),
zap.String("block", block),
)
w.ccqSendQueryResponseForError(logger, queryRequest, query.QueryFatalError)
return
}
// We build two slices. The first is the batch submitted to the RPC call. It contains one entry for each query plus one to query the block.
// The second is the data associated with each request (but not the block request). The index into both is the index into the request call data.
batch, evmCallData := ccqBuildBatchFromCallData(req, callBlockArg)
// Add the block query to the batch.
var blockResult connectors.BlockMarshaller
var blockError error
batch = append(batch, rpc.BatchElem{
Method: blockMethod,
Args: []interface{}{
block,
false, // no full transaction details
},
Result: &blockResult,
Error: blockError,
})
// Query the RPC.
timeout, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
err = w.ethConn.RawBatchCallContext(timeout, batch)
if err != nil {
logger.Error("failed to process eth_call_with_finality query request",
zap.Error(err),
zap.String("block", block),
zap.Any("batch", batch),
)
w.ccqSendQueryResponseForError(logger, queryRequest, query.QueryRetryNeeded)
return
}
if blockError != nil {
logger.Error("failed to process eth_call_with_finality query block request",
zap.Error(blockError),
zap.String("block", block),
zap.Any("batch", batch),
)
w.ccqSendQueryResponseForError(logger, queryRequest, query.QueryRetryNeeded)
return
}
if blockResult.Number == nil {
logger.Error("invalid eth_call_with_finality query block result",
zap.String("eth_network", w.networkName),
zap.String("block", block),
zap.Any("batch", batch),
)
w.ccqSendQueryResponseForError(logger, queryRequest, query.QueryRetryNeeded)
return
}
if blockResult.Number.ToInt().Cmp(w.ccqMaxBlockNumber) > 0 {
logger.Error("block number too large for eth_call_with_finality",
zap.String("eth_network", w.networkName),
zap.String("block", block),
zap.Any("batch", batch),
)
w.ccqSendQueryResponseForError(logger, queryRequest, query.QueryRetryNeeded)
return
}
blockNumber := blockResult.Number.ToInt().Uint64()
var latestBlockNum uint64
if safeMode {
latestBlockNum = w.getLatestSafeBlockNumber()
} else {
latestBlockNum = w.GetLatestFinalizedBlockNumber()
}
if blockNumber > latestBlockNum {
logger.Info("requested block for eth_call_with_finality has not yet reached the requested finality",
zap.String("finality", req.Finality),
zap.Uint64("requestedBlockNumber", blockNumber),
zap.Uint64("latestBlockNumber", latestBlockNum),
zap.String("eth_network", w.networkName),
zap.String("block", block),
zap.Any("batch", batch),
)
w.ccqSendQueryResponseForError(logger, queryRequest, query.QueryRetryNeeded)
return
}
resp := query.EthCallWithFinalityQueryResponse{
BlockNumber: blockNumber,
Hash: blockResult.Hash,
Time: time.Unix(int64(blockResult.Time), 0),
Results: [][]byte{},
}
errFound := false
for idx := range req.CallData {
if evmCallData[idx].callErr != nil {
logger.Error("failed to process eth_call_with_finality query call request",
zap.Error(evmCallData[idx].callErr),
zap.String("block", block),
zap.Int("errorIdx", idx),
zap.Any("batch", batch),
)
w.ccqSendQueryResponseForError(logger, queryRequest, query.QueryRetryNeeded)
errFound = true
break
}
// Nil or Empty results are not valid
// eth_call_with_finality will return empty when the state doesn't exist for a block
if len(*evmCallData[idx].callResult) == 0 {
logger.Error("invalid call result for eth_call_with_finality",
zap.String("eth_network", w.networkName),
zap.String("block", block),
zap.Int("errorIdx", idx),
zap.Any("batch", batch),
)
w.ccqSendQueryResponseForError(logger, queryRequest, query.QueryRetryNeeded)
errFound = true
break
}
logger.Info("query result for eth_call_with_finality",
zap.String("eth_network", w.networkName),
zap.String("block", block),
zap.String("finality", req.Finality),
zap.Uint64("requestedBlockNumber", blockNumber),
zap.Uint64("latestBlockNumber", latestBlockNum),
zap.String("blockHash", blockResult.Hash.Hex()),
zap.String("blockTime", blockResult.Time.String()),
zap.Int("idx", idx),
zap.String("to", evmCallData[idx].to.Hex()),
zap.Any("data", evmCallData[idx].data),
zap.String("result", evmCallData[idx].callResult.String()),
)
resp.Results = append(resp.Results, *evmCallData[idx].callResult)
}
if !errFound {
queryResponse := query.CreatePerChainQueryResponseInternal(queryRequest.RequestID, queryRequest.RequestIdx, queryRequest.Request.ChainId, query.QuerySuccess, &resp)
select {
case w.queryResponseC <- queryResponse:
logger.Debug("published query response error to handler", zap.String("component", "ccqevm"))
default:
logger.Error("failed to published query response error to handler", zap.String("component", "ccqevm"))
}
}
}

View File

@ -125,8 +125,11 @@ type (
unsafeDevMode bool
latestFinalizedBlockNumber uint64
latestSafeBlockNumber uint64
l1Finalizer interfaces.L1Finalizer
safeBlocksSupported bool
// These parameters are currently only used for Polygon and should be set via SetRootChainParams()
rootChainRpc string
rootChainContract string
@ -198,8 +201,8 @@ func (w *Watcher) Run(parentCtx context.Context) error {
ctx, watcherContextCancelFunc := context.WithCancel(parentCtx)
defer watcherContextCancelFunc()
var useFinalizedBlocks, safeBlocksSupported bool
useFinalizedBlocks, safeBlocksSupported, err = w.getFinality(ctx)
var useFinalizedBlocks bool
useFinalizedBlocks, w.safeBlocksSupported, err = w.getFinality(ctx)
if err != nil {
return fmt.Errorf("failed to determine finality: %w", err)
}
@ -213,7 +216,7 @@ func (w *Watcher) Run(parentCtx context.Context) error {
defer cancel()
if useFinalizedBlocks {
if safeBlocksSupported {
if w.safeBlocksSupported {
logger.Info("using finalized blocks, will publish safe blocks")
} else {
logger.Info("using finalized blocks")
@ -225,7 +228,7 @@ func (w *Watcher) Run(parentCtx context.Context) error {
p2p.DefaultRegistry.AddErrorCount(w.chainID, 1)
return fmt.Errorf("dialing eth client failed: %w", err)
}
w.ethConn, err = connectors.NewBlockPollConnector(ctx, baseConnector, finalizers.NewDefaultFinalizer(), 250*time.Millisecond, true, safeBlocksSupported)
w.ethConn, err = connectors.NewBlockPollConnector(ctx, baseConnector, finalizers.NewDefaultFinalizer(), 250*time.Millisecond, true, w.safeBlocksSupported)
if err != nil {
ethConnectionErrors.WithLabelValues(w.networkName, "dial_error").Inc()
p2p.DefaultRegistry.AddErrorCount(w.chainID, 1)
@ -327,11 +330,6 @@ func (w *Watcher) Run(parentCtx context.Context) error {
}
})
// Track the current block numbers so we can compare it to the block number of
// the message publication for observation requests.
var currentBlockNumber uint64
var currentSafeBlockNumber uint64
common.RunWithScissors(ctx, errC, "evm_fetch_objs_req", func(ctx context.Context) error {
for {
select {
@ -356,8 +354,8 @@ func (w *Watcher) Run(parentCtx context.Context) error {
// In the primary watcher flow, this is of no concern since we assume the node
// always sends the head before it sends the logs (implicit synchronization
// by relying on the same websocket connection).
blockNumberU := atomic.LoadUint64(&currentBlockNumber)
safeBlockNumberU := atomic.LoadUint64(&currentSafeBlockNumber)
blockNumberU := atomic.LoadUint64(&w.latestFinalizedBlockNumber)
safeBlockNumberU := atomic.LoadUint64(&w.latestSafeBlockNumber)
timeout, cancel := context.WithTimeout(ctx, 5*time.Second)
blockNumber, msgs, err := MessageEventsForTransaction(timeout, w.ethConn, w.contract, w.chainID, tx)
@ -385,7 +383,7 @@ func (w *Watcher) Run(parentCtx context.Context) error {
continue
}
if msg.ConsistencyLevel == vaa.ConsistencyLevelSafe && safeBlocksSupported {
if msg.ConsistencyLevel == vaa.ConsistencyLevelSafe && w.safeBlocksSupported {
if safeBlockNumberU == 0 {
logger.Error("no safe block number available, ignoring observation request",
zap.String("eth_network", w.networkName))
@ -603,13 +601,12 @@ func (w *Watcher) Run(parentCtx context.Context) error {
blockNumberU := ev.Number.Uint64()
if ev.Safe {
atomic.StoreUint64(&currentSafeBlockNumber, blockNumberU)
atomic.StoreUint64(&w.latestSafeBlockNumber, blockNumberU)
} else {
p2p.DefaultRegistry.SetNetworkStats(w.chainID, &gossipv1.Heartbeat_Network{
Height: ev.Number.Int64(),
ContractAddress: w.contract.Hex(),
})
atomic.StoreUint64(&currentBlockNumber, blockNumberU)
atomic.StoreUint64(&w.latestFinalizedBlockNumber, blockNumberU)
currentEthHeight.WithLabelValues(w.networkName).Set(float64(ev.Number.Int64()))
}
@ -617,7 +614,7 @@ func (w *Watcher) Run(parentCtx context.Context) error {
for key, pLock := range w.pending {
// If this block is safe, only process messages wanting safe.
// If it's not safe, only process messages wanting finalized.
if safeBlocksSupported {
if w.safeBlocksSupported {
if ev.Safe != (pLock.message.ConsistencyLevel == vaa.ConsistencyLevelSafe) {
continue
}
@ -883,6 +880,11 @@ func (w *Watcher) GetLatestFinalizedBlockNumber() uint64 {
return atomic.LoadUint64(&w.latestFinalizedBlockNumber)
}
// getLatestSafeBlockNumber() returns the latest safe block seen by this watcher..
func (w *Watcher) getLatestSafeBlockNumber() uint64 {
return atomic.LoadUint64(&w.latestSafeBlockNumber)
}
// SetRootChainParams is used to enabled checkpointing (currently only for Polygon). It handles
// if the feature is either enabled or disabled, but ensures the configuration is valid.
func (w *Watcher) SetRootChainParams(rootChainRpc string, rootChainContract string) error {

View File

@ -12,6 +12,7 @@ import {
EthCallData,
EthCallQueryRequest,
EthCallByTimestampQueryRequest,
EthCallWithFinalityQueryRequest,
PerChainQueryRequest,
QueryRequest,
sign,
@ -71,7 +72,11 @@ async function getEthCallByTimestampArgs(): Promise<[bigint, bigint, bigint]> {
let targetBlockNumber = BigInt(0);
let targetBlockTime = BigInt(0);
while (targetBlockNumber === BigInt(0)) {
const followingBlock = await web3.eth.getBlock(followingBlockNumber);
let followingBlock = await web3.eth.getBlock(followingBlockNumber);
while (Number(followingBlock) <= 0) {
await sleep(1000);
followingBlock = await web3.eth.getBlock(followingBlockNumber);
}
const targetBlock = await web3.eth.getBlock(
(Number(followingBlockNumber) - 1).toString()
);
@ -85,6 +90,10 @@ async function getEthCallByTimestampArgs(): Promise<[bigint, bigint, bigint]> {
return [targetBlockTime, targetBlockNumber, followingBlockNumber];
}
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
describe("eth call", () => {
test("serialize request", () => {
const toAddress = "0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270";
@ -493,7 +502,7 @@ describe("eth call", () => {
err = true;
expect(error.response.status).toBe(400);
expect(error.response.data).toBe(
`failed to validate request: failed to validate per chain query 0: chain specific query is invalid: target block id must be a hex number or hash starting with 0x\n`
`failed to validate request: failed to validate per chain query 0: chain specific query is invalid: target block id is required\n`
);
});
expect(err).toBe(true);
@ -537,7 +546,136 @@ describe("eth call", () => {
err = true;
expect(error.response.status).toBe(400);
expect(error.response.data).toBe(
`failed to validate request: failed to validate per chain query 0: chain specific query is invalid: following block id must be a hex number or hash starting with 0x\n`
`failed to validate request: failed to validate per chain query 0: chain specific query is invalid: following block id is required\n`
);
});
expect(err).toBe(true);
});
test("serialize eth_call_with_finality request", () => {
const toAddress = "0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270";
const nameCallData = createTestEthCallData(toAddress, "name", "string");
const totalSupplyCallData = createTestEthCallData(
toAddress,
"totalSupply",
"uint256"
);
const ethCall = new EthCallWithFinalityQueryRequest(
"0x28d9630",
"finalized",
[nameCallData, totalSupplyCallData]
);
const chainId = 5;
const ethQuery = new PerChainQueryRequest(chainId, ethCall);
const nonce = 1;
const request = new QueryRequest(nonce, [ethQuery]);
const serialized = request.serialize();
expect(Buffer.from(serialized).toString("hex")).toEqual(
"01000000010100050300000053000000093078323864393633300000000966696e616c697a6564020d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000406fdde030d500b1d8e8ef31e21c99d1db9a6444d3adf12700000000418160ddd"
);
});
test("successful eth_call_with_finality query", async () => {
const nameCallData = createTestEthCallData(WETH_ADDRESS, "name", "string");
const totalSupplyCallData = createTestEthCallData(
WETH_ADDRESS,
"totalSupply",
"uint256"
);
// Jump into the future a bit so the watcher has to wait for finality.
const blockNumber =
Number(await web3.eth.getBlockNumber(ETH_DATA_FORMAT)) + 10;
const ethCall = new EthCallWithFinalityQueryRequest(
blockNumber.toString(16),
"finalized",
[nameCallData, totalSupplyCallData]
);
const chainId = 2;
const ethQuery = new PerChainQueryRequest(chainId, ethCall);
const nonce = 1;
const request = new QueryRequest(nonce, [ethQuery]);
const serialized = request.serialize();
const digest = QueryRequest.digest(ENV, serialized);
const signature = sign(PRIVATE_KEY, digest);
const response = await axios.put(
QUERY_URL,
{
signature,
bytes: Buffer.from(serialized).toString("hex"),
},
{ headers: { "X-API-Key": "my_secret_key" } }
);
expect(response.status).toBe(200);
});
test("eth_call_with_finality query without finality should fail", async () => {
const nameCallData = createTestEthCallData(WETH_ADDRESS, "name", "string");
const totalSupplyCallData = createTestEthCallData(
WETH_ADDRESS,
"totalSupply",
"uint256"
);
const ethCall = new EthCallWithFinalityQueryRequest("0x28d9630", "", [
nameCallData,
totalSupplyCallData,
]);
const chainId = 2;
const ethQuery = new PerChainQueryRequest(chainId, ethCall);
const nonce = 1;
const request = new QueryRequest(nonce, [ethQuery]);
const serialized = request.serialize();
const digest = QueryRequest.digest(ENV, serialized);
const signature = sign(PRIVATE_KEY, digest);
let err = false;
const response = await axios
.put(
QUERY_URL,
{
signature,
bytes: Buffer.from(serialized).toString("hex"),
},
{ headers: { "X-API-Key": "my_secret_key" } }
)
.catch(function (error) {
err = true;
expect(error.response.status).toBe(400);
expect(error.response.data).toBe(
`failed to validate request: failed to validate per chain query 0: chain specific query is invalid: finality is required\n`
);
});
expect(err).toBe(true);
});
test("eth_call_with_finality query with bad finality should fail", async () => {
const nameCallData = createTestEthCallData(WETH_ADDRESS, "name", "string");
const totalSupplyCallData = createTestEthCallData(
WETH_ADDRESS,
"totalSupply",
"uint256"
);
const ethCall = new EthCallWithFinalityQueryRequest(
"0x28d9630",
"HelloWorld",
[nameCallData, totalSupplyCallData]
);
const chainId = 2;
const ethQuery = new PerChainQueryRequest(chainId, ethCall);
const nonce = 1;
const request = new QueryRequest(nonce, [ethQuery]);
const serialized = request.serialize();
const digest = QueryRequest.digest(ENV, serialized);
const signature = sign(PRIVATE_KEY, digest);
let err = false;
const response = await axios
.put(
QUERY_URL,
{
signature,
bytes: Buffer.from(serialized).toString("hex"),
},
{ headers: { "X-API-Key": "my_secret_key" } }
)
.catch(function (error) {
err = true;
expect(error.response.status).toBe(400);
expect(error.response.data).toBe(
`failed to validate request: failed to validate per chain query 0: chain specific query is invalid: finality must be "finalized" or "safe", is "HelloWorld"\n`
);
});
expect(err).toBe(true);

View File

@ -16,16 +16,7 @@ export class EthCallQueryRequest implements ChainSpecificQuery {
blockTag: string;
constructor(blockTag: BlockTag, public callData: EthCallData[]) {
if (typeof blockTag === "number") {
this.blockTag = `0x${blockTag.toString(16)}`;
} else if (isValidHexString(blockTag)) {
if (!blockTag.startsWith("0x")) {
blockTag = `0x${blockTag}`;
}
this.blockTag = blockTag;
} else {
throw new Error(`Invalid block tag: ${blockTag}`);
}
this.blockTag = parseBlockId(blockTag);
}
type(): ChainQueryType {
@ -47,3 +38,24 @@ export class EthCallQueryRequest implements ChainSpecificQuery {
return writer.data();
}
}
export function parseBlockId(blockId: BlockTag): string {
if (!blockId || blockId === "") {
throw new Error(`block tag is required`);
}
if (typeof blockId === "number") {
if (blockId < 0) {
throw new Error(`block tag must be positive`);
}
blockId = `0x${blockId.toString(16)}`;
} else if (isValidHexString(blockId)) {
if (!blockId.startsWith("0x")) {
blockId = `0x${blockId}`;
}
} else {
throw new Error(`Invalid block tag: ${blockId}`);
}
return blockId;
}

View File

@ -45,14 +45,16 @@ export class EthCallByTimestampQueryRequest implements ChainSpecificQuery {
function parseBlockHint(blockHint: BlockTag): string {
// Block hints are not required.
if (blockHint != "") {
if (blockHint !== "") {
if (typeof blockHint === "number") {
if (blockHint < 0) {
throw new Error(`block tag must be positive`);
}
blockHint = `0x${blockHint.toString(16)}`;
} else if (isValidHexString(blockHint)) {
if (!blockHint.startsWith("0x")) {
blockHint = `0x${blockHint}`;
}
blockHint = blockHint;
} else {
throw new Error(`Invalid block tag: ${blockHint}`);
}

View File

@ -0,0 +1,39 @@
import { BinaryWriter } from "./BinaryWriter";
import { BlockTag, EthCallData, parseBlockId } from "./ethCall";
import { ChainQueryType, ChainSpecificQuery } from "./request";
import { hexToUint8Array, isValidHexString } from "./utils";
export class EthCallWithFinalityQueryRequest implements ChainSpecificQuery {
blockId: string;
finality: string;
constructor(
blockId: BlockTag,
finality: string,
public callData: EthCallData[]
) {
this.blockId = parseBlockId(blockId);
this.finality = finality;
}
type(): ChainQueryType {
return ChainQueryType.EthCallWithFinality;
}
serialize(): Uint8Array {
const writer = new BinaryWriter()
.writeUint32(this.blockId.length)
.writeUint8Array(Buffer.from(this.blockId))
.writeUint32(this.finality.length)
.writeUint8Array(Buffer.from(this.finality))
.writeUint8(this.callData.length);
this.callData.forEach(({ to, data }) => {
const dataArray = hexToUint8Array(data);
writer
.writeUint8Array(hexToUint8Array(to))
.writeUint32(dataArray.length)
.writeUint8Array(dataArray);
});
return writer.data();
}
}

View File

@ -2,4 +2,5 @@ export * from "./request";
export * from "./utils";
export * from "./ethCall";
export * from "./ethCallByTimestamp";
export * from "./ethCallWithFinality";
export * from "./consts";

View File

@ -69,4 +69,5 @@ export interface ChainSpecificQuery {
export enum ChainQueryType {
EthCall = 1,
EthCallByTimeStamp = 2,
EthCallWithFinality = 3,
}

View File

@ -94,6 +94,10 @@ Note that there may be a need to support the use of tags like `latest` and `fina
Note that for `eth_call_by_timestamp` queries, the `timestamp` must be specified. Additionally, the `hint_target_block_id` and `hint_following_block_id` may be specified to assist the guardians in scoping the query. Note that if one of the two is specified, they both must be specified. The format of the hints is the same as the `block_id` in `eth_call`.
#### Desired Finality in eth_call_with_finality
Note that for `eth_call_with_finality` queries, the `finality` must be specified. The only valid values are `finalized` and `safe`. The `block_id` is required and has the same format as in `eth_call`.
#### Signature Verification
Requests messages MUST include a signature in the payload in order to distinguish between a requestor and (potentially, third-party) p2p relayer.
@ -114,6 +118,8 @@ Once the request has been validated, the query module will submit the individual
to the node and return the results. Communication between the query module and the watchers is via a pair of golang channels per watcher, one inbound and one outbound. The watchers will
use batch requests to minimize RPC overhead. For this to work effectively, the integrator should properly group RPC calls into the minimal set of per-chain queries.
For `eth_call_with_finality` requests, the watcher will not return the result until the requested block as reached the desired level of finality. Also, on chains that do not publish safe blocks, a request for a finality of "safe" will be treated as "finalized" rather than throwing an error.
The query module will listen for responses for all of the per-chain queries. When all per-chain responses are received, the module will post the result to be published on the gossip network.
If any of the responses fails or times out, the query module will retry periodically for up to one minute. If after a minute some of the per-chain queries were not successful, the query
module will drop the request.
@ -268,6 +274,21 @@ Currently the only supported query types are `eth_call` and `eth_call_by_timesta
[]byte batch_call_data
```
3. eth_call_with_finality (query type 3)
This query type is similar to `eth_call` but ensures that the specified block has reached the specified finality before returning the query results. The finality may be "finalized" or "safe". Note that "safe" is only supported on chains that publish safe blocks. The request MUST include the both the block_id and the finality.
The guardian code MUST enforce the finality before signing the result.
```go
u32 block_id_len
[]byte block_id
u32 finality_len
[]byte finality
u8 num_batch_call_data
[]byte batch_call_data
```
## Query Response
- Off-Chain
@ -313,6 +334,7 @@ Currently the only supported query types are `eth_call` and `eth_call_by_timesta
```
2. eth_call_by_timestamp (query type 2) Response Body
```go
u64 target_block_number
[32]byte target_block_hash
@ -323,11 +345,15 @@ Currently the only supported query types are `eth_call` and `eth_call_by_timesta
u8 num_results
[]byte results
```
```go
u32 result_len
[]byte result
```
3. eth_call_with_finality (query type 3) Response Body
The response for `eth_call_with_finality` is the same as the response for `eth_call`, although the query type will be three instead of one.
## REST Service
### Request