El paquete ethclient de Go-Ethereum (Geth) proporciona un envoltorio de API para las solicitudes JSON-RPC a la red Ethereum, similar a web3.js y ethers.js.
Sin embargo, algunas capacidades de JSON-RPC, como el rastreo de transacciones (transaction tracing), no están expuestas en la API de ethclient (ni en web3.js ni en ethers.js).
Este tutorial muestra cómo usar ethclient para acciones donde ethclient soporta la llamada JSON-RPC y cuando no lo hace.
Como ilustra el siguiente diagrama, a veces podemos lograr una acción usando una API en ethclient, pero a veces tenemos que elaborar la llamada RPC nosotros mismos:

Al final del tutorial, mostraremos cómo ejecutar una transacción blob (blob transaction), para la cual Ethereum agregó soporte recientemente en la actualización Decun.
También llevaremos a cabo algunos conceptos relacionados con las transacciones de Ethereum, como la firma y verificación de firmas digitales.
Requisitos previos
- El lenguaje Go debe estar instalado en su computadora; si no es así, consulte las instrucciones de descarga.
- Conocimientos básicos de programación en Go.
Empezando
Usaremos la red Sepolia a lo largo de este tutorial, pero lo que mostramos también funcionará en la mainnet u otras testnets. Asegúrese de tener Sepolia ETH.
Operaciones que realizaremos en este tutorial:
- Obtener el precio del gas sugerido (suggested gas price) en la red
- Estimar el gas para una transacción
- Construir y enviar una transacción raw EIP1559
- Firmar y verificar mensajes de Ethereum
- Recuperar el nonce de una cuenta (número de transacciones)
- Rastrear una transacción (tracing)
- Finalmente, enviar una transacción blob EIP-4844
Para comenzar, cree una nueva carpeta de proyecto, ábrala e inicialícela con:
go mod init eth-rpc
Acabamos de crear nuestro módulo de proyecto. Si tuvo éxito, debería ver un archivo ‘go.mod’ que se ve así

Instale las dependencias necesarias:
go get -u github.com/ethereum/go-ethereum@v1.13.14
go get github.com/ethereum/go-ethereum/rpc@v1.13.14
Esto generará un archivo ‘go.sum’.
Consejo para la solución de problemas:
Si encuentra problemas relacionados con el módulo, intente lo siguiente:
-
Elimine sus archivos ‘go.mod’ y ‘go.sum’ y vuelva a inicializar con
go mod init eth-rpc. -
Ejecute
go mod tidypara sincronizar las dependencias. -
Si el problema persiste, borre la caché de su módulo con
go clean -modcachey repita los pasos 1 y 2.
Ahora pegue el siguiente código en un archivo ‘main.go’ dentro del proyecto:
package main
import "fmt"
const (
sepoliaRpcUrl = "https://rpc.sepolia.ethpandaops.io" // sepolia rpc url
mainnetRpcUrl = "https://rpc.builder0x69.io/" // mainnet rpc url
from = "0x571B102323C3b8B8Afb30619Ac1d36d85359fb84"
to = "0x4924Fb92285Cb10BC440E6fb4A53c2B94f2930c5"
data = "Hello Ethereum!"
privKey = "2843e08c0fa87258545656e44955aa2c6ca2ebb92fa65507e4e5728570d36662"
gasLimit = uint64(21500) // adjust this if necessary
wei = uint64(0) // 0 Wei
)
func main() {
fmt.Println("using ethclient...")
}
Actualizaremos el archivo main.go a medida que avancemos.
Puede ejecutar esto con go run main.go
Ahora, comencemos a crear las funciones de nuestro proyecto.
1. Obtener el precio del gas sugerido en la red
Con el paquete ethclient de Geth, podemos usar la API SuggestGasPrice para establecer el precio del gas de nuestra transacción de manera adecuada a las condiciones actuales de la red.
Detrás de escena, este método llama a la API JSON-RPC eth_gasPrice.
Cree un archivo getGasPrice.go en el directorio del proyecto y pegue el siguiente código:
package main
import (
"context"
"fmt"
"log"
"github.com/ethereum/go-ethereum/ethclient"
)
// getSuggestedGasPrice connects to an Ethereum node via RPC and retrieves the current suggested gas price.
func getSuggestedGasPrice(rpcUrl string) {
// Connect to the Ethereum network using the provided RPC URL.
client, err := ethclient.Dial(rpcUrl)
if err != nil {
log.Fatalf("Failed to connect to the Ethereum client: %v", err)
}
// Retrieve the currently suggested gas price for a new transaction.
gasPrice, err := client.SuggestGasPrice(context.Background())
if err != nil {
log.Fatalf("Failed to suggest gas price: %v", err)
}
// Print the suggested gas price to the terminal.
fmt.Println("Suggested Gas Price:", gasPrice.String())
}
Ahora actualice la función main en main.go:
func main() {
fmt.Println("using ethclient...")
getSuggestedGasPrice(sepoliaRpcUrl)
// get gas price on sepolia testnet. This was just added.
}
y ejecute:
go run .
La razón por la que usamos el comando go run . en lugar de go run main.go es para compilar y ejecutar todos los archivos fuente de Go dentro del directorio actual que pertenecen al mismo paquete. Esto incluye el archivo main.go (que contiene la función main) y cualquier otro archivo, como el que contiene nuestra función getSuggestedGasPrice.
Usaremos este comando de ahora en adelante.
Después de ejecutar el comando, el precio del gas sugerido debería imprimirse en la terminal. Tenga en cuenta que está en Wei.

