2022-11-11 07:24:55 -08:00
package near
import (
"context"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
2023-05-23 23:38:18 -07:00
"fmt"
2022-11-11 07:24:55 -08:00
"strconv"
"strings"
"time"
"github.com/certusone/wormhole/node/pkg/common"
"github.com/certusone/wormhole/node/pkg/watchers/near/nearapi"
eth_common "github.com/ethereum/go-ethereum/common"
"github.com/mr-tron/base58"
"github.com/tidwall/gjson"
"github.com/wormhole-foundation/wormhole/sdk/vaa"
"go.uber.org/zap"
)
type NearWormholePublishEvent struct {
Standard string ` json:"standard" `
Event string ` json:"event" `
Data string ` json:"data" `
Nonce uint32 ` json:"nonce" `
Emitter string ` json:"emitter" `
Seq uint64 ` json:"seq" `
BlockHeight uint64 ` json:"block" `
}
// processTx fetches a transaction's receipt_outcomes and looks for wormhole messages in it.
// we go through all receipt outcomes (result.receipts_outcome) and look for log emissions from the Wormhole core contract.
// sender_account_id is required to help determine which shard to query.
func ( e * Watcher ) processTx ( logger * zap . Logger , ctx context . Context , job * transactionProcessingJob ) error {
logger . Debug ( "processTx" , zap . String ( "log_msg_type" , "info_process_tx" ) , zap . String ( "tx_hash" , job . txHash ) )
tx_receipts , err := e . nearAPI . GetTxStatus ( ctx , job . txHash , job . senderAccountId )
if err != nil {
return err
}
receiptOutcomes := gjson . ParseBytes ( tx_receipts ) . Get ( "result.receipts_outcome" )
if ! receiptOutcomes . Exists ( ) {
// no outcomes means nothing to look at
logger . Debug ( "processTx: No receipt outcomes" , zap . String ( "tx_hash" , job . txHash ) )
return nil
}
for _ , receiptOutcome := range receiptOutcomes . Array ( ) {
err = e . processOutcome ( logger , ctx , job , receiptOutcome )
if err != nil {
logger . Debug ( "ProcessOutcome error: " , zap . Error ( err ) )
2023-05-23 23:38:18 -07:00
return err
2022-11-11 07:24:55 -08:00
}
}
return nil
}
func ( e * Watcher ) processOutcome ( logger * zap . Logger , ctx context . Context , job * transactionProcessingJob , receiptOutcome gjson . Result ) error {
outcome := receiptOutcome . Get ( "outcome" )
if ! outcome . Exists ( ) {
logger . Warn ( "NEAR RPC malformed response: receipts_outcome.outcome does not exist" , zap . String ( "error_type" , "nearapi_inconsistent" ) , zap . String ( "json" , receiptOutcome . Str ) )
return errors . New ( "NEAR RPC malformed response" )
}
executor_id := outcome . Get ( "executor_id" )
if ! executor_id . Exists ( ) {
logger . Warn ( "NEAR RPC malformed response: receipts_outcome.outcome does not exist" , zap . String ( "error_type" , "nearapi_inconsistent" ) , zap . String ( "json" , receiptOutcome . Str ) )
return errors . New ( "NEAR RPC malformed response: receipts_outcome.outcome does not exist" )
}
// SECURITY CRITICAL: Check that the outcome relates to the Wormhole core contract on NEAR.
// according to near source documentation, executor_id is the id of the account on which the execution happens:
// for transaction this is signer_id
// for receipt this is receiver_id, i.e. the account on which the receipt has been applied
if executor_id . String ( ) == "" || executor_id . String ( ) != e . wormholeAccount {
return nil
}
logger . Debug ( "Found a Wormhole Transaction... Now checking if it's a valid log emission." , zap . String ( "tx_hash" , job . txHash ) )
outcomeBlockHash := receiptOutcome . Get ( "block_hash" )
if ! outcomeBlockHash . Exists ( ) {
logger . Warn ( "NEAR RPC malformed response: receipts_outcome.block_hash does not exist" , zap . String ( "error_type" , "nearapi_inconsistent" ) , zap . String ( "json" , receiptOutcome . Str ) )
return errors . New ( "NEAR RPC malformed response: receipts_outcome.block_hash does not exist" )
}
l := outcome . Get ( "logs" )
if ! l . Exists ( ) {
logger . Warn ( "NEAR RPC malformed response: receipts_outcome.outcome.logs does not exist" , zap . String ( "error_type" , "nearapi_inconsistent" ) , zap . String ( "json" , receiptOutcome . Str ) )
return errors . New ( "NEAR RPC malformed response: receipts_outcome.outcome.logs does not exist" )
}
// SECURITY CRITICAL: Check that block has been finalized.
outcomeBlockHeader , isFinalized := e . finalizer . isFinalized ( logger , ctx , outcomeBlockHash . String ( ) )
if ! isFinalized {
2022-11-29 03:26:46 -08:00
// If it has not, we return an error such that this transaction can be put back into the queue.
2023-05-23 23:38:18 -07:00
return fmt . Errorf ( "block %s not finalized yet" , outcomeBlockHash . String ( ) )
2022-11-11 07:24:55 -08:00
}
successValue := outcome . Get ( "status.SuccessValue" )
if ! successValue . Exists ( ) || successValue . String ( ) == "" {
return errors . New ( "outcome.status.SuccessValue does not exist" )
}
for _ , log := range l . Array ( ) {
err := e . processWormholeLog ( logger , ctx , job , outcomeBlockHeader , successValue . String ( ) , log )
if err != nil {
// SECURITY defense-in-depth: If one of the logs is malformed, we skip processing the other logs for defense in depth
return err
}
}
return nil // SUCCESS
}
2023-04-11 16:54:08 -07:00
func ( e * Watcher ) processWormholeLog ( logger * zap . Logger , _ context . Context , job * transactionProcessingJob , outcomeBlockHeader nearapi . BlockHeader , successValue string , log gjson . Result ) error {
2022-11-11 07:24:55 -08:00
event := log . String ( )
// SECURITY CRITICAL: Ensure that we're reading a correct log message.
// Unfortunately, NEAR does not yet support structured event emission like Ethereum.
if ! strings . HasPrefix ( event , "EVENT_JSON:" ) {
return nil
}
eventJsonStr := event [ 11 : ]
logger . Info ( "event" , zap . String ( "log_msg_type" , "wormhole_event" ) , zap . String ( "event" , eventJsonStr ) )
// SECURITY: Wormhole is following NEP-297 (https://nomicon.io/Standards/EventsFormat)
// First, check that we're looking at a "publish" event type from the "wormhole" standard.
if ! isWormholePublishEvent ( logger , eventJsonStr ) {
return nil
}
// SECURITY: If we get this far, the checks below should be true, otherwise something has seriously gone wrong.
var pubEvent NearWormholePublishEvent
if err := json . Unmarshal ( [ ] byte ( eventJsonStr ) , & pubEvent ) ; err != nil {
logger . Error ( "Wormhole publish event malformed" , zap . String ( "error_type" , "malformed_wormhole_event" ) , zap . String ( "json" , eventJsonStr ) )
return errors . New ( "Wormhole publish event malformed" )
}
if pubEvent . Standard != "wormhole" || pubEvent . Event != "publish" || pubEvent . Emitter == "" || pubEvent . Seq <= 0 || pubEvent . BlockHeight == 0 {
logger . Error ( "Wormhole publish event malformed" , zap . String ( "error_type" , "malformed_wormhole_event" ) , zap . String ( "json" , eventJsonStr ) )
return errors . New ( "Wormhole publish event malformed" )
}
successValueInt , err := successValueToInt ( successValue )
// SECURITY defense-in-depth: check that outcome.status.SuccessValue should equal to the base64 encoded sequence number
if err != nil || successValueInt == 0 || uint64 ( successValueInt ) != pubEvent . Seq {
logger . Error (
"SuccessValue does not match sequence number" ,
zap . String ( "error_type" , "malformed_wormhole_event" ) ,
zap . String ( "log_msg_type" , "tx_processing_error" ) ,
zap . String ( "SuccessValue" , successValue ) ,
zap . Int ( "int(SuccessValue)" , successValueInt ) ,
zap . Uint64 ( "log.seq" , pubEvent . Seq ) ,
)
return errors . New ( "Wormhole publish event.seq does not match SuccessValue" )
}
// SECURITY: For defense-in-depth, check that the block height from the event matches the block height from the RPC node
if pubEvent . BlockHeight != outcomeBlockHeader . Height {
logger . Error (
"Wormhole publish event.block does not equal receipt_outcome[x].block_height" ,
zap . String ( "error_type" , "malformed_wormhole_event" ) ,
zap . String ( "log_msg_type" , "tx_processing_error" ) ,
zap . Uint64 ( "event.block" , pubEvent . BlockHeight ) ,
zap . Uint64 ( "receipt_outcome[x].block_height" , outcomeBlockHeader . Height ) ,
)
return errors . New ( "Wormhole publish event.block does not equal receipt_outcome[x].block_height" )
}
// SECURITY: extract emitter address and ensure that it has the correct format
emitter , err := hex . DecodeString ( pubEvent . Emitter )
if err != nil {
return err
}
// emitter is sha256(account_name), so it should be 32 bytes long.
if len ( emitter ) != 32 {
logger . Error (
"Wormhole publish event malformed" ,
zap . String ( "error_type" , "malformed_wormhole_event" ) ,
zap . String ( "log_msg_type" , "tx_processing_error" ) ,
zap . String ( "json" , eventJsonStr ) ,
zap . String ( "field" , "emitter" ) ,
)
return errors . New ( "Wormhole publish event malformed" )
}
// Assemble the Message Publication Event
var a vaa . Address
copy ( a [ : ] , emitter )
txHashBytes , err := base58 . Decode ( job . txHash )
if err != nil {
return err
}
if len ( txHashBytes ) != 32 {
logger . Error (
"Transaction hash is not 32 bytes" ,
zap . String ( "error_type" , "malformed_wormhole_event" ) ,
zap . String ( "log_msg_type" , "tx_processing_error" ) ,
zap . String ( "txHash" , job . txHash ) ,
)
return errors . New ( "Transaction hash is not 32 bytes" )
}
var txHashEthFormat = eth_common . BytesToHash ( txHashBytes )
pl , err := hex . DecodeString ( pubEvent . Data )
if err != nil {
return err
}
if len ( pl ) * 2 != len ( pubEvent . Data ) {
logger . Error (
"Wormhole publish event malformed" ,
zap . String ( "error_type" , "malformed_wormhole_event" ) ,
zap . String ( "log_msg_type" , "tx_processing_error" ) ,
zap . String ( "field" , "data" ) ,
zap . String ( "data" , pubEvent . Data ) ,
)
return errors . New ( "Wormhole publish event malformed" )
}
// SECURITY the timestamp of an observation is the timestamp of the block in which the wormhole core receipt has been finalized.
ts := outcomeBlockHeader . Timestamp
observation := & common . MessagePublication {
TxHash : txHashEthFormat ,
Timestamp : time . Unix ( int64 ( ts ) , 0 ) ,
Nonce : pubEvent . Nonce ,
Sequence : pubEvent . Seq ,
EmitterChain : vaa . ChainIDNear ,
EmitterAddress : a ,
Payload : pl ,
ConsistencyLevel : 0 ,
2023-09-29 08:42:44 -07:00
IsReobservation : job . isReobservation ,
2022-11-11 07:24:55 -08:00
}
// tell everyone about it
job . hasWormholeMsg = true
e . eventChan <- EVENT_NEAR_MESSAGE_CONFIRMED
logger . Info ( "message observed" ,
zap . String ( "log_msg_type" , "wormhole_event_success" ) ,
zap . Uint64 ( "ts" , ts ) ,
zap . Time ( "timestamp" , observation . Timestamp ) ,
zap . Uint32 ( "nonce" , observation . Nonce ) ,
zap . Uint64 ( "sequence" , observation . Sequence ) ,
zap . Stringer ( "emitter_chain" , observation . EmitterChain ) ,
zap . Stringer ( "emitter_address" , observation . EmitterAddress ) ,
zap . Binary ( "payload" , observation . Payload ) ,
zap . Uint8 ( "consistency_level" , observation . ConsistencyLevel ) ,
)
e . msgC <- observation
return nil
}
// TODO test this code
func successValueToInt ( successValue string ) ( int , error ) {
successValueBytes , err := base64 . StdEncoding . DecodeString ( successValue )
if err != nil {
return 0 , err
}
successValueInt , err := strconv . Atoi ( string ( successValueBytes ) )
if err != nil {
return 0 , err
}
return successValueInt , nil
}
func isWormholePublishEvent ( logger * zap . Logger , eventJsonStr string ) bool {
if ! gjson . Valid ( eventJsonStr ) {
logger . Error (
"event is invalid json" ,
zap . String ( "error_type" , "malformed_wormhole_event" ) ,
zap . String ( "log_msg_type" , "tx_processing_error" ) ,
zap . String ( "json" , eventJsonStr ) ,
)
return false
}
eventJson := gjson . Parse ( eventJsonStr )
standard := eventJson . Get ( "standard" )
event_type := eventJson . Get ( "event" )
if standard . Exists ( ) && standard . String ( ) == "wormhole" && event_type . Exists ( ) && event_type . String ( ) == "publish" {
return true
}
return false
}