Search code examples
goblockchainethereumsmartcontractsgo-ethereum

Why signed tx should be encoded to bytes before sending raw transaction?


I would like to write the Ethereum blockchain with a contract call. I already found two solutions which are almost the same, but one of them is manipulating the signed transaction, doing some byte encodings before sending it and I couldn't figure out why. My question is that why solution #2 and solution #3 uses the extra lines compared to solution #1? What is the purpose of the extra byte manipulation part? signedTx and txToSend are both *types.Transaction types, I don't understand why is it needed to do the encodings. The documentation of the go-ethereum package states that:

SendTransaction injects a signed transaction into the pending pool for execution.

It doesn't give further information about the tx and types.SignTx() returns *types.Transaction type.

Solution #1

This is the simplest solution without doing any manipulation with signedTx.

tx := types.NewTransaction(nonce, toAddress, value, gasLimit, gasPrice, data)
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey)
if err != nil {
    log.Fatal(err)
}
txErr := client.SendTransaction(context.Background(), tx)
if txErr != nil {
    log.Fatalf("Error calling contract: %v", err)
}

Solution #2

This is the implementation used by the Go Ethereum Book's creating raw transaction and sending raw transaction part.

tx_signed := types.NewTransaction(nonce, toAddress, value, gasLimit, gasPrice, data)

signedTx, err := types.SignTx(tx_signed, types.NewEIP155Signer(chainID), privateKey)
if err != nil {
  log.Fatal(err)
}

ts := types.Transactions{signedTx}
rawTxBytes := ts.GetRlp(0)
rawTxHex := hex.EncodeToString(rawTxBytes)
rawBytes, err := hex.DecodeString(rawTxHex)

tx := new(types.Transaction)
rlp.DecodeBytes(rawBytes, &tx)

txErr := client.SendTransaction(context.Background(), tx)
if txErr != nil {
    log.Fatalf("Error calling contract: %v", err)
}

Solution #3

This implementation is almost the same as the previous, but it uses the newer EncodeIndex(i int, w *bytes.Buffer) function for the byte manipulation. Source

tx_signed := types.NewTransaction(nonce, toAddress, value, gasLimit, gasPrice, data)

signedTx, err := types.SignTx(tx_signed, types.NewEIP155Signer(chainID), privateKey)
if err != nil {
  log.Fatal(err)
}

ts := types.Transactions{signedTx}
b := new(bytes.Buffer)
ts.EncodeIndex(0, b)
rawTxBytes := b.Bytes()

txToSend := new(types.Transaction)
rlp.DecodeBytes(rawTxBytes, &txToSend)
txErr := client.SendTransaction(context.Background(), tx)
if txErr != nil {
    log.Fatalf("Error calling contract: %v", err)
}

Solution

  • There are different types of ethereum transactions: EIP1559, EIP2711, EIP2718

    Not all geth clients supports each transaction type. I think the transactions that encoded in your question are EIP2718.

    https://blog.mycrypto.com/new-transaction-types-on-ethereum

    From the above article:

    EIP-2718 defines a new generalised envelope for typed transactions. In the new standard, transactions look like this:

    TransactionType || TransactionPayload

    Where the fields are defined as:

    TransactionType: a number between 0 and 0x7f, for a total of 128 possible transaction types. TransactionPayload: an arbitrary byte array, defined by the transaction type. These fields are concatenated (combined) to form a typed transaction. The standard does not describe a format for the transaction payload; it can be any arbitrary series of bytes, encoded with any encoder as defined by the new transaction type (e.g., RLP, SSZ, …). Simple byte concatenation was chosen because it's trivial to read the first byte of a byte array without the need for any libraries or tools: You don't need an RLP or SSZ parser to check the transaction type.

    or maybe a different EIP type. check your geth client version, and which ones that support