2. Estimar el uso de gas de una transacción
ethclient también incluye un método EstimateGas. Devuelve una estimación de la cantidad de gas requerida para procesar exitosamente una transacción.
El método EstimateGas llama a la API JSON-RPC eth_estimateGas con el mensaje construido como parámetro.
Cree un archivo estimateGas.go y pegue el siguiente código:
package main import (
"context"
"log"
"math/big"
"strings"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/ethclient"
)
// estimateGas tries to estimate the suggested amount of gas that is required to execute a given transaction.
func estimateGas(rpcUrl, from, to, data string, value uint64) uint64 {
// Establish an RPC connection to the specified RPC url
client, err := ethclient.Dial(rpcUrl)
if err != nil {
log.Fatalln(err)
}
var ctx = context.Background()
var (
fromAddr = common.HexToAddress(from) // Convert the from address from hex to an Ethereum address.
toAddr = common.HexToAddress(to) // Convert the to address from hex to an Ethereum address.
amount = new(big.Int).SetUint64(value) // Convert the value from uint64 to *big.Int.
bytesData []byte
)
// Encode the data if it's not already hex-encoded.
if data != "" {
if ok := strings.HasPrefix(data, "0x"); !ok {
data = hexutil.Encode([]byte(data))
}
bytesData, err = hexutil.Decode(data)
if err != nil {
log.Fatalln(err)
}
}
// Create a message which contains information about the transaction.
msg := ethereum.CallMsg{
From: fromAddr,
To: &toAddr,
Gas: 0x00,
Value: amount,
Data: bytesData,
}
// Estimate the gas required for the transaction.
gas, err := client.EstimateGas(ctx, msg)
if err != nil {
log.Fatalln(err)
}
return gas
}
func main() {
fmt.Println("using ethclient...")
getSuggestedGasPrice(sepoliaRpcUrl)
eGas := estimateGas(sepoliaRpcUrl, from, to, data, wei) // This was just added.
fmt.Println("\nestimate gas for the transaction is:", eGas) // This was just added.
}
Ejecute el código con go run .
Deberíamos obtener esto:

3. Construcción de una transacción raw EIP1559
Las transacciones raw de Ethereum son transacciones en su forma no procesada, codificadas utilizando el método de serialización Recursive Length Prefix (RLP).
Esta técnica de codificación es utilizada por la Capa de Ejecución (Execution Layer o EL) de Ethereum para serializar y deserializar datos.
Los datos de la transacción raw son la codificación del nonce, la dirección del destinatario (to), el valor de la transacción, la carga útil de datos (data payload) y el límite de gas (gas limit).
Tipos de transacciones
Al crear manualmente transacciones raw para Ethereum, hay varios tipos de transacciones para elegir, que van desde la antigua transacción legacy (también conocida como type 0), con especificación explícita del precio del gas, hasta las transacciones EIP-1559 (type 2), que introducen una tarifa base (base fee), una tarifa de prioridad (priority fee o propina para los mineros) y una tarifa máxima por gas (max fee per gas) para una mejor previsibilidad del precio del gas.
La tarifa base (base fee) es determinada por la red y permanece fija para todas las transacciones dentro de un bloque. Sin embargo, se ajusta entre bloques según la congestión de la red. Puede influir en la prioridad de su transacción aumentando la tarifa de prioridad (propina) ofrecida a los mineros.
Además, existe la transacción EIP-2930 (type 1) y las transacciones blob EIP-4844 (type 3, de las que hablaremos más adelante en este artículo).
Elección de un tipo de transacción en Go con Geth
El cliente Geth, a través de su paquete types, admite estos diversos tipos de transacciones. Para nuestros propósitos, nos centraremos en types.DynamicFeeTx, que corresponde al modelo de transacción EIP-1559.
Todo el proceso no implica hacer ninguna llamada JSON-RPC, simplemente construimos la transacción, la firmamos y la serializamos con el esquema de codificación RLP.
Cree un archivo createEIP1559RawTX.go y pegue el siguiente código:
package main
import (
"bytes"
"context"
"crypto/ecdsa"
"encoding/hex"
"fmt"
"log"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/params"
)
// createRawTransaction creates a raw EIP-1559 transaction and returns it as a hex string.
func createRawTransaction(rpcURL, to, data, privKey string, gasLimit, wei uint64) string {
// Connect to the Ethereum client using the provided RPC URL.
client, err := ethclient.Dial(rpcURL)
if err != nil {
log.Fatalln(err)
}
// Retrieve the chain ID for the target Ethereum network.
chainID, err := client.ChainID(context.Background())
if err != nil {
log.Fatalln(err)
}
// Suggest the base fee for inclusion in a block.
baseFee, err := client.SuggestGasPrice(context.Background())
if err != nil {
log.Fatalln(err)
}
// Suggest a gas tip cap (priority fee) for miner incentive.
priorityFee, err := client.SuggestGasTipCap(context.Background())
if err != nil {
log.Fatalln(err)
}
// Calculate the maximum gas fee cap, adding a 2 GWei margin to the base fee plus priority fee.
increment := new(big.Int).Mul(big.NewInt(2), big.NewInt(params.GWei))
gasFeeCap := new(big.Int).Add(baseFee, increment)
gasFeeCap.Add(gasFeeCap, priorityFee)
// Decode the provided private key.
pKeyBytes, err := hexutil.Decode("0x" + privKey)
if err != nil {
log.Fatalln(err)
}
// Convert the private key bytes to an ECDSA private key.
ecdsaPrivateKey, err := crypto.ToECDSA(pKeyBytes)
if err != nil {
log.Fatalln(err)
}
// Extract the public key from the ECDSA private key.
publicKey := ecdsaPrivateKey.Public()
publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
if !ok {
log.Fatal("Error casting public key to ECDSA")
}
// Compute the Ethereum address of the signer from the public key.
fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA)
// Retrieve the nonce for the signer's account, representing the transaction count.
nonce, err := client.PendingNonceAt(context.Background(), fromAddress)
if err != nil {
log.Fatal(err)
}
// Prepare data payload.
var hexData string
if strings.HasPrefix(data, "0x") {
hexData = data
} else {
hexData = hexutil.Encode([]byte(data))
}
bytesData, err := hexutil.Decode(hexData)
if err != nil {
log.Fatalln(err)
}
// Set up the transaction fields, including the recipient address, value, and gas parameters.
toAddr := common.HexToAddress(to)
amount := new(big.Int).SetUint64(wei)
txData := types.DynamicFeeTx{
ChainID: chainID,
Nonce: nonce,
GasTipCap: priorityFee,
GasFeeCap: gasFeeCap,
Gas: gasLimit,
To: &toAddr,
Value: amount,
Data: bytesData,
}
// Create a new transaction object from the prepared data.
tx := types.NewTx(&txData)
// Sign the transaction with the private key of the sender.
signedTx, err := types.SignTx(tx, types.LatestSignerForChainID(chainID), ecdsaPrivateKey)
if err != nil {
log.Fatalln(err)
}
// Encode the signed transaction into RLP (Recursive Length Prefix) format for transmission.
var buf bytes.Buffer
err = signedTx.EncodeRLP(&buf)
if err != nil {
log.Fatalln(err)
}
// Return the RLP-encoded transaction as a hexadecimal string.
rawTxRLPHex := hex.EncodeToString(buf.Bytes())
return rawTxRLPHex
}
Actualice la función main en main.go:
func main() {
fmt.Println("using ethclient...")
getSuggestedGasPrice(sepoliaRpcUrl)
eGas := estimateGas(sepoliaRpcUrl, from, to, data, wei) fmt.Println("\nestimate gas for the transaction is:", eGas)
rawTxRLPHex := createRawTransaction(sepoliaRpcUrl, to, data, privKey, gasLimit, wei) // This was just added.
fmt.Println("\nRaw TX:\n", rawTxRLPHex) // This was just added.
}
La transacción raw se creará usando la clave privada almacenada en la variable ‘privKey’. Para asegurar una transacción exitosa en la testnet de Sepolia, reemplácela (la clave privada) con una clave privada que contenga ETH de prueba de Sepolia.
Ejecute el código con go run .
Deberíamos obtener la transacción raw, como se muestra a continuación:

