Ethereum Tx Type & Signature

#ethereum #blockchain #cryptography #golang

Tx type별 특징

1. LegacyTx(EIP-155)

type LegacyTx struct {
	Nonce     uint64          `json:"nonce"`
	GasPrice  *big.Int        `json:"gasPrice"`
	Gas       uint64          `json:"gas"`
	To        *common.Address `json:"to"`
	Value     *big.Int        `json:"value"`
	Data      []byte          `json:"data"`
	V         *big.Int        `json:"v"`
	R         *big.Int        `json:"r"`
	S         *big.Int        `json:"s"`
}
rlp([nonce, gasPrice, gasLimit, to, value, data, v, r, s])

https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md

2. AccessListTx(EIP-2930)

type AccessListTx struct {
	ChainID    *big.Int        `json:"chainId"`
	Nonce      uint64          `json:"nonce"`
	GasPrice   *big.Int        `json:"gasPrice"`
	Gas        uint64          `json:"gas"`
	To         *common.Address `json:"to"`
	Value      *big.Int        `json:"value"`
	Data       []byte          `json:"data"`
	AccessList AccessList      `json:"accessList"`
	V          *big.Int        `json:"v"`
	R          *big.Int        `json:"r"`
	S          *big.Int        `json:"s"`
}
0x01 || rlp([chainId, nonce, gasPrice, gasLimit, to, value, data, accessList, signatureYParity, signatureR, signatureS])

3. DynamicFeeTx(EIP-1559)

type DynamicFeeTx struct {
	ChainID    *big.Int        `json:"chainId"`
	Nonce      uint64          `json:"nonce"`
	GasTipCap  *big.Int        `json:"maxPriorityFeePerGas"`
	GasFeeCap  *big.Int        `json:"maxFeePerGas"`
	Gas        uint64          `json:"gas"`
	To         *common.Address `json:"to"`
	Value      *big.Int        `json:"value"`
	Data       []byte          `json:"data"`
	AccessList AccessList      `json:"accessList"`
	V          *big.Int        `json:"v"`
	R          *big.Int        `json:"r"`
	S          *big.Int        `json:"s"`
}
0x02 || rlp([chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, accessList, signatureYParity, signatureR, signatureS])

4. BlobTx(EIP-4844)

EIP-4844는 “Shard Blob Transactions”라는 제목으로 제안됨. (Proto-Danksharding)

type BlobTx struct {
	ChainID    *uint256.Int
	Nonce      uint64
	GasTipCap  *uint256.Int // a.k.a. maxPriorityFeePerGas
	GasFeeCap  *uint256.Int // a.k.a. maxFeePerGas
	Gas        uint64
	To         common.Address
	Value      *uint256.Int
	Data       []byte
	AccessList AccessList
	BlobFeeCap *uint256.Int // a.k.a. maxFeePerBlobGas
	BlobHashes []common.Hash

	// A blob transaction can optionally contain blobs. This field must be set when BlobTx
	// is used to create a transaction for signing.
	Sidecar *BlobTxSidecar `rlp:"-"`

	// Signature values
	V *uint256.Int `json:"v" gencodec:"required"`
	R *uint256.Int `json:"r" gencodec:"required"`
	S *uint256.Int `json:"s" gencodec:"required"`
}
0x03 || rlp([
    chainId,
    nonce,
    maxPriorityFeePerGas,
    maxFeePerGas,
    gasLimit,
    to,
    value,
    data,
    accessList,
    maxFeePerDataGas,
    blobVersionedHashes,
    signatureYParity,
    signatureR,
    signatureS
])

5. Transaction 구조 및 tx RLP encoding 코드

type Transaction struct {
	inner TxData    // Consensus contents of a transaction
	time  time.Time // Time first seen locally (spam avoidance)

	// caches
	hash atomic.Pointer[common.Hash]
	size atomic.Uint64
	from atomic.Pointer[sigCache]
}

type TxData interface {
	txType() byte // returns the type ID
	copy() TxData // creates a deep copy and initializes all fields

	chainID() *big.Int
	accessList() AccessList
	data() []byte
	gas() uint64
	gasPrice() *big.Int
	gasTipCap() *big.Int
	gasFeeCap() *big.Int
	value() *big.Int
	nonce() uint64
	to() *common.Address

	rawSignatureValues() (v, r, s *big.Int)
	setSignatureValues(chainID, v, r, s *big.Int)

	// effectiveGasPrice computes the gas price paid by the transaction, given
	// the inclusion block baseFee.
	//
	// Unlike other TxData methods, the returned *big.Int should be an independent
	// copy of the computed value, i.e. callers are allowed to mutate the result.
	// Method implementations can use 'dst' to store the result.
	effectiveGasPrice(dst *big.Int, baseFee *big.Int) *big.Int

	encode(*bytes.Buffer) error
	decode([]byte) error
}
func (tx *Transaction) Hash() common.Hash {
	if hash := tx.hash.Load(); hash != nil {
		return *hash
	}

	var h common.Hash
	if tx.Type() == LegacyTxType {
		h = rlpHash(tx.inner)
	} else {
		h = prefixedRlpHash(tx.Type(), tx.inner)
	}
	tx.hash.Store(&h)
	return h
}
func prefixedRlpHash(prefix byte, x interface{}) (h common.Hash) {
	sha := hasherPool.Get().(crypto.KeccakState)
	defer hasherPool.Put(sha)
	sha.Reset()
	sha.Write([]byte{prefix})
	rlp.Encode(sha, x)
	sha.Read(h[:])
	return h
}

ECDSA ecrecover 기능

