Go-ethereum으로 트랜잭션 sender 구하는 법

박재훈·2023년 1월 7일
0

GO

목록 보기
10/23
post-custom-banner

이더리움 트랜잭션은 송신자와 수신자가 정해져 있다. 그런데 go-ethereum으로 트랜잭션을 조회하면 송신자 정보를 바로 얻을 수 없다.
Go에서의 예시에 앞서 트랜잭션을 조회하는 다른 방식들에 대해 먼저 살펴보겠다.

Etherscan

이더스캔으로 테스트넷인 세폴리아에서 임의의 트랜잭션을 조회했을 때의 송신자와 수신자는 다음과 같이 나온다.

to는 EOA가 될 수도 있고 CA가 될 수도 있으며 from은 무조건 EOA이다.
이렇게 그냥 해시를 가지고 바로 얻을 수 있다.

Node.js & ethers

Node.js에서 ethers 라이브러리로 특정 해시를 조회하는 코드는 다음과 같다.

const provider = new ethers.providers.JsonRpcProvider("RPC_URL");
const hash = "0xfe78504b06b1152b0e07d753ef7e48c904f659eea9436d9eda6d579129e5aec0";

console.log(await provider.getTransaction(hash));
console.log(await provider.getTransactionReceipt(hash));

getTransaction을 통해 트랜잭션 자체에 대한 정보를 얻을 수 있으며 getTransactionReceipt를 통해 receipt 정보를 얻을 수 있다.

각각을 출력한 내용은 다음과 같다.

// tx
{
  hash: '0xfe78504b06b1152b0e07d753ef7e48c904f659eea9436d9eda6d579129e5aec0',
  type: 0,
  accessList: null,
  blockHash: '0x7cd24df5591051f6a2a0c34429ab34d00135e2536f3065447fdf4437782e37f5',
  blockNumber: 2638035,
  transactionIndex: 0,
  confirmations: 15,
  from: '0x35503B7013a79257A8aa91451A1e7861109FD2E7',
  gasPrice: BigNumber { _hex: '0x3b9aca00', _isBigNumber: true },
  gasLimit: BigNumber { _hex: '0x4c4b40', _isBigNumber: true },
  to: '0x9ed6a23b7264A1b19e0F4e9d38047b3E87Ba1F5a',
  value: BigNumber { _hex: '0x00', _isBigNumber: true },
  nonce: 44,
  data: '0x2cfd3005000000000000000000000000cb3f757c1185c1d7e517cbc0be246b74fe35388c1e8c1f2d441b6d745c53b6dee61a2ad369189e650de6987b1c2399150bf35ad6',
  r: '0x7094ed2432467d9438e6b56d4fb0d8dbe6be8825b761325cc2effaacd97faa21',
  s: '0x644755d36576277466a3008e55f45f4e7b0f4c083baba45029c42f0ef199e674',
  v: 22310258,
  creates: null,
  chainId: 11155111,
  wait: [Function (anonymous)]
}

// receipt
{
  to: '0x9ed6a23b7264A1b19e0F4e9d38047b3E87Ba1F5a',
  from: '0x35503B7013a79257A8aa91451A1e7861109FD2E7',
  contractAddress: null,
  transactionIndex: 0,
  gasUsed: BigNumber { _hex: '0xc7b9', _isBigNumber: true },
  logsBloom: '0x00000000000000000000000000020000000000000400000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000100000800000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000200000040000000000000020200200000000000000000000000080000000000000000000000020000000000000',
  blockHash: '0x7cd24df5591051f6a2a0c34429ab34d00135e2536f3065447fdf4437782e37f5',
  transactionHash: '0xfe78504b06b1152b0e07d753ef7e48c904f659eea9436d9eda6d579129e5aec0',
  logs: [
    {
      transactionIndex: 0,
      blockNumber: 2638035,
      transactionHash: '0xfe78504b06b1152b0e07d753ef7e48c904f659eea9436d9eda6d579129e5aec0',
      address: '0x9ed6a23b7264A1b19e0F4e9d38047b3E87Ba1F5a',
      topics: [Array],
      data: '0x0000000000000000000000009ed6a23b7264a1b19e0f4e9d38047b3e87ba1f5a0000000000000000000000009ed6a23b7264a1b19e0f4e9d38047b3e87ba1f5a',
      logIndex: 0,
      blockHash: '0x7cd24df5591051f6a2a0c34429ab34d00135e2536f3065447fdf4437782e37f5'
    }
  ],
  blockNumber: 2638035,
  confirmations: 15,
  cumulativeGasUsed: BigNumber { _hex: '0xc7b9', _isBigNumber: true },
  effectiveGasPrice: BigNumber { _hex: '0x3b9aca00', _isBigNumber: true },
  status: 1,
  type: 0,
  byzantium: true
}