Propagaremos una transacción raw a la red en la siguiente sección.
4. Envío de una transacción raw
Después de crear una transacción raw de cualquier tipo, podemos propagarla a la red con la función ‘ethclient.SendTransaction’, que toma la transacción raw decodificada por RLP y realiza una llamada JSON-RPC eth_sendRawTransaction.
Hay un código adicional aquí (la función convertHexField del struct Transaction) que no es obligatorio pero ayuda a imprimir mejor el resultado de la transacción.
Cree un archivo sendRawTX.go en el proyecto y pegue el código a continuación:
package main
import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
"log"
"reflect"
"strconv"
"time"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/rlp"
)
// Transaction represents the structure of the transaction JSON.
type Transaction struct {
Type string `json:"type"`
ChainID string `json:"chainId"`
Nonce string `json:"nonce"`
To string `json:"to"`
Gas string `json:"gas"`
GasPrice string `json:"gasPrice,omitempty"`
MaxPriorityFeePerGas string `json:"maxPriorityFeePerGas"`
MaxFeePerGas string `json:"maxFeePerGas"` Value string `json:"value"`
Input string `json:"input"`
AccessList []string `json:"accessList"`
V string `json:"v"`
R string `json:"r"`
S string `json:"s"`
YParity string `json:"yParity"`
Hash string `json:"hash"`
TransactionTime string `json:"transactionTime,omitempty"`
TransactionCost string `json:"transactionCost,omitempty"`
}
// sendRawTransaction sends a raw Ethereum transaction.
func sendRawTransaction(rawTx, rpcURL string) {
rawTxBytes, err := hex.DecodeString(rawTx)
if err != nil {
log.Fatalln(err)
}
// Initialize an empty Transaction struct to hold the decoded data.
tx := new(types.Transaction)
// Decode the raw transaction bytes from hexadecimal to a Transaction struct.
// This step converts the RLP (Recursive Length Prefix) encoded bytes back into
// a structured Transaction format understood by the Ethereum client.
err = rlp.DecodeBytes(rawTxBytes, &tx)
if err != nil {
log.Fatalln(err)
}
// Establish an RPC connection to the specified RPC url client, err := ethclient.Dial(rpcURL)
if err != nil {
log.Fatalln(err)
}
// Propagate the transaction
err = client.SendTransaction(context.Background(), tx)
if err != nil {
log.Fatalln(err)
}
// Unmarshal the transaction JSON into a struct
var txDetails Transaction
txBytes, err := tx.MarshalJSON()
if err != nil {
log.Fatalln(err)
}
if err := json.Unmarshal(txBytes, &txDetails); err != nil {
log.Fatalln(err)
}
// Add additional transaction details
txDetails.TransactionTime = tx.Time().Format(time.RFC822)
txDetails.TransactionCost = tx.Cost().String()
// Format some hexadecimal string fields to decimal string
convertFields := []string{"Nonce", "MaxPriorityFeePerGas", "MaxFeePerGas", "Value", "Type", "Gas"}
for _, field := range convertFields {
if err := convertHexField(&txDetails, field); err != nil {
log.Fatalln(err)
}
}
// Marshal the struct back to JSON
txJSON, err := json.MarshalIndent(txDetails, "", "\t")
if err != nil {
log.Fatalln(err)
}
// Print the entire JSON with the added fields
fmt.Println("\nRaw TX Receipt:\n", string(txJSON))
}
func convertHexField(tx *Transaction, field string) error {
// Get the type of the Transaction struct
typeOfTx := reflect.TypeOf(*tx)
// Get the value of the Transaction struct
txValue := reflect.ValueOf(tx).Elem()
// Parse the hexadecimal string as an integer
hexStr := txValue.FieldByName(field).String()
intValue, err := strconv.ParseUint(hexStr[2:], 16, 64)
if err != nil {
return err
}
// Convert the integer to a decimal string
decimalStr := strconv.FormatUint(intValue, 10)
// Check if the field exists
_, ok := typeOfTx.FieldByName(field)
if !ok {
return fmt.Errorf("field %s does not exist in Transaction struct", field)
}
// Set the field value to the decimal string
txValue.FieldByName(field).SetString(decimalStr)
return nil
}
Ahora actualice la función main en main.go:
func main() {
fmt.Println("using ethclient...")
getSuggestedGasPrice(sepoliaRpcUrl)
eGas := estimateGas(sepoliaRpcUrl, from, to, data, wei)
fmt.Println("\nestimate gas for the transaction is:", eGas)
rawTxRLPHex := createRawTransaction(sepoliaRpcUrl, to, data, privKey, gasLimit, wei)
fmt.Println("\nRaw TX:\n", rawTxRLPHex)
sendRawTransaction(rawTxRLPHex, sepoliaRpcUrl) // This was just added.
}
Ejecute con go run .

