Evmos의 Ethereum Tx 처리

2024. 1. 23. 16:50Blockchain/Cosmos

728x90
반응형

Ethermint로 잘 알려져 있는 Evmos는 Cosmos 기반의 체인에서 ethereum tx 처리를 할 수 있도록 지원한다. 8545 EVM JSONRPC를 지원하여, 기존의 ethereum 개발자는 다른 부가적인 처리 없이 API를 똑같이 사용할 수 있다. 모든 ethereum API를 지원하지는 않지만, 개발자들이 자주 사용하는 API는 대부분 지원한다. (Evmos에서 지원하는 EVM API는 여기서 확인 할 수 있다.)

많은 수의 dApp 및 서비스가 ethereum 기반으로 이루어져 있다는 것을 생각해보면, 많은 서비스가 Cosmos 진영으로의 매끄러운 온보딩이 가능하도록 만들 수 있기 때문에 Evmos의 evm 모듈은 Cosmos 생태계 확장에 큰 기여를 할 수 있는 기술이라 생각된다.

해당 글에서는 Evmos가 제공 하는 evm 모듈에서 어떤 식으로 ethereum tx가 cosmos/tendermint 기반에서 처리되는지에 대한 과정을 확인해 보려 한다.

RPC Server

체인 데몬을 실행하면 cosmos app chain들은 startInProcess()에서 서버 listen을 시작한다. Evmos도 마찬가지이며, config 파일에서 JSONRPC가 enable 되어 있다면 EVM JSONRPC를 service한다. (코드)

Evm 모듈에서 지원하기 위한 API들은 init()으로 설정되어 있고, 이를 go-ethereum RPC new server를 통해 API들을 등록한 뒤 routing을 설정한다. 아래의 코드는 Evmos 내에서 지정한 API들을 routing 선언을 하는 것을 보여주며, 최종적으로 HTTP handler에 포함된다.


rpcServer := ethrpc.NewServer()

allowUnprotectedTxs := config.JSONRPC.AllowUnprotectedTxs
rpcAPIArr := config.JSONRPC.API

apis := rpc.GetRPCAPIs(ctx, clientCtx, tmWsClient, allowUnprotectedTxs, indexer, rpcAPIArr)

for _, api := range apis {
    if err := rpcServer.RegisterName(api.Namespace, api.Service); err != nil {
        ctx.Logger.Error(
            "failed to register service in JSON RPC namespace",
            "namespace", api.Namespace,
            "service", api.Service,
        )
        return nil, nil, err
    }
}

r := mux.NewRouter()
r.HandleFunc("/", rpcServer.ServeHTTP).Methods("POST")

handlerWithCors := cors.Default()
if config.API.EnableUnsafeCORS {
    handlerWithCors = cors.AllowAll()
}

httpSrv := &http.Server{
    Addr:              config.JSONRPC.Address,
    Handler:           handlerWithCors.Handler(r),
    ReadHeaderTimeout: config.JSONRPC.HTTPTimeout,
    ReadTimeout:       config.JSONRPC.HTTPTimeout,
    WriteTimeout:      config.JSONRPC.HTTPTimeout,
    IdleTimeout:       config.JSONRPC.HTTPIdleTimeout,
}

이제 서버가 실행되고 ethereum API에 대해 listen을 시작한다.

Tx 전송 및 인입

Tx 전송은 solidity contract deploy로 진행하며, solidity는 remix의 디폴트 컨트랙트(Store.sol)를 사용한다. 아래는 해당 컨트랙트의 ABI와 바이트코드는 아래와 같다.

  • ABI