1. 비대칭키 암호화 서명의 검증

2. 코드를 통해 확인하기

evmos의 Tx msg

"msg": {
        "txHash": "ce2a75e27a92d7ed26fe89b2eebc713c58eea81f938bd7e84155326bca7ad5af",
        "code": 5,
        "height": 22460308,
        "time": 1722256590257,
        "chainId": "evmos_9001-2",
        "chainIdentifier": "evmos_9001",
        "relation": "error",
        "msgIndex": 0,
        "msg": {
          "@type": "/ethermint.evm.v1.MsgEthereumTx",
          "data": {
            "@type": "/ethermint.evm.v1.DynamicFeeTx", // txType === 2
            "chain_id": "9001",
            "nonce": "6408",
            "gas_tip_cap": "27500000000",
            "gas_fee_cap": "27500000000",
            "gas": "700000",
            "to": "0x2A9C55b6Dc56da178f9f9a566F1161237b73Ba66",
            "value": "96250000000000000000",
            "data": "k3xUFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACcQ",
            "accesses": [ ],
            "v": null, // 0
            "r": "CB4oyKpB+4Yg07dyy2wI9A57fJtlN2vgkBrpJa9FNt0=",
            "s": "WlvzV0aE9+GZ92SjneqA9zPuTU0bLnqUgDJzY2NhXGE="
          },
          "size": 0,
          "hash": "0xb8bc6e9e090cc12a15129f208022af5a41452e807c4586a43491b40bc944f83f",
          "from": ""
        },
        "eventStartIndex": 0,
        "eventEndIndex": 0,
        "search": "",
        "meta": {
          "error": "Invalid bech32PrefixAccAddr"
        }
      },
      "prices": { }
    }

코드

from address: 0x7500A226f292156c63176a89BeA7F95335B4E0e2

7500a226f292156c63176a89bea7f95335b4e0e2

https://github.com/chemonoworld/ts-ecrecover-test.git

import * as secp from '@noble/secp256k1';
import {PubKeySecp256k1, Hash} from '@keplr-wallet/crypto'
import {utils, UnsignedTransaction} from 'ethers'

const start = async() => {
    const data = {
        "@type": "/ethermint.evm.v1.DynamicFeeTx",
        "chain_id": "9001",
        "nonce": "6408",
        "gas_tip_cap": "27500000000",
        "gas_fee_cap": "27500000000",
        "gas": "700000",
        "to": "0x2A9C55b6Dc56da178f9f9a566F1161237b73Ba66",
        "value": "96250000000000000000",
        "data": "k3xUFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACcQ",
        "accesses": [ ],
        "v": null,
        "r": "CB4oyKpB+4Yg07dyy2wI9A57fJtlN2vgkBrpJa9FNt0=",
        "s": "WlvzV0aE9+GZ92SjneqA9zPuTU0bLnqUgDJzY2NhXGE="
    };
    // additional tx msg data
    // "txHash": "ce2a75e27a92d7ed26fe89b2eebc713c58eea81f938bd7e84155326bca7ad5af",
    // "code": 5,
    // "height": 22460308,
    // "time": 1722256590257,
    // "chainId": "evmos_9001-2",
    // "chainIdentifier": "evmos_9001",
    // "size": 0,
    // "hash": "0xb8bc6e9e090cc12a15129f208022af5a41452e807c4586a43491b40bc944f83f",
    // "from": ""

    // mintscan
    // https://www.mintscan.io/evmos/tx/ce2a75e27a92d7ed26fe89b2eebc713c58eea81f938bd7e84155326bca7ad5af/?height=22460308

    const {r, s, v} = data;
    console.log("r: ", Buffer.from(r, 'base64').toString('hex'))
    console.log("s: ",Buffer.from(s, 'base64').toString('hex'))
    const signatureBuffer = Buffer.concat([Buffer.from(r, 'base64'), Buffer.from(s, 'base64')]);
    const signature = secp.Signature.fromCompact(signatureBuffer);
    const rec = v === null ? parseInt(Buffer.alloc(1, 0x0).toString('hex'), 16) : parseInt(Buffer.from(v, 'base64').toString('hex'), 16);
    console.log("recovery: ", rec)
    const signatureWithRecovery = signature.addRecoveryBit(rec);

    const transaction: UnsignedTransaction = {
        type: 2, // DynamicFeeTx
        chainId: Number(data.chain_id),
        nonce: Number(data.nonce),
        maxPriorityFeePerGas: BigInt(data.gas_tip_cap),
        maxFeePerGas: BigInt(data.gas_fee_cap),
        gasLimit: BigInt(data.gas),
        to: data.to,
        value: BigInt(data.value),
        data: Buffer.from(data.data, 'base64'),
        accessList: [...data.accesses],
    }

    const serialized = utils.serializeTransaction(transaction);
    console.log("serialized: ",serialized)
    const encoded = Buffer.from(serialized.slice(2), 'hex')

    const prefixedMsg = utils.concat([encoded]);
    const hash = utils.keccak256(new Uint8Array(Buffer.from(prefixedMsg))).slice(2)

    const pubkey = signatureWithRecovery.recoverPublicKey(hash);
    const hexPubkey = pubkey.toHex();
    console.log("hex pubkey: ", hexPubkey)
    const pubkeyBuffer = Buffer.from(hexPubkey, 'hex')

    const keplrPubkey = new PubKeySecp256k1(pubkeyBuffer);
    const hexAddress = Buffer.from(keplrPubkey.getEthAddress()).toString('hex')
    console.log("hex address: ", hexAddress)
}   

start();