CKB/以太坊 RPC 接口使用
本文的目的是编写相关代码, 从以太坊节点获取我们感兴趣的链上数据. 鉴于目前同步一个以太坊节点过于耗时, 因此我将使用 Infura 提供的公共以太坊节点来完成这一切. 那么我们开始吧!
$ go get -u -v github.com/ethereum/go-ethereum/ethclient
查询转账记录
以太坊交易数据中并不包含发送者, 要从交易中恢复发送者的地址, 可参考以下两个网页. 简单来说, 我们须首先从交易的签名中恢复发送者的公钥, 然后通过发送者公钥计算发送者地址.
- https://github.com/ethereum/go-ethereum/issues/22918
- https://ethereum.stackexchange.com/questions/67449/given-a-raw-transaction-how-to-get-senders-address
package main
import (
"context"
"log"
"math/big"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/godump/doa"
)
func main() {
// Site: https://www.infura.io/zh
// Docs: https://docs.infura.io/networks/ethereum/json-rpc-methods
ethClient := doa.Try(ethclient.Dial("https://mainnet.infura.io/v3/5c17ecf14e0d4756aa81b6a1154dc599"))
blockNumber := doa.Try(ethClient.BlockNumber(context.Background()))
block := doa.Try(ethClient.BlockByNumber(context.Background(), big.NewInt(int64(blockNumber))))
for _, tx := range block.Transactions() {
sender := doa.Try(types.Sender(types.LatestSignerForChainID(tx.ChainId()), tx))
amount, _ := tx.Value().Float64()
log.Printf("%s -> %s %14.6f", sender, tx.To(), amount/1e18)
}
}
程序将获取以太坊最后一个交易区块, 并依次打印交易哈希, 交易发送者, 交易接收者和交易金额.
查询 USDT-ERC20 转账记录
Tether USD 在以太坊上是一个 ERC20 Token, 可以在 Etherscan 查询该合约的完整信息. 我们通过解析交易的 Log 来查询 USDT 转账记录, 代码如下.
package main
import (
"context"
"log"
"math/big"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/godump/doa"
)
func main() {
ethClient := doa.Try(ethclient.Dial("https://mainnet.infura.io/v3/5c17ecf14e0d4756aa81b6a1154dc599"))
blockNumber := doa.Try(ethClient.BlockNumber(context.Background()))
query := ethereum.FilterQuery{
Addresses: []common.Address{common.HexToAddress("0xdac17f958d2ee523a2206206994597c13d831ec7")},
Topics: [][]common.Hash{
{common.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef")},
},
FromBlock: big.NewInt(int64(blockNumber)),
ToBlock: big.NewInt(int64(blockNumber)),
}
txlog := doa.Try(ethClient.FilterLogs(context.Background(), query))
for _, e := range txlog {
amount := new(big.Int)
amount.SetBytes(e.Data)
valuef, _ := amount.Float64()
log.Printf("%s %s -> %s %14.6f USDT",
e.TxHash,
common.HexToAddress(e.Topics[1].Hex()),
common.HexToAddress(e.Topics[2].Hex()),
valuef/1e6,
)
}
}
查询 USDT 余额
我们还是以 USDT 合约为例, 我们的目标是查询 F977814e90dA44bFA03b6295A0616a897441aceC
这个地址的余额. 通过 USDT 合约代码可知, 有一个 balanceOf(address)
函数可以取得任意地址的余额, 因此我们只需要想办法调用这个函数即可.
要调用函数, 首先要获取函数的签名. 以 balanceOf(address)
为例, 其函数签名为 70a08231
, 签名计算方式为 sha3("balanceOf(address)")
的前 4 个 Byte. 之后构建函数调用, 其构造方式为函数签名 + 参数. 注意所有参数都需要以 uint256 表示, 因此结果为 70a08231000000000000000000000000F977814e90dA44bFA03b6295A0616a897441aceC
.
package main
import (
"context"
"encoding/hex"
"log"
"math/big"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/godump/doa"
)
func main() {
ethClient := doa.Try(ethclient.Dial("https://mainnet.infura.io/v3/5c17ecf14e0d4756aa81b6a1154dc599"))
blockNumber := doa.Try(ethClient.BlockNumber(context.Background()))
contract := common.HexToAddress("0xdAC17F958D2ee523a2206206994597C13D831ec7")
ret := doa.Try(ethClient.CallContract(context.Background(), ethereum.CallMsg{
To: &contract,
Data: doa.Try(hex.DecodeString("70a08231000000000000000000000000F977814e90dA44bFA03b6295A0616a897441aceC")),
}, big.NewInt(int64(blockNumber))))
balance := big.Int{}
balance.SetBytes(ret)
data, _ := balance.Float64()
log.Println(data / 1e6)
}
查询随机地址余额
现在让我们来写一个彩票程序. 随机生成新的私钥, 并查询该私钥对应的地址的余额. 如果我们运气棒棒哒, 说不定能碰撞出一个有币的私钥捏~
package main
import (
"context"
"encoding/hex"
"log"
"math/big"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/godump/doa"
)
func main() {
ethClient := doa.Try(ethclient.Dial("https://mainnet.infura.io/v3/5c17ecf14e0d4756aa81b6a1154dc599"))
for {
pri := doa.Try(crypto.GenerateKey())
pub := pri.PublicKey
adr := crypto.PubkeyToAddress(pub)
val, err := ethClient.BalanceAt(context.Background(), adr, nil)
if err != nil {
log.Println(err)
continue
}
log.Println("0x"+hex.EncodeToString(crypto.FromECDSA(pri)), adr, val)
if val.Cmp(big.NewInt(0)) != 0 {
break
}
}
}
签名验签
以下示例代码使用 go-ethereum 提供的 secp256k1 实现, 对任意数据的哈希进行签名和验签.
package main
import (
"crypto/rand"
"encoding/hex"
"log"
"slices"
"github.com/ethereum/go-ethereum/crypto"
"github.com/godump/doa"
)
func main() {
prikey := doa.Try(crypto.HexToECDSA("0000000000000000000000000000000000000000000000000000000000000001"))
pubkey := prikey.PublicKey
log.Println("prikey", hex.EncodeToString(crypto.FromECDSA(prikey)))
log.Println("pubkey", hex.EncodeToString(crypto.FromECDSAPub(&pubkey)))
msg := make([]byte, 32)
rand.Read(msg)
log.Println("msg", hex.EncodeToString(msg))
sig := doa.Try(crypto.Sign(msg, prikey))
log.Println("sig", hex.EncodeToString(sig))
doa.Doa(crypto.VerifySignature(crypto.FromECDSAPub(&pubkey), msg, sig[:64]))
doa.Doa(slices.Equal(doa.Try(crypto.Ecrecover(msg, sig)), crypto.FromECDSAPub(&pubkey)))
}
2024/03/26 15:35:16 prikey 0000000000000000000000000000000000000000000000000000000000000001
2024/03/26 15:35:16 pubkey 0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8
2024/03/26 15:35:16 msg 73d811670c997143c797190087543f8cb16494ddd662ca247b721dde6248ab0b
2024/03/26 15:35:16 sig 8a5dad0cbf16771758662cf3cd6c94f3cc10980d98367762b419ccf23e0bae736dda0fe3908e68ccf1090568e7cabe3a0bfdd054477070c040c58de4baeec82001
交易转账
进行一笔转账交易. 下面的代码会向黑洞地址转账 1 ETH.
package main
import (
"context"
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/godump/doa"
)
func main() {
client := doa.Try(ethclient.Dial("https://mainnet.infura.io/v3/5c17ecf14e0d4756aa81b6a1154dc599"))
prikey := doa.Try(crypto.HexToECDSA("")) // Private key without 0x-prefix
pubkey := prikey.PublicKey
addr := crypto.PubkeyToAddress(pubkey)
nonce := doa.Try(client.PendingNonceAt(context.Background(), addr))
value := big.NewInt(1000000000000000000) // In wei (1 eth)
gasLimit := uint64(21000) // In units
gasPrice := doa.Try(client.SuggestGasPrice(context.Background()))
recv := common.HexToAddress("0x0000000000000000000000000000000000000000")
data := []byte{}
id := doa.Try(client.NetworkID(context.Background()))
tx := types.NewTransaction(nonce, recv, value, gasLimit, gasPrice, data)
txSign := doa.Try(types.SignTx(tx, types.NewEIP155Signer(id), prikey))
doa.Nil(client.SendTransaction(context.Background(), txSign))
fmt.Println(txSign.Hash().Hex())
}
略微修改上述代码, 即可向目标地址转账全部可用余额:
package main
import (
"context"
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/godump/doa"
)
func main() {
client := doa.Try(ethclient.Dial("https://mainnet.infura.io/v3/5c17ecf14e0d4756aa81b6a1154dc599"))
prikey := doa.Try(crypto.HexToECDSA("")) // Private key without 0x-prefix
pubkey := prikey.PublicKey
addr := crypto.PubkeyToAddress(pubkey)
nonce := doa.Try(client.PendingNonceAt(context.Background(), addr))
gasLimit := uint64(21000) // In units
gasPrice := doa.Try(client.SuggestGasPrice(context.Background()))
gasValue := big.NewInt(0).Mul(big.NewInt(int64(gasLimit)), gasPrice)
whole := doa.Try(client.BalanceAt(context.Background(), addr, nil))
value := big.NewInt(0).Sub(whole, gasValue)
recv := common.HexToAddress("0x0000000000000000000000000000000000000000")
data := []byte{}
id := doa.Try(client.NetworkID(context.Background()))
tx := types.NewTransaction(nonce, recv, value, gasLimit, gasPrice, data)
txSign := doa.Try(types.SignTx(tx, types.NewEIP155Signer(id), prikey))
doa.Nil(client.SendTransaction(context.Background(), txSign))
fmt.Println(txSign.Hash().Hex())
}
发布合约
首先将以下 Solidity 代码编译到字节码. 这一步可以使用在线编译器, 例如 Remix. 将编译结果保存到本地 storage
文件.
pragma solidity >=0.8.2 <0.9.0;
contract Storage {
uint256 number;
function set(uint256 num) public { number = num; }
function get() public view returns (uint256){ return number; }
}
package main
import (
"context"
"fmt"
"math/big"
"os"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/godump/doa"
)
func main() {
client := doa.Try(ethclient.Dial("https://mainnet.infura.io/v3/5c17ecf14e0d4756aa81b6a1154dc599"))
prikey := doa.Try(crypto.HexToECDSA("")) // Private key without 0x-prefix
pubkey := prikey.PublicKey
addr := crypto.PubkeyToAddress(pubkey)
gasLimit := uint64(3000000)
gasPrice := doa.Try(client.SuggestGasPrice(context.Background()))
value := big.NewInt(0)
nonce := doa.Try(client.PendingNonceAt(context.Background(), addr))
data := doa.Try(os.ReadFile("storage"))
id := doa.Try(client.NetworkID(context.Background()))
tx := types.NewContractCreation(nonce, value, gasLimit, gasPrice, data)
txSign := doa.Try(types.SignTx(tx, types.NewEIP155Signer(id), prikey))
doa.Nil(client.SendTransaction(context.Background(), txSign))
fmt.Println(txSign.Hash().Hex())
for {
_, isp, err := client.TransactionByHash(context.Background(), txSign.Hash())
if isp || err != nil {
time.Sleep(time.Second)
continue
}
break
}
cddr := doa.Try(client.TransactionReceipt(context.Background(), txSign.Hash())).ContractAddress
fmt.Println(cddr)
}
执行合约
package main
import (
"context"
"fmt"
"math/big"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/godump/doa"
"golang.org/x/crypto/sha3"
)
func hash(data []byte) []byte {
h := sha3.NewLegacyKeccak256()
h.Write(data)
return h.Sum(nil)
}
func main() {
client := doa.Try(ethclient.Dial("https://mainnet.infura.io/v3/5c17ecf14e0d4756aa81b6a1154dc599"))
prikey := doa.Try(crypto.HexToECDSA("")) // Private key without 0x-prefixkey without 0x-prefix
pubkey := prikey.PublicKey
addr := crypto.PubkeyToAddress(pubkey)
dest := common.HexToAddress("0xa28aFDa14Be5789564aE5fA03665c4180e3c680b")
gasLimit := uint64(3000000)
gasPrice := doa.Try(client.SuggestGasPrice(context.Background()))
value := big.NewInt(0)
nonce := doa.Try(client.PendingNonceAt(context.Background(), addr))
data := []byte{}
data = append(data, hash([]byte("set(uint256)"))[:4]...)
data = append(data, math.U256Bytes(big.NewInt(42))...)
id := doa.Try(client.NetworkID(context.Background()))
tx := types.NewTransaction(nonce, dest, value, gasLimit, gasPrice, data)
txSign := doa.Try(types.SignTx(tx, types.NewEIP155Signer(id), prikey))
doa.Nil(client.SendTransaction(context.Background(), txSign))
fmt.Println(txSign.Hash().Hex())
ret := doa.Try(client.CallContract(context.Background(), ethereum.CallMsg{
To: &dest,
Data: hash([]byte("get()"))[:4],
}, nil))
fmt.Println(big.NewInt(0).SetBytes(ret))
}
本地开发节点搭建
$ git clone https://github.com/ethereum/go-ethereum --branch release/1.13
$ cd go-ethereum
$ make geth
$ geth --dev --http
$ geth attach /tmp/geth.ipc
> eth.sendTransaction({from: eth.accounts[0], to: '0x7e5f4552091a69125d5dfcb7b8c2659029395bdf', value: web3.toWei(10000, 'ether')})
0000000000000000000000000000000000000000000000000000000000000001 0x7e5f4552091a69125d5dfcb7b8c2659029395bdf
0000000000000000000000000000000000000000000000000000000000000002 0x2b5ad5c4795c026514f8317c7a215e218dccd6cf