Những ai làm về Blockchain chắc không xa lạ gì với ngôn ngữ GoLang, đây là ngôn ngữ sử dụng để tạo ra blochain Ethereum rất nổi tiếng, sau này nhiều nền tảng blockchain khác đã tham khảo source code của Ethereum. Bài viết này sẽ tập trung tìm hiểu và sử dụng ngôn ngữ GoLang để tương tác với Blockchain.
Mục lục
Cài đặt và bắt đầu làm quen GoLang
Cài đặt GoLang
Nếu bạn muốn lập trình GoLang trên máy, bạn cần phải cài đặt Go. Để cài đặt Go bảng 1.19.13 bạn sử dụng lệnh sau:
# Cài đặt Go 1.19.13
sudo -i
rm -rf /usr/local/go
wget https://go.dev/dl/go1.19.13.linux-amd64.tar.gz
tar -C /usr/local -xzf go1.19.13.linux-amd64.tar.gz
rm go1.19.13.linux-amd64.tar.gz
exit
# Cài đặt công cụ hỗ trợ build các thư viện sau này
sudo apt-get install build-essential
# Thêm đường dẫn vào biến môi trường PATH
export PATH=$PATH:/usr/local/go/bin
# Tốt nhất hãy thêm dòng dưới vào cuối tệp .profile hoặc /etc/profile
# Để các lần sau ta không phải đánh lệnh trên nữa
PATH=$PATH:/usr/local/go/bin
# Kiểm tra lại
go version
# Xem thông tin cấu hình môi trường của Go
go env
Bạn nên lựa chọn cài đặt phiên bản cho phù hợp, chi tiết cài đặt xem tại: https://go.dev/doc/install.
IDE lập trình GoLang bạn có thể sử dụng GoLand, hoặc có thể sử dụng các công cụ khác như Visual Studio Code… Nếu bạn chỉ sử dụng với mục đích học tập và tìm hiểu thì bạn có thể dùng các trình soạn thảo Online như:
- Go Playgound: Trình soạn thảo dễ nhìn, chọn được nhiều phiên bản Go và có một số ví dụ sẵn để tìm hiểu cho người mới.
- Programiz – Go Online Compiler: Hỗ trợ nhiều ngôn ngữ trong đó có GoLang.
- …
Viết ứng dụng đầu tiên
GoLang mặc định tìm hàm main() trong gói main để chạy đầu tiên. Nên khi viết ứng dụng bằng GoLang thì bạn bắt buộc phải cài đặt hàm này. Ví dụ tệp hello-world.go:
package main
import "fmt"
func main() {
fmt.Println("Hello World!!!")
}
Lệnh để chạy như sau:
go run hello-world.go
Viết ứng dụng sử dụng thư viện trên Github
Ví dụ ta viết ứng dụng use-ethclient.go sử dụng ethclient trong thư viện go-ethereum:
package main
import (
"context"
"fmt"
"log"
"github.com/ethereum/go-ethereum/ethclient"
)
func main() {
client, err := ethclient.Dial("https://rpc.ankr.com/eth")
if err != nil {
log.Fatalf("Failed to connect to the Ethereum network: %v", err)
} else {
fmt.Println("Success! you are connected to the Ethereum Network")
fmt.Printf("EthClient: %+v\n", client)
}
header, err := client.HeaderByNumber(context.Background(), nil)
if err != nil {
log.Fatal(err)
} else {
fmt.Println("BlockNumber:", header.Number.String())
}
}
Trong đoạn mã trên ta có sử dụng thêm thư viện ethclient trên github. Để chạy được tệp này ta đánh các lệnh sau:
# Tạo tệp go.mod và go.sum để quản lý thư viện
# Điều này sẽ đảm bảo ethclient được tải từ Github và phiên bản mới nhất được sử dụng
go mod init use-ethclient
# Trong trường hợp gặp vấn đề cài đặt ethclient
# Hãy sử dụng lệnh dưới để tải trực tiếp
go get github.com/ethereum/go-ethereum/ethclient
# Chạy ứng dụng
go run use-ethclient.go
# Nếu tiếp đó bạn sửa code và thêm các thư viện mới bạn gọi lệnh dưới để cập nhật lại thư viện
go mod tidy
Khi chạy bạn nhận được kết quả như sau:
Success! you are connected to the Ethereum Network
EthClient: &{c:0xc00015c090}
BlockNumber: 18218977
Chi tiết xem How to connect to Ethereum network using Go
Cách chạy tệp .go thế nào phần này đã nói rất rõ. Các phần sau trở đi tôi không tập trung vào phần code, tôi cố gắng viết code ngắn gọn nhất có thể để các bạn có thể dễ dàng đọc hiểu.
Một số lệnh cơ bản trên GoLang
Về cơ bản trên các ngôn ngữ đều có các lệnh cơ bản tương tự nhau nhưng khác nhau ở cách khai báo. Phần này là phần cơ bản, bạn nên tham khảo ở link dưới để nắm được:
- Go cheatsheet => Sẽ dùng rất nhiều. Vì lập trình viên thường phải biết nhiều ngôn ngữ, nếu lâu ko dùng một ngôn ngữ nào đó sẽ dần quên hoặc lẫn cú pháp khai báo. Vì thế link này sẽ rất tiện để tra cứu lại.
- Go Cheat Sheet => Một trong khác tương tự
- Lập trình Golang => Nắm kiến thức cơ bản, một số khai báo đặc biệt
Ở đây tôi chỉ ra một số phần đặc biệt bạn cần chú ý:
- Các thư viện hay dùng
- fmt: Dùng để hiển thị log ra màn hình nhưng không có thời gian. Hai hàm ba dùng nhất là fmt.Printf(), fmt.Println() và fmt.Sprintf().
- log: Các hàm tương tự như trong thư viện fmt nhưng có thời gian. Các hàm trong thư viện này mặc định hiển thị thời gian đến mức giây, nếu bạn muốn hiển thị cả mức nhỏ hơn giây thì thiết lập cấu hình bằng lệnh sau:
log.SetFlags(log.LstdFlags | log.Lmicroseconds) - math: Các hàm toán học cơ bản
- Cú pháp gán “:=” là cú pháp ngắn gọn để chúng ta khai báo 1 biến và gán giá trị cho nó. Với cu pháp này chúng ta không cần phải dùng từ khóa var và không cần khai báo kiểu dữ liệu
- Cách khai báo nhiều biến cùng kiểu, nhiều biến khác kiểu, khởi tạo biến và hằng
- Các kiểu dữ liệu trong Golang:
- bool: Chỉ nhận giá trị true và false
- string: Chuỗi kí tự nằm trong dấu nháy kép
- Kiểu số nguyên: int, int8, int16, int32, int64. Bạn muốn biết range các kiểu này có thể sử dụng thư viện math: math.MinInt8, math.MaxInt8,… Kiểu int sẽ là int32 hay int64 tùy thuộc vào hệ điều hành 32 bit hay 64 bit
- Kiểu số nguyên dương: uint, uint8, uint16, uint32, uint64
- byte: Kiểu kí tự, có thể dùng dạng kí tự với dấu nháy đơn hoặc số.
- Kiểu số thực: float32, float64
- Kiểu số phức: complex64, complex128
var z1 complex64 = 10 + 2i
var z2 = complex64 20 + 3i
var z = z1 + z2 - rune: Một kiểu số nguyên 32-bit và được sử dụng để đại diện cho các ký tự Unicode. Rune cung cấp khả năng xử lý và lưu trữ các ký tự Unicode trong mã nguồn của chương trình.
- uintptr: Lưu địa chỉ của con trỏ
- Kiểu dữ liệu mảng: Thường chúng ta hay khai báo mảng tĩnh, tức là mảng với số phần tử cố định. Nếu muốn tạo mảng động, bạn phải sử dụng hàm make():
b := make([]byte, 8)
pairInfos := make([]PairInfo, n)
- Kiểu dữ liệu cấu trúc struct
- Hàm trả về nhiều giá trị
- Giá trị mặc định: Trong Golang, nếu bạn khai báo một biến mà không khởi tạo thì nó sẽ có giá trị mặc định. Giá trị mặc định phụ thuộc vào kiểu dữ liệu:
- 0 cho các kiểu số
- false cho kiểu bool
- “” cho kiểu string
- Chuyển đổi kiểu dữ liệu
- Golang không hỗ trợ tự động chuyển kiểu dữ liệu, mà trong code bạn phải thực hiện chuyển kiểu tường minh, chẳng hạn:
var a int = 1
var b float64 = 2.1
fmt.Println(float64(a) + b) - Cách chuyển đổi từ dạng interface{} sang các sạng khác như Map hoặc Array => Tham khảo: How to convert interface{} to map
- Hãy tìm hiểu kỹ hơn qua bài Type Assertions
- Golang không hỗ trợ tự động chuyển kiểu dữ liệu, mà trong code bạn phải thực hiện chuyển kiểu tường minh, chẳng hạn:
- Cách truyền giá trị và tham chiếu trong Go: Tham khảo bài viết Truyền giá trị và truyền tham chiếu trong Go. Chú ý map là đối tượng đặc biệt luôn luôn là tham chiếu, nếu muốn dùng tham trị ta phải truyền vào bản copy của nó.
- Tìm hiểu về Defer: Defer Keyword in Golang
Ngoài ra một số trường hợp đặc biệt bạn phải để ý như:
Xây dựng cấu trúc thư mục cho dự án Golang
Đối với các project nhỏ bạn có thể đặt các tệp cùng một package và cùng một thư mục, nhưng với một dự án lớn thì bạn cần phải phân chia theo package, theo thư mục theo các nghiệp vụ của dự án. Khi tôi thử đặt các file khác nhau, các package khác nhau trong các thư mục khác nhau theo hướng dẫn trong bài trên StackOverFlow “package XXX is not in GOROOT” when building a Go project, tôi nhận được lỗi phát sinh như:
"package XXX is not in GOROOT" when building a Go project
Sau khi tham khảo bài viết Package Is Not in Goroot: Why It Happens and How To Fix It in Golang, tôi đã biết cách sửa cho dự án của mình.
Chúng ta có thể sử dụng cấu trúc thư mục theo phiên bản sau:
calculatorv3
├── go.mod
└── src
├─── main.go
├─── basic/
│ ├─── add.go
│ ├─── add_test.go
│ ├─── multiply.go
│ └─── multiply_test.go
└─── advanced/
├─── square.go
├─── square_test.go
└─── scientific/
├─── declog.go
└─── declog_test.go
Các lệnh để chúng ta có thể chạy project này như sau:
# Tắt chế độ sử dụng GO111MODULE
go env -w GO111MODULE=off
# Thiết lập lại biến GOPATH
export GOPATH=$(pwd)
# Khởi tạo tệp go.mod
go mod init src/main.go
# Run project
go run ./src/...
# Build project
go build -o bin/calculatorv3 ./src
Lập trình Golang thực hiện một số thao tác tới Blockchain
Đọc dữ liệu từ Blockchain
Các thao tác với blockchain thường chúng ta phải sử dụng thư viện ethclient, chi tiết các hàm bạn hãy tìm hiểu trong tài liệu của thư viện.
Đọc dữ liệu cơ bản từ blockchain
Trong ví dụ phần trước, tôi đã sử dụng hàm HeaderByNumber(), các hàm khác bạn sử dụng tương tự. Bây giờ chúng ta nâng cấp tệp use-ethclient.go để lấy thêm dữ liệu:
- Lấy balance ETH của 1 địa chỉ
- Lấy thông tin block từ block number
Với yêu cầu này chúng ta chỉ cần dùng các hàm của thư viện ethclient là đủ:
package main
import (
"context"
"fmt"
"log"
"math/big"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/common"
)
func main() {
// Connect to RPC
client, err := ethclient.Dial("https://rpc.ankr.com/eth")
if err != nil {
log.Fatalf("Failed to connect to the Ethereum network: %v", err)
} else {
fmt.Println("Success! you are connected to the Ethereum Network")
fmt.Printf("EthClient: %+v\n", client)
}
// Get header
header, err := client.HeaderByNumber(context.Background(), nil)
if err != nil {
log.Fatal(err)
} else {
fmt.Println("Lastest Block Number:", header.Number.String())
}
// Get ETH balance
addr := "0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5"
balance, err := client.BalanceAt(context.Background(), common.HexToAddress(addr), nil)
if err != nil {
log.Fatal(err)
} else {
fmt.Println("Balance of ETH", addr, balance.String())
}
// Get current gas price
gasPrice, err := client.SuggestGasPrice(context.Background())
if err != nil {
log.Fatal(err)
} else {
fmt.Println("Current Gas Price", gasPrice.String())
}
// Get Block Info
blockNumber := big.NewInt(18224273)
blockInfo, err := client.BlockByNumber(context.Background(), blockNumber)
if err != nil {
log.Fatal(err)
} else {
fmt.Println("BlockInfo for block number:", blockNumber.String())
fmt.Println("\tHash:", blockInfo.Hash())
fmt.Println("\tGasUsed:", blockInfo.GasUsed())
fmt.Println("\tDiffculty:", blockInfo.Difficulty())
fmt.Println("\tNonce:", blockInfo.Nonce())
fmt.Println("\tBaseFee:", blockInfo.BaseFee())
}
}
Do chúng ta sử dụng thêm thư viện nên chúng ta cần đánh lệnh sau để chạy:
# Cập nhật lại thư viện
go mod tidy
# Chạy ứng dụng
go run use-ethclient.go
Kết quả chúng ta sẽ có thông tin như dưới:
Success! you are connected to the Ethereum Network
EthClient: &{c:0xc00015c090}
Lastest Block Number: 18224491
Balance of ETH 0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5 6315453006381396322
Current Gas Price 7042837432
BlockInfo for block number: 18224273
Hash: 0x423bcc58da9a26e9fc9a6ddcbb6c123be0fd54a62f5ec52cbe291936fcd1f47e
GasUsed: 14630445
Diffculty: 0
Nonce: 0
BaseFee: 7949807025
Đọc dữ liệu blockchain thông qua gọi hàm của Smart Contract
Hiện tại qua tìm hiểu thì tôi thấy có ba cách để thực hiện gọi hàm của Smart Contract:
- C1: Sử dụng qua ABI thu gọn
- B1: Đầu tiên chúng ta có string chứa ABI thu gọn (ABI chỉ chứa các hàm cần)
- B2: Gọi hàm abi.JSON() trong thư viện abi => Được đối tượng
- B3: Gọi hàm Pack() của đối tượng để đóng gói tên hàm và dữ liệu đầu vào thành dữ liệu
- B4: Tạo đối tượng CallMsg
- B5: Gọi hàm CallContract của đối tượng ethclient
- B6: Gọi hàm Unpack() của đối tượng abi để được dữ liệu đầu ra
- C2: Sử dụng qua hàm NewMethod
Quá trình này phức tạp hơn nhiều vì chúng ta phải tự làm phần chuyển đổi từ ABI sang đối tượng Method- B1: Tạo đối tượng abi.Arguments cho phần inputs
- B2: Tạo đối tượng abi.Arguments cho phần outputs
- B3: Tạo đối tượng Method qua hàm NewMethod().
- B4: Dùng hàm crypto.Keccak256() để tạo signature (Chú ý chỉ lấy 4 byte đầu tiên)
- B5: Mã hóa inputs sử dụng hàm Pack()
- B6: Nối signature với inputs đã mã hóa ta được dữ liệu data
- B7: Tạo đối tượng CallMsg
- B8: Gọi hàm CallContract của đối tượng ethclient
- B9: Gọi hàm Unpack() của đối tượng abi để được dữ liệu đầu ra
- C3: Sinh tệp .go từ tệp ABI thông qua công cụ abigen
=> Nói chi tiết ở phần sau
Chúng ta tạo tệp call-smartcontract.go gọi hàm smart contract theo C1 và C2:
- Gọi hàm balanceOf() để lấy lượng token USDT theo C1
- Gọi hàm getReserves() để lấy thông tin dự trữ của Pair V2 theo C2
package main
import (
"context"
"fmt"
"log"
"strings"
"encoding/hex"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/crypto"
)
func main() {
// Connect to RPC
client, err := ethclient.Dial("https://rpc.ankr.com/eth")
if err != nil {
log.Fatalf("Failed to connect to the Ethereum network: %v", err)
return
}
fmt.Println("Success! you are connected to the Ethereum Network")
fmt.Printf("EthClient: %+v\n", client)
fmt.Println("------------------------------------------------------")
// Call balanceOf() of USDT contract over ABI
ABI := "[{\"constant\":true,\"inputs\":[{\"name\":\"who\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"}]"
tokenAbi, err := abi.JSON(strings.NewReader(ABI))
data, err := tokenAbi.Pack("balanceOf", common.HexToAddress("0xF977814e90dA44bFA03b6295A0616a897441aceC"))
tokenAddr := common.HexToAddress("0xdac17f958d2ee523a2206206994597c13d831ec7")
msg := ethereum.CallMsg{
To: &tokenAddr,
Data: data,
}
outputData, err := client.CallContract(context.Background(), msg, nil)
if err != nil {
log.Fatalf("Call balanceOf() ERROR: %v", err)
return
}
balance, err := tokenAbi.Unpack("balanceOf", outputData)
if err != nil {
log.Fatalf("Decode output of balanceOf() ERROR: %v", err)
return
}
fmt.Println("USDT:", balance[0])
fmt.Println("------------------------------------------------------")
// Call getReserves() using NewMethod
funcName := "getReserves"
var inputs abi.Arguments = nil
uint112Type, err := abi.NewType("uint112", "uint112", nil)
outputs := abi.Arguments{
abi.Argument{
Name: "reserve0",
Type: uint112Type,
Indexed: false,
},
abi.Argument{
Name: "reserve1",
Type: uint112Type,
Indexed: false,
},
}
method := abi.NewMethod(funcName, funcName, abi.FunctionType(3), "view", false, false, inputs, outputs)
sig := crypto.Keccak256([]byte(method.Sig))
sig = []byte{sig[0], sig[1], sig[2], sig[3]}
fmt.Println("Method:", method.String(), " => Sig:", "0x" + hex.EncodeToString(sig))
data = sig
pairV2Addr := common.HexToAddress("0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc")
msg = ethereum.CallMsg{
To: &pairV2Addr,
Data: data,
}
outputData, err = client.CallContract(context.Background(), msg, nil)
if err != nil {
log.Fatalf("Call getReserves() ERROR: %v", err)
return
}
reserves, err := outputs.Unpack(outputData)
if err != nil {
log.Fatalf("Decode output of getReserves() ERROR: %v", err)
return
}
fmt.Println("Reserves:", reserves)
fmt.Println("------------------------------------------------------")
}
Khi chạy chúng ta nhận kết quả như sau:
Success! you are connected to the Ethereum Network
EthClient: &{c:0xc00016a090}
------------------------------------------------------
USDT: 3165153978018000
------------------------------------------------------
Method: function getReserves() view returns(uint112 reserve0, uint112 reserve1) => Sig: 0x0902f1ac
Reserves: [27717096146069 17239779481733920536936]
------------------------------------------------------
Tương tác với Smart Contract thông qua công cụ abigen
Abigen là một công cụ giúp chuyển đổi ABI sang GoLang, qua đó giúp bạn dễ dàng tương tác với Smart Contract. Khi bạn sử dụng thư viện go-ethereum thì sẽ có source code của công cụ này, bạn tìm bằng lệnh sau:
# Tìm vị trí mã nguồn abigen
# Tôi tìm thấy trong: /home/ubuntu/go/pkg/mod/github.com/ethereum/go-ethereum@v1.13.1/cmd/abigen
locate abigen
# Build công cụ abigen
cd /home/ubuntu/go/pkg/mod/github.com/ethereum/go-ethereum@v1.13.1
go build ./cmd/abigen
# Sau khi build xong copy abigen vào thư mục /usr/local/bin
Nếu bạn chạy lệnh trên báo lỗi dạng:
github.com/ethereum/go-ethereum/cmd/abigen: go build github.com/ethereum/go-ethereum/cmd/abigen: copying /tmp/go-build1832924738/b001/exe/a.out: open abigen: permission denied
Thì bạn phải chuyển sang quyền sudo để build:
sudo go build ./cmd/abigen
Nếu bạn lại nhận lỗi khác:
sudo: go: command not found
Lỗi này do ở account root, biến môi trường PATH chưa có đường dẫn đến Go. Chúng ta sẽ làm thủ công như sau:
# Chuyển sang user root
sudo -i
export PATH=$PATH:/usr/local/go/bin
# Vào lại thư mục mã nguồn
cd /home/ubuntu/go/pkg/mod/github.com/ethereum/go-ethereum@v1.13.1
go build ./cmd/abigen
# Tệp abigen sinh ra ngay thư mục hiện tại
# Kiểm tra lại bằng lệnh dưới
abigen --version
Bây giờ chúng ta sử dụng công cụ này để tương tác với Uniswap Pair V3 0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640 (USDC / ETH). Đầu tiên chúng ta sẽ copy ABI của contract về lưu trong tệp UniswapPairV3.abi, sau đó chúng ta đánh lệnh sau:
# Sinh tệp UniswapPairV3.go
abigen --abi UniswapPairV3.abi --pkg main --type UniswapPairV3 --out UniswapPairV3.go
Tệp này khá lớn, bạn có thể xem tại UniswapPairV3.go. Chú ý trong tệp này chúng ta sẽ thấy có 3 hàm mà chúng ta sẽ dùng:
func NewUniswapPairV3(address common.Address, backend bind.ContractBackend) (*UniswapPairV3, error) {
// ...
}
func (_UniswapPairV3 *UniswapPairV3Caller) Slot0(opts *bind.CallOpts) (struct {
SqrtPriceX96 *big.Int
Tick *big.Int
ObservationIndex uint16
ObservationCardinality uint16
ObservationCardinalityNext uint16
FeeProtocol uint8
Unlocked bool
}, error) {
// ...
}
func (_UniswapPairV3 *UniswapPairV3Caller) Liquidity(opts *bind.CallOpts) (*big.Int, error) {
// ...
}
Bây giờ ta tạo tệp call-smartcontract-02.go sử dụng các hàm trong tệp UniswapPairV3.go:
package main
import (
"fmt"
"log"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/common"
)
func main() {
// Connect to RPC
client, err := ethclient.Dial("https://rpc.ankr.com/eth")
if err != nil {
log.Fatalf("Failed to connect to the Ethereum network: %v", err)
return
}
fmt.Println("Success! you are connected to the Ethereum Network")
fmt.Printf("EthClient: %+v\n", client)
fmt.Println("------------------------------------------------------")
// Create UniswapPairV3
pairV3, err := NewUniswapPairV3(common.HexToAddress("0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640"), client)
slot0, err := pairV3.Slot0(nil)
liquidity, err := pairV3.Liquidity(nil)
fmt.Println("Slot0:", slot0)
fmt.Println("Liquidity:", liquidity)
fmt.Println("------------------------------------------------------")
}
Chạy bằng lệnh sau:
go run call-smartcontract-02.go UniswapPairV3.go
Output như dưới:
Success! you are connected to the Ethereum Network
EthClient: &{c:0xc00011e3f0}
------------------------------------------------------
Slot0: {1979924313596069596897820693414386 202534 29 722 722 0 true}
Liquidity: 40784110340099305750
------------------------------------------------------
Sử dụng Multicall3
Multicall3 là contract hết sức tiện lợi khi cần lấy dữ liệu trên Blockchain. Để sử dụng Multicall3 trên Go, ta làm như sau. Đầu tiên ta vào 0xcA11bde05977b3631167028862bE2a173976CA11, xuống phần Contract ABI, ta chỉ lấy ABI cho hàm aggregate3, vì chúng ta chỉ sử dụng hàm này thôi. Ta tạo tệp Multicall3.abi và copy ABI của aggregate3 vào như sau (Chú ý bạn phải đổi stateMutability từ payable thành view):
[
{
"inputs":[
{
"components":[
{
"internalType":"address",
"name":"target",
"type":"address"
},
{
"internalType":"bool",
"name":"allowFailure",
"type":"bool"
},
{
"internalType":"bytes",
"name":"callData",
"type":"bytes"
}
],
"internalType":"struct Multicall3.Call3[]",
"name":"calls",
"type":"tuple[]"
}
],
"name":"aggregate3",
"outputs":[
{
"components":[
{
"internalType":"bool",
"name":"success",
"type":"bool"
},
{
"internalType":"bytes",
"name":"returnData",
"type":"bytes"
}
],
"internalType":"struct Multicall3.Result[]",
"name":"returnData",
"type":"tuple[]"
}
],
"stateMutability":"view",
"type":"function"
}
]
Bây giờ ta đánh lệnh sau để sinh ra tệp Multicall3.go
abigen --abi Multicall3.abi --pkg main --type Multicall3 --out Multicall3.go
Bây giờ ta viết tệp use-multicall.go để sử dụng các hàm trong Multicall3.go được sinh ra ở trên:
xxxxxx
Khi thực hiện kết nối để lấy dữ liệu trên Base Blockchain (https://mainnet.base.org) thì mọi thứ chạy OKIE. Nhưng khi chạy Hardhat để Fork:
npx hardhat node --fork https://mainnet.base.org --fork-block-number 5967376
Và thực hiện lấy dữ liệu trên Hardhat Forking (http://127.0.0.1:8545) thì báo lỗi “Error: Transaction reverted without a reason string“
Ghi dữ liệu vào blockchain
Để tiết kiệm tiền, các giao dịch ghi dữ liệu tôi sẽ chạy trên Goerli Testnet
Chuyển ETH giữa các ví
Chuyển ETH giữa các ví bạn có thể xem hướng dẫn trong bài: Transferring ETH. Mã nguồn tệp transfer-eth.go như dưới:
package main
import (
"context"
"fmt"
"log"
"math/big"
"crypto/ecdsa"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/core/types"
)
func main() {
client, err := ethclient.Dial("https://rpc.ankr.com/eth_goerli")
if err != nil {
log.Fatalf("Failed to connect to the Ethereum network: %v", err)
} else {
fmt.Println("Success! you are connected to the Ethereum Network")
fmt.Printf("EthClient: %+v\n", client)
}
// Load private key
// Account: 0x96216849c49358B10257cb55b28eA603c874b05E
privateKey, err := crypto.HexToECDSA("fad9c8855b740a0b7ed4c221dbad0f33a83a49cad6b3fe8d5817ac83d38b6a19")
publicKey := privateKey.Public()
publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
if !ok {
log.Fatal("error casting public key to ECDSA")
}
fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA)
fmt.Println("From Address:", fromAddress.Hex())
// Get nonce
nonce, err := client.PendingNonceAt(context.Background(), fromAddress)
if err != nil {
log.Fatal(err)
}
fmt.Println("Nonce:", nonce)
// Transfer value
value := big.NewInt(1000000000000) // in wei (0.000001 eth)
fmt.Println("Value:", value)
// Gas Limit
gasLimit := uint64(30000) // in units
fmt.Println("Gas Limit:", gasLimit)
// Gas Price
gasPrice, err := client.SuggestGasPrice(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Println("Gas Price:", gasPrice)
// Chain ID
chainID, err := client.NetworkID(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Println("ChainID:", chainID)
// To Address
toAddress := common.HexToAddress("0x4592d8f8d7b001e72cb26a73e4fa1806a51ac79d")
fmt.Println("To Address:", toAddress.Hex())
// Create transaction
var data []byte
tx := types.NewTransaction(nonce, toAddress, value, gasLimit, gasPrice, data)
// Sign transaction
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey)
if err != nil {
log.Fatal(err)
}
// Send transaction
err = client.SendTransaction(context.Background(), signedTx)
if err != nil {
log.Fatal(err)
}
fmt.Println("TxId:", signedTx.Hash().Hex())
}
Kết quả sau khi chạy (Xem giao dịch):
Success! you are connected to the Ethereum Network
EthClient: &{c:0xc00015c090}
From Address: 0x96216849c49358B10257cb55b28eA603c874b05E
Nonce: 645
Value: 1000000000000
Gas Limit: 30000
Gas Price: 49
ChainID: 5
To Address: 0x4592D8f8D7B001e72Cb26A73e4Fa1806a51aC79d
TxId: 0xb55dfb72a9f7a51bfdd9cee1281a5c10e96b3ea3ff4915746180e47bae8bebb0
Chuyển token giữa các ví
Chuyển token giữa các ví bạn có thể xem bài viết: Transferring Tokens. Do lỗi phát sinh nên tôi có thay đổi chút để chạy được:
- Tôi không sử dụng thư viện sha3 do báo lỗi không có thư viện này (Có thể liên quan tới version thư viện của ethereum)
- Tôi fix cứng Gas Limit do hàm EstimateGas() có vấn đề
Toàn bộ code trong tệp transfer-token.go:
package main
import (
"context"
"fmt"
"log"
"math/big"
"crypto/ecdsa"
// "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/common/hexutil"
)
func main() {
client, err := ethclient.Dial("https://rpc.ankr.com/eth_goerli")
if err != nil {
log.Fatalf("Failed to connect to the Ethereum network: %v", err)
} else {
fmt.Println("Success! you are connected to the Ethereum Network")
fmt.Printf("EthClient: %+v\n", client)
}
// Load private key
// Account: 0x96216849c49358B10257cb55b28eA603c874b05E
privateKey, err := crypto.HexToECDSA("fad9c8855b740a0b7ed4c221dbad0f33a83a49cad6b3fe8d5817ac83d38b6a19")
publicKey := privateKey.Public()
publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
if !ok {
log.Fatal("error casting public key to ECDSA")
}
fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA)
fmt.Println("From Address:", fromAddress.Hex())
// Get nonce
nonce, err := client.PendingNonceAt(context.Background(), fromAddress)
if err != nil {
log.Fatal(err)
}
fmt.Println("Nonce:", nonce)
// Transfer value
value := big.NewInt(0)
fmt.Println("Value:", value)
// Gas Price
gasPrice, err := client.SuggestGasPrice(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Println("Gas Price:", gasPrice)
// Chain ID
chainID, err := client.NetworkID(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Println("ChainID:", chainID)
// To Address
toAddress := common.HexToAddress("0x4592d8f8d7b001e72cb26a73e4fa1806a51ac79d")
tokenAddress := common.HexToAddress("0x326C977E6efc84E512bB9C30f76E30c160eD06FB")
fmt.Println("To Address:", toAddress.Hex())
fmt.Println("Token Address:", tokenAddress.Hex())
// Transfer amount
amount := big.NewInt(1000000000000000) // 0.001
fmt.Println("Amount:", amount)
// Generate 4 bytes function signature
transferFnSignature := []byte("transfer(address,uint256)")
hash := crypto.Keccak256(transferFnSignature)
methodID := []byte{hash[0], hash[1], hash[2], hash[3]}
fmt.Println("Method ID:", hexutil.Encode(methodID)) // 0xa9059cbb
// Generate data
var data []byte
data = append(data, methodID...)
paddedAddress := common.LeftPadBytes(toAddress.Bytes(), 32)
data = append(data, paddedAddress...)
paddedAmount := common.LeftPadBytes(amount.Bytes(), 32)
data = append(data, paddedAmount...)
fmt.Println("Data:", hexutil.Encode(data))
// Estimate gas
//gasLimit, err := client.EstimateGas(context.Background(), ethereum.CallMsg{
// To: &tokenAddress,
// Data: data,
//})
//if err != nil {
// log.Fatal("Gas Limit: ", err)
//}
gasLimit := uint64(120000)
fmt.Println("Gas Limit:", gasLimit)
// Create transaction
tx := types.NewTransaction(nonce, tokenAddress, value, gasLimit, gasPrice, data)
// Sign transaction
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey)
if err != nil {
log.Fatal(err)
}
// Send transaction
err = client.SendTransaction(context.Background(), signedTx)
if err != nil {
log.Fatal(err)
}
fmt.Println("TxId:", signedTx.Hash().Hex())
}
Kết quả sau khi chay (Xem giao dịch):
Success! you are connected to the Ethereum Network
EthClient: &{c:0xc00015c090}
From Address: 0x96216849c49358B10257cb55b28eA603c874b05E
Nonce: 650
Value: 0
Gas Price: 28
ChainID: 5
To Address: 0x4592D8f8D7B001e72Cb26A73e4Fa1806a51aC79d
Token Address: 0x326C977E6efc84E512bB9C30f76E30c160eD06FB
Amount: 1000000000000000
Method ID: 0xa9059cbb
Data: 0xa9059cbb0000000000000000000000004592d8f8d7b001e72cb26a73e4fa1806a51ac79d00000000000000000000000000000000000000000000000000038d7ea4c68000
Gas Limit: 120000
TxId: 0xc917e323dad00f9a5569e1b94f5b71ba980fd1be4dd7df907d20398630a7cad9
Swap token trên Pool V3
Trong ví dụ này tôi sẽ swap từ ETH sang UNI, thông tin chi tiết swap sẽ thực hiện như sau:
- SwapRouter: 0xE592427A0AEce92De3Edee1F18E0157C05861564
- UNI-WETH-500: 0x07A4f63f643fE39261140DF5E613b9469eccEC86
- WETH: 0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6
- UNI: 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984
Nếu bạn muốn thử swap trên pool khác, bạn có thể lấy thông tin pool trong bài viết: Hướng dẫn triển khai Flashloan / Flashswap sử dụng Uniswap V3 trên Goerli Testnet
Ta tạo tệp swap-token.go để thực hiện swap token:
package main
import (
"context"
"fmt"
"log"
"math/big"
"crypto/ecdsa"
// "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/common/hexutil"
)
func main() {
client, err := ethclient.Dial("https://rpc.ankr.com/eth_goerli")
if err != nil {
log.Fatalf("Failed to connect to the Ethereum network: %v", err)
} else {
fmt.Println("Success! you are connected to the Ethereum Network")
fmt.Printf("EthClient: %+v\n", client)
}
// Load private key
// Account: 0x96216849c49358B10257cb55b28eA603c874b05E
privateKey, err := crypto.HexToECDSA("fad9c8855b740a0b7ed4c221dbad0f33a83a49cad6b3fe8d5817ac83d38b6a19")
publicKey := privateKey.Public()
publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
if !ok {
log.Fatal("error casting public key to ECDSA")
}
fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA)
fmt.Println("From Address:", fromAddress.Hex())
// Get nonce
nonce, err := client.PendingNonceAt(context.Background(), fromAddress)
if err != nil {
log.Fatal(err)
}
fmt.Println("Nonce:", nonce)
// Transfer value
value := big.NewInt(1000000000000000) // 0.001 ETH
fmt.Println("Value:", value)
// Gas Price
gasPrice, err := client.SuggestGasPrice(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Println("Gas Price:", gasPrice)
// Chain ID
chainID, err := client.NetworkID(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Println("ChainID:", chainID)
// To Address
recipientAddress := common.HexToAddress("0x4592d8f8d7b001e72cb26a73e4fa1806a51ac79d")
swapRouter := common.HexToAddress("0xE592427A0AEce92De3Edee1F18E0157C05861564")
tokenIn := common.HexToAddress("0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6") // WETH
tokenOut := common.HexToAddress("0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984") // UNI
poolFee := big.NewInt(500)
fmt.Println("Recipient Address:", recipientAddress.Hex())
fmt.Println("Swap Router:", swapRouter.Hex())
fmt.Println("Token In:", tokenIn.Hex())
fmt.Println("Token Out:", tokenOut.Hex())
fmt.Println("Pool Fee:", poolFee)
// Generate 4 bytes function signature
transferFnSignature := []byte("exactInputSingle((address,address,uint24,address,uint256,uint256,uint256,uint160))")
hash := crypto.Keccak256(transferFnSignature)
methodID := []byte{hash[0], hash[1], hash[2], hash[3]}
fmt.Println("Method ID:", hexutil.Encode(methodID)) // 0x414bf389
// Generate data
var data []byte
data = append(data, methodID...)
tempData := common.LeftPadBytes(tokenIn.Bytes(), 32)
data = append(data, tempData...)
tempData = common.LeftPadBytes(tokenOut.Bytes(), 32)
data = append(data, tempData...)
tempData = common.LeftPadBytes(poolFee.Bytes(), 32)
data = append(data, tempData...)
tempData = common.LeftPadBytes(recipientAddress.Bytes(), 32)
data = append(data, tempData...)
tempData = common.LeftPadBytes(big.NewInt(2999999999).Bytes(), 32)
data = append(data, tempData...)
tempData = common.LeftPadBytes(value.Bytes(), 32)
data = append(data, tempData...)
tempData = common.LeftPadBytes(big.NewInt(0).Bytes(), 32)
data = append(data, tempData...)
tempData = common.LeftPadBytes(big.NewInt(0).Bytes(), 32)
data = append(data, tempData...)
fmt.Println("Data:", hexutil.Encode(data))
// Fix gas
gasLimit := uint64(200000)
fmt.Println("Gas Limit:", gasLimit)
// Create transaction
tx := types.NewTransaction(nonce, swapRouter, value, gasLimit, gasPrice, data)
// Sign transaction
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey)
if err != nil {
log.Fatal(err)
}
// Send transaction
err = client.SendTransaction(context.Background(), signedTx)
if err != nil {
log.Fatal(err)
}
fmt.Println("TxId:", signedTx.Hash().Hex())
}
Kết quả sau khi chạy như sau (Xem giao dịch):
Success! you are connected to the Ethereum Network
EthClient: &{c:0xc00015c090}
From Address: 0x96216849c49358B10257cb55b28eA603c874b05E
Nonce: 651
Value: 1000000000000000
Gas Price: 63
ChainID: 5
Recipient Address: 0x4592D8f8D7B001e72Cb26A73e4Fa1806a51aC79d
Swap Router: 0xE592427A0AEce92De3Edee1F18E0157C05861564
Token In: 0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6
Token Out: 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984
Pool Fee: 500
Method ID: 0x414bf389
Data: 0x414bf389000000000000000000000000b4fbf271143f4fbf7b91a5ded31805e42b2208d60000000000000000000000001f9840a85d5af5bf1d1762f925bdaddc4201f98400000000000000000000000000000000000000000000000000000000000001f40000000000000000000000004592d8f8d7b001e72cb26a73e4fa1806a51ac79d00000000000000000000000000000000000000000000000000000000b2d05dff00000000000000000000000000000000000000000000000000038d7ea4c6800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
Gas Limit: 200000
TxId: 0xe4065836134889e078d13541970c40ec83ebef64241f14fba89f5398479ac028
Lắng nghe sự kiện từ Blockchain
Lắng nghe sự kiện khi có block mới
Chi tiết hướng dẫn, bạn tham khảo bài viết: Subscribing to New Blocks. Trong ví dụ dưới, tôi sẽ lắng nghe sự kiện có block mới trên Base blockchain. Chúng ta cần xác định block mới nhất là block nào và dữ liệu đang trễ bao nhiều so với hiện tại, do đó tôi sửa lại mã nguồn. Trong ví dụ này tôi sử dụng node nội bộ nên link websocket là ws://localhost:8546, bạn không có node nội bộ, bạn có thể sử dụng public websocket như wss://base.publicnode.com. Mã nguồn tệp block_subscribe.go như sau:
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
)
func main() {
client, err := ethclient.Dial("ws://localhost:8546")
// client, err := ethclient.Dial("wss://base.publicnode.com")
if err != nil {
log.Fatal("Connect websocket: ", err)
}
fmt.Println("Success! you are connected to the Base Network")
fmt.Printf("EthClient: %+v\n", client)
headers := make(chan *types.Header)
sub, err := client.SubscribeNewHead(context.Background(), headers)
if err != nil {
log.Fatal("Subcribe New head: ", err)
}
for {
select {
case err := <-sub.Err():
log.Fatal("Data Error", err)
case header := <-headers:
fmt.Println("----------------------------------")
fmt.Println(" Block Number:", header.Number)
fmt.Println(" Block Time:", header.Time)
fmt.Println(" Block Hash:", header.Hash().Hex())
fmt.Println(" Delay Time (Ms)", time.Now().UnixMilli() - 1000*int64(header.Time))
fmt.Println("----------------------------------")
}
}
}
Kết quả sau khi chạy chương trình như sau:
Success! you are connected to the Base Network
EthClient: &{c:0xc00015c360}
----------------------------------
Block Number: 4591872
Block Time: 1695973091
Block Hash: 0x1cc41c80d7b4837b74d40b889edd891679022f8dee314811aa9be285a6391bed
Delay Time (Ms) 43248
----------------------------------
----------------------------------
Block Number: 4591873
Block Time: 1695973093
Block Hash: 0x38d8c6ca14e5770dd9e21b388902753d4cf930f547c7ec200877b71d2800193f
Delay Time (Ms) 41294
----------------------------------
----------------------------------
Block Number: 4591874
Block Time: 1695973095
Block Hash: 0xe9680a7ccd0d8fc96f9cf42d2d55d0185c3fc2329341af441c232db410416c9b
Delay Time (Ms) 39427
----------------------------------
Lắng nghe Event Logs
Phần này tôi tham khảo Subscribing to Event Logs và Reading Event Logs. Dựa vào hướng dẫn, tôi xây dựng ví dụ lắng nghe hai sự kiện:
- Sự kiện Sync (Trên các Pool của Uniswap V2)
- Sự kiện Swap (Trên các Pool của Uniswap V3)
Để cập nhật thông tin mới nhất các pool. Mã nguồn đóng gói trong tệp event_subscribe.go như dưới:
package main
import (
"context"
"fmt"
"log"
"strings"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/accounts/abi"
)
func main() {
// client, err := ethclient.Dial("ws://localhost:8546")
client, err := ethclient.Dial("wss://base.publicnode.com")
if err != nil {
log.Fatal(err)
}
fmt.Println("Success! you are connected to the Base Network")
fmt.Printf("EthClient: %+v\n", client)
// Event ABI
eventABI := "[{\"anonymous\": false,\"inputs\": [{\"indexed\": false,\"internalType\": \"uint112\",\"name\": \"reserve0\",\"type\": \"uint112\"},{\"indexed\": false,\"internalType\": \"uint112\",\"name\": \"reserve1\",\"type\": \"uint112\"}],\"name\": \"Sync\",\"type\": \"event\"},{\"anonymous\": false,\"inputs\": [{\"indexed\": true,\"internalType\": \"address\",\"name\": \"sender\",\"type\": \"address\"},{\"indexed\": true,\"internalType\": \"address\",\"name\": \"recipient\",\"type\": \"address\"},{\"indexed\": false,\"internalType\": \"int256\",\"name\": \"amount0\",\"type\": \"int256\"},{\"indexed\": false,\"internalType\": \"int256\",\"name\": \"amount1\",\"type\": \"int256\"},{\"indexed\": false,\"internalType\": \"uint160\",\"name\": \"sqrtPriceX96\",\"type\": \"uint160\"},{\"indexed\": false,\"internalType\": \"uint128\",\"name\": \"liquidity\",\"type\": \"uint128\"},{\"indexed\": false,\"internalType\": \"int24\",\"name\": \"tick\",\"type\": \"int24\"}],\"name\": \"Swap\",\"type\": \"event\"}]"
eventAbi, err := abi.JSON(strings.NewReader(eventABI))
if err != nil {
log.Fatal("Event ABI ERROR: ", err)
}
// Events:
// Sync event for Pair V2
// Swap event for Pair V3
var syncTopic = common.HexToHash("0x1c411e9a96e071241c2f21f7726b17ae89e3cab4c78be50e062b03a9fffbbad1")
var swapV3Topic = common.HexToHash("0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67")
// Create filter
topics := [][]common.Hash{{syncTopic, swapV3Topic}}
query := ethereum.FilterQuery{
Topics: topics,
}
logs := make(chan types.Log)
sub, err := client.SubscribeFilterLogs(context.Background(), query, logs)
if err != nil {
log.Fatal(err)
}
eventSyncDataMap := map[string]interface{}{}
eventSwapDataMap := map[string]interface{}{}
for {
select {
case err := <-sub.Err():
log.Fatal(err)
case vLog := <-logs:
//fmt.Println(vLog) // Print log data to debug
if (vLog.Topics[0]==syncTopic) {
// Sync event for Pair V2
fmt.Println("Sync event for V2:")
fmt.Println(" Pair:", vLog.Address.Hex())
err := eventAbi.UnpackIntoMap(eventSyncDataMap, "Sync", vLog.Data)
if err!=nil {
fmt.Println(" Parse data error:", err)
} else {
fmt.Println(" Reserves:", eventSyncDataMap)
}
} else if (vLog.Topics[0]==swapV3Topic) {
// Swap event for Pair V3
fmt.Println("Swap event for V3:")
fmt.Println(" Pair:", vLog.Address.Hex())
err := eventAbi.UnpackIntoMap(eventSwapDataMap, "Swap", vLog.Data)
if err!=nil {
fmt.Println(" Parse data error:", err, common.Bytes2Hex(vLog.Data))
} else {
fmt.Println(" SwapInfo:", eventSwapDataMap)
}
}
//fmt.Println(vLog) // pointer to event log
}
}
}
Success! you are connected to the Base Network
EthClient: &{c:0xc00016a1b0}
Swap event for V3:
Pair: 0x4C36388bE6F416A29C8d8Eee81C771cE6bE14B18
SwapInfo: map[amount0:128000000000000000 amount1:-214596119 liquidity:324535014823069391 sqrtPriceX96:3244820116208715158447683 tick:-202071]
Sync event for V2:
Pair: 0x6D3c5a4a7aC4B1428368310E4EC3bB1350d01455
Reserves: map[reserve0:1045335009077065580521023 reserve1:1045741040809]
Swap event for V3:
Pair: 0x3B8000CD10625ABdC7370fb47eD4D4a9C6311fD5
SwapInfo: map[amount0:5049996066912133 amount1:-8466734 liquidity:20979686887511447 sqrtPriceX96:3244229835509471844999839 tick:-202075]
Làm việc với thư viện mã hóa trong Crypto
Khi làm việc với Blockchain và Crypto thì chắc chắn bạn phải cần làm việc với Mã hóa / Giải mã sử dụng khóa bất đối xứng. Bạn tham khảo thư viện crypto/rsa trong GoLang.
Bây giờ chúng ta viết tệp rsa.go thực hiện các việc sau:
- Sinh ra cặp khóa RSA Private/Public key 4096 bit và thực hiện lưu key này xuống tệp PEM (keys/private.pem và keys/public.pem)
- Mã hóa đoạn văn bản sử dụng Public key được load lên từ tệp keys/public.pem
- Giải mã văn bản sử dụng Private key được load lên từ tệp keys/private.pem
Chi tiết mã nguồn như dưới:
package main
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/hex"
"encoding/pem"
"fmt"
"log"
"os"
)
func GenerateKey() *rsa.PrivateKey {
var size int = 4096
priv, err := rsa.GenerateKey(rand.Reader, size)
if err != nil {
log.Printf("GenerateKey(%d): %v\n", size, err)
return nil
}
if bits := priv.N.BitLen(); bits != size {
log.Printf("Key too short (%d vs %d)\n", bits, size)
return nil
}
return priv
}
func SavePrivateKey(fileName string, key *rsa.PrivateKey) bool {
outFile, err := os.Create(fileName)
if err != nil {
log.Println("Error to create file:", fileName, err)
return false
}
defer outFile.Close()
var privateKey = &pem.Block{
Type: "PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(key),
}
err = pem.Encode(outFile, privateKey)
if err != nil {
log.Println("Error to save key to file:", fileName, err)
return false
}
return true
}
func PrivateKey2Bytes(priv *rsa.PrivateKey) []byte {
data := x509.MarshalPKCS1PrivateKey(priv)
if data == nil {
fmt.Println("Unable to encode private key")
return nil
}
return data
}
func Bytes2PrivateKey(data []byte) *rsa.PrivateKey {
privKey, err := x509.ParsePKCS1PrivateKey(data)
if err != nil {
fmt.Println("Unable to decode public key", err)
return nil
}
return privKey
}
func PublicKey2Bytes(pub *rsa.PublicKey) []byte {
data := x509.MarshalPKCS1PublicKey(pub)
if data == nil {
fmt.Println("Unable to encode public key")
return nil
}
return data
}
func Bytes2PublicKey(data []byte) *rsa.PublicKey {
pubKey, err := x509.ParsePKCS1PublicKey(data)
if err != nil {
fmt.Println("Unable to decode public key", err)
return nil
}
return pubKey
}
// keyType: "PUBLIC KEY" / "PRIVATE KEY"
func SaveKeyToFile(fileName string, data []byte, keyType string) bool {
var pemkey = &pem.Block{
Type: keyType,
Bytes: data,
}
pemfile, err := os.Create(fileName)
if err != nil {
log.Println("Error to create file:", fileName, err)
return false
}
defer pemfile.Close()
err = pem.Encode(pemfile, pemkey)
if err != nil {
log.Println("Error to save key to file:", fileName, err)
return false
}
return true
}
func LoadKeyFromFile(fileName string) []byte {
data, err := os.ReadFile(fileName)
if err != nil {
log.Println("Error to read file:", fileName, err)
return nil
}
block, _ := pem.Decode(data)
if block == nil {
log.Println("Error to decode key from file:", fileName, err)
return nil
}
return block.Bytes
}
func ShowHelp() {
fmt.Println("Commands:")
fmt.Println(" go run rsa.go generate-key")
fmt.Println(" go run rsa.go encrypt \"Testing data\"")
fmt.Println(" go run rsa.go decrypt <data>")
}
// Main function
func main() {
if len(os.Args) < 2 {
ShowHelp()
return
}
privateKeyFile := "keys/private.pem"
publicKeyFile := "keys/public.pem"
arg := os.Args[1]
if arg == "generate-key" {
os.Mkdir("keys", 0750)
// Generate key
privKey := GenerateKey()
// Save private key
privData := PrivateKey2Bytes(privKey)
fmt.Println("PrivateKey:", hex.EncodeToString(privData))
SaveKeyToFile(privateKeyFile, privData, "PRIVATE KEY")
fmt.Println("Save private key to file:", privateKeyFile)
// Save public key
pubData := PublicKey2Bytes(&privKey.PublicKey)
fmt.Println("PublicKey:", hex.EncodeToString(pubData))
SaveKeyToFile(publicKeyFile, pubData, "PUBLIC KEY")
fmt.Println("Save public key to file:", publicKeyFile)
} else if arg == "encrypt" {
// Encrypt message
if len(os.Args) < 3 {
ShowHelp()
return
}
message := os.Args[2]
pubData := LoadKeyFromFile(publicKeyFile)
pubKey := Bytes2PublicKey(pubData)
if pubKey == nil {
return
}
data, err := rsa.EncryptPKCS1v15(rand.Reader, pubKey, []byte(message))
if err != nil {
fmt.Println("Unable to encrypt message", message, err)
} else {
strHexa := hex.EncodeToString(data)
fmt.Println("Encrypted Data:", strHexa)
}
} else if arg == "decrypt" {
// Decrypt message
if len(os.Args) < 3 {
ShowHelp()
return
}
strHexa := os.Args[2]
encryptedData, err := hex.DecodeString(strHexa)
if err != nil {
fmt.Println("Unable to convert hexa to bytes", strHexa)
return
}
privData := LoadKeyFromFile(privateKeyFile)
privKey := Bytes2PrivateKey(privData)
if privKey == nil {
return
}
data, err := rsa.DecryptPKCS1v15(nil, privKey, encryptedData)
if err != nil {
fmt.Println("Unable to decrypt message:", err)
} else {
message := string(data)
fmt.Println("Message:", message)
}
} else {
ShowHelp()
}
}
Các bước để chạy như sau:
# B1: Sinh cặp khóa RSA
go run rsa.go generate-key
# B2: Mã hóa dữ liệu sử dụng Public key được sinh ra ở B1
# Mặc dù cùng 1 message đầu vào, bạn chạy các lần khác nhau cho kết quả khác nhau do cơ chế làm nhiều đầu ra
# Nhưng khi bạn giải mã đều cho cùng 1 kết quả nếu cùng 1 message đầu vào
go run rsa.go encrypt "Testing data"
# B3: Giải mã dữ liệu sử dụng Private key được sinh ra ở B1
# Thay cipher-data bằng dữ liệu đã được mã hóa ở B2
go run rsa.go decrypt <cipher-data>
Lập trình Golang kết nối đến một số dịch vụ khác
Kết nối tới MongoDb
Chi tiết tài liệu kết nối tới MongoDb, bạn xem tại: MongoDB Go Driver. Thư viện chúng ta sử dụng sẽ là mongo.
Một số lỗi phát sinh
* Unrecognized field ‘apiVersion’
Lỗi này do khi khởi tạo Mongo thì xác định chính xác version api nhưng mà trên server lại không hỗ trợ:
serverAPI := options.ServerAPI(options.ServerAPIVersion1)
opts := options.Client().ApplyURI(appData.Config.MongoUri).SetServerAPIOptions(serverAPI)
Chỉ cần sửa lại bỏ thiết lập serverAPI là xong:
opts := options.Client().ApplyURI(appData.Config.MongoUri)
Kết nối tới RESTFul API
Unrecognized field ‘apiVersion’
Tham khảo:
- Ethereum Development with Go (Hầu hết mọi thứ đều nằm trong Tài liệu này)
- How to connect to Ethereum network using Go
- Lập trình Golang
Trả lời