synapsecns/sanguine

View on GitHub
services/omnirpc/proxy/standardize.go

Summary

Maintainability
C
1 day
Test Coverage
package proxy

import (
    "context"
    "errors"
    "fmt"
    "github.com/synapsecns/sanguine/ethergo/client"
    "github.com/synapsecns/sanguine/ethergo/parser/rpc"

    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/common/hexutil"
    "github.com/ethereum/go-ethereum/core/types"
    "github.com/goccy/go-json"
    "golang.org/x/sync/errgroup"
)

// JSONRPCMessage is A value of this type can a JSON-RPC request, notification, successful response or
// error response. Which one it is depends on the fields.
type JSONRPCMessage struct {
    Version string          `json:"jsonrpc,omitempty"`
    ID      int             `json:"id,omitempty"`
    Method  string          `json:"method,omitempty"`
    Params  json.RawMessage `json:"params,omitempty"`
    Error   *JSONError      `json:"error,omitempty"`
    Result  json.RawMessage `json:"result,omitempty"`
}

// JSONError is used to hold a json error.
type JSONError struct {
    Code    int         `json:"code"`
    Message string      `json:"message"`
    Data    interface{} `json:"data,omitempty"`
}

// rpcTransaction is an eth rpc transaction (copied from ethclient).
type rpcTransaction struct {
    //nolint: unused
    tx *types.Transaction
    txExtraInfo
}

// txExtraInfo contains extra txinfo (Copied from ethclient).
type txExtraInfo struct {
    BlockNumber *string         `json:"blockNumber,omitempty"`
    BlockHash   *common.Hash    `json:"blockHash,omitempty"`
    From        *common.Address `json:"from,omitempty"`
}

// rpcBlock is an eth rpc block.
type rpcBlock struct {
    Hash         common.Hash      `json:"hash"`
    Transactions []rpcTransaction `json:"transactions"`
    UncleHashes  []common.Hash    `json:"uncles"`
}

// rpcBlockNoTx is an eth rpc block without transactions (used by eth_getBlockByNumber calls with tx flag set to false).
type rpcBlockNoTx struct {
    Hash        common.Hash   `json:"hash"`
    UncleHashes []common.Hash `json:"uncles"`
}

// fullRPCBlock is used to ensure parity by encoding both the header and the block.
type fullRPCBlock struct {
    Block  rpcBlock      `json:"rpc_block"`
    Header *types.Header `json:"header"`
}

// fullRPCBlock is used to ensure parity by encoding both the header and the block.
type fullRPCBlockNoTx struct {
    Block  rpcBlockNoTx  `json:"rpc_block"`
    Header *types.Header `json:"header"`
}

// rpcProgress is a copy of SyncProgressMethod with hex-encoded fields.
// copied from ethclient.
type rpcProgress struct {
    StartingBlock hexutil.Uint64
    CurrentBlock  hexutil.Uint64
    HighestBlock  hexutil.Uint64

    PulledStates hexutil.Uint64
    KnownStates  hexutil.Uint64

    SyncedAccounts      hexutil.Uint64
    SyncedAccountBytes  hexutil.Uint64
    SyncedBytecodes     hexutil.Uint64
    SyncedBytecodeBytes hexutil.Uint64
    SyncedStorage       hexutil.Uint64
    SyncedStorageBytes  hexutil.Uint64
    HealedTrienodes     hexutil.Uint64
    HealedTrienodeBytes hexutil.Uint64
    HealedBytecodes     hexutil.Uint64
    HealedBytecodeBytes hexutil.Uint64
    HealingTrienodes    hexutil.Uint64
    HealingBytecode     hexutil.Uint64
}

// feeHistoryResultMarshaling is used for parity checking against fee history
// copied from ethclient.
type feeHistoryResultMarshaling struct {
    OldestBlock  *hexutil.Big     `json:"oldestBlock"`
    Reward       [][]*hexutil.Big `json:"reward,omitempty"`
    BaseFee      []*hexutil.Big   `json:"baseFeePerGas,omitempty"`
    GasUsedRatio []float64        `json:"gasUsedRatio"`
}

