301 lines
11 KiB
Go
301 lines
11 KiB
Go
package near
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"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))
|
|
}
|
|
}
|
|
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 {
|
|
// If it has not, we return an error such that this transaction can be put back into the queue.
|
|
return errors.New("block not finalized yet")
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (e *Watcher) processWormholeLog(logger *zap.Logger, ctx context.Context, job *transactionProcessingJob, outcomeBlockHeader nearapi.BlockHeader, successValue string, log gjson.Result) error {
|
|
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,
|
|
}
|
|
|
|
// 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
|
|
}
|