Podemos ver el recibo de la transacción (transaction receipt) en la imagen de arriba
Firma de mensajes de Ethereum (firma digital)
Los mensajes firmados de Ethereum se pueden utilizar para crear sistemas de verificación. Es una forma de verificar la propiedad o el consentimiento sin realizar una transacción on-chain.
Por ejemplo, si el Usuario A firma un mensaje con su clave privada y lo envía a una plataforma, la plataforma toma la dirección pública del usuario, el mensaje y la firma, y verifica si la firma fue efectivamente realizada por el Usuario A; en caso afirmativo, podría servir como autorización para que la plataforma haga algo (cualquiera que haya sido el motivo de la firma).
La firma de mensajes de Ethereum utiliza el algoritmo de firma digital de curva elíptica secp256k1 (ECDSA) para la seguridad criptográfica.
Los mensajes firmados de Ethereum también tienen un prefijo, de modo que son reconocibles y únicos en la red.
El prefijo es: \x19Ethereum Signed Message:\n" + len(message), y luego aplicamos un hash al prefix+message antes de firmarlo: sign(keccak256("\x19Ethereum Signed Message:\n" + len(message) + message))).
Ethereum también tiene un ID de recuperación (recovery ID) que se agrega al último byte de la firma. La firma tiene 65 bytes de longitud, dividida en 3 partes: v, r y s. r son los primeros 32 bytes, s son los siguientes 32 bytes, y v es un byte que representa el ID de recuperación.
El ID de recuperación es 27 (0x1b) o 28 (0x1c) para Ethereum. Usualmente verá esto al final de todas las firmas digitales de Ethereum (o mensajes firmados).
El paquete crypto de Geth utilizado para firmar no agrega el ID de recuperación como lo hace el personal_sign de Metamask, por lo que tenemos que agregarlo manualmente después de firmar con sig[64]+=27 como verá en el código a continuación.
Tenga en cuenta que la firma de mensajes se realiza completamente off-chain y offline. No realiza ninguna llamada JSON-RPC.
Agregue el código a continuación a un archivo ‘signMessage.go’, en el directorio del proyecto.
package mainimport (
"crypto/ecdsa"
"encoding/json"
"fmt"
"log"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
)
// SignatureResponse represents the structure of the signature response.
type SignatureResponse struct {
Address string `json:"address,omitempty"`
Msg string `json:"msg,omitempty"`
Sig string `json:"sig,omitempty"`
Version string `json:"version,omitempty"`
}
// signMessage signs a message using the provided private key.
func signMessage(message, privKey string) (string, string) {
// Convert the private key from hex to ECDSA format
ecdsaPrivateKey, err := crypto.HexToECDSA(privKey)
if err != nil {
log.Fatalln(err)
}
// Construct the message prefix
prefix := []byte(fmt.Sprintf("\x19Ethereum Signed Message:\n%d", len(message))) messageBytes := []byte(message)
// Hash the prefix and message using Keccak-256
hash := crypto.Keccak256Hash(prefix, messageBytes)
// Sign the hashed message
sig, err := crypto.Sign(hash.Bytes(), ecdsaPrivateKey)
if err != nil {
log.Fatalln(err)
}
// Adjust signature ID to Ethereum's format
sig[64] += 27
// Derive the public key from the private key
publicKeyBytes := crypto.FromECDSAPub(ecdsaPrivateKey.Public().(*ecdsa.PublicKey))
pub, err := crypto.UnmarshalPubkey(publicKeyBytes)
if err != nil {
log.Fatal(err)
}
rAddress := crypto.PubkeyToAddress(*pub)
// Construct the signature response
res := SignatureResponse{
Address: rAddress.String(),
Msg: message,
Sig: hexutil.Encode(sig),
Version: "2", }
// Marshal the response to JSON with proper formatting
resBytes, err := json.MarshalIndent(res, " ", "\t")
if err != nil {
log.Fatalln(err)
}
return res.Sig, string(resBytes)
}
Nuevamente, actualice la función main en main.go:
func main() {
fmt.Println("using ethclient...")
getSuggestedGasPrice(sepoliaRpcUrl)
eGas := estimateGas(sepoliaRpcUrl, from, to, data, wei)
fmt.Println("\nestimate gas for the transaction is:", eGas)
rawTxRLPHex := createRawTransaction(sepoliaRpcUrl, to, data, privKey, gasLimit, wei)
fmt.Println("\nRaw TX:\n", rawTxRLPHex)
sendRawTransaction(rawTxRLPHex, sepoliaRpcUrl)
sig, sDetails := signMessage(data, privKey) // This was just added.
fmt.Println("\nsigned message:", sDetails) // This was just added.
}
Debería obtener esto cuando ejecute el código:

5. Verificación de firmas de mensajes firmados de Ethereum
Como se mencionó en la última sección, podemos firmar y verificar mensajes firmados offline. Para verificar un mensaje firmado, necesitamos la firma, la dirección del firmante y el mensaje original.
La función verifySig a continuación toma estos parámetros, decodifica la firma en bytes y elimina el ID de recuperación de Ethereum. La razón de esto es porque el paquete crypto utilizado para firmar y verificar firmas comprueba que el ID de recuperación (el byte 65) de la firma sea menor que 4 (mi suposición es que esto se hace para no limitarse solo a las firmas de Ethereum).
Después de esto, reconstruimos los parámetros necesarios (detalles en el código a continuación) y llamamos a la función crypto.Ecrecover, que funciona de manera similar al contrato precompilado EVM Ecrecover en la dirección (0x01), el cual devuelve la dirección que firmó el mensaje (creó la firma).
Cree un archivo verifySignedMessage.go en el proyecto y agregue este código:
package main
import (
"fmt"
"log" "strings"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
)
// handleVerifySig verifies the signature against the provided public key and hash.
func verifySig(signature, address, message string) bool {
// Decode the signature into bytes
sig, err := hexutil.Decode(signature)
if err != nil {
log.Fatalln(err)
}
// Adjust signature to standard format (remove Ethereum's recovery ID)
sig[64] = sig[64] - 27
// Construct the message prefix
prefix := []byte(fmt.Sprintf("\x19Ethereum Signed Message:\n%d", len(message)))
data := []byte(message)
// Hash the prefix and data using Keccak-256
hash := crypto.Keccak256Hash(prefix, data)
// Recover the public key bytes from the signature
sigPublicKeyBytes, err := crypto.Ecrecover(hash.Bytes(), sig)
if err != nil {
log.Fatalln(err)
}
ecdsaPublicKey, err := crypto.UnmarshalPubkey(sigPublicKeyBytes)
if err != nil {
log.Fatalln(err)
}
// Derive the address from the recovered public key
rAddress := crypto.PubkeyToAddress(*ecdsaPublicKey)
// Check if the recovered address matches the provided address
isSigner := strings.EqualFold(rAddress.String(), address)
return isSigner
}
Actualice la función main en main.go:
func main() {
fmt.Println("using ethclient...")
getSuggestedGasPrice(sepoliaRpcUrl)
eGas := estimateGas(sepoliaRpcUrl, from, to, data, wei)
fmt.Println("\nestimate gas for the transaction is:", eGas)
rawTxRLPHex := createRawTransaction(sepoliaRpcUrl, to, data, privKey, gasLimit, wei)
fmt.Println("\nRaw TX:\n", rawTxRLPHex)
sendRawTransaction(rawTxRLPHex, sepoliaRpcUrl)
sig, sDetails := signMessage(data, privKey)
fmt.Println("\nsigned message:", sDetails)
if isSigner := verifySig(sig, from, data); isSigner { // This was just added.
fmt.Printf("\n%s signed %s\n", from, data)
} else {
fmt.Printf("\n%s did not sign %s\n", from, data)
}
}
Ahora podemos confirmar si la clave privada firmó el mensaje, lo cual hizo. Ejecute go run .:

Ejercicio: Pase un mensaje diferente a la función verifySig. Debería obtener: 0x571B102323C3b8B8Afb30619Ac1d36d85359fb84 did not sign Hello Ethereum!, debido a datos incorrectos.
6. Recuperar el nonce de una cuenta (número de transacciones)
Para obtener el nonce de una cuenta, podemos usar la función “PendingNonceAt” o “NonceAt”. PendingNonceAt devuelve el siguiente nonce no utilizado para la cuenta, mientras que NonceAt devuelve el nonce actual para la cuenta.
Otra diferencia es que PendingNonceAt simplemente obtiene el siguiente nonce, mientras que NonceAt intenta obtener el nonce de la cuenta en un número de bloque especificado; si no se pasa ninguno, devuelve el nonce de la cuenta en el último bloque conocido.
Ambos métodos inician llamadas JSON-RPC utilizando eth_getTransactionCount; sin embargo, el primero incluye un segundo parámetro como “pending”, mientras que el otro especifica el número de bloque.
Ahora, cree un archivo ‘getNonce.go’ y pegue el código a continuación:
package main
import (
"context"
"fmt"
"log"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
)
// getNonce fetches and prints the current and next nonce for a given Ethereum address.
func getNonce(address, rpcUrl string) (uint64, uint64) {
client, err := ethclient.Dial(rpcUrl)
if err != nil {
log.Fatalln(err)
}
// Retrieve the next nonce for the address
nextNonce, err := client.PendingNonceAt(context.Background(), common.HexToAddress(address))
if err != nil {
log.Fatalln(err)
}
var currentNonce uint64 // Variable to hold the current nonce.
if nextNonce > 0 {
currentNonce = nextNonce - 1
}
return currentNonce, nextNonce
}
Actualice la función main:
func main() {
fmt.Println("using ethclient...")
getSuggestedGasPrice(sepoliaRpcUrl)
eGas := estimateGas(sepoliaRpcUrl, from, to, data, wei)
fmt.Println("\nestimate gas for the transaction is:", eGas)
rawTxRLPHex := createRawTransaction(sepoliaRpcUrl, to, data, privKey, gasLimit, wei)
fmt.Println("\nRaw TX:\n", rawTxRLPHex)
sendRawTransaction(rawTxRLPHex, sepoliaRpcUrl)
sig, sDetails := signMessage(data, privKey)
fmt.Println("\nsigned message:", sDetails)
if isSigner := verifySig(sig, from, data); isSigner {
fmt.Printf("\n%s signed %s\n", from, data)
} else {
fmt.Printf("\n%s did not sign %s\n", from, data)
}
cNonce, nNonce := getNonce(to, sepoliaRpcUrl) // This was just added.
fmt.Printf("\n%s current nonce: %v\n", to, cNonce) // This was just added.
fmt.Printf("%s next nonce: %v\n", to, nNonce) // This was just added.
}
Ejecute go run . en el programa, debería ver esto:

