In the previous article, [Interacting with the Ethereum Blockchain Using Go (Part 1)], we explored how to establish a connection to the Ethereum blockchain using Go, retrieve the latest block number, and send a transaction where the private key is stored directly on the node. In this follow-up guide, we’ll dive deeper into using go-ethereum to generate private keys, sign transactions off-chain, and broadcast them securely to the Ethereum network—whether you're running Geth or Parity.
This article assumes you already know how to connect to an Ethereum node. For setup details, refer to the first part of this series.
We also won’t cover Go environment configuration—please search online if you need help setting up your Go development environment.
Generating a Private Key
Before interacting with the blockchain, you need a cryptographic identity. In Ethereum, this begins with a private key, which allows you to control funds and sign transactions. With go-ethereum, generating or restoring a private key is straightforward.
Restore from Hex String
You can recover an existing private key from its hexadecimal representation:
privKey, err := crypto.HexToECDSA("your-private-key-here")
if err != nil {
log.Fatal(err)
}Alternatively, decode from other formats:
privKey, err := crypto.ToECDSA(decodeStringToBytes("your-private-key"))
if err != nil {
log.Fatal(err)
}Generate a New Key Pair
To create a brand-new private key:
privKey, err := crypto.GenerateKey()
if err != nil {
log.Fatal(err)
}Once you have the private key, derive the public key and Ethereum address:
publicKey := privKey.Public().(*ecdsa.PublicKey)
address := crypto.PubkeyToAddress(*publicKey).Hex()
fmt.Println("Ethereum Address:", address)This address is what others use to send you Ether or interact with your smart contracts.
👉 Learn how to securely manage keys for blockchain development
Signing Transactions Off-Chain
One of the core advantages of using Go with go-ethereum is the ability to sign transactions offline, enhancing security by keeping your private key away from networked systems.
The process involves three steps:
- Create a transaction object.
- Initialize a signer.
- Sign the transaction using your private key.
Step 1: Create the Transaction
Use types.NewTransaction to define the transaction parameters:
amount := big.NewInt(1) // Amount in Wei
gasLimit := uint64(90000) // Standard gas limit for simple transfers
gasPrice := big.NewInt(1000000000) // 1 Gwei
data := []byte{} // Optional payload
nonce := uint64(0) // Retrieved from the blockchain
to := common.HexToAddress("0x...") // Recipient address
tx := types.NewTransaction(nonce, to, amount, gasLimit, gasPrice, data)Step 2: Choose the Right Signer
Ethereum supports different signing schemes based on network upgrades. The most commonly used are:
HomesteadSigner: For older networks without EIP-155 replay protection.NewEIP155Signer(chainID): Recommended for modern networks like Rinkeby, Goerli, or mainnet.
For testnets and production chains, always use EIP-155 to prevent replay attacks:
chainID := big.NewInt(4) // Rinkeby testnet
signer := types.NewEIP155Signer(chainID)Note: I initially looked for signing functions inside thecryptopackage, but they’re actually located intypes. TheSignTxfunction handles both hashing and signature generation correctly.
Step 3: Sign the Transaction
Now sign it with your private key:
signedTx, err := types.SignTx(tx, signer, privKey)
if err != nil {
log.Fatal(err)
}The resulting signedTx contains all fields (r, s, v) required for network validation.
Broadcasting the Signed Transaction
While ethclient.Client.SendTransaction() broadcasts raw transactions, it doesn't return the transaction hash directly—making it harder to track confirmation status.
To get immediate feedback, extend the client with a custom method:
func (ec *Client) SendRawTransaction(ctx context.Context, tx *types.Transaction) (common.Hash, error) {
var txHash common.Hash
data, err := rlp.EncodeToBytes(tx)
if err != nil {
return txHash, err
}
err = ec.c.CallContext(ctx, &txHash, "eth_sendRawTransaction", common.ToHex(data))
return txHash, err
}Now broadcast and capture the hash:
txHash, err := client.SendRawTransaction(context.TODO(), signedTx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Transaction sent: %s\n", txHash.Hex())Alternatively, compute the transaction hash manually (though not recommended due to edge cases):
// Hash = keccak256(RLP(tx with signature))👉 Discover secure ways to deploy and manage blockchain transactions
Real-World Test on Rinkeby Testnet
Let’s put everything together with a real example on the Rinkeby testnet:
amount := big.NewInt(100000000000) // 0.0001 ETH
gasLimit := uint64(90000)
gasPrice := big.NewInt(1000000000) // 1 Gwei
data := []byte("Sent from Go using go-ethereum")
nonce := getNonceFromBlockchain() // Query via eth_getTransactionCount
to := common.HexToAddress("0xRecipientAddr")
tx := types.NewTransaction(nonce, to, amount, gasLimit, gasPrice, data)
signer := types.NewEIP155Signer(big.NewInt(4)) // Rinkeby chain ID = 4
signedTx, _ := types.SignTx(tx, signer, privKey)
txHash, err := client.SendRawTransaction(context.TODO(), signedTx)
if err != nil {
log.Fatal(err)
}
fmt.Println("View on Etherscan:", "https://rinkeby.etherscan.io/tx/"+txHash.Hex())After a few seconds, you can view the transaction on Rinkeby Etherscan.
⚠️ Always verify the correct chain ID when using EIP-155. Mainnet uses1, Rinkeby4, Goerli5, and Sepolia11155111.
Frequently Asked Questions (FAQ)
Q: Why should I sign transactions off-chain instead of using node-managed keys?
A: Off-chain signing keeps your private keys secure and never exposes them to potentially compromised nodes. It's essential for production-grade applications and wallet development.
Q: Can I use the same private key across multiple networks?
A: Yes—your private key works across all Ethereum-compatible networks (mainnet, testnets, L2s). However, always use EIP-155 signing with the correct chain ID to avoid replay attacks.
Q: What happens if I reuse a nonce?
A: Reusing a nonce will overwrite the pending transaction. If confirmed, only one version will be processed—the other becomes invalid.
Q: How do I get the current nonce for my address?
A: Use eth_getTransactionCount via client.PendingNonceAt(context, address) in ethclient.
Q: Is RLP encoding necessary when sending raw transactions?
A: Yes—Ethereum nodes expect RLP-encoded byte streams for eth_sendRawTransaction. The rlp.EncodeToBytes() function handles this automatically.
Q: Can I estimate gas dynamically in Go?
A: Yes—use client.EstimateGas(context, callMsg) to simulate execution and retrieve optimal gas limits.
Conclusion
This guide walked you through generating Ethereum private keys, signing transactions securely in Go using go-ethereum, and broadcasting them to the network. By handling signing off-node, you gain greater control and security—critical for building robust decentralized applications.
While only partial code snippets are shown here, the full implementation is available on GitHub for reference and experimentation.
Whether you're developing a wallet backend, automating DeFi interactions, or building infrastructure tools, mastering low-level transaction handling in Go empowers you with precision and performance.
👉 Start building secure blockchain applications today
Core Keywords:
- Ethereum blockchain
- Go Ethereum
- Sign transaction
- Private key
- Off-chain signing
- Geth
- Rinkeby testnet
- Smart contract interaction