이더리움 트랜잭션은 송신자와 수신자가 정해져 있다. 그런데 go-ethereum
으로 트랜잭션을 조회하면 송신자 정보를 바로 얻을 수 없다.
Go에서의 예시에 앞서 트랜잭션을 조회하는 다른 방식들에 대해 먼저 살펴보겠다.
이더스캔으로 테스트넷인 세폴리아에서 임의의 트랜잭션을 조회했을 때의 송신자와 수신자는 다음과 같이 나온다.
to
는 EOA가 될 수도 있고 CA가 될 수도 있으며 from
은 무조건 EOA이다.
이렇게 그냥 해시를 가지고 바로 얻을 수 있다.
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
필드를 포함하고 있다. 이는 이더스캔 조회 결과와 동일하다.
여기까지 보면 송신자 정보는 당연히 확인이 되는 것 같고, 블록체인스럽게 생각해봐도 그게 맞는 것 같다.
하지만 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
과 같은 결과가 나온다.
그렇다면 geth
로 from
조회 그냥 하면 되는 거 아니냐? 라고 할 수도 있는데 막상 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)
파라미터로 Signer
와 Transaction
을 받는데, Signer
는 LatestSignerForChainID
함수를 통해 얻을 수 있다.
sender, _ := types.Sender(types.LatestSignerForChainID(tx.ChainId()), tx)
fmt.Println(sender)
// 0x35503B7013a79257A8aa91451A1e7861109FD2E7
이렇게 geth
로 트랜잭션 송신자 정보를 얻을 수 있다.