위처럼 온갖 정보를 다 확인할 수 있으며, 두 객체 전부 from 필드를 포함하고 있다. 이는 이더스캔 조회 결과와 동일하다.
여기까지 보면 송신자 정보는 당연히 확인이 되는 것 같고, 블록체인스럽게 생각해봐도 그게 맞는 것 같다.

Go & Go-ethereum

하지만 geth는 다르다. Node.js로 짤 때 당연히 되던 게 안돼서 너무나 당황했었는데..
일단 먼저 geth로 트랜잭션 정보를 얻는 코드를 짜면 다음과 같이 된다.

ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()

client, _ := ethclient.DialContext(ctx, "RPC_URL")

hash := common.HexToHash("0xfe78504b06b1152b0e07d753ef7e48c904f659eea9436d9eda6d579129e5aec0")

tx, _, _ := client.TransactionByHash(ctx, hash)
fmt.Printf("%+v\n", tx)

receipt, _ := client.TransactionReceipt(ctx, hash)
fmt.Printf("%+v\n", receipt)

Node.js에서처럼 트랜잭션과 receipt 정보를 모두 가져왔다.
이걸 출력해보면 아래처럼 나온다.

// tx
&{inner:0xc0000a7320 time:{wall:13899928115880187336 ext:933670033 loc:0x165f680} hash:{v:<nil>} size:{v:<nil>} from:{v:{signer:0xc0000c88c0 from:[53 80 59 112 19 167 146 87 168 170 145 69 26 30 120 97 16 159 210 231]}}}

// receipt
&{Type:0 PostState:[] Status:1 CumulativeGasUsed:51129 Bloom:[0 0 0 0 0 0 0 0 0 0 0 0 0 2 0 0 0 0 0 0 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 32 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 0 0 0 0 0 0 1 0 0 8 0 0 0 0 0 0 0 8 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 8 0 0 0 0 0 0 0 0 0 0 0 0 0 0 32 0 0 4 0 0 0 0 0 0 0 32 32 2 0 0 0 0 0 0 0 0 0 0 0 0 128 0 0 0 0 0 0 0 0 0 0 0 32 0 0 0 0 0 0] Logs:[0xc000288160] TxHash:0xfe78504b06b1152b0e07d753ef7e48c904f659eea9436d9eda6d579129e5aec0 ContractAddress:0x0000000000000000000000000000000000000000 GasUsed:51129 BlockHash:0x7cd24df5591051f6a2a0c34429ab34d00135e2536f3065447fdf4437782e37f5 BlockNumber:+2638035 TransactionIndex:0}

너무 길긴 한데, 그래도 꾹 참고 확인해보면 먼저 receipt에는 확실히 from이 없다.
이건 geth 코드를 확인해봐도 그렇다.

// Receipt represents the results of a transaction.
type Receipt struct {
	// Consensus fields: These fields are defined by the Yellow Paper
	Type              uint8  `json:"type,omitempty"`
	PostState         []byte `json:"root"`
	Status            uint64 `json:"status"`
	CumulativeGasUsed uint64 `json:"cumulativeGasUsed" gencodec:"required"`
	Bloom             Bloom  `json:"logsBloom"         gencodec:"required"`
	Logs              []*Log `json:"logs"              gencodec:"required"`

	// Implementation fields: These fields are added by geth when processing a transaction.
	// They are stored in the chain database.
	TxHash          common.Hash    `json:"transactionHash" gencodec:"required"`
	ContractAddress common.Address `json:"contractAddress"`
	GasUsed         uint64         `json:"gasUsed" gencodec:"required"`

	// Inclusion information: These fields provide information about the inclusion of the
	// transaction corresponding to this receipt.
	BlockHash        common.Hash `json:"blockHash,omitempty"`
	BlockNumber      *big.Int    `json:"blockNumber,omitempty"`
	TransactionIndex uint        `json:"transactionIndex"`
}

위 코드가 geth에서 Receipt라는 구조체가 정의된 코드인데, 어딜 봐도 from에 대한 내용은 없다.

그런데 tx는 잘 보면 from 필드가 있다.

from:{v:{signer:0xc0000c88c0 from:[53 80 59 112 19 167 146 87 168 170 145 69 26 30 120 97 16 159 210 231]}}

여기서 안쪽의 from에 해당하는 게 바이트 배열로 되어 있는데, 위 값을 16진수 문자열로 바꾸면 놀랍게도