// StandardizeResponse produces a standardized json response for hashing (strips extra fields)
//
//nolint:gocognit,cyclop
func standardizeResponse(ctx context.Context, req *rpc.Request, rpcMessage JSONRPCMessage) (out []byte, err error) {
    // TODO: use a sync.pool for acquiring/releasing these structs
    method := req.Method

OUTER:
    switch client.RPCMethod(method) {
    case client.ChainIDMethod, client.BlockNumberMethod, client.TransactionCountByHashMethod, client.GetBalanceMethod, client.GasPriceMethod, client.MaxPriorityMethod:
        var result hexutil.Big
        if err = json.Unmarshal(rpcMessage.Result, &result); err != nil {
            return nil, fmt.Errorf("could not parse: %w", err)
        }
        out, err = json.Marshal(result)
    case client.StorageAtMethod, client.GetCodeMethod:
        var result hexutil.Bytes
        if err = json.Unmarshal(rpcMessage.Result, &result); err != nil {
            return nil, fmt.Errorf("could not parse: %w", err)
        }
        out, err = json.Marshal(result)
    case client.TransactionCountMethod, client.EstimateGasMethod:
        var result hexutil.Uint64
        if err = json.Unmarshal(rpcMessage.Result, &result); err != nil {
            return nil, fmt.Errorf("could not parse: %w", err)
        }
        out, err = json.Marshal(result)
    case client.PendingTransactionCountMethod:
        var result hexutil.Uint
        if err = json.Unmarshal(rpcMessage.Result, &result); err != nil {
            return nil, fmt.Errorf("could not parse: %w", err)
        }
        out, err = json.Marshal(result)
    case client.TransactionByHashMethod, client.TransactionByBlockHashAndIndexMethod:
        var rpcBody rpcTransaction
        if err = json.Unmarshal(rpcMessage.Result, &rpcBody); err != nil {
            return nil, fmt.Errorf("could not parse: %w", err)
        }
        out, err = json.Marshal(rpcBody)
    case client.GetLogsMethod:
        var result []types.Log
        if err = json.Unmarshal(rpcMessage.Result, &result); err != nil {
            return nil, fmt.Errorf("could not parse: %w", err)
        }
        out, err = json.Marshal(result)
    case client.TransactionReceiptByHashMethod:
        var rpcBody *types.Receipt
        if err = json.Unmarshal(rpcMessage.Result, &rpcBody); err != nil {
            return nil, fmt.Errorf("could not parse: %w", err)
        }
        out, err = json.Marshal(rpcBody)
    case client.SyncProgressMethod:
        var syncing bool
        if err = json.Unmarshal(rpcMessage.Result, &syncing); err == nil {
            out, err = json.Marshal(syncing)
            break OUTER
        }
        var p rpcProgress
        if err = json.Unmarshal(rpcMessage.Result, &p); err != nil {
            return nil, fmt.Errorf("could not parse: %w", err)
        }

        out, err = json.Marshal(p)
    case client.FeeHistoryMethod:
        var rpcBody feeHistoryResultMarshaling
        if err := json.Unmarshal(rpcMessage.Result, &rpcBody); err != nil {
            return nil, fmt.Errorf("could not parse: %w", err)
        }
        out, err = json.Marshal(rpcBody)

    case client.BlockByHashMethod, client.BlockByNumberMethod:
        var head *types.Header
        var rpcBody rpcBlock
        var rpcBlockNoTx rpcBlockNoTx
        groupCtx, _ := errgroup.WithContext(ctx)
        groupCtx.Go(func() error {
            if err = json.Unmarshal(rpcMessage.Result, &head); err != nil {
                return fmt.Errorf("could not parse: %w", err)
            }
            return nil
        })

        var txFlag bool
        err := json.Unmarshal(req.Params[1], &txFlag)
        if txFlag {
            groupCtx.Go(func() error {
                if err = json.Unmarshal(rpcMessage.Result, &rpcBody); err != nil {
                    return fmt.Errorf("could not parse: %w", err)
                }
                return nil
            })
        } else {
            groupCtx.Go(func() error {
                if err = json.Unmarshal(rpcMessage.Result, &rpcBlockNoTx); err != nil {
                    return fmt.Errorf("could not parse: %w", err)
                }
                return nil
            })
        }

        err = groupCtx.Wait()
        if err != nil {
            //nolint: wrapcheck
            return nil, err
        }

        if head == nil {
            return nil, errors.New("header was empty")
        }

        // If tx flag is false, return output block with fullRPCBlockNoTx type.
        if !txFlag {
            outputBlock := fullRPCBlockNoTx{
                Block:  rpcBlockNoTx,
                Header: head,
            }
            out, err = json.Marshal(outputBlock)

            // Bypass the following uncle block verification.
            return out, nil
        }

        // Quick-verify transaction and uncle lists. This mostly helps with debugging the server.
        if head.UncleHash == types.EmptyUncleHash && len(rpcBody.UncleHashes) > 0 {
            return nil, fmt.Errorf("server returned non-empty uncle list but block header indicates no uncles")
        }
        if head.UncleHash != types.EmptyUncleHash && len(rpcBody.UncleHashes) == 0 {
            return nil, fmt.Errorf("server returned empty uncle list but block header indicates uncles")
        }
        if head.TxHash == types.EmptyRootHash && len(rpcBody.Transactions) > 0 {
            return nil, fmt.Errorf("server returned non-empty transaction list but block header indicates no transactions")
        }
        if head.TxHash != types.EmptyRootHash && len(rpcBody.Transactions) == 0 {
            return nil, fmt.Errorf("server returned empty transaction list but block header indicates transactions")
        }

        outputBlock := fullRPCBlock{
            Block:  rpcBody,
            Header: head,
        }
        out, err = json.Marshal(outputBlock)
        if err != nil {
            return nil, fmt.Errorf("could not unmarshall full block: %w", err)
        }
    // we don't do anything here, kept for exhaustiveness
    case client.CallMethod, client.SendRawTransactionMethod:
        return out, nil
    case client.NetVersionMethod, client.SubscribeMethod:
        return out, fmt.Errorf("method %s is not supported", rpcMessage.Method)
    }

    //nolint: wrapcheck
    return out, err
}