7. Rastreo de una transacción (Tracing)
Como se mencionó anteriormente, usaremos el paquete ‘rpc’ de Geth para el rastreo de transacciones, una funcionalidad que no es compatible directamente con ethclient.
Al rastrear transacciones, podemos visualizar la ruta de ejecución y obtener información sobre cualquier registro de eventos (event logs) durante la ejecución de la transacción.
Para esto, nos centraremos en dos métodos principales: debug_traceTransaction y un método RPC personalizado por otterscan ots_traceTransaction (explicado a continuación).
debug_traceTransaction utiliza el rastreo de transacciones nativo de Geth, que toma el hash de la transacción y una configuración de rastreo, especificando el tipo de rastreo a realizar. Geth tiene diferentes trazadores (tracers) nativos, pero usaremos el “callTracer”. Para ver todos los trazadores nativos de Geth disponibles, puede leer la documentación más tarde.
debug_traceTransaction aprovecha las capacidades integradas de rastreo de transacciones de Geth. Requiere dos argumentos:
-
El hash de la transacción, y
-
Una configuración de rastreo: esto especifica los detalles del rastreo, como el tipo de información a capturar. Geth ofrece varios trazadores nativos, pero para este ejemplo, nos centraremos en “callTracer”. Este trazador rastrea todos los marcos de llamada (call frames o function calls) ejecutados durante la ejecución de una transacción.
Un ejemplo de un rastreo generado utilizando la configuración ‘callTracer’:
client.CallContext(
context.Background(),
&result,
"debug_traceTransaction",
"0xd12e31c3274ff32d5a73cc59e8deacbb0f7ac4c095385add3caa2c52d01164c1",
map[string]any{
"tracer": "callTracer",
"tracerConfig": map[string]any{"withLog": true}}
)
El parámetro de configuración, que sigue al hash de la transacción, indica al nodo Geth conectado que realice un rastreo de llamadas (call trace) e incluya los registros de eventos generados.
Usaremos ots_traceTransaction (consulte la explicación después del código).
Cree un archivo traceTx.go en nuestro proyecto y pegue el código a continuación:
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/ethereum/go-ethereum/rpc"
)
func traceTx(hash, rpcUrl string) string {
var (
client *rpc.Client // Define a variable to hold the RPC client.
err error // Variable to catch errors.
)
// Connect to the Ethereum RPC endpoint using the provided URL.
client, err = rpc.Dial(rpcUrl)
if err != nil {
log.Fatalln(err)
}
var result json.RawMessage // Variable to hold the raw JSON result of the call.
// Make the RPC call to trace the transaction using its hash. `ots_traceTransaction` is the method name.
err = client.CallContext(context.Background(), &result, "ots_traceTransaction", hash) // or use debug_traceTransaction with a supported RPC URL and params: hash, map[string]any{"tracer": "callTracer", "tracerConfig": map[string]any{"withLog": true}} for Geth tracing
if err != nil {
log.Fatalln(err)
}
// Marshal the result into a formatted JSON string
resBytes, err := json.MarshalIndent(result, " ", "\t")
if err != nil {
log.Fatalln(err)
}
return string(resBytes))
}
Actualice la función main:
func main() {
fmt.Println("using ethclient...")
getSuggestedGasPrice(sepoliaRpcUrl)
eGas := estimateGas(sepoliaRpcUrl, from, to, data, wei)
fmt.Println("\nestimate gas for the transaction is:", eGas)
rawTxRLPHex := createRawTransaction(sepoliaRpcUrl, to, data, privKey, gasLimit, wei)
fmt.Println("\nRaw TX:\n", rawTxRLPHex)
sendRawTransaction(rawTxRLPHex, sepoliaRpcUrl)
sig, sDetails := signMessage(data, privKey)
fmt.Println("\nsigned message:", sDetails)
if isSigner := verifySig(sig, from, data); isSigner {
fmt.Printf("\n%s signed %s\n", from, data) }
else {
fmt.Printf("\n%s did not sign %s\n", from, data)
}
cNonce, nNonce := getNonce(to, sepoliaRpcUrl)
fmt.Printf("\n%s current nonce: %v\n", to, cNonce)
fmt.Printf("%s next nonce: %v\n", to, nNonce)
res := traceTx("0xd12e31c3274ff32d5a73cc59e8deacbb0f7ac4c095385add3caa2c52d01164c1", mainnetRpcUrl) // This was just added.
fmt.Println("\ntrace result:\n", res) // This was just added.
}
ots_traceTransaction es un método JSON-RPC personalizado de Ethereum para el rastreo de transacciones desarrollado por Otterscan y no es parte de Geth. Solo requiere el hash de la transacción como entrada y devuelve una salida de rastreo estructurada sin ningún registro (logs).
Tenga en cuenta que la URL RPC de Sepolia en la variable sepoliaRpcUrl no soporta el método ots_traceTransaction. Para este ejemplo, usaremos la URL RPC de mainnet almacenada en la variable mainnetRpcUrl, que sí lo soporta.
Después de ejecutar el programa, deberíamos ver el rastreo de llamadas.

Ejercicio: Modifique la función traceTx para usar el debug_traceTransaction de Geth con la configuración callTracer demostrada anteriormente. Use sepoliaRpcUrl y un hash de transacción de Sepolia correspondiente para el rastreo.
Debería ver una salida de rastreo ligeramente diferente a la anterior que se ve así:

8. Crear y enviar una transacción blob EIP-4844
El dencun hard-fork acaba de lanzarse en Ethereum e introdujo varias EIP, siendo una de ellas EIP-4844 con un nuevo tipo de transacción llamada transacción blob (type 3).
Blobs es la forma abreviada de Binary Large Objects (Objetos Binarios Grandes). En el caso de Ethereum, se trata de datos de transacciones que persisten en la capa de consenso (consensus layer) de ethereum y no en la capa de ejecución (execution layer) como otras transacciones. Por lo tanto, se necesitan clientes de consenso como Prysm para acceder a ellos y no Geth, que es un cliente de ejecución.
Campos de transacciones blob
La transacción blob tiene campos similares a la transacción EIP-1559, pero con algunos campos añadidos como blob_versioned_hashes (vector de hashes sha256) y max_fee_per_blob_gas(uint256), y una regla de que el campo to de la transacción no debe ser nulo (nil).
El hash versionado (versioned hash) de un blob, que es de 32 bytes, está compuesto por un solo byte que representa la versión (actualmente 0x01, probablemente cambiará cuando Ethereum pase a full-sharding) al principio, seguido por los últimos 31 bytes del hash SHA256 del compromiso KZG (KZG commitment) del blob (explicado a continuación).
hash versionado
/ go-ethereum/crypto/kzg4844/kzg4844.go
// CalcBlobHashV1 calculates the 'versioned blob hash' of a commitment.
func CalcBlobHashV1(hasher hash.Hash, commit *Commitment) (vh [32]byte) {
if hasher.Size() != 32 {
panic("wrong hash size")
}
hasher.Reset()
hasher.Write(commit[:])
hasher.Sum(vh[:0]) // save the commitment hash to `vh`
vh[0] = 0x01 // set hash version
return vh
}
¿Dónde se almacenan los blobs?
El contenido completo de un blob no está incrustado en un bloque ni se conserva en la capa de ejecución y no es accesible en la EVM; en su lugar, son administrados por separado por la beacon chain (capa de consenso) como blob sidecars con el fin de ahorrar espacio en el bloque para la ejecución de transacciones normales.
Un Sidecar puede contener uno o una lista de blobs (128 bytes cada uno), una lista de su correspondiente compromiso kzg (48 bytes cada uno) y una lista de su correspondiente prueba kzg (kzg proof) (48 bytes cada una).
Los blobs se almacenan en la beacon chain durante 18 días y luego se podan (pruned). Los rollups pueden lidiar con este vencimiento almacenando también los blobs ellos mismos o utilizando almacenamiento p2p para guardar los blobs.
Blob sidecar
// go-ethereum/core/types/tx_blob.go
// BlobTxSidecar contains the blobs of a blob transaction.
type BlobTxSidecar struct {
Blobs []kzg4844.Blob // Blobs
Commitments []kzg4844.Commitment // Blob commitments
Proofs []kzg4844.Proof // Blob KZG proofs
}
El blob se utiliza para calcular el compromiso KZG, y el blob junto con el compromiso KZG se utiliza para calcular la prueba KZG. Esta prueba se utiliza para verificar el blob contra el compromiso.
¿Para qué se usan los blobs?
El caso de uso principal de los blobs es manejar los datos de bloques de capas 2 (layer 2s) y rollups, en lugar de usar calldata que los usuarios también utilizan, lo que lleva a una competencia por el espacio de bloque. Usar una transacción separada (blobs) reduce el costo para las capas 2 y los rollups.
Sin embargo, los blobs no se limitan solo a los rollups y pueden ser usados por cualquiera. Demostraré cómo enviar una transacción blob más adelante.
Límite de blob por transacción
Podemos enviar más de un blob por transacción, sin embargo, hay un objetivo de 3 y un límite máximo de 6 blobs por bloque, por lo que técnicamente una transacción blob puede contener hasta seis blobs si fuera la única transacción blob en el bloque. Esto también implica que el sidecar que contiene estos blobs contendrá la misma cantidad de compromisos de blob (blob commitment) y hashes versionados como la cantidad de blobs en él, seis en este caso.
Gas de blob
Las transacciones blob utilizan un tipo de gas diferente llamado gas de blob (blob gas) y tienen los siguientes parámetros: MAX_BLOB_GAS_PER_BLOCK de 786.432; TARGET_BLOB_GAS_PER_BLOCK de 393.216; y MIN_BLOB_BASE_FEE de 1.
Está separado del gas de transacción existente que conocemos. La tarifa del gas de blob tiene un mecanismo de precios similar a EIP-1559 en el sentido de que aumenta y disminuye según la congestión de la red. Aumenta si el bloque anterior utiliza más gas que el TARGET_BLOB_GAS_PER_BLOCK (~3 blobs) y disminuye cuando el bloque anterior utiliza menos.
Tenga en cuenta que los hashes versionados de blob se almacenan como referencias a los blobs en la capa de ejecución. Pero los blobs no se almacenan en la capa de ejecución. Los blobs no necesitan una tarifa de prioridad (priority fee) porque los datos del blob no se ejecutarán.
Finalmente, crear una transacción blob en Go sigue pasos muy similares a una transacción normal, excepto que usamos el struct types.BlobTx y pasamos los campos relacionados al blob como se insinuó anteriormente.
Cree un archivo blobTx.go y pegue el siguiente código:
package transaction
import (
"context"
"fmt"
"regexp"
"strings"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/kzg4844"=
"github.com/ethereum/go-ethereum/ethclient"
"github.com/holiman/uint256"
)
// SendBlobTX sends a transaction with an EIP-4844 blob payload to the Ethereum network.
func sendBlobTX(rpcURL, toAddress, data, privKey string) (string, error) {
// Connect to the Ethereum client
client, err := ethclient.Dial(rpcURL)
if err != nil {
return "", fmt.Errorf("failed to dial RPC client: %s", err)
}
defer client.Close() // Ensure the connection is closed after completing the function
// Retrieve the current chain ID
chainID, err := client.ChainID(context.Background())
if err != nil {
return "", fmt.Errorf("failed to get chain ID: %s", err)
}
var Blob [131072]byte // Define a blob array to hold the large data payload, blobs are 128kb in length
// If necessary, convert the input data to a byte slice in hex format
var bytesData []byte
if data != "" {
// Check if the data is in hex format, with or without the '0x' prefix
if IsHexWithOrWithout0xPrefix(data) {
// Ensure the data has the '0x' prefix
if !strings.HasPrefix(data, "0x") {
data = "0x" + data
}
// Decode the hex-encoded data
bytesData, err = hexutil.Decode(data)
if err != nil {
return "", fmt.Errorf("failed to decode data: %s", err)
}
// Copy the decoded data into the blob array
copy(Blob[:], bytesData)
} else {
// If the data is not in hex format, copy it directly into the blob array
copy(Blob[:], data)
}
}
// Compute the commitment for the blob data using KZG4844 cryptographic algorithm
BlobCommitment, err := kzg4844.BlobToCommitment(Blob)
if err != nil {
return "", fmt.Errorf("failed to compute blob commitment: %s", err)
}
// Compute the proof for the blob data, which will be used to verify the transaction
BlobProof, err := kzg4844.ComputeBlobProof(Blob, BlobCommitment)
if err != nil {
return "", fmt.Errorf("failed to compute blob proof: %s", err)
}
// Prepare the sidecar data for the transaction, which includes the blob and its cryptographic proof
sidecar := types.BlobTxSidecar{
Blobs: []kzg4844.Blob{Blob},
Commitments: []kzg4844.Commitment{BlobCommitment},
Proofs: []kzg4844.Proof{BlobProof},
}
// Decode the sender's private key
pKeyBytes, err := hexutil.Decode("0x" + privKey)
if err != nil {
return "", fmt.Errorf("failed to decode private key: %s", err)
}
// Convert the private key into the ECDSA format
ecdsaPrivateKey, err := crypto.ToECDSA(pKeyBytes)
if err != nil {
return "", fmt.Errorf("failed to convert private key to ECDSA: %s", err)
}
// Compute the sender's address from the public key
fromAddress := crypto.PubkeyToAddress(ecdsaPrivateKey.PublicKey)
// Retrieve the nonce for the transaction
nonce, err := client.PendingNonceAt(context.Background(), fromAddress)
fmt.Println(nonce)
if err != nil {
return "", fmt.Errorf("failed to get nonce: %s", err)
}
// Create the transaction with the blob data and cryptographic proofs
tx, err := types.NewTx(&types.BlobTx{
ChainID: uint256.MustFromBig(chainID),
Nonce: nonce,
GasTipCap: uint256.NewInt(1e10), // max priority fee per gas
GasFeeCap: uint256.NewInt(50e10), // max fee per gas
Gas: 250000, // gas limit for the transaction
To: common.HexToAddress(toAddress), // recipient's address
Value: uint256.NewInt(0), // value transferred in the transaction
Data: nil, // No additional data is sent in this transaction
BlobFeeCap: uint256.NewInt(3e10), // fee cap for the blob data
BlobHashes: sidecar.BlobHashes(), // blob hashes in the transaction
Sidecar: &sidecar, // sidecar data in the transaction
}), err
if err != nil {
return "", fmt.Errorf("failed to create transaction: %s", err)
}
// Sign the transaction with the sender's private key
signedTx, err := types.SignTx(tx, types.LatestSignerForChainID(chainID), ecdsaPrivateKey)
if err != nil {
return "", fmt.Errorf("failed to sign transaction: %s", err)
}
// Send the signed transaction to the Ethereum network
if err = client.SendTransaction(context.Background(), signedTx); err != nil {
return "", fmt.Errorf("failed to send transaction: %s", err)
}
// Return the transaction hash
txHash := signedTx.Hash().Hex()
return txHash, nil
}
// IsHexWithOrWithout0xPrefix checks if a string is hex with or without `0x` prefix using regular expression.
func IsHexWithOrWithout0xPrefix(data string) bool {
pattern := `^(0x)?[0-9a-fA-F]+$`
matched, _ := regexp.MatchString(pattern, data)
return matched
}
Actualice la función main:
func main() {
fmt.Println("using ethclient...")
getSuggestedGasPrice(sepoliaRpcUrl)
eGas := estimateGas(sepoliaRpcUrl, from, to, data, wei)
fmt.Println("\nestimate gas for the transaction is:", eGas)
rawTxRLPHex := createRawTransaction(sepoliaRpcUrl, to, data, privKey, gasLimit, wei)
fmt.Println("\nRaw TX:\n", rawTxRLPHex) sendRawTransaction(rawTxRLPHex, sepoliaRpcUrl)
sig, sDetails := signMessage(data, privKey)
fmt.Println("\nsigned message:", sDetails)
if isSigner := verifySig(sig, from, data); isSigner {
fmt.Printf("\n%s signed %s\n", from, data)
} else {
fmt.Printf("\n%s did not sign %s\n", from, data)
}
cNonce, nNonce := getNonce(to, sepoliaRpcUrl)
fmt.Printf("\n%s current nonce: %v\n", to, cNonce)
fmt.Printf("%s next nonce: %v\n", to, nNonce)
res := traceTx("0xd12e31c3274ff32d5a73cc59e8deacbb0f7ac4c095385add3caa2c52d01164c1", mainnetRpcUrl)
fmt.Println("\ntrace result:\n", res)
blob, err := sendBlobTX(sepoliaRpcUrl, to, data, privKey) // This was just added.
if err != nil {
log.Fatalln(err)
}
fmt.Println("\nBlob transaction hash:", blob) // This was just added.
}
Antes de ejecutar nuestro programa, comente temporalmente las llamadas a las funciones sendRawTransaction y traceTx. Esto se debe a que una transacción pendiente de sendRawTransaction puede causar un conflicto de nonce (error de brecha de nonce o nonce gap error) al crear la transacción blob, y la subsiguiente llamada a traceTx saturará la salida de la terminal.
Después de hacer eso, ejecute con go run .. Debería obtener el hash de la transacción.

Puede buscarlo en Etherscan, aquí está el que yo hice:
Resumen
El paquete ethclient de Go-Ethereum (Geth) simplifica muchas interacciones comunes con Ethereum. Sin embargo, al igual que cualquier otro cliente existente, no proporciona métodos para todas las API JSON-RPC de Ethereum, como hemos visto con el rastreo de transacciones (transaction tracing). En casos como este, es necesario construir manualmente una llamada JSON-RPC. Afortunadamente, el paquete rpc de Geth facilita esto para los desarrolladores de Go.
Publicado originalmente el 3 de abril de 2024