t.Log(hex.EncodeToString([]byte{53, 80, 59, 112, 19, 167, 146, 87, 168, 170, 145, 69, 26, 30, 120, 97, 16, 159, 210, 231}))
// 35503b7013a79257a8aa91451a1e7861109fd2e7

위의 이더스캔과 Node.js를 통해 조회한 from과 같은 결과가 나온다.
그렇다면 gethfrom 조회 그냥 하면 되는 거 아니냐? 라고 할 수도 있는데 막상 Transaction 구조체 정의 코드를 확인해보면

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

	// caches
	hash atomic.Value
	size atomic.Value
	from atomic.Value
}

from 필드는 프라이빗으로 되어 있다.
물론 뭐 당연히 퍼블릭 메소드들도 있긴 한데, pkg.go.dev에서 확인해보면 마찬가지로 from 정보를 얻을 수 있는 퍼블릭 메소드는 존재하지 않는다.

type Transaction
  func MustSignNewTx(prv *ecdsa.PrivateKey, s Signer, txdata TxData) *Transaction
  func NewContractCreation(nonce uint64, amount *big.Int, gasLimit uint64, gasPrice *big.Int, data []byte) *Transaction
  func NewTransaction(nonce uint64, to common.Address, amount *big.Int, gasLimit uint64, ...) *Transaction
  func NewTx(inner TxData) *Transaction
  func SignNewTx(prv *ecdsa.PrivateKey, s Signer, txdata TxData) (*Transaction, error)
  func SignTx(tx *Transaction, s Signer, prv *ecdsa.PrivateKey) (*Transaction, error)
  func (tx *Transaction) AccessList() AccessList
  func (tx *Transaction) AsMessage(s Signer, baseFee *big.Int) (Message, error)
  func (tx *Transaction) ChainId() *big.Int
  func (tx *Transaction) Cost() *big.Int
  func (tx *Transaction) Data() []byte
  func (tx *Transaction) DecodeRLP(s *rlp.Stream) error
  func (tx *Transaction) EffectiveGasTip(baseFee *big.Int) (*big.Int, error)
  func (tx *Transaction) EffectiveGasTipCmp(other *Transaction, baseFee *big.Int) int
  func (tx *Transaction) EffectiveGasTipIntCmp(other *big.Int, baseFee *big.Int) int
  func (tx *Transaction) EffectiveGasTipValue(baseFee *big.Int) *big.Int
  func (tx *Transaction) EncodeRLP(w io.Writer) error
  func (tx *Transaction) Gas() uint64
  func (tx *Transaction) GasFeeCap() *big.Int
  func (tx *Transaction) GasFeeCapCmp(other *Transaction) int
  func (tx *Transaction) GasFeeCapIntCmp(other *big.Int) int
  func (tx *Transaction) GasPrice() *big.Int
  func (tx *Transaction) GasTipCap() *big.Int
  func (tx *Transaction) GasTipCapCmp(other *Transaction) int
  func (tx *Transaction) GasTipCapIntCmp(other *big.Int) int
  func (tx *Transaction) Hash() common.Hash
  func (tx *Transaction) MarshalBinary() ([]byte, error)
  func (t *Transaction) MarshalJSON() ([]byte, error)
  func (tx *Transaction) Nonce() uint64
  func (tx *Transaction) Protected() bool
  func (tx *Transaction) RawSignatureValues() (v, r, s *big.Int)
  func (tx *Transaction) Size() common.StorageSize
  func (tx *Transaction) To() *common.Address
  func (tx *Transaction) Type() uint8
  func (tx *Transaction) UnmarshalBinary(b []byte) error
  func (t *Transaction) UnmarshalJSON(input []byte) error
  func (tx *Transaction) Value() *big.Int
  func (tx *Transaction) WithSignature(signer Signer, sig []byte) (*Transaction, error)

그럼 어떻게?

Sender 함수를 쓰면 된다.

func types.Sender(signer types.Signer, tx *types.Transaction) (common.Address, error)

파라미터로 SignerTransaction을 받는데, SignerLatestSignerForChainID 함수를 통해 얻을 수 있다.

sender, _ := types.Sender(types.LatestSignerForChainID(tx.ChainId()), tx)
fmt.Println(sender)
// 0x35503B7013a79257A8aa91451A1e7861109FD2E7

이렇게 geth로 트랜잭션 송신자 정보를 얻을 수 있다.

profile
생각대로 되지 않을 때, 비로소 코딩은 재미있는 법.
post-custom-banner

0개의 댓글