[
    {
        "inputs": [],
        "name": "retrieve",
        "outputs": [
            {
                "internalType": "uint256",
                "name": "",
                "type": "uint256"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "inputs": [
            {
                "internalType": "uint256",
                "name": "num",
                "type": "uint256"
            }
        ],
        "name": "store",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function"
    }
]

  • Bytecode
    • 608060405234801561001057600080fd5b50610150806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80632e64cec11461003b5780636057361d14610059575b600080fd5b610043610075565b60405161005091906100d9565b60405180910390f35b610073600480360381019061006e919061009d565b61007e565b005b60008054905090565b8060008190555050565b60008135905061009781610103565b92915050565b6000602082840312156100b3576100b26100fe565b5b60006100c184828501610088565b91505092915050565b6100d3816100f4565b82525050565b60006020820190506100ee60008301846100ca565b92915050565b6000819050919050565b600080fd5b61010c816100f4565b811461011757600080fd5b5056fea26469706673582212209a159a4f3847890f10bfb87871a61eba91c5dbf5ee3cf6398207e292eee22a1664736f6c63430008070033

Evmos에 컨트랙트 배포를 위한 트랜잭션을 전송하면, go-ethereum 서버 핸들러에서 아래와 같은 형태로 메시지를 수신한다.

전송한 메시지의 메소드는 eth_sendRawTransaction으로써, eth API의 트랜잭션 실행 메소드이고, params는 바이트코드를 포함한 메시지가 인입된다.

서버 내부 로직을 처리하고, evmos에서는 API에 따라 기선언된 SendRawTransaction() function을 호출한다. (코드)


func (e *PublicAPI) SendRawTransaction(data hexutil.Bytes) (common.Hash, error) {
    e.logger.Debug("eth_sendRawTransaction", "length", len(data))
    return e.backend.SendRawTransaction(data)
}

인입되는 data는 go-ethereum의 hexutil.Bytes 타입으로 들어오며, 실제 데이터는 아래와 같음을 확인할 수 있다.

Tx 변환

인입된 데이터는 위 코드에서 확인가능한 것처럼 Backend receiver로 인입되는 e.backend.SendRawTransaction()로 전달되며, 코드는 아래와 같다.


// SendRawTransaction send a raw Ethereum transaction.
func (b *Backend) SendRawTransaction(data hexutil.Bytes) (common.Hash, error) {
    // RLP decode raw transaction bytes
    tx := &ethtypes.Transaction{}
    if err := tx.UnmarshalBinary(data); err != nil {
        b.logger.Error("transaction decoding failed", "error", err.Error())
        return common.Hash{}, err
    }

    // check the local node config in case unprotected txs are disabled
    if !b.UnprotectedAllowed() && !tx.Protected() {
        // Ensure only eip155 signed transactions are submitted if EIP155Required is set.
        return common.Hash{}, errors.New("only replay-protected (EIP-155) transactions allowed over RPC")
    }

    ethereumTx := &evmtypes.MsgEthereumTx{}
    if err := ethereumTx.FromEthereumTx(tx); err != nil {
        b.logger.Error("transaction converting failed", "error", err.Error())
        return common.Hash{}, err
    }

    if err := ethereumTx.ValidateBasic(); err != nil {
        b.logger.Debug("tx failed basic validation", "error", err.Error())
        return common.Hash{}, err
    }

    // Query params to use the EVM denomination
    res, err := b.queryClient.QueryClient.Params(b.ctx, &evmtypes.QueryParamsRequest{})
    if err != nil {
        b.logger.Error("failed to query evm params", "error", err.Error())
        return common.Hash{}, err
    }

    cosmosTx, err := ethereumTx.BuildTx(b.clientCtx.TxConfig.NewTxBuilder(), res.Params.EvmDenom)
    if err != nil {
        b.logger.Error("failed to build cosmos tx", "error", err.Error())
        return common.Hash{}, err
    }

    // Encode transaction by default Tx encoder
    txBytes, err := b.clientCtx.TxConfig.TxEncoder()(cosmosTx)
    if err != nil {
        b.logger.Error("failed to encode eth tx using default encoder", "error", err.Error())
        return common.Hash{}, err
    }

    txHash := ethereumTx.AsTransaction().Hash()

    syncCtx := b.clientCtx.WithBroadcastMode(flags.BroadcastSync)
    rsp, err := syncCtx.BroadcastTx(txBytes)
    if rsp != nil && rsp.Code != 0 {
        err = errorsmod.ABCIError(rsp.Codespace, rsp.Code, rsp.RawLog)
    }
    if err != nil {
        b.logger.Error("failed to broadcast tx", "error", err.Error())
        return txHash, err
    }

    return txHash, nil
}

해당 코드를 주요 기능에 따라 순서대로 살펴보자.

Unmarshal data


// RLP decode raw transaction bytes
tx := &ethtypes.Transaction{}
if err := tx.UnmarshalBinary(data); err != nil {
    b.logger.Error("transaction decoding failed", "error", err.Error())
    return common.Hash{}, err
}

전달받은 data를 go-ethereum의 transaction type으로 unmarshal을 진행한다. tx는 아래와 같다.

MsgEthereumTx

Unmarshal tx를 Evmos가 제공하는 MsgEthereumTx 타입으로 변환한다.


ethereumTx := &evmtypes.MsgEthereumTx{}
if err := ethereumTx.FromEthereumTx(tx); err != nil {
    b.logger.Error("transaction converting failed", "error", err.Error())
    return common.Hash{}, err
}

if err := ethereumTx.ValidateBasic(); err != nil {
    b.logger.Debug("tx failed basic validation", "error", err.Error())
    return common.Hash{}, err
}

MsgEthereumTx는 ethereum 트랜잭션을 Cosmos SDK 메시지로 캡슐화하고 필요한 트랜잭션 데이터 필드를 포함하기 위해 사용된다. 변환된 메시지는 아래와 같으며, 메시지 타입이 /ethermint.evm.v1.LegacyTx로 변환된다.

Build Cosmos Tx


// Query params to use the EVM denomination
res, err := b.queryClient.QueryClient.Params(b.ctx, &evmtypes.QueryParamsRequest{})
if err != nil {
    b.logger.Error("failed to query evm params", "error", err.Error())
    return common.Hash{}, err
}

cosmosTx, err := ethereumTx.BuildTx(b.clientCtx.TxConfig.NewTxBuilder(), res.Params.EvmDenom)
if err != nil {
    b.logger.Error("failed to build cosmos tx", "error", err.Error())
    return common.Hash{}, err
}

Genesis에서 선언된 evm의 denomination을 기져온 후, ethereumTx.BuildTx()를 통해 cosmos에서 처리 가능하도록 tx를 최종적으로 변환한다.

Local broadcast


// Encode transaction by default Tx encoder
txBytes, err := b.clientCtx.TxConfig.TxEncoder()(cosmosTx)
if err != nil {
    b.logger.Error("failed to encode eth tx using default encoder", "error", err.Error())
    return common.Hash{}, err
}

txHash := ethereumTx.AsTransaction().Hash()

syncCtx := b.clientCtx.WithBroadcastMode(flags.BroadcastSync)
rsp, err := syncCtx.BroadcastTx(txBytes)
if rsp != nil && rsp.Code != 0 {
    err = errorsmod.ABCIError(rsp.Codespace, rsp.Code, rsp.RawLog)
}
if err != nil {
    b.logger.Error("failed to broadcast tx", "error", err.Error())
    return txHash, err
}

TxEncoder를 통해 tx를 byte 변환 후 tendermint RPC local을 통한 broadcast를 진행함으로써 go-ethereum 메시지를 cosmos SDK 메시지(/ethermint.evm.v1.MsgEthereumTx)로의 변환이 완료된다.

Tx 처리

이후의 처리 과정은 일반 cosmos 트랜잭션 처리 과정과 동일하다. CheckTx를 진행하고, AnteHandler를 통해 트랜잭션의 이상 유무를 판단한다.

참고적으로 Evmos의 ante handler는 아래와 같이 정의 되어 있다.


func NewAnteHandler(options HandlerOptions) sdk.AnteHandler {
    return func(
        ctx sdk.Context, tx sdk.Tx, sim bool,
    ) (newCtx sdk.Context, err error) {
        var anteHandler sdk.AnteHandler

        txWithExtensions, ok := tx.(authante.HasExtensionOptionsTx)
        if ok {
            opts := txWithExtensions.GetExtensionOptions()
            if len(opts) > 0 {
                switch typeURL := opts[0].GetTypeUrl(); typeURL {
                case "/ethermint.evm.v1.ExtensionOptionsEthereumTx":
                    // handle as *evmtypes.MsgEthereumTx
                    anteHandler = newEVMAnteHandler(options)
                case "/ethermint.types.v1.ExtensionOptionsWeb3Tx":
                    // handle as normal Cosmos SDK tx, except signature is checked for EIP712 representation
                    anteHandler = newLegacyCosmosAnteHandlerEip712(options)
                case "/ethermint.types.v1.ExtensionOptionDynamicFeeTx":
                    // cosmos-sdk tx with dynamic fee extension
                    anteHandler = newCosmosAnteHandler(options)
                default:
                    return ctx, errorsmod.Wrapf(
                        errortypes.ErrUnknownExtensionOptions,
                        "rejecting tx with unsupported extension option: %s", typeURL,
                    )
                }

                return anteHandler(ctx, tx, sim)
            }
        }

        // handle as totally normal Cosmos SDK tx
        switch tx.(type) {
        case sdk.Tx:
            anteHandler = newCosmosAnteHandler(options)
        default:
            return ctx, errorsmod.Wrapf(errortypes.ErrUnknownRequest, "invalid transaction type: %T", tx)
        }

        return anteHandler(ctx, tx, sim)
    }
}

MsgEthereumTx는 선언된 decorator들을 거쳐 인입되고, 최종적으로 블록에 포함되면 evm module의 keeper 동작에 따라 처리된다.

Conclusion

Evmos는 내부적으로 tx의 변환, 캡슐화 과정을 통해 기존의 ethereum 개발자들이 큰힘을 들이지 않고 cosmos에 안착시킬 수 있도록 지원하는 멋진 체인이라 생각한다.

타 체인 플랫폼에서 수행하는 플랫폼 간의 연결 및 호환성 R&D 보다 Cosmos는 더욱 적극적이다. 이미 많은 사용자들을 보유한 거대 플랫폼(ethereum)과의 연동을 위해서 보다 작은 플랫폼(cosmos)이 시도 하는 것은 당연할 수 있지만, 이렇게 다양한 시도를 보면서 Cosmos의 비전인 Interchain을 실현하기 위해 다각도에서 노력하는 생태계 구성원들의 결과물을 보며 필자도 더 노력해서 생태계에 건강한 기여를 할 수 있도록 해야겠다.

참고
https://github.com/evmos/evmos

 

728x90
반응형