Golang Web3 Development Book
Welcome to the world of decentralized blockchain
This book will guide you through the development of a decentralized application, including:
- golang web3 development
This book is not for complete beginners.
requires: base solidity knowledge
Alright, let’s get started!
Useful Links
- This book is hosted on GitHub: https://github.com/yuhuajing/web3-development-with-go-book
Initialization
初始化参数
golang
程序变量的初始化和变量的初始依赖相关- 初始化过程会存在多轮周期,每轮周期只初始化没有任何外部依赖的变量
- 初始化过程一直持续到全部变量完成初始化
var (
a int = b + 1
b int = 1
)
func main() {
fmt.Println(a) // 2 第二轮初始化
fmt.Println(b)//1 第一轮初始化
}
init
init()
在包函数被执行前以及包内变量初始化后被调用变量初始化-> init() ->main()
- 每个包可以包含多个
init()
函数
init
多用于初始化全局变量,先于main
函数执行,并且不能被其他函数调用init
函数没有传参和返回值,仅用来初始化当前包内的参数init
的执行顺序和包的引入顺序相关
import _ "net/http/pprof"
表示仅导入包的init
函数
Example:
- 先执行全部变量的初始化
- 在执行
init
函数 - 最后执行 主函数
var T int64 = a()
func init() {
fmt.Println("init in main.go ")
}
func a() int64 {
fmt.Println("calling a()")
return 2
}
func main() {
fmt.Println("calling main")
}
/**calling a()
init in main.go
calling main**/
[]Rune
[]rune
是golang
的基本数据类型string
是只读的byte
数据,string
字符使用utf-8
编码,每个字符占位1~3 bytes
rune
占位4 bytes
- 对于英文字符,
string
和rune
类型没有区别 - 对于中文字符,
rune
类型占位4 bytes
字符,用于操作中文字符不会出现乱码
Examples:
// string & rune compare,
package main
import "fmt"
// string & rune compare,
func stringAndRuneCompare() {
// string,
s := "hello你好"
fmt.Printf("%s, type: %T, len: %d\n", s, s, len(s))
fmt.Printf("s[%d]: %v, type: %T\n", 0, s[0], s[0])
li := len(s) - 1 // last index,
fmt.Printf("s[%d]: %v, type: %T\n\n", li, s[li], s[li])
// []rune
rs := []rune(s)
fmt.Printf("%v, type: %T, len: %d\n", rs, rs, len(rs))
}
func main() {
stringAndRuneCompare()
}
OutPut:
hello你好, type: string, len: 11
s[0]: 104, type: uint8
s[10]: 189, type: uint8
[104 101 108 108 111 20320 22909], type: []int32, len: 7
Analysis:
hello你好
占位11 bytes(5 * 1 + 2 * 3 = 11)
string
转rune
时,一共 7 个 utf-8 字符,因此转换成 size = 7 的 []rune
数组和切片
数组
- 数组定义–数组定长
var
定义固定大小的数组,在定义时需要指定大小,分配一片连续的内存[...]
自动推断数组大小,核心在于数组定义的时候已经确定了数组大小,分配固定的连续内存空间
package main
import (
"fmt"
"unsafe"
)
func main() {
// 指定数组大小
var a1 [5]int // int == int64
// 自动推断数组大小
a2 := [...]int{1, 2, 3}
fmt.Printf("a1 = %v , a2 = %v,a1Len = %d, a2Len = %d, a1Size = %d, a2Size = %d \n", a1, a2, len(a1), len(a2), unsafe.Sizeof(a1), unsafe.Sizeof(a2))
// a1 = [0 0 0 0 0] , a2 = [1 2 3],a1Len = 5, a2Len = 3, a1Size = 40, a2Size = 24
// 按索引赋值
a3 := [...]int{2: 2, 4: 4} //... 表示自动核算大小,分配固定内存
// 按照索引复制[0,0,2,0,4] // len = 5, Sizeof(a3) = 40
fmt.Println(a3)
// 按索引赋值
a4 := [5]int{2: 2, 4: 4}
fmt.Println(a4) //[0 0 2 0 4] // len = 5, Sizeof(a4) = 40
}
切片
- 切片定义– 数组不定长
- 通过
make
分配内存,返回的是引用类型本身- make是生成一个可变大小的内存块,并返回一个它的引用
- 通过
new
分配内存,返回的是指向类型的指针,并且内存置为0new
可以申请任何类型变量内存块并返回一个指针指向它
var
直接定义不定长的数组
- 通过
func main() {
// -------------------- 切片 -----------------
sli := []int{1, 2, 3, 4, 5, 6}
fmt.Printf("len=%d cap=%d slice=%v\n", len(sli[0:3:4]), cap(sli[0:3:4]), sli[0:3:4]) //len=3 cap=4 slice=[1 2 3]
// 定义切片
var b1 []int //切片不定长
b1 = append(b1, 1)
fmt.Println(b1) //[1]
list := new([]int)
*list = append(*list, 2)
fmt.Println(*list) //[2]
var b3 = []int{1, 2, 3} //切片不定长
b3 = append(b3, 4)
fmt.Println(b3) //[1,2,3,4]
// make初始化
b2 := make([]int, 5, 5+3) // make([],len,cap)
fmt.Printf("b2 = %v ,len=%d,cap=%d\n", b2, len(b2), cap(b2)) //b2 = [0 0 0 0 0] ,len=5,cap=8
}
切片扩容
- 通过
make
定义的切片结构由三部分组成:make(type,len,cap)
type
表明数据类型len
用来初始化内存数据cap
表示当前切片的容量,用来分配初始化内存大小
- 当数据超出
cap
容量的时候,就会重新分配内存扩容:- 当原切片长度小于
1024
时,新的切片长度直接加上append
元素的个数,容量则会直接*2
- 当原切片长度大于等于
1024
时,新的切片长度直接加上append
元素的个数,容量则会增加1/4
- 当原切片长度小于
func growslice(et *_type, old slice, cap int) slice {
//cap 输入的新cap值
newcap := old.cap
doublecap := newcap + newcap
// 如果新容量大于旧容量的两倍,则直接按照新容量大小申请
if cap > doublecap {
newcap = cap
} else {
// 如果原有长度小于1024,则新容量是旧容量的2倍
if old.len < 1024 {
newcap = doublecap
} else {
// 按照原有容量的 1/4 增加,直到满足新容量的需要
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
if newcap <= 0 {
newcap = cap
}
}
}
}
切片数据赋值
- 切片通过
append
在末尾增加数据- 切片之间通过
append
值传递,两部分数据互不影响 append
时没必要初始化新切片的len
,append
时会自动将数据依次加到新切片末尾
- 切片之间通过
func appendvalue() {
// -------------------- 切片 直接引用 复制 -----------------
a := make([]int, 0, 6) //默认0值为空 容量3
a = append(append(append(a, 1), 2), 3)
fmt.Printf("alen=%d acap=%d aslice=%v \n", len(a), cap(a), a) // alen=3 acap=6 aslice=[1 2 3]
b := make([]int, 0) //声明一个长度为a 切片指针变量b
b = append(b, a...)
fmt.Printf("alen=%d acap=%d aslice=%v blen=%d bcap=%d bslice=%v \n", len(a), cap(a), a, len(b), cap(b), b) //alen=3 acap=6 aslice=[1 2 3] blen=3 bcap=3 bslice=[1 2 3]
// 值传递,两部分数据互不影响
a[0] = 99
fmt.Printf("alen=%d acap=%d aslice=%v blen=%d bcap=%d bslice=%v \n", len(a), cap(a), a, len(b), cap(b), b) //alen=3 acap=6 aslice=[99 2 3] blen=3 bcap=3 bslice=[1 2 3]
b[1] = 100
fmt.Printf("alen=%d acap=%d aslice=%v blen=%d bcap=%d bslice=%v \n", len(a), cap(a), a, len(b), cap(b), b) //alen=3 acap=6 aslice=[99 2 3] blen=3 bcap=3 bslice=[1 100 3]
}
copy
复制切片数组copy
值传递,实现单独的内存分配,复制出的切片和原切片处于独立的内存区,两部分数据互不影响copy(dst, src)
从源数据拷贝min(len(dst), len(src))
个元素- 因此,拷贝的
dst
数组需要初始化长度
- 因此,拷贝的
func copyvalue() {
// -------------------- 切片 直接引用 复制 -----------------
a := make([]int, 0, 6) //默认0值为空 容量3
a = append(append(append(a, 1), 2), 3)
fmt.Printf("alen=%d acap=%d aslice=%v \n", len(a), cap(a), a) // alen=3 acap=6 aslice=[1 2 3]
b := make([]int, len(a)) //声明一个长度为a 切片指针变量b
copy(b[:], a[:])
fmt.Printf("alen=%d acap=%d aslice=%v blen=%d bcap=%d bslice=%v \n", len(a), cap(a), a, len(b), cap(b), b) //alen=3 acap=6 aslice=[1 2 3] blen=3 bcap=3 bslice=[1 2 3]
// 值传递,两部分数据互不影响
a[0] = 99
fmt.Printf("alen=%d acap=%d aslice=%v blen=%d bcap=%d bslice=%v \n", len(a), cap(a), a, len(b), cap(b), b) //alen=3 acap=6 aslice=[99 2 3] blen=3 bcap=3 bslice=[1 2 3]
b[1] = 100
fmt.Printf("alen=%d acap=%d aslice=%v blen=%d bcap=%d bslice=%v \n", len(a), cap(a), a, len(b), cap(b), b) //alen=3 acap=6 aslice=[99 2 3] blen=3 bcap=3 bslice=[1 100 3]
}
- 直接复制出的新切片是引用传递
- 只要新切片的
cap
不超过原切片,则新切片和旧切片指向同一个引用类型,修改任意切片数据都会影响相同的引用类型 - 但是,新切片的
cap
超过原切片后,就会重新申请内存容量,此时两者指向不同的内存引用,对于数组数据的操作就互不影响
- 只要新切片的
func reference() {
// -------------------- 切片 直接引用 复制 -----------------
a := make([]int, 0, 6) //默认0值为空 容量3
a = append(append(append(a, 1), 2), 3)
fmt.Printf("alen=%d acap=%d aslice=%v \n", len(a), cap(a), a) // alen=3 acap=6 aslice=[1 2 3]
b := make([]int, 0) //声明一个长度为0 切片指针变量b
b = a
fmt.Printf("alen=%d acap=%d aslice=%v blen=%d bcap=%d bslice=%v \n", len(a), cap(a), a, len(b), cap(b), b) //alen=3 acap=6 aslice=[1 2 3] blen=3 bcap=3 bslice=[1 2 3]
// 引用复制,在未超过旧切片的 cap 时,两部分指向相同的地址
a[0] = 99
fmt.Printf("alen=%d acap=%d aslice=%v blen=%d bcap=%d bslice=%v \n", len(a), cap(a), a, len(b), cap(b), b) //alen=3 acap=6 aslice=[99 2 3] blen=3 bcap=6 bslice=[99 2 3]
b[1] = 100
fmt.Printf("alen=%d acap=%d aslice=%v blen=%d bcap=%d bslice=%v \n", len(a), cap(a), a, len(b), cap(b), b) //alen=3 acap=6 aslice=[99 100 3] blen=3 bcap=6 bslice=[99 100 3]
b = append(append(append(b, 4), 5), 6)
fmt.Printf("alen=%d acap=%d aslice=%v blen=%d bcap=%d bslice=%v \n", len(a), cap(a), a, len(b), cap(b), b) //alen=3 acap=6 aslice=[99 100 3] blen=6 bcap=6 bslice=[99 100 3 4 5 6]
// 再次新增数据,超出旧切片的容量
b = append(b, 7)
b[1] = 98
fmt.Printf("alen=%d acap=%d aslice=%v blen=%d bcap=%d bslice=%v \n", len(a), cap(a), a, len(b), cap(b), b) //alen=3 acap=6 aslice=[99 100 3] blen=7 bcap=12 bslice=[99 98 3 4 5 6 7]
}
Map
map
是无序的,每次获取的值都是不固定的顺序,且不能通过index
获取,只能通过遍历key
获取map
是一种引用类型,复制出的值的改变会影响本来的数据map
的值可以直接通过map["key"] = new value
修改map
不是线程安全的,在多个go-routine
存取时,必须使用mutex lock
机制- 可以通过
delete
关键字删除map
数据delete(map,key)
- 初始化
map
必须分配内存空间,用make
或者直接赋值分配
package main
func main() {
contracts := make(map[string]string)
contract, ok := contracts[key]
if !ok {
// todo
}
type Limit struct {
LimitationForOnce int64
Whitelist [2]string
}
var LimitsFromSymbol = map[string]Limit{
"symbol": {
300,
[2]string{
"wl1",
"wl2",
},
},
}
}
通道
Channle
channle
需要make
初始化分配内存make
初始化内存时可以设定缓存空间,在缓存空间被占用完之前都不会阻塞通道写入channel := make(chan int, len)
- 有缓存的可以在协程外将数据写入缓存区
- 缓存区数据写满后,再次写入会陷入阻塞,但是程序编译不会报错·
make
初始化可以不设置缓存空间,表示通道处理同步消息:在通道数据被读取前都会阻塞通道写入channel := make(chan int)
- 无缓存通道数据的读写都必须在
go
协程中处理
package main
import (
"fmt"
"sync"
)
var (
wg sync.WaitGroup
ch = make(chan int, 11)
)
func main() {
wg.Add(2)
go write()
go read()
wg.Wait()
}
func write() {
defer wg.Done()
for i := 0; i < 10; i++ {
ch <- i
}
defer close(ch) // 在写入完成后关闭通道
}
func read() {
defer wg.Done()
for a := range ch { // 使用 range 语法读取通道
fmt.Println("a: ", a)
}
fmt.Println("close")
}
Channel 数据读取
读数据
- 从空通道读取数据–>阻塞
- 从
nil
通道读取数据–>阻塞 - 从有数据通道读取数据–>返回数据
- 从 关闭的通道读取数据–>可以读取缓存区的数据,缓存区为空后,再次读取会返回
默认值和false
写数据
- 往
nil
通道写数据–>阻塞 - 往关闭的通道写数据–>
panic
异常 - 往无缓存通道写数据,在数据被读取前会陷入阻塞
- 往缓存区通道写入数据,缓存区写满后会陷入阻塞
Func
golang
函数中允许传递不定长的数据,或者传递 interface{}
空接口,用来接收任意类型的数据
package main
import (
"fmt"
"math/big"
)
func main() {
type Tree struct {
leaves int
name string
}
myFunc(1, big.NewInt(23), Tree{99, "trace"}, "hello world", []byte("hello"))
}
func myFunc(args ...interface{}) {
for _, v := range args {
fmt.Println(v)
}
}
函数之间传递指针内存地址可以共同操作相同的数据,避免 copy data
带来的拷贝负担
package main
import (
"fmt"
)
func main() {
a := 1
b := add(&a)
fmt.Println(a)
fmt.Println(b)
}
func add(a *int) (addone int) {
*a = *a + 1
addone = *a
return
}
函数也可以作为参数传参,处理特定的业务逻辑
package main
import (
"fmt"
)
type testInt func(int) bool
func check(a int) bool {
switch a % 2 {
case 0:
return true
case 1:
return false
}
return false
}
func fileter(a []int, f testInt) (b []int) {
for _, v := range a {
if f(v) {
b = append(b, v)
}
}
return
}
func main() {
a := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
odd := fileter(a, check)
fmt.Println(odd)
}
Defer
- stack
包内执行过程中,defer
存储在栈空间
2. return
defer
执行在 return
后,先执行 return
的内容,在按照栈内存储顺序从栈顶向下依次执行 defer
语句
3. panic
遇到 panic
之后,按照出栈的方式执行 defer
函数,直到被 recover()
函数捕获
- recover 函数捕获异常
- 栈内的
defer
仍然会执行,直到空栈
Examples-Panic-Without-Recover
package main
import (
"fmt"
)
func main() {
defer_call()
fmt.Println("main 正常结束")
}
func defer_call() {
defer func() { fmt.Println("defer: panic 之前1") }()
defer func() { fmt.Println("defer: panic 之前2") }()
panic("异常内容") //触发defer出栈
defer func() { fmt.Println("defer: panic 之后,永远执行不到") }()
}
/**
defer: panic 之前2
defer: panic 之前1
panic: 异常内容
//... 异常堆栈信息**/
Examples-Panic-With-Recover
package main
import "fmt"
func main() {
defer_call()
fmt.Println("main 正常结束")
}
func defer_call() {
defer func() { fmt.Println("defer: before recover()") }()
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
fmt.Println("defer: panic 之前1, 捕获异常")
}()
defer func() { fmt.Println("defer: panic 之前2, 不捕获") }()
panic("异常内容") //触发defer出栈
defer func() { fmt.Println("defer: panic 之后, 永远执行不到") }()
}
/**
defer: panic 之前2, 不捕获
异常内容
defer: panic 之前1, 捕获异常
defer: before recover()
main 正常结束**/ //recover()defer func()之前的defer也会执行,在输出Panic内容后,会执行完所有的defer栈
Examples-Multi-Panic-With-Recover
panic仅有最后一个可以被revover捕获
package main
import (
"fmt"
)
func main() {
defer func() {
if err := recover(); err != nil{
fmt.Println(err)
}else {
fmt.Println("fatal")
}
}()
defer func() {
panic("defer panic")
}()
panic("panic")
}
/**
defer panic**/
Examples-Defer-With-Nested-Func
defer
下的函数包含子函数的话:
- 在入栈函数的时候,需要获取该函数所有的参数
- 因此如果函数中嵌套了子函数的话,在入栈时会先执行这个子函数
package main
import "fmt"
func function(index int, value int) int {
fmt.Println(index)
return index
}
func main() {
defer function(1, function(3, 0))
defer function(2, function(4, 0))
}
/**
defer压栈function1,压栈函数地址、形参1、形参2(调用function3) --> 打印3
defer压栈function2,压栈函数地址、形参1、形参2(调用function4) --> 打印4
defer出栈function2, 调用function2 --> 打印2
defer出栈function1, 调用function1--> 打印1**/
package main
import "fmt"
func DeferFunc1(i int) (t int) {
t = i //赋值t=1
defer func() {
t += 3 //defer 执行在return之后,返回 t = 4
}()
return t //t=1
}
func DeferFunc2(i int) int {
t := i //t =1
defer func() {
t += 3 //局部变量
}()
return t //将t赋值给返回的值 返回t=1
}
func DeferFunc3(i int) (t int) {
defer func() {
t += i // 返回 t + 1 = 3
}()
return 2 //t = 2
}
func DeferFunc4() (t int) {
defer func(i int) {
fmt.Println(i) //i = 0
fmt.Println(t) // t =2
}(t)
t = 1
return 2 //t = 2
}
func main() {
fmt.Println(DeferFunc1(1))
fmt.Println(DeferFunc2(1))
fmt.Println(DeferFunc3(1))
DeferFunc4()
}
GMP
一、Golang“调度器”的由来?
(1) 单进程时代不需要调度器
一切的软件都是跑在操作系统上,真正用来干活(计算)的是 CPU
。
- 早期的操作系统每个程序就是一个进程,直到一个程序运行完,才能进行下一个进程,就是“单进程时代”
- 一切的程序只能串行发生。
早期的单进程操作系统,面临2个问题:
1.单一的执行流程,计算机只能依次处理任务
2.单次任务进程阻塞所带来的CPU时间浪费
那么能不能有多个进程来宏观一起来执行多个任务呢?
(2)多进程/线程时代有了调度器需求
多进程并发
- 在多进程/多线程的操作系统中,因为一个进程阻塞,
cpu
可以立刻切换到其他进程中去执行 - 调度
cpu
的算法可以保证在运行的进程都可以被分配到cpu
的运行时间片 - 从宏观来看,似乎多个进程是在同时被运行
但是对于 Linux
操作系统来讲,cpu
对进程的态度和线程的态度是一样的
但新的问题就又出现了:
- 调度的高消耗
CPU
:- 进程拥有太多的资源,进程的创建、切换、销毁,都会占用很长的时间,但如果进程过多,
CPU
有很大的一部分都被用来调度进程 CPU
调度切换的是进程和线程,实际上多线程开发要考虑同步竞争等问题,如锁、竞争冲突等
- 进程拥有太多的资源,进程的创建、切换、销毁,都会占用很长的时间,但如果进程过多,
- 高内存占用: 为每个任务都创建一个线程是不现实的,因为会消耗大量的内存(进程虚拟内存会占用4GB[32位操作系统], 而线程也要大约4MB)
怎么才能提高 CPU
的利用率呢?
(3)协程来提高CPU利用率
其实一个线程分为“内核态“线程和”用户态“线程。
一个“用户态线程”必须要绑定一个“内核态线程”,但是 CPU
并不知道有“用户态线程”的存在,它只知道它运行的是一个“内核态线程”(Linux的PCB
进程控制块)。
这样,内核线程依然叫“线程(thread
)”,用户线程叫“协程(co-routine
)“.
_既然一个协程(co-routine
)可以绑定一个线程(thread
),那么能不能多个协程(co-routine
)绑定一个或者多个线程(thread
)上呢?_
N:1关系
N个用户态协程绑定1个内核态线程
- 优点
- 协程在用户态线程即完成切换,不会陷入到内核态,这种切换非常的轻量快速
- 缺点
- 1个进程的所有协程都绑定在1个线程上
- 某个程序用不了硬件的多核加速能力
- 一旦某协程阻塞,造成线程阻塞,本进程的其他协程都无法执行了,根本就没有并发的能力了。
1:1 关系
1个协程绑定1个线程
- 优点
- 协程的调度都由
CPU
完成了,不存在N:1中阻塞的问题
- 协程的调度都由
- 缺点:
- 协程的创建、删除和切换的代价都由
CPU
完成,和进程线程调度一样,调度的高消耗CPU
- 协程的创建、删除和切换的代价都由
M:N关系
M个协程绑定N个线程,是N:1和1:1类型的结合,克服了以上2种模型的缺点,但实现起来最为复杂
协程跟线程是有区别的,线程由CPU调度是抢占式的,协程由用户态调度是协作式的,一个协程让出CPU后,才执行下一个协程
(4)Go语言的协程goroutine
Go为了提供更容易使用的并发方法,使用了 goroutine
和 channel
。
- 协程被称为
goroutine
- 一个
goroutine
只占几KB
,并且这几KB
就足够goroutine
运行完,goroutine
的内存栈是可伸缩的,如果需要更多内容,runtime
会自动为goroutine
分配。 - 支持在有限的内存空间内并发大量
goroutine
,
- 一个
goroutine
让一组可复用的函数运行在一组线程之上,即使有协程阻塞,该线程的其他协程也可以被runtime
调度,转移到其他可运行的线程上
Goroutine
特点:
- 占用内存更小(
几kb
) - 调度更灵活(
runtime
调度)
(5)被废弃的goroutine调度器
最关键的一点就是调度协程的调度器的实现了。
G来表示Goroutine,用M来表示线程,那么我们也会用这种表达的对应关系。
Go之前被废弃的调度器是如何运作的?
M(线程)
想要执行/放回G(协程)
都必须访问全局 G
队列,并且 M
有多个,即多线程访问同一资源需要加锁进行保证互斥/同步,所以全局 G
队列是有互斥锁进行保护的
- 创建、销毁、调度
G
都需要每个M
获取锁,这就形成了激烈的锁竞争。 M
转移G
会造成延迟和额外的系统负载。比如当G
中包含创建新协程的时候,M 创建了
G’
,为了继续执行G
,需要把G’
交给M’
执行,也造成了很差的局部性,因为G’
和G
是相关的,最好放在M
上执行,而不是其他M'
。- 系统调用(
CPU
在M
之间的切换)导致频繁的线程阻塞和取消阻塞操作增加了系统开销。
二、Goroutine调度器的GMP模型的设计思想
面对之前调度器的问题,Go
设计了新的调度器。
在新调度器中,除了M(thread)
和G(goroutine)
,又引进了P(Processor)
** Processor
,它包含了运行 goroutine
的资源**,如果线程 M
想运行 goroutine
,必须先获取 P
,P
中还包含了可运行的 G
队列。
(1)GMP模型
在 Go
中,线程 M
是运行 goroutine
的实体,调度器的功能是把用户态中可运行的 goroutine
分配到工作线程上。
- 全局队列(
Global Queue
):存放等待运行的G(goroutine)
P
的本地队列:同全局队列类似,存放的也是等待运行的G
,存的数量有限,不超过256个。新建G'
时,G'
优先加入到P
的本地队列,如果队列满了,则会把本地队列中一半的G
移动到全局队列P
列表:所有的P
都在程序启动时创建,并保存在数组中,最多有GOMAXPROCS
(可配置)个。M
:线程想运行任务就得获取P
,从P
的本地队列获取G
,P
队列为空时,M
也会尝试从全局队列取一批G
放到P
的本地队列,或从其他P
的本地队列拿出一半放到自己P
的本地队列。M
运行G
,G
执行之后,M
会从P
获取下一个G
,不断重复下去
Goroutine
调度器和OS
调度器是通过线程M
结合起来的,每个M都代表了1个内核线程,OS调度器负责把内核线程分配到CPU的核上执行。
有关P和M的个数问题
P
的数量:
- 由启动时环境变量
$GOMAXPROCS
或者是由runtime
的方法GOMAXPROCS()
决定。这意味着在程序执行的任意时刻都只有$GOMAXPROCS
个goroutine
在同时运行。
M
线程的数量:
go
程序启动时,会设置M
的最大数量,默认10000
.但是内核很难支持这么多的线程数,所以这个限制可以忽略。runtime/debug
中的SetMaxThreads
函数,设置M
的最大数量- 一个
M
阻塞了,会创建新的M
M
与 P
的数量没有绝对关系,一个 M
线程阻塞,P
就会去创建或者切换另一个 M
,所以,即使 P
的默认数量是1,也有可能会创建很多个 M
出来。
P和M何时会被创建
P
何时创建:在确定了P
的最大数量n
后,运行时系统会根据这个数量创建n个P
M
何时创建:古国没有足够的M
线程来关联P
并运行其中的可运行的G
- 比如所有的
M
线程此时都阻塞住了,而P
中还有很多就绪任务- 先去就会去寻找空闲的
M
执行协程任务 - 如果没有空闲的,就会去创建新的
M
- 先去就会去寻找空闲的
(2)调度器的设计策略
复用线程:避免频繁的创建、销毁线程,而是对线程的复用
- work stealing机制
当本线程无可运行的 G
时,尝试从全局队列或者其他线程绑定的 P
获取待执行的任务 G
,而不是销毁线程
- hand off机制
当本线程 M
因为 G
进行系统调用阻塞时,线程 M
释放绑定的 P
,把 P
转移给其他空闲的线程执行
- 抢占
- 在
goroutine
中要等待一个协程主动让出CPU
才执行下一个协程 - 一个
goroutine
最多占用CPU 10ms
,防止其他goroutine
被阻塞饿死
(3) go func() 调度流程
go
关键字来创建一个程序调用的goroutine
;- 有两个存储
G
的队列,一个是局部调度器P
的本地队列、一个是全局G
队列。
- 新创建的
G
会先保存在P
的本地队列中 - 如果
P
的本地队列已经满了就会保存在全局的队列中
G
只能运行在M
中,一个M
必须持有一个P
,M与P是1:1
的关系。
M
会从P
的本地队列弹出一个可执行状态的G
来执行- 如果
P
的本地队列为空,就会从其他的MP
组合获取一个可执行的G
来执行或者从全局G
队列获取任务
- 一个
M
调度G
执行的过程是一个循环机制;
- 当
M
执行某一个G
时候如果发生了syscall
或则其余阻塞操作引起的M
阻塞- 如果当前
P
中有一些G
在执行,runtime
会把这个线程M
从P
中摘除(detach
),然后再创建一个新的操作系统的线程(如果有空闲的线程可用就复用空闲线程)来服务于这个P
;
- 如果当前
- 当
M
系统调用结束时候G
会尝试获取空闲P
任务,并放入到这个P
的本地队列- 如果
G
获取不到空闲P
,那么这个线程M
变成休眠状态,G
放回到全局队列
(4)调度器的生命周期
特殊的 M0和G0
M0
是启动程序后的编号为0的主线程,这个M
对应的实例会在全局变量runtime.m0
,不需要在heap
上分配,M0
负责执行初始化操作和启动第一个G
, 在之后M0
就和其他的M
一样G0
是每次启动一个M
都会第一个创建的goroutine
,G0
仅用于负责调度的G
,G0
不指向任何可执行的函数, 每个M
都会有一个自己的G0
。在调度或系统调用时会使用G0
的栈空间, 全局变量的G0
是M0
的G0
。
package main
import "fmt"
func main() {
fmt.Println("Hello world")
}
分析:
runtime
创建最初的线程m0
和goroutine g0
,并把2者关联。- 调度器初始化:初始化
m0
、栈、垃圾回收,以及创建和初始化由GOMAXPROCS
个P
构成的P
列表。 - 示例代码中的
main
函数是main.main
runtime
中也有1个main
函数——runtime.main
- 代码经过编译后,
runtime.main
会调用main.main
- 程序启动时会为
runtime.main
创建goroutine
- 然后把
main goroutine
加入到P
的本地队列。
- 启动
m0
,m0
已经绑定了P
,会从P
的本地队列获取G
,获取到main goroutine
。 G
拥有栈,M
根据G
中的栈信息和调度信息设置运行环境M
运行G
G
退出,再次回到M
获取可运行的G
- 重复直到
main.main
退出runtime.main
执行Defer
和Panic
处理,或调用runtime.exit
退出程序。
(5)可视化GMP编程
有2种方式可以查看一个程序的 GMP
的数据。
方式1:go tool trace
trace
记录了运行时的信息,能提供可视化的 Web
页面。
package main
import (
"os"
"fmt"
"runtime/trace"
)
func main() {
//创建trace文件
f, err := os.Create("trace.out")
if err != nil {
panic(err)
}
defer f.Close()
//启动trace goroutine
err = trace.Start(f)
if err != nil {
panic(err)
}
defer trace.Stop()
//main
fmt.Println("Hello World")
}
运行程序
$ go tool trace trace.out
2020/02/23 10:44:11 Parsing trace...
2020/02/23 10:44:11 Splitting trace...
2020/02/23 10:44:11 Opening browser. Trace viewer is listening on http://127.0.0.1:33479
我们可以通过浏览器打开http://127.0.0.1:33479
网址,点击view trace
能够看见可视化的调度流程。
G信息
点击 Goroutines
那一行可视化的数据条,我们会看到一些详细的信息。
一共有两个 G
在程序中,一个是特殊的 G0
,是每个M必须有的一个初始化的 G
其中 G1
应该就是 main goroutine
(执行 main
函数的协程),在一段时间内处于可运行和运行的状态。
M信息
点击 Threads
那一行可视化的数据条,我们会看到一些详细的信息。
一共有两个 M
在程序中,一个是特殊的 M0
,用于初始化使用
P信息
G1
中调用了main.main
,创建了trace goroutine g18
。G1
运行在 P1
上,G18
运行在 P0
上。
这里有两个 P
,我们知道,一个 P
必须绑定一个 M
才能调度 G
。
G18
在 P0
上被运行的时候,确实在 Threads
多了一个 M
的数据,点击查看如下:
多了一个 M2
应该就是 P0
为了执行 G18
而动态创建的 M2
.
方式2:Debug trace
package main
import (
"fmt"
"time"
)
func main() {
for i := 0; i < 5; i++ {
time.Sleep(time.Second)
fmt.Println("Hello World")
}
}
编译
$ go build trace2.go
通过Debug方式运行
$ GODEBUG=schedtrace=1000 ./trace2
SCHED 0ms: gomaxprocs=2 idleprocs=0 threads=4 spinningthreads=1 idlethreads=1 runqueue=0 [0 0]
Hello World
SCHED 1003ms: gomaxprocs=2 idleprocs=2 threads=4 spinningthreads=0 idlethreads=2 runqueue=0 [0 0]
Hello World
SCHED 2014ms: gomaxprocs=2 idleprocs=2 threads=4 spinningthreads=0 idlethreads=2 runqueue=0 [0 0]
Hello World
SCHED 3015ms: gomaxprocs=2 idleprocs=2 threads=4 spinningthreads=0 idlethreads=2 runqueue=0 [0 0]
Hello World
SCHED 4023ms: gomaxprocs=2 idleprocs=2 threads=4 spinningthreads=0 idlethreads=2 runqueue=0 [0 0]
Hello World
SCHED
:调试信息输出标志字符串,代表本行是goroutine
调度器的输出;0ms
:即从程序启动到输出这行日志的时间;gomaxprocs
: P的数量,本例有2个P
, 因为默认的P
的属性是和cpu
核心数量默认一致,当然也可以通过GOMAXPROCS
来设置;idleprocs
: 处于idle状态的P的数量;通过gomaxprocs
和idleprocs
的差值,我们就可知道执行go
代码的P
的数量;- t
hreads: os threads/M
的数量,包含scheduler
使用m
数量,加上runtime
自用的类似sysmon
这样的thread
的数量; spinningthreads
: 处于自旋状态的os thread
数量;idlethread
: 处idle
状态的os thread
的数量;runqueue=0
:Scheduler
全局队列中G
的数量;[0 0]
: 分别为2个P
的local queue
中的G
的数量。
三、Go调度器调度场景过程全解析
(1)场景1
P
拥有代运行的协程任务 G1
,M1
线程获取 P
后开始运行 G1
G1
使用go func()
创建了 G2
, G2
优先加入到 P1
的本地队列。
G1
运行完成后(函数:goexit
),M
上运行的 goroutine
切换为 G0
G0
负责调度时协程的切换(函数:schedule
)
从 P
的本地队列取 G2
,从 G0
切换到 G2
,并开始运行 G2
(函数:execute
),实现了线程 M1
的复用。
(2)场景2
假设每个 P
的本地队列只能存 4个G
。
G2
要创建了6个G
,前4个G(G3, G4, G5,G6)
已经加入 p1
的本地队列,p1
本地队列满了。
G2
在创建 G7
的时候,发现 P1
的本地队列已满,需要执行负载均衡
把 P1
中本地队列中前一半的 G
,还有新创建 G
转移到全局队列
(实现中并不一定是新的
G
,如果G
是G2
之后就执行的,会被保存在本地队列,利用某个老的
G
替换新G
加入全局队列)
这些
G
被转移到全局队列时,会被打乱顺序。所以 G3,G4,G7
被转移到全局队列。
G2
创建 G8
时,P1
的本地队列未满,所以 G8
会被加入到 P1
的本地队列。
G8
加入到 P1
点本地队列的原因:
P1
此时在与 M1
绑定,而 G2
此时是 M1
在执行
所以 G2
创建的新的 G
会优先放置到自己的M绑定的 P
上
(3)场景3
在创建 G
时,运行的 G
会尝试唤醒其他空闲的 P
和 M
组合去执行。
假定 G2
唤醒了 M2
,M2
绑定了 P2
,并运行 G0
但 P2
本地队列没有 G
,M2
此时为自旋线程(没有 G
但为运行状态的线程,不断寻找 G
)
M2
首先尝试从全局队列(简称“GQ
”)取一批 G
放到 P2
的本地队列(函数:findrunnable()
)
M2
从全局队列取的 G
数量符合下面的公式:
n = min(len(GQ)/GOMAXPROCS + 1, len(GQ)/2)
至少从全局队列取 1个G
,但每次不要从全局队列移动太多的 G
到 P
本地队列,给其他 P
留点
这是从全局队列到 P
本地队列的负载均衡。
(4)场景4
假设 G2
一直在 M1
上运行
经过2轮后,M2
已经把 G7、G4
从全局队列获取到了 P2
的本地队列并完成运行
全局队列和 P2
的本地队列都空了,如场景8图的左半部分。
**全局队列已经没有 G
,那m就要执行 work stealing
(借取)
从其他有 G
的 P
哪里借取一半 G
过来,放到自己的 P
本地队列
P2
从 P1
的本地队列尾部取一半的 G
,本例中一半则只有 1个G8
,放到 P2
的本地队列并执行
G1
本地队列 G5、G6
已经被其他 M
运行完成
当前 M1
和 M2
分别在运行 G2
和 G8
M3
和 M4
没有 goroutine
可以运行,M3
和 M4
处于自旋状态,它们不断寻找 goroutine
。
为什么不销毁线程,来节约CPU资源?
因为创建和销毁CPU也会浪费时间
希望当有新 goroutine
创建时,立刻能有 M
运行它
如果销毁再新建就增加了时延,降低了效率。当然也考虑了过多的自旋线程是浪费CPU,所以系统中最多有GOMAXPROCS
个自旋的线程
(5)场景5
假定当前除了 M3
和 M4
为自旋线程
还有 M5
和 M6
为空闲的线程(P
的数量应该永远是 M>=P
, 大部分都是 M
在抢占需要运行的 P
)
G8
创建了 G9
,G8
进行了阻塞的系统调用
M2
和P2
立即解绑- 如果
P2
本地队列有G
、全局队列有G
或有空闲的M
,P2
会立马唤醒1个M
和它绑定 - 否则
P2
则会加入到空闲P
列表,等待M
来获取可用的P
- 如果
本场景中,P2
本地队列有 G9
,可以和其他空闲的线程 M5
绑定。
G8
创建了 G9
,G8
进行了阻塞的系统调用
本场景中,P2
本地队列有 G9
,可以和其他空闲的线程 M5
绑定。
M2
和 P2
会解绑,但 M2
会记住 P2
,然后 G8
和 M2
进入系统调用状态
当 G8
和 M2
退出系统调用时
- 尝试获取
P2
- 如果无法获取,则获取空闲的
P
- 如果没有空闲
P
,G8
会被记为可运行状态,并加入到全局队列,M2
因为没有P
的绑定而变成休眠状态(长时间休眠等待GC
回收销毁)。
- 如果无法获取,则获取空闲的
四、小结
总结,Go
调度器很轻量也很简单,足以撑起 goroutine
的调度工作,并且让 Go
具有了原生(强大)并发的能力。Go
调度本质是把大量的 goroutine
分配到少量线程上去执行,并利用多核并行,实现更强大的并发
Preference
https://github.com/aceld/golang/tree/main
Golang三色标记+混合写屏障GC模式全分析
垃圾回收(Garbage Collection
,简称 GC
)是自动的内存管理机制,自动释放不需要的对象,让出存储器资源.
Golang
中的垃圾回收主要应用 三色标记法
一、Go V1.3之前的标记-清除(mark and sweep)算法
此算法主要有两个主要的步骤:
- 标记(
Mark phase
) - 清除(
Sweep phase
)
第一步,暂停程序业务逻辑 STW(stop the world)
,分类出可达和不可达的对象,然后做上标记。
图中表示是程序与对象的可达关系,目前程序的可达对象有对象 1-2-3
,对象 4-7
等五个对象。
第二步, 开始标记,程序找出它所有可达的对象,并做上标记。如下图所示:
所以对象 1-2-3
、对象 4-7
等五个对象被做上标记。
第三步, 标记完了之后,然后开始清除未标记的对象. 结果如下.
Mark and Sweep
算法标记
STW
的过程中,CPU
不执行用户代码,全部用于垃圾回收,程序出现卡顿- 标记需要扫描整个
heap
- 清除数据会产生
heap
碎片
第四步, 停止暂停,让程序继续跑。然后循环重复这个过程,直到 process
程序生命周期结束。
Go V1.3
做了简单的优化,将 STW
提前, 减少 STW
暂停的时间范围.如下所示
这里面最重要的问题就是:STW
的过程中, CPU
不执行用户代码,全部用于垃圾回收,程序出现卡顿 。
三、Go V1.5的三色并发标记法
三色标记法 实际上就是通过三个阶段的标记来确定清楚的对象都有哪些
第一步 , 就是只要是新创建的对象,默认的颜色都是标记为“白色”.
第二步, 每次 GC
回收开始, 首先从根节点开始遍历所有对象,把遍历到的对象从白色集合放入“灰色”集合。
第三步, 遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,之后将此灰色对象放入黑色集合
第四步, 重复第三步, 直到灰色中无任何对象.
第五步: 回收所有的白色标记表的对象. 也就是回收垃圾.
以上便是三色并发标记法
, 那么又是如何实现并行的呢?
Go是如何解决标记-清除(mark and sweep)算法中的卡顿(stw,stop the world)问题的呢?
四、没有STW的三色标记法
上述的三色并发标记法依赖 STW
的暂停程序
程序的逻辑改变对象引用关系, 这种动作如果在标记阶段做了修改,会影响标记结果的正确性。
如果三色标记法, 标记过程不使用 STW
将会发生什么事情?
可以看出,有两个问题, 在三色标记法中,是不希望被发生的
- 条件1: 一个白色对象被黑色对象引用**(白色被挂在黑色下)**
- 条件2: 灰色对象与它之间的可达关系的白色对象遭到破坏**(灰色同时丢了该白色)**
当以上两个条件同时满足时, 就会出现对象丢失现象!
当然, 如果上述中的白色对象3, 如果他还有很多下游对象的话, 也会一并都清理掉.
为了防止这种现象的发生,最简单的方式就是 STW
直接禁止掉其他用户程序对对象引用关系的干扰
如何能在保证对象不丢失的情况下合理的尽可能的提高 GC
效率,减少 STW
时间呢?
五、屏障机制
(1) “强-弱” 三色不变式
- 强三色不变式
不存在黑色对象引用到白色对象的指针。
- 弱三色不变式
所有被黑色对象引用的白色对象都处于灰色保护状态.
为了遵循上述的两个方式, 得到了如下具体的两种屏障方式“插入屏障”, “删除屏障”.
(2) 插入屏障
具体操作
: 在 A
对象引用 B
对象的时候,B
对象被标记为灰色。(将 B
挂在 A
下游,B
必须被标记为灰色)
满足
: 强三色不变式. (不存在黑色对象引用白色对象的情况了, 因为白色会强制变成灰色)
伪码如下:
添加下游对象(当前下游对象 slot, 新下游对象 ptr) {
//1
标记灰色(新下游对象 ptr)
//2
当前下游对象 slot = 新下游对象 ptr`
}
黑色对象的内存槽有两种位置, 栈
和堆
.
栈空间的特点是容量小,但是要求相应速度快
因为函数调用弹出频繁使用, 所以“插入屏障”机制,在栈空间的对象操作中不使用. 而仅仅使用在堆空间对象的操作中.
但是如果栈不添加,当全部三色标记扫描之后,栈上有可能依然存在白色对象被引用的情况(如上图的对象9).
所以要对栈重新进行三色标记扫描, 但这次为了对象不丢失, 要对本次标记扫描启动 STW
暂停. 直到栈空间的三色标记结束.
最后将栈和堆空间 扫描剩余的全部白色节点清除. 这次 STW
大约的时间在 10~100ms
.
(3) 删除屏障
具体操作
: 被删除的对象,如果自身为灰色或者白色,那么被标记为灰色。
满足
: 弱三色不变式. (保护灰色对象到白色对象的路径不会断)
伪代码:
添加下游对象(当前下游对象slot, 新下游对象ptr) {
//1
if (当前下游对象slot是灰色 || 当前下游对象slot是白色) {
标记灰色(当前下游对象slot) //slot为被删除对象, 标记为灰色
}
//2
当前下游对象slot = 新下游对象ptr
}
这种方式的回收精度低,一个对象即使被删除了最后一个指向它的指针也依旧可以活过这一轮,在下一轮 GC
中被清理掉。
六、Go V1.8的混合写屏障(hybrid write barrier)机制
插入写屏障和删除写屏障的短板:
-
插入写屏障:结束时需要
STW
来重新扫描栈,标记栈上引用的白色对象的存活; -
删除写屏障:回收精度低,
GC
开始时STW
扫描堆栈来记录初始快照,这个过程会保护开始时刻的所有存活对象。
Go V1.8版本引入了混合写屏障机制(hybrid write barrier
),避免了对栈re-scan
的过程,极大的减少了 STW
的时间。结合了两者的优点。
(1) 混合写屏障规则
具体操作
:
1、GC
开始将栈上的对象全部扫描并标记为黑色(之后不再进行第二次重复扫描,无需 STW
),
2、GC
期间,任何在栈上创建的新对象,均为黑色。
3、被删除的对象标记为灰色。
4、被添加的对象标记为灰色。
满足
: 变形的弱三色不变式.
伪代码:
添加下游对象(当前下游对象slot, 新下游对象ptr) {
//1
标记灰色(当前下游对象slot) //只要当前下游对象被移走,就标记灰色
//2
标记灰色(新下游对象ptr)
//3
当前下游对象slot = 新下游对象ptr
}
这里我们注意, 屏障技术是不在栈上应用的,因为要保证栈的运行效率。
(2) 混合写屏障的具体场景分析
注意混合写屏障是Gc的一种屏障机制,所以只是当程序执行GC的时候,才会触发这种机制。
GC开始:扫描栈区,将可达对象全部标记为黑
场景一: 对象被一个堆对象删除引用,成为栈对象的下游
伪代码
//前提:堆对象4->对象7 = 对象7; //对象7 被 对象4引用
栈对象1->对象7 = 堆对象7; //将堆对象7 挂在 栈对象1 下游
堆对象4->对象7 = null; //对象4 删除引用 对象7
场景二: 对象被一个栈对象删除引用,成为另一个栈对象的下游
伪代码
new 栈对象9;
对象8->对象3 = 对象3; //将栈对象3 挂在 栈对象9 下游
对象2->对象3 = null; //对象2 删除引用 对象3
场景三:对象被一个堆对象删除引用,成为另一个堆对象的下游
伪代码
堆对象10->对象7 = 堆对象7; //将堆对象7 挂在 堆对象10 下游
堆对象4->对象7 = null; //对象4 删除引用 对象7
场景四:对象从一个栈对象删除引用,成为另一个堆对象的下游
伪代码
堆对象10->对象7 = 堆对象7; //将堆对象7 挂在 堆对象10 下游
堆对象4->对象7 = null; //对象4 删除引用 对象7
Golang中的混合写屏障满足弱三色不变式
结合了删除写屏障和插入写屏障的优点
只需要在开始时并发扫描各个 goroutine
的栈,使其变黑并一直保持,这个过程不需要 STW
而标记结束后,因为栈在扫描后始终是黑色的,也无需再进行 re-scan
操作了,减少了 STW
的时间
七、总结
以上便是 Golang
的 GC
全部的标记-清除逻辑及场景演示全过程。
GoV1.3-
普通标记清除法,整体过程需要启动STW,效率极低。
GoV1.5-
三色标记法, 堆空间启动写屏障,栈空间不启动,全部扫描之后,需要重新扫描一次栈(需要 STW
),效率普通
GoV1.8-
三色标记法,混合写屏障机制,栈空间不启动,堆空间启动。整个过程几乎不需要 STW
,效率较高。
Preference
https://github.com/aceld/golang/tree/main
Database
mysql
mysql Docker
下载 mysql
Docker
镜像
docker pull mysql:latest
Docker
启动本地 mysql
容器
docker run -p 3306:3306 --name root -e MYSQL_ROOT_PASSWORD=123456 -d mysql:latest
进入 Docker
容器,通过用户名和密码操作数据库
docker exec -it root bash
输入用户名/密码,执行 mysql
的命令行代码创建数据库 create database XXX;
mysql -u root -p123456
create database eventLog;
Gorm
Gorm
通过 golang
结构体简化数据库建表和表查询的过程
Gorm
Golang
结构体能够包含的数据类型Golang
基础类型,uint,int,bool,string
- 指针,
*string
- 类型别名
- 自定义类型
- 结构体参数中首字母大写表示支持导出该表字段,否则该值不支持导出
type User struct {
ID uint // Standard field for the primary key
Name string // A regular string field
Email *string // A pointer to a string, allowing for null values
Age uint8 // An unsigned 8-bit integer
Birthday *time.Time // A pointer to time.Time, can be null
MemberNumber sql.NullString // Uses sql.NullString to handle nullable strings
ActivatedAt sql.NullTime // Uses sql.NullTime for nullable time fields
CreatedAt time.Time // Automatically managed by GORM for creation time
UpdatedAt time.Time // Automatically managed by GORM for update time
ignored string // fields that aren't exported are ignored
}
gorm.Model
GORM
有预定义的数据类型,包含 PrimaryKey
主键、数据被创建、更新和软删除时间
// gorm.Model definition
type Model struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time // Set to current time if it is zero on creating
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
自定义时间:
Updated int64 `gorm:"autoUpdateTime:nano"` // Use unix nano seconds as updating time
Updated int64 `gorm:"autoUpdateTime:milli"`// Use unix milli seconds as updating time
Created int64 `gorm:"autoCreateTime"` // Use unix seconds as creating time
gorm
关于结构体数据的权限设置:
type User struct {
Name string `gorm:"<-:create"` // allow read and create
Name string `gorm:"<-:update"` // allow read and update
Name string `gorm:"<-"` // allow read and write (create and update)
Name string `gorm:"<-:false"` // allow read, disable write permission
Name string `gorm:"->"` // readonly (disable write permission unless it configured)
Name string `gorm:"->;<-:create"` // allow read and create
Name string `gorm:"->:false;<-:create"` // createonly (disabled read from db)
Name string `gorm:"-"` // ignore this field when write and read with struct
Name string `gorm:"-:all"` // ignore this field when write, read and migrate with struct
Name string `gorm:"-:migration"` // ignore this field when migrate with struct
}
GormEmbeddedStruct
GORM
数据结构体支持多结构体嵌套
type Author struct {
Name string
Email string
}
type Blog struct {
ID int
Author Author `gorm:"embedded"`
Upvotes int32
}
// equals
type Blog struct {
ID int64
Name string
Email string
Upvotes int32
}
embeddedPrefix
为嵌入的结构体设置统一的前缀:
type Blog struct {
ID int
Author Author `gorm:"embedded;embeddedPrefix:author_"`
Upvotes int32
}
// equals
type Blog struct {
ID int64
AuthorName string
AuthorEmail string
Upvotes int32
}
Mysql Preference:
MongoDB
MongoDB Docker
下载 mongo
Docker
镜像
docker pull mongo:latest
Docker
启动本地 mysql
容器
docker run -d -p 27017:27017 --name some-mongo \
-e MONGO_INITDB_ROOT_USERNAME=mongoadmin \
-e MONGO_INITDB_ROOT_PASSWORD=secret \
mongo:latest
Download
https://www.mongodb.com/try/download/community
勾选 MongoDB Compass
工具,可视化数据库数据
MongoDB
MongoDB 将数据按照 golang 结构体定义存储成 document,并汇集在集合表中
db.collection.insertOne()
db.collection.insertMany()
db.collection.updateOne() 与 upsert: true 选项一起使用时
db.collection.updateMany() 与 upsert: true 选项一起使用时
db.collection.findAndModify() 与 upsert: true 选项一起使用时
db.collection.findOneAndUpdate() 与 upsert: true 选项一起使用时
db.collection.findOneAndReplace() 与 upsert: true 选项一起使用时
db.collection.bulkWrite()
MongoDB Preference:
初始化区块链连接
建立连接
用 Go
初始化以太坊客户端是和区块链交互所需的基本步骤。
首先,导入 go-etherem
的 ethclient
包并通过调用接收区块链服务提供者 URL的Dial
来初始化它。
package main
/**
There are serveral Ethereum client getting ways
1. local server
client, err := ethclient.Dial("http://localhost:8545")
OR
client, err := ethclient.Dial("/home/user/.ethereum/geth.ipc")
2. RPC
client, err := ethclient.Dial("https://mainnet.infura.io")
**/
import (
"github.com/ethereum/go-ethereum/ethclient"
"log"
)
var (
client *ethclient.Client
err error
)
func init() {
client, err = ethclient.Dial("url")
if err != nil {
log.Fatal(err)
}
}
区块链Id
区块链 peer-to-peer
网络结构下,全部节点基于 networkID
建立连接,但是发送链交易的时候,使用的 chainID
防止交易的重放攻击
networkID, err := client.NetworkID(ctx)
chainId, err := client.ChainID(ctx)
完整代码:
package milestone1
import (
"context"
"errors"
"fmt"
"github.com/ethereum/go-ethereum/ethclient"
"log"
"main/config"
)
var (
client *ethclient.Client
err error
)
func init() {
client = config.NewClient(config.SymbolETH)
if client == nil {
checkError(errors.New(fmt.Sprintf("Error in building new client err = %v", err)))
}
}
func checkError(err error) {
if err != nil {
log.Fatalf("Error = %v", err)
}
}
// NetworkId returns the network ID for this client.
func NetworkId(ctx context.Context) (string, error) {
networkID, err := client.NetworkID(ctx)
if err != nil {
checkError(errors.New(fmt.Sprintf("Error in get networkID, err = %v", err)))
}
return networkID.String(), nil
}
func ChainId(ctx context.Context) (string, error) {
chainId, err := client.ChainID(ctx)
if err != nil {
checkError(errors.New(fmt.Sprintf("Error in get chainID, err = %v", err)))
}
return chainId.String(), nil
}
账户信息
读取账户余额
eth_getBalance
支持读取传参地址在特定区块高度、特定区块hash、最新区块中、Pending池的余额
读取最新区块高度的账户余额
balance, err := client.BalanceAt(ctx, account, nil) //nil is the latest block
读取特定区块中账户余额-BlockHeight
blockNum := big.NewInt(99999)
balance, err := client.BalanceAt(ctx, account, blockNum)
区块 num
的相关异常情况:
func toBlockNumArg(number *big.Int) string {
if number == nil {
return "latest"
}
// Sign returns:
// - -1 if x < 0;
// - 0 if x == 0;
// - +1 if x > 0.
if number.Sign() >= 0 {
return hexutil.EncodeBig(number)
}
// It's negative.
// IsInt64 reports whether x can be represented as an int64.
if number.IsInt64() {
return rpc.BlockNumber(number.Int64()).String()
}
// It's negative and large, which is invalid.
return fmt.Sprintf("<invalid %d>", number)
}
blockNum == nil
, 表示基于最新区块高度的合约状态读取slot
数值blockNum
为负数:- 数值
-1 ~-4
都有具体类型的对应 - 数值
<-4
,报错invalid argument 1: hex string without 0x prefix
- 数值
func (bn BlockNumber) String() string {
switch bn {
case EarliestBlockNumber: //0
return "earliest"
case LatestBlockNumber://-2
return "latest"
case PendingBlockNumber://-1
return "pending"
case FinalizedBlockNumber://-3
return "finalized"
case SafeBlockNumber://-4
return "safe"
default:
if bn < 0 {
return fmt.Sprintf("<invalid %d>", bn)
}
return hexutil.Uint64(bn).String()
}
}
- blockNum 为正数: 读取截止当前区块的合约内部存储的 slot 数值
- 提供的区块高度
> latestBlockHeight
,报错ethereum.NotFound
读取特定区块中账户余额-BlockHash
blockHash := common.HexToHash("0x0fa8fe23357be11db6273d5744a091b7f5baa70d7824addd680c8ed1fd2fbf0b")
balance, err := client.BalanceAtHash(ctx, account, blockHash)
}
读取 Pending 池中账户余额
账户提交的待处理交易进入 pending
池等待校验打包
- 账户的余额随着构建交易会付出
gas
或者转账 - 在
pending
此种获取账户最新余额,有可能比直接读取余额的值要小- 取决于
RPC
的速度,如果RPC
在网络中已经接收到了该交易,则返回执行该交易后的剩余余额,否则返回正常余额
- 取决于
balance, err := client.PendingBalanceAt(ctx, account)
以太坊中的数字是使用尽可能小的单位来处理的,因为它们是定点精度,在 ETH
中它是 wei
。
要读取 ETH
值,您必须做计算 wei/10^18
WEI/Eth converter
func calcuBalanceToEth(bal *big.Int) *big.Float {
fbalance := new(big.Float)
fbalance.SetString(bal.String())
fbalance = fbalance.Quo(fbalance, big.NewFloat(math.Pow10(18)))
return fbalance
}
完整代码
package milestone1
import (
"context"
"errors"
"fmt"
"github.com/ethereum/go-ethereum/common"
"math"
"math/big"
)
func BalanceFromLatestBlock(account common.Address, ctx context.Context) (*big.Int, error) {
balance, err := client.BalanceAt(ctx, account, nil) //nil is the latest block
if err != nil {
checkError(errors.New(fmt.Sprintf("Error in get account balance err = %v", err)))
}
return balance, nil
}
func BalanceFromBlock(account common.Address, number *big.Int, ctx context.Context) (*big.Int, error) {
balance, err := client.BalanceAt(ctx, account, number)
if err != nil {
checkError(errors.New(fmt.Sprintf("Error in get account balance err = %v", err)))
}
return balance, nil
}
func BalanceFromBlockHash(account common.Address, hash common.Hash, ctx context.Context) (*big.Int, error) {
balance, err := client.BalanceAtHash(ctx, account, hash)
if err != nil {
checkError(errors.New(fmt.Sprintf("Error in get account balance err = %v", err)))
}
return balance, nil
}
func BalanceFromPendingPool(account common.Address, ctx context.Context) (*big.Int, error) {
balance, err := client.PendingBalanceAt(ctx, account)
if err != nil {
checkError(errors.New(fmt.Sprintf("Error in get account balance err = %v", err)))
}
return balance, nil
}
func calcuBalanceToEth(bal *big.Int, decimal int) *big.Float {
balance := new(big.Float)
balance.SetString(bal.String())
balance = balance.Quo(balance, big.NewFloat(math.Pow10(decimal)))
return balance
}
账户信息
读取账户数据
EOA
:从私钥导出的账户地址,允许直接构建交易操作账户余额Smart Contract
:EOA
创建的合约地址,账户下存储特定的合约逻辑- 合约账户不允许直接构造交易
- EIP4337介绍如何实现合约账户的交易代付
- 合约地址和EOA地址的区别在于合约地址在账户中存储合约代码,通过判断地址数据长度可以判断当前地址是否是合约地址
eth_getCode
支持读取传参地址在特定区块高度、特定区块hash
、最新区块中、Pending
池中的账户contract codes
读取最新区块高度的账户contract codes
将区块号设置为 nil
将使用最新的区块高度
- 先使用简单的正则表达式来检查以太坊地址是否有效
- 获取地址存储的代码
- 如果长度为空,表示目前该地址时 EOA 地址
- 如果长度不为空,表明该地址是合约地址
- Solidity判断
bytecode, err := client.CodeAt(context.Background(), addr, nil) //nil is the latest block
读取特定区块中的账户codes-BlockHash
blockNum := big.NewInt(99999)
bytecode, err := client.CodeAt(context.Background(), addr, blockNum)
读取特定区块中的账户codes-BlockHeight
blockHash := common.HexToHash("0x0fa8fe23357be11db6273d5744a091b7f5baa70d7824addd680c8ed1fd2fbf0b")
bytecode, err := client.CodeAtHash(ctx, account, blockHash)
}
获取账户在 pending 池中的codes
合约部署交易已经提交,但是还没有得到确定
此时,如果 RPC 收到该交易,验证执行后返沪i该地址的 codes
bytecode, err := client.PendingCodeAt(ctx, account)
完整代码
package milestone1
import (
"context"
"errors"
"fmt"
"github.com/ethereum/go-ethereum/common"
"math/big"
"regexp"
)
// check the address whether it is a valid address
func validAddress(addr common.Address) bool {
// 16 hex 0-f
re := regexp.MustCompile("0x[0-9a-fA-F]{40}$")
return re.MatchString(addr.Hex())
}
// check the address whether is a smart contract address
func checkContractAddressInLatestBlock(addr common.Address, ctx context.Context) bool {
if !validAddress(addr) {
return false
}
bytecode, err := client.CodeAt(ctx, addr, nil) //nil is the latest block
if err != nil {
checkError(errors.New(fmt.Sprintf("Error in get account codes, error = %v", err)))
}
isContract := len(bytecode) > 0
if isContract {
return true
}
//fmt.Println("This is normal address, but we want a smart contract address")
return false
}
func checkContractAddressInBlock(addr common.Address, number *big.Int, ctx context.Context) bool {
if !validAddress(addr) {
return false
}
bytecode, err := client.CodeAt(ctx, addr, number)
if err != nil {
checkError(errors.New(fmt.Sprintf("Error in get account codes, error = %v", err)))
}
isContract := len(bytecode) > 0
if isContract {
return true
}
//fmt.Println("This is normal address, but we want a smart contract address")
return false
}
func checkContractAddressInBlockHash(addr common.Address, hash common.Hash, ctx context.Context) bool {
if !validAddress(addr) {
return false
}
bytecode, err := client.CodeAtHash(ctx, addr, hash)
if err != nil {
checkError(errors.New(fmt.Sprintf("Error in get account codes, error = %v", err)))
}
isContract := len(bytecode) > 0
if isContract {
return true
}
//fmt.Println("This is normal address, but we want a smart contract address")
return false
}
func checkContractAddressInPendingPool(addr common.Address, ctx context.Context) bool {
if !validAddress(addr) {
return false
}
bytecode, err := client.PendingCodeAt(ctx, addr)
if err != nil {
checkError(errors.New(fmt.Sprintf("Error in get account codes, error = %v", err)))
}
isContract := len(bytecode) > 0
if isContract {
return true
}
//fmt.Println("This is normal address, but we want a smart contract address")
return false
}
New account
ECDSA 生成新的地址
要首先生成一个新的钱包,需要导入 go-ethereum crypto
包,该包提供用于生成随机私钥的 GenerateKey
方法
- 生成全新的私钥,并将私钥存储在本地文件
- 基于私钥导出公钥信息
- 基于公钥导出账户地址
package main
import (
"crypto/ecdsa"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"log"
"strings"
)
func NewAccount() common.Address {
ecdsaPrivateKey, err := crypto.GenerateKey()
newAddress := EcdsaAddressFromPrivateKey(ecdsaPrivateKey)
}
func AddressFromKey(key string) common.Address {
if strings.HasPrefix(key, "0x") {
key = strings.Trim(key, "0x")
}
ecdsaPrivateKey, err := crypto.HexToECDSA(key)
if err != nil {
log.Fatal(err)
}
rAddress := EcdsaAddressFromPrivateKey(ecdsaPrivateKey)
return rAddress
}
func EcdsaAddressFromPrivateKey(ecdsaPrivateKey *ecdsa.PrivateKey) common.Address {
publicKeyBytes := crypto.FromECDSAPub(ecdsaPrivateKey.Public().(*ecdsa.PublicKey))
pub, err := crypto.UnmarshalPubkey(publicKeyBytes)
if err != nil {
log.Fatal(err)
}
rAddress := crypto.PubkeyToAddress(*pub)
return rAddress
}
生成助记词,从助记词导出新地址
地址胜场方式:
- 生成助记词
- 拼接助记词和
secret
, 构造哈希种子seed
- 基于
seed
逐步生成ecdsa
私钥 - 基于私钥导出钱包地址
导出账户密钥
- 基于助记词导出密钥时:
- 必须提供相匹配的
secret
值 - 拼接出生成账户用的哈希种子
seed
- 基于
seed
产生相同的私钥和地址 - 因此,
secret
用于进一步保证助记词的安全
func main() {
secret := "ert"
mnemonic, address := KeyFromMnemonic(secret)
fmt.Println(mnemonic, address)
KeyFromMnemonicInput(mnemonic, secret)
}
func KeyFromMnemonic(secret string) (string, common.Address) {
// Generate a mnemonic
entropy, _ := bip39.NewEntropy(256)
mnemonic, _ := bip39.NewMnemonic(entropy)
// Generate a Bip32 HD wallet for the mnemonic and a user supplied passphrase
seed := bip39.NewSeed(mnemonic, secret)
masterPrivateKey, _ := bip32.NewMasterKey(seed)
ecdsaPrivateKey := crypto.ToECDSAUnsafe(masterPrivateKey.Key)
address := EcdsaAddressFromPrivateKey(ecdsaPrivateKey)
return mnemonic, address
}
func KeyFromMnemonicInput(mnemonic, secret string) string {
seed := bip39.NewSeed(mnemonic, secret)
masterPrivateKey, _ := bip32.NewMasterKey(seed)
ecdsaPrivateKey := crypto.ToECDSAUnsafe(masterPrivateKey.Key)
privateKeyHex := fmt.Sprintf("%x", ecdsaPrivateKey.D)
address := EcdsaAddressFromPrivateKey(ecdsaPrivateKey)
fmt.Println(address)
return privateKeyHex
}
从Keystore导出地址
- 创建新的钱包地址,账户信息通过
secret
加密存储并导出keystore
文件 - 基于
secret
解密keystore
文件
私钥签名
keystore
直接基于secret
签名数据keystore
可以先基于 secretTimedUnlock
解锁一段时间后可以直接用于签名
import (
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"log"
"os"
)
func NewKeystoreAccount(secret string) common.Address {
ks := keystore.NewKeyStore("./wallets", keystore.StandardScryptN, keystore.StandardScryptP)
account, err := ks.NewAccount(secret)
if err != nil {
log.Fatal(err)
}
return account.Address
}
func AddressFromKeystore(file, secret string) common.Address {
ks := keystore.NewKeyStore("./tmp", keystore.StandardScryptN, keystore.StandardScryptP)
jsonBytes, err := os.ReadFile(file)
if err != nil {
log.Fatal(err)
}
account, err := ks.Import(jsonBytes, secret, secret)
if err != nil {
log.Fatal(err)
}
return account.Address
}
func SignatureFromKeystoreAfterUnlock(file, secret string) string {
ks := keystore.NewKeyStore("./tmp", keystore.StandardScryptN, keystore.StandardScryptP)
jsonBytes, err := os.ReadFile(file)
if err != nil {
checkError(errors.New(fmt.Sprintf("Error in reading keystore, error = %v", err)))
}
account, err := ks.Import(jsonBytes, secret, secret)
if err != nil {
checkError(errors.New(fmt.Sprintf("Error in exporting keystore account, error = %v", err)))
}
err = ks.Unlock(account, secret)
if err != nil {
checkError(errors.New(fmt.Sprintf("Error in unlocking keystore account, error = %v", err)))
}
signatureBytes, err := ks.SignHash(account, []byte("ww"))
return hexutil.Encode(signatureBytes)
}
func SignatureFromKeystore(file, secret string) string {
ks := keystore.NewKeyStore("./tmp", keystore.StandardScryptN, keystore.StandardScryptP)
jsonBytes, err := os.ReadFile(file)
if err != nil {
log.Fatal(err)
}
account, err := ks.Import(jsonBytes, secret, secret)
if err != nil {
log.Fatal(err)
}
signatureBytes, err := ks.SignHashWithPassphrase(account, secret, []byte("ww"))
return hexutil.Encode(signatureBytes)
}
GetBlocks
区块Header结构
type Bloom [256]byte
type BlockNonce [8]byte
// Header represents a block header in the Ethereum blockchain.
type Header struct {
ParentHash common.Hash `json:"parentHash" gencodec:"required"`
UncleHash common.Hash `json:"sha3Uncles" gencodec:"required"`
Coinbase common.Address `json:"miner"`
Root common.Hash `json:"stateRoot" gencodec:"required"`
TxHash common.Hash `json:"transactionsRoot" gencodec:"required"`
ReceiptHash common.Hash `json:"receiptsRoot" gencodec:"required"`
Bloom Bloom `json:"logsBloom" gencodec:"required"`
Difficulty *big.Int `json:"difficulty" gencodec:"required"`
Number *big.Int `json:"number" gencodec:"required"`
GasLimit uint64 `json:"gasLimit" gencodec:"required"`
GasUsed uint64 `json:"gasUsed" gencodec:"required"`
Time uint64 `json:"timestamp" gencodec:"required"`
Extra []byte `json:"extraData" gencodec:"required"`
MixDigest common.Hash `json:"mixHash"`
Nonce BlockNonce `json:"nonce"`
// BaseFee was added by EIP-1559 and is ignored in legacy headers.
BaseFee *big.Int `json:"baseFeePerGas" rlp:"optional"`
// WithdrawalsHash was added by EIP-4895 and is ignored in legacy headers.
WithdrawalsHash *common.Hash `json:"withdrawalsRoot" rlp:"optional"`
// BlobGasUsed was added by EIP-4844 and is ignored in legacy headers.
BlobGasUsed *uint64 `json:"blobGasUsed" rlp:"optional"`
// ExcessBlobGas was added by EIP-4844 and is ignored in legacy headers.
ExcessBlobGas *uint64 `json:"excessBlobGas" rlp:"optional"`
// ParentBeaconRoot was added by EIP-4788 and is ignored in legacy headers.
ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"`
// RequestsHash was added by EIP-7685 and is ignored in legacy headers.
RequestsHash *common.Hash `json:"requestsRoot" rlp:"optional"`
}
返回特定区块的区块头信息-BlockHash
返回最新区块的区块头信息,不包含具体的交易区块体
package main
import (
"context"
"fmt"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"log"
"math/big"
)
var (
client *ethclient.Client
err error
)
func init() {
client, err = ethclient.Dial("https://eth.llamarpc.com")
if err != nil {
log.Fatal(err)
}
}
func checkError(err error) {
if err != nil {
log.Fatal(err)
}
}
func main() {
var ctx = context.Background()
var blockhash = "0x6d7977fbf9333267c5bc25b596eacc2ef89461289e078dd7283c6872008646bc"
headers, err := getBlockHeader(ctx, common.HexToHash(blockhash))
checkError(err)
fmt.Println(fmt.Sprintf("Latest block header info = %s", headers))
var header types.Header
if err = header.UnmarshalJSON([]byte(headers)); err != nil {
log.Fatalf("decode error: %v ", err)
}
fmt.Println(header.Number)
}
func getBlockHeader(ctx context.Context, hash common.Hash) (string, error) {
header, err := client.HeaderByHash(ctx, hash)
if err != nil {
if errors.Is(err, ethereum.NotFound) {
return "", errors.New("non-exist blockHash")
}else{
checkError(err)
}
}
headerBytes, err := header.MarshalJSON()
if err != nil {
return string(headerBytes), err
}
return string(headerBytes), err
}
Examples:
{
"parentHash":"0x220987cbab8e6bc276671b33b8d6f1207dab1fc80ffe58165b1b070d34f73fc7",
"sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
"miner":"0x1f9090aae28b8a3dceadf281b0f12828e676c326",
"stateRoot":"0x2e4cfaff4b5f165bd7178091f6b13ff74e5492278fdc1ff1efbe9866347cedc2",
"transactionsRoot":"0x0558cbcf91c9de46e950988836c0e305f335292a1f821cff5505c12e79e5ada9",
"receiptsRoot":"0x98b457f5ac24528e16d6c26107ac3f210f4d820bbfe587e8df5562457f950683",
"logsBloom":"0x0000004000000004000010000200000030000100000000001000000000000000000000000000000000000000200001002200000008002000028000000000000000000008010000080000020800000000000000900000000000000014002000000008000000200220000000000000000002000000000400000000001000080000002000000a0000000000000000200000000080000000000000000000001000002500000000000000000000a0000000800000040000000000000000000000000001000022000808000000080000400800000000000000000000000000000020020200282000040000000000000000000000201000000000800000000000000200",
"difficulty":"0x0",
"number":"0x1447225",
"gasLimit":"0x1c9c380",
"gasUsed":"0xe16bf",
"timestamp":"0x6744146f",
"extraData":"0x7273796e632d6275696c6465722e78797a",
"mixHash":"0x1f49ed8baffafa6b5fd66b1f2577f47eb508a6d0c5444c28b588b5e76a9a40c2",
"nonce":"0x0000000000000000",
"baseFeePerGas":"0x1ba8af27d",
"withdrawalsRoot":"0x80ecd553069724318de66efd3099be1f97f62df4ff3fb94c30f16c3aee275f62",
"blobGasUsed":"0x0",
"excessBlobGas":"0x4b20000",
"parentBeaconBlockRoot":"0xffd644329d46413352298de6bde7ca71752862a2bae34f0e3ead6970a3e9912a",
"requestsRoot":null,
"hash":"0x6d7977fbf9333267c5bc25b596eacc2ef89461289e078dd7283c6872008646bc"
}
返回特定区块的区块头信息-BlockHeight
func checkError(err error) {
if err != nil {
log.Fatalf("error = %v", err)
}
}
func main() {
var ctx = context.Background()
headers, err := getTargetBlockHeader(ctx, big.NewInt(-2))//nil is the latest block height
if err != nil {
if errors.Is(err, ethereum.NotFound) {
fmt.Println("invalid block height")
} else {
checkError(err)
}
} else {
headerBytes, err := headers.MarshalJSON()
if err == nil {
fmt.Println(fmt.Sprintf("Target block header info = %s", string(headerBytes)))
var header types.Header
if err = header.UnmarshalJSON([]byte(string(headerBytes))); err != nil {
log.Fatalf("decode error: %v ", err)
}
fmt.Println(header.Number)
} else {
checkError(err)
}
}
}
func getTargetBlockHeader(ctx context.Context, number *big.Int) (*types.Header, error) {
header, err := client.HeaderByNumber(ctx, number)
return header, err
}
BlocksDataEncode
rlp
编码区块信息
func main() {
var ctx = context.Background()
hash := "0x6d7977fbf9333267c5bc25b596eacc2ef89461289e078dd7283c6872008646bc"
blockInfo := getBlock(ctx, common.HexToHash(hash))
fmt.Println(fmt.Printf("Blocks data %s within block hash = %s", blockInfo, hash))
txs := decodeBlock(blockInfo)
fmt.Println(txs)
}
func getBlock(ctx context.Context, hash common.Hash) string {
block, err := client.BlockByHash(ctx, hash)
if err != nil {
if errors.Is(err, ethereum.NotFound) {
return "", errors.New("non-exist blockHash")
}else{
checkError(err)
}
}
BlockEnc, err := rlp.EncodeToBytes(&block)
if err != nil {
log.Fatalf("Encode blocks err = %v", err)
}
blockStr := common.Bytes2Hex(BlockEnc)
return blockStr
}
func decodeBlock(blockInfo string) []string {
blockEnc := common.FromHex(blockInfo)
var block types.Block
if err := rlp.DecodeBytes(blockEnc, &block); err != nil {
log.Fatalf("decode error: %v", err)
}
tx := make([]string, len(block.Body().Transactions))
for _, trans := range block.Body().Transactions {
transB, _ := trans.MarshalJSON()
tx = append(tx, string(transB))
}
return tx
//check := func(f string, got, want interface{}) {
// if !reflect.DeepEqual(got, want) {
// log.Fatalf("%s mismatch: got %v, want %v", f, got, want)
// }
//}
}
BlocksReceipt
获取当前区块体中全部交易的收据信息,用于判断每条交易的执行状态
func checkError(err error) {
if err != nil {
log.Fatalf("error = %v", err)
}
}
func main() {
var ctx = context.Background()
hash := "0x6d7977fbf9333267c5bc25b596eacc2ef89461289e078dd7283c6872008646bc"
var filter rpc.BlockNumberOrHash = rpc.BlockNumberOrHashWithHash(common.HexToHash(hash), false)
count := getBlockReceipt(ctx, filter)
fmt.Println(count)
}
func getBlockReceipt(ctx context.Context, filter rpc.BlockNumberOrHash) int {
receipts, err := client.BlockReceipts(ctx, filter)
if err != nil {
if errors.Is(err, ethereum.NotFound) {
fmt.Println("invalid block height")
} else {
checkError(err)
}
}
return len(receipts)
}
GetTransactions
根据区块hash 查询区块体中交易总数量
传参区块哈希值,查询该区块体中的交易数量
- 传参无效的
hash
值- 无法找到有效的区块,因此在解析数据时会报错
- 报错
json: cannot unmarshal non-string into Go value of type hexutil.Uint
func main() {
var ctx = context.Background()
hash := "0x6d7977fbf9333267c5bc25b596eacc2ef89461289e078dd7283c6872008646bc"
count := getTxCountBlockHeader(ctx, common.HexToHash(hash))
fmt.Println(count)
}
func getTxCountBlockHeader(ctx context.Context, hash common.Hash) uint {
count, err := client.TransactionCount(ctx, hash)
if err != nil {
if errors.Is(err, ethereum.NotFound) {
fmt.Println("invalid block height")
} else {
checkError(err)
}
}
return count
}
根据交易hash获取交易信息
每笔交易在钱包处构建并产生区块 hash
,但是可能存在多种原因导致该交易并未提交到链上处理
此时,对于区块链上来说,这笔交易并不存在。
因此,执行交易会报错 ethereum.NotFound
func main() {
var txHash = "0x8742cd8c26d22fb7e7c38eb63c0cae5afc35aaff6bf768a802b57ac675240822"
GetTxByhash(txHash)
}
func GetTxByhash(hash string) {
tx, pending, err := client.TransactionByHash(context.Background(), common.HexToHash(hash))
if err != nil {
if errors.Is(err, ethereum.NotFound) {
// todo
return
} else {
// todo
return
}
}
if !pending {
signer := types.LatestSignerForChainID(tx.ChainId())
sender, err := signer.Sender(tx)
if err != nil {
fmt.Printf("Error in rebuilding transactions's sender: %s", err)
return
}
nonce, err := client.NonceAt(context.Background(), sender, nil)
if err != nil {
fmt.Printf("Error in getting transactions's sender's nonce: %s", err)
return
}
pendingNonce, err := client.PendingNonceAt(context.Background(), sender)
if err != nil {
fmt.Printf("Error in rebuilding transactions's sender's pending nonce: %s", err)
return
}
fmt.Printf("txHash: %s, sender = %s, isPending: %v, nonce: %d, pendingNocne: %d", hash, sender, pending, nonce, pendingNonce)
}
}
根据hash获取交易收据
- 先获取交易数据,判断当前交易是否存在以及当前交易是否仍然处在
pending
状态 - 只有当前交易
hash
有效以及被打包出块的情况下,才能有效的获取该交易hash
的收据状态- 收据树中记录该交易执行完毕触发的全部
logs
信息 - 收据树记录当前交易的执行状态,
Failed(0),Success(1)
- 收据树中记录该交易执行完毕触发的全部
if !pending {
receipt, err := client.TransactionReceipt(ctx, hash)
if err != nil {
fmt.Printf("Error in getting transactions's receipt: %s", err)
return
}
if receipt.Status == 0 {
fmt.Printf("transactions failed")
} else {
txReceiptB, _ := receipt.MarshalJSON()
fmt.Printf("transactions success with receiprt: %s", string(txReceiptB))
}
}
Examples:
{
"type":"0x2",
"root":"0x",
"status":"0x1",
"cumulativeGasUsed":"0x127522",
"logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010002000000080020000080000000000000000000000000000800000008000000000000001000000000000000040000000000000000002000000000000000000000000000000000000000000010000800000000000000000000000000000000000000000000000000000000000000100000040000000000000000000080000000000000000000000000000000000000000000000002000008000000000000000800000000000000000000000000000000020000202000000000000000000000000000000000000000000000000000000000",
"logs":[
{
"address":"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"topics":[
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x000000000000000000000000c7bbec68d12a0d1830360f8ec58fa599ba1b0e9b",
"0x0000000000000000000000001f2f10d1c40777ae1da742455c65828ff36df387"
],
"data":"0x000000000000000000000000000000000000000000000001657e41135b73353a",
"blockNumber":"0x144739e",
"transactionHash":"0x8742cd8c26d22fb7e7c38eb63c0cae5afc35aaff6bf768a802b57ac675240822",
"transactionIndex":"0x3",
"blockHash":"0x4bb79e27c629d2014159dabb7ca612cbda517b73c651ef8d5aa89a6efbb3736e",
"logIndex":"0x21",
"removed":false
},
{
"address":"0xdac17f958d2ee523a2206206994597c13d831ec7",
"topics":[
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x0000000000000000000000001f2f10d1c40777ae1da742455c65828ff36df387",
"0x000000000000000000000000c7bbec68d12a0d1830360f8ec58fa599ba1b0e9b"
],
"data":"0x000000000000000000000000000000000000000000000000000000142dbe2e00",
"blockNumber":"0x144739e",
"transactionHash":"0x8742cd8c26d22fb7e7c38eb63c0cae5afc35aaff6bf768a802b57ac675240822",
"transactionIndex":"0x3",
"blockHash":"0x4bb79e27c629d2014159dabb7ca612cbda517b73c651ef8d5aa89a6efbb3736e",
"logIndex":"0x22",
"removed":false
},
{
"address":"0xc7bbec68d12a0d1830360f8ec58fa599ba1b0e9b",
"topics":[
"0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67",
"0x0000000000000000000000001f2f10d1c40777ae1da742455c65828ff36df387",
"0x0000000000000000000000001f2f10d1c40777ae1da742455c65828ff36df387"
],
"data":"0xfffffffffffffffffffffffffffffffffffffffffffffffe9a81beeca48ccac6000000000000000000000000000000000000000000000000000000142dbe2e0000000000000000000000000000000000000000000003ce3f9e66833b15f71c2c00000000000000000000000000000000000000000000000008a38c3903c19846fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd05f0",
"blockNumber":"0x144739e",
"transactionHash":"0x8742cd8c26d22fb7e7c38eb63c0cae5afc35aaff6bf768a802b57ac675240822",
"transactionIndex":"0x3",
"blockHash":"0x4bb79e27c629d2014159dabb7ca612cbda517b73c651ef8d5aa89a6efbb3736e",
"logIndex":"0x23",
"removed":false
}
],
"transactionHash":"0x8742cd8c26d22fb7e7c38eb63c0cae5afc35aaff6bf768a802b57ac675240822",
"contractAddress":"0x0000000000000000000000000000000000000000",
"gasUsed":"0x36f17",
"effectiveGasPrice":"0xc4f0d4dc4",
"blockHash":"0x4bb79e27c629d2014159dabb7ca612cbda517b73c651ef8d5aa89a6efbb3736e",
"blockNumber":"0x144739e",
"transactionIndex":"0x3"
}
根据区块hash查询区块体中交易
遍历区块体中的交易数据:
func main() {
var ctx = context.Background()
hash := "0x6d7977fbf9333267c5bc25b596eacc2ef89461289e078dd7283c6872008646bc"
block, err := client.BlockByHash(ctx, common.HexToHash(hash))
if err != nil {
if errors.Is(err, ethereum.NotFound) {
return "", errors.New("non-exist blockHash")
}else{
checkError(err)
}
}
tx := make([]string, len(block.Body().Transactions))
for _, trans := range block.Body().Transactions {
transB, _ := trans.MarshalJSON()
tx = append(tx, string(transB))
}
fmt.Println(fmt.Printf("Transaction details :%v, within blockhash = %s", tx, hash))
}
根据交易索引获取交易信息
区块体中的交易是有序的,保证全部验证者执行顺序的一致,从而有效校验区块
- 根据区块信息和交易顺序,能够有效获取当前交易
- 提供无效的区块hash时,报错
ethereum.NotFound
- 提供无效的区块Index值时:
- 提供的值 < 0, 报错
constant -1 overflows uint
- 提供的值 > 最大值,报错
ethereum.NotFound
- 提供的值 < 0, 报错
func main() {
var ctx = context.Background()
var blockHash = "0x6d7977fbf9333267c5bc25b596eacc2ef89461289e078dd7283c6872008646bc"
GetTxByBlockHashAndIndex(ctx, common.HexToHash(blockHash), uint(3))
}
func GetTxByBlockHashAndIndex(ctx context.Context, hash common.Hash, index uint) {
tx, err := client.TransactionInBlock(ctx, hash, index)
if err != nil {
if errors.Is(err, ethereum.NotFound) {
fmt.Println(fmt.Printf("Invalid block hash = %s", hash))
return
} else {
checkError(err)
}
}
txB, _ := tx.MarshalJSON()
fmt.Printf("transactions info= %s within blockHash = %s, blockIndex = %d\n", string(txB), hash, index)
parseTx(string(txB))
}
func parseTx(tx string) {
check := func(f string, got, want interface{}) {
if !reflect.DeepEqual(got, want) {
log.Fatalf("%s mismatch: got %v, want %v", f, got, want)
}
}
var transaction types.Transaction
err := transaction.UnmarshalJSON([]byte(tx))
if err != nil {
log.Fatalf("Unmarshal tx err = %v", err)
}
txB, _ := transaction.MarshalJSON()
check("TxInfo", string(txB), tx)
}
Parse Smart Contract transactions
package main
import (
"context"
"errors"
"fmt"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"io"
"log"
"os"
"strings"
)
var (
client *ethclient.Client
err error
)
func init() {
client, err = ethclient.Dial("https://eth.llamarpc.com")
if err != nil {
log.Fatal(err)
}
}
func checkError(err error) {
if err != nil {
log.Fatalf("error = %v", err)
}
}
func main() {
var ctx = context.Background()
var txHash = "0x7527da7c98477c6961fe7c3218255c8e355c41b0f41c24633b78faa1967ff526"
data := getTxdata(ctx, txHash)
path := "./examples.json"
abiFilter := getABI(path)
DecodeTransactionInputData(abiFilter, data)
}
func getTxdata(ctx context.Context, hash string) []byte {
tx, pending, err := client.TransactionByHash(ctx, common.HexToHash(hash))
if err != nil {
if errors.Is(err, ethereum.NotFound) {
fmt.Println(fmt.Printf("Invalid block hash = %s", hash))
return []byte{}
} else {
checkError(err)
}
}
if !pending {
return tx.Data()
}
return []byte{}
}
func getABI(path string) string {
abiFile, err := os.Open(path)
if err != nil {
log.Fatal(err)
}
defer abiFile.Close()
result, err := io.ReadAll(abiFile)
if err != nil {
log.Fatal(err)
}
return string(result)
}
func DecodeTransactionInputData(jsondata string, data []byte) {
// The first 4 bytes of the t represent the ID of the method in the ABI
contractABI, err := abi.JSON(strings.NewReader(jsondata))
if err != nil {
log.Fatalf("parse abi err :%v", err)
}
methodSigData := data[:4]
method, err := contractABI.MethodById(methodSigData)
if err != nil {
log.Fatal(err)
}
inputsSigData := data[4:]
inputsMap := make(map[string]interface{})
if err := method.Inputs.UnpackIntoMap(inputsMap, inputsSigData); err != nil {
log.Fatal(err)
}
fmt.Printf("Method Name: %s\n", method.Name)
fmt.Printf("Method inputs: %v\n", inputsMap)
}
//Method Name: deposit
//Method inputs: map[_verifierAddress:nillion1mn4ce97demxwcwe2nr5djlm3q32e2apch770vs]
subscribeNewHead
通过 RPC
节点从网络订阅新区块信息,基于 websocket
和 channel
实现。
RPC
节点服务作为服务商,在收到新区块时将数据写入通道
Golang
本地作为使用方,从区块中即使读取数据,防止写入阻塞
- 如果不及时读取通道数据
- 报错
subscribe new block error: websocket: close 1006 (abnormal closure): unexpected EOF
- 报错
- 通过
for select
循环读取通道数据- 订阅对象包含
error
通道,返回订阅过程的报错信息 - 判断读取
header
通道数据,并进行处理
- 订阅对象包含
package main
import (
"context"
"errors"
"fmt"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"log"
)
var (
client *ethclient.Client
err error
)
func init() {
client, err = ethclient.Dial("wss://eth.drpc.org")
if err != nil {
checkError(errors.New(fmt.Sprintf("subclient failed to dial: %v", err)))
}
}
func checkError(err error) {
if err != nil {
log.Fatalf("error = %v", err)
}
}
func main() {
subscribeNewHead()
}
func subscribeNewHead() {
headers := make(chan *types.Header)
sub, err := client.SubscribeNewHead(context.Background(), headers)
if err != nil {
checkError(errors.New(fmt.Sprintf("subclient failed to subscribe new block headers: %v", err)))
}
for {
select {
case err := <-sub.Err():
log.Printf("subscribe new block error: %v", err)
subscribeNewHead()
case header := <-headers:
fmt.Print(fmt.Sprintf("Receive new blocks hash = %s\n", header.Hash().Hex()))
// block, _ := client.BlockByNumber(context.Background(), header.Number)
// for _, tx := range block.Transactions() {
// msg := tx.To()
// }
}
}
}
subscribePendingTx
geth
提供查询 pending
交易的函数
subscribeFullPendingTransactions
用于接收完整的待处理交易subscribePendingTransactions
用于接收待处理交易hash
package main
import (
"context"
"errors"
"fmt"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient/gethclient"
"github.com/ethereum/go-ethereum/rpc"
"log"
"sync"
)
var (
subgclient *gethclient.Client
client *rpc.Client
err error
)
func init() {
client, err = rpc.Dial("wss://eth.drpc.org")
if err != nil {
checkError(errors.New(fmt.Sprintf("subclient failed to dial: %v", err)))
}
subgclient = gethclient.New(client)
}
func checkError(err error) {
if err != nil {
log.Fatalf("error = %v", err)
}
}
var (
wg sync.WaitGroup
Tx = "Tx"
TxHash = "TxHash"
)
func main() {
commands := []string{Tx, TxHash}
lock := len(commands)
wg.Add(lock)
DoSubscribe(commands)
wg.Wait()
}
func DoSubscribe(strs []string) {
for _, str := range strs {
switch str {
case Tx:
go subscribeFullPendingTx()
case TxHash:
go subscribePendingTxHash()
}
}
}
func subscribeFullPendingTx() {
defer wg.Done()
pendingTx := make(chan *types.Transaction)
sub, err := subgclient.SubscribeFullPendingTransactions(context.Background(), pendingTx)
if err != nil {
checkError(errors.New(fmt.Sprintf("subclient failed to subscribe new transctions: %v", err)))
}
for {
select {
case err := <-sub.Err():
log.Printf("subscribe new transactions error: %v", err)
//subscribeFullPendingTx()
case tx := <-pendingTx:
//todo
data, _ := tx.MarshalJSON()
fmt.Println(string(data))
/**
{"type":"0x2","chainId":"0x1","nonce":"0x2","to":"0x417a5538c0af25ecea6a7eb87e66d553b34ad9ab","gas":"0x5208","gasPrice":null,"maxPriorityFeePerGas":"0x5f5e100","maxFeePerGas":"0x8a63c4190","value":"0xfd9caec58e1cce","input":"0x","accessList":[],"v":"0x0","r":"0xa878da21c2227d29bb4ae28d19238a80957880a2a04d04467f9aa3bde7dacc24","s":"0x3c267fb8d0348c2cec77d88179635db78055a16ee9da25a2c5d8beb51d8c2460","hash":"0xf3b2eb14180d1876f067c21397684874d22f1fc5b89219fb64868fad56712dec"}
**/
}
}
}
func subscribePendingTxHash() {
defer wg.Done()
pendingTxHash := make(chan common.Hash)
sub, err := subgclient.SubscribePendingTransactions(context.Background(), pendingTxHash)
if err != nil {
checkError(errors.New(fmt.Sprintf("subclient failed to subscribe new transctions hash: %v", err)))
}
for {
select {
case err := <-sub.Err():
log.Printf("subscribe new transactions hash error: %v", err)
case hash := <-pendingTxHash:
log.Printf("Pending tx hash = %s", hash.Hex())
}
}
}
FilterLogs
过滤Logs
BlockHash + Addresses + Topics
用于过滤特定区块中的logs
BlockHash
不为空的时候,FromBlock,ToBlock
必须为空
FromBlock + ToBlock + Addresses + Topics
用于过滤区间logs
FromBlock
为空时arg["fromBlock"] = "0x0"
ToBlock
为空时return "latest"
Topics
,每笔交易中触发的事件是有顺序的,每个事件在当前区块中的顺序也是固定的{} or nil
获取全部的logs
,推使用switch xx case{}
解析不同的事件{{A}}
当前交易触发事件中,只获取第一位是A
的log
{{},{B}}
当前交易触发事件中,只获取第二位是B
的log
{{A},{B}}
当前交易触发事件中,只获取第一位是A
并且 第二位是B
的log
{{A,B},{C,D}}
当前交易触发事件中,只获取第一位是A或者B
并且 第二位是C或者D
的log
// FilterQuery contains options for contract log filtering.
type FilterQuery struct {
BlockHash *common.Hash // used by eth_getLogs, return logs only from block with this hash
FromBlock *big.Int // beginning of the queried range, nil means genesis block
ToBlock *big.Int // end of the range, nil means latest block
Addresses []common.Address // restricts matches to events created by specific contracts
// The Topic list restricts matches to particular event topics. Each event has a list
// of topics. Topics matches a prefix of that list. An empty element slice matches any
// topic. Non-empty elements represent an alternative that matches any of the
// contained topics.
//
// Examples:
// {} or nil matches any topic list
// {{A}} matches topic A in first position
// {{}, {B}} matches any topic in first position AND B in second position
// {{A}, {B}} matches topic A in first position AND B in second position
// {{A, B}, {C, D}} matches topic (A OR B) in first position AND (C OR D) in second position
Topics [][]common.Hash
}
无法获取监听合约的 abi
合约事件中,indexed
标记的字符存储在 Topic
数组,其余字段经过 abi.encode
编码后存储在 data
中
package main
import (
"context"
"errors"
"fmt"
"github.com/ethereum/go-ethereum"
"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/ethclient"
"log"
"math/big"
"strconv"
"sync"
)
var (
client *ethclient.Client
err error
)
// wss://ethereum.callstaticrpc.com wss://mainnet.gateway.tenderly.co wss://ws-rpc.graffiti.farm
func init() {
client, err = ethclient.Dial("wss://ethereum-rpc.publicnode.com")
if err != nil {
checkError(errors.New(fmt.Sprintf("subclient failed to dial: %v", err)))
}
}
func checkError(err error) {
if err != nil {
log.Fatalf("error = %v", err)
}
}
var (
wg sync.WaitGroup
logsChan = make(chan types.Log, 0)
)
func main() {
contracts := []string{"0x8D22d933ad982Fa7EbCc589A7cA69438E9320e55"}
topics := []string{"0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62", "0xb48a3f4dab3b7d9c6a59f8fd2b97e1336ce85b34bc2c9bc96b59f964cfa7b0c3"}
wg.Add(2)
go FilterLogs(21270408, 21270409, contracts, topics)
go parseLogs()
wg.Wait()
}
func FilterLogs(startBlockHeight, latestBlockNum int64, addresses []string, topics []string) {
defer wg.Done()
i := startBlockHeight
for i <= latestBlockNum {
from := &big.Int{}
from = from.SetInt64(startBlockHeight)
i += 5000
to := &big.Int{}
if i > latestBlockNum {
to = to.SetInt64(latestBlockNum)
} else {
to = to.SetInt64(i)
}
query := ethereum.FilterQuery{
FromBlock: from,
ToBlock: to,
}
for _, address := range addresses {
query.Addresses = append(query.Addresses, common.HexToAddress(address))
}
top := make([]common.Hash, 0)
for _, topic := range topics {
top = append(top, common.HexToHash(topic))
}
query.Topics = append(query.Topics, top)
fmt.Println(query)
logs, err := client.FilterLogs(context.Background(), query)
if err != nil {
checkError(errors.New(fmt.Sprintf("Error in filter logs :%v", err)))
}
for _, logData := range logs {
logsChan <- logData
}
}
}
func parseLogs() {
defer wg.Done()
for {
select {
case logData := <-logsChan:
switch logData.Topics[0] {
case common.HexToHash("0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62"):
//time.Sleep(500 * time.Millisecond)
Operator := common.HexToAddress(logData.Topics[1].Hex())
From := common.HexToAddress(logData.Topics[2].Hex())
To := common.HexToAddress(logData.Topics[3].Hex())
Id, _ := strconv.ParseInt(hexutil.Encode(logData.Data)[2:66], 16, 64)
Value, _ := strconv.ParseInt(hexutil.Encode(logData.Data)[66:], 16, 64)
fmt.Printf("NFT: %s, Operator: %s, From: %s, To: %s, NFTId: %d, Value: %d\n", logData.Address, Operator, From, To, Id, Value)
case common.HexToHash("0xb48a3f4dab3b7d9c6a59f8fd2b97e1336ce85b34bc2c9bc96b59f964cfa7b0c3"):
start := 2
end := start + 64
Tier, _ := strconv.ParseInt(hexutil.Encode(logData.Data)[start:end], 16, 64)
start = end
end = start + 64
Collection := common.HexToAddress(hexutil.Encode(logData.Data)[start:end])
start = end
end = start + 64
Id, _ := strconv.ParseInt(hexutil.Encode(logData.Data)[start:end], 16, 64)
fmt.Printf("Collection: %s, Tier: %d, NFTId: %d\n", Collection, Tier, Id)
}
}
}
}
能够获取监听合约的 abi
通过 abi
文件直接按照 abi.decode
的规则直接解析 logs
数据
abigen –abi=xxx.abi –bin=xxx.bin –pkg=xxx –out=xxx.go
package main
import (
"context"
"errors"
"fmt"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"log"
"math/big"
"sync"
)
var (
client *ethclient.Client
err error
)
// wss://ethereum.callstaticrpc.com wss://mainnet.gateway.tenderly.co wss://ws-rpc.graffiti.farm
func init() {
client, err = ethclient.Dial("wss://ethereum-rpc.publicnode.com")
if err != nil {
checkError(errors.New(fmt.Sprintf("subclient failed to dial: %v", err)))
}
}
func checkError(err error) {
if err != nil {
log.Fatalf("error = %v", err)
}
}
var (
wg sync.WaitGroup
logsChan = make(chan types.Log, 0)
)
func main() {
contracts := []string{"0x8D22d933ad982Fa7EbCc589A7cA69438E9320e55"}
topics := []string{"0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62", "0xb48a3f4dab3b7d9c6a59f8fd2b97e1336ce85b34bc2c9bc96b59f964cfa7b0c3"}
wg.Add(2)
go FilterLogs(21270480, 21270481, contracts, topics)
go parseLogs()
wg.Wait()
}
func FilterLogs(startBlockHeight, latestBlockNum int64, addresses []string, topics []string) {
defer wg.Done()
i := startBlockHeight
for i <= latestBlockNum {
from := &big.Int{}
from = from.SetInt64(startBlockHeight)
i += 5000
to := &big.Int{}
if i > latestBlockNum {
to = to.SetInt64(latestBlockNum)
} else {
to = to.SetInt64(i)
}
query := ethereum.FilterQuery{
FromBlock: from,
ToBlock: to,
}
for _, address := range addresses {
query.Addresses = append(query.Addresses, common.HexToAddress(address))
}
top := make([]common.Hash, 0)
for _, topic := range topics {
top = append(top, common.HexToHash(topic))
}
query.Topics = append(query.Topics, top)
logs, err := client.FilterLogs(context.Background(), query)
if err != nil {
checkError(errors.New(fmt.Sprintf("Error in filter logs :%v", err)))
}
for _, logData := range logs {
logsChan <- logData
}
}
}
func parseLogs() {
defer wg.Done()
for {
select {
case logData := <-logsChan:
switch logData.Topics[0] {
case common.HexToHash("0xb48a3f4dab3b7d9c6a59f8fd2b97e1336ce85b34bc2c9bc96b59f964cfa7b0c3"):
stoneFilterer := stoneDropFilterer(logData.Address)
stoneDropped, err := stoneFilterer.ParseMysteryBoxDropped(logData)
if err != nil {
checkError(errors.New(fmt.Sprintf("Error in parse logs = %v", err)))
}
Tier := stoneDropped.Tier
Collection := stoneDropped.Collection
Id := stoneDropped.Id
fmt.Printf("Collection: %s, Tier: %d, NFTId: %d\n", Collection, Tier, Id)
}
}
}
}
func stoneDropFilterer(address common.Address) *StonedropFilterer {
StoneFilterer, err := NewStonedropFilterer(address, client)
if err != nil {
checkError(errors.New(fmt.Sprintf("Error in bind contract filter")))
}
return StoneFilterer
}
订阅Logs
func SubStakingEvent() {
defer wg.Done()
query := ethereum.FilterQuery{
Addresses: []common.Address{common.HexToAddress(contract)},
Topics: [][]common.Hash{{common.HexToHash("0x5ad8141c164356bdef9e16f08312a7034ac6682a7413ce4fecfc44da5e18fec7")}, {common.HexToHash("0xeb879c9d6d39266b9caad39ced3788f8b8f47bb316e3fb55f3f44cb0f638cbc6")}},
}
subevents, err := mainnetLPClient.SubscribeFilterLogs(context.Background(), query, logsChan)
if err != nil {
fmt.Println(fmt.Errorf("Subscribe Event error: %v", err))
log.Fatal(err)
}
for {
select {
case err := <-subevents.Err():
fmt.Println(fmt.Errorf("Parse Event error: %v", err))
SubStakingEvent()
case lplog := <-logs:
time.Sleep(500 * time.Millisecond)
parseStakingEventLogs(StakingFilterer, lplog, 1)
}
}
SendValue
以太坊账户分为: EOA
和 合约账户
EOA
, 通过私钥直接签名/发送交易- 合约账户由
EOA
交易触发,交易中的data
发送到合约执行逻辑
构造交易
LegacyTransactions
1559
升级前的交易类型,不需要考虑当前手续费类型,从直接给定的手续费中扣除 baseFee+ExecuteFee
后的 Fee * GasUsed
就是矿工小费
Nonce
: 链上递增值,防止重放交易- 构建交易需要先获取当前交易的最新
nonce
值,推荐使用pendingNonce
- 构建交易需要先获取当前交易的最新
GasPrice
: 发送地址附加的gasFee
Gas
:发送方附加的gas
数量- 当前账户的余额必须大于
gas * gasPrice
- 当前账户的余额必须大于
To
: 交易的接收方to == nil
, 该交易是合约创建交易,交易的data
就是合约创建的代码to = EOA
,交易仅仅能转账,data
作为附加值放在转账data
中to = SmartComtract
, 合约的调用交易,data
整体发送到合于地址按照合约逻辑执行
Value
: 发送方转账到to
地址的余额Data
: 发送交易附加的data
- 在
to == nil
的时候,表示合约的创建代码 - 在
to = EOA
的时候,表示一个备注,没有实际意义 - 在
to = SmartContract
的时候,表示需要在合约代码中执行的函数以及传参数据
- 在
// LegacyTx is the transaction data of the original Ethereum transactions.
type LegacyTx struct {
Nonce uint64 // nonce of sender account
GasPrice *big.Int // wei per gas
Gas uint64 // gas limit
To *common.Address `rlp:"nil"` // nil means contract creation
Value *big.Int // wei amount
Data []byte // contract invocation input data
V, R, S *big.Int // signature values
}
DynamicTransactions
1559
升级后将手续费分成三部分
baseFee
: 链网络全局数据,和当前网络拥堵请情况相关maxPriorityFeePerGas
: 发送发愿意发送到矿工的最大小费单位maxFeePerGas
: 发送方愿意为当前交易付出的最大gas
价格- 实际运行过程中的
gasPricePerGas = min(maxPriorityFeePerGas + baseFee, maxFeePerGas)
AccessList
用于在构建交易提前指定当前交易会sload/sstore
的slot
键值
// DynamicFeeTx represents an EIP-1559 transaction.
type DynamicFeeTx struct {
ChainID *big.Int
Nonce uint64
GasTipCap *big.Int // a.k.a. maxPriorityFeePerGas
GasFeeCap *big.Int // a.k.a. maxFeePerGas
Gas uint64
To *common.Address `rlp:"nil"` // nil means contract creation
Value *big.Int
Data []byte
AccessList AccessList
// Signature values
V *big.Int `json:"v" gencodec:"required"`
R *big.Int `json:"r" gencodec:"required"`
S *big.Int `json:"s" gencodec:"required"`
}
构建转账交易
- 从私钥导出交易发送方
- 获取发送方的
Nonce
值 - 获取
gasPrice
(两种转账类型获取不同的gasPrice
) - 基于
Nonce,To,Value,gasPrice,data
构建交易 - 私钥签名数据
- 通过
RPC
节点发送交易
package main
import (
"bytes"
"context"
"crypto/ecdsa"
"encoding/hex"
"errors"
"fmt"
"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"
"log"
"math/big"
"strings"
)
var (
client *ethclient.Client
err error
)
// wss://ethereum.callstaticrpc.com wss://mainnet.gateway.tenderly.co wss://ws-rpc.graffiti.farm wss://ethereum-rpc.publicnode.com
func init() {
client, err = ethclient.Dial("wss://sepolia.drpc.org")
if err != nil {
checkError(errors.New(fmt.Sprintf("subclient failed to dial: %v", err)))
}
}
func checkError(err error) {
if err != nil {
log.Fatalf("error = %v", err)
}
}
func main() {
DynamicTransferEth("0x604427A2d0805F7037d2747c2B4D882116616cb9", "", 10, 21000)
}
func DynamicTransferEth(to, data string, value, gasLimit uint64) {
var ctx = context.Background()
// Import the from address
//0x96216849c49358B10257cb55b28eA603c874b05E
key := "fad9c8855b740a0b7ed4c221dbad0f33a83a49cad6b3fe8d5817ac83d38b6a19"
// Decode the provided private key.
if ok := strings.HasPrefix(key, "0x"); ok {
key, _ = strings.CutPrefix(key, "0x")
}
ecdsaPrivateKey, err := crypto.HexToECDSA(key)
if err != nil {
log.Fatalf("Errors in parsing key %v", err)
}
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.
if ok := strings.HasPrefix(data, "0x"); !ok {
data = hexutil.Encode([]byte(data))
}
bytesData, err := hexutil.Decode(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(value)
_, priorityFee, gasFeeCap := DynamicGasPrice(ctx)
chainID, err := client.ChainID(ctx)
if err != nil {
log.Fatalln(err)
}
txData := types.DynamicFeeTx{
ChainID: chainID,
Nonce: nonce,
GasTipCap: priorityFee,
GasFeeCap: gasFeeCap,
Gas: gasLimit,
To: &toAddr,
Value: amount,
Data: bytesData,
}
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())
//fmt.Printf("tx details: %s, build hash: %s", rawTxRLPHex, signedTx.Hash().Hex())
err = client.SendTransaction(ctx, signedTx)
if err != nil {
log.Fatal(err)
} else {
fmt.Printf("tx details: %s, build hash: %s", rawTxRLPHex, signedTx.Hash().Hex())
//https://sepolia.etherscan.io/tx/0x536b59ae57a4af9593a964c274a27fc35e31f002bab099b8ab1f3c14dc2f1827
}
}
func DynamicGasPrice(ctx context.Context) (*big.Int, *big.Int, *big.Int) {
// Suggest the base fee for inclusion in a block.
baseFee, err := client.SuggestGasPrice(ctx)
if err != nil {
log.Fatalln(err)
}
// Suggest a gas tip cap (priority fee) for miner incentive.
priorityFee, err := client.SuggestGasTipCap(ctx)
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)
return baseFee, priorityFee, gasFeeCap
}
LegacyTransactions 获取 gasPrice
func main() {
LegacyTransferEth("0x604427A2d0805F7037d2747c2B4D882116616cb9", "", 10, 21000)
}
func LegacyTransferEth(to, data string, value, gasLimit uint64) {
gasPrice := SuggestedGasPrice(ctx)
txData := types.LegacyTx{
Nonce: nonce,
GasPrice: gasPrice,
Gas: gasLimit,
To: &toAddr,
Value: amount,
Data: bytesData,
}
}
func SuggestedGasPrice(ctx context.Context) *big.Int {
// Retrieve the currently suggested gas price for a new transaction.
gasPrice, err := client.SuggestGasPrice(ctx)
if err != nil {
log.Fatalf("Failed to suggest gas price: %v", err)
}
return gasPrice
}
//tx details: f8684585044dc8d30782520894604427a2d0805f7037d2747c2b4d882116616cb90a808401546d72a0b3f69a384be92af611e0c9a48f9bfb7e5e878d13b672f7bb750f3e80255a951ca0110af4963209ab2636adbc7b16d8500b53beb79694db044c534a21d0ef0c7d05, build hash: 0x3735c2ef92c4631fef1d7b927d085e3b5a942cb9115e117c8566fd1262cdb390
Estimate
模拟当前交易执行需要的 gas
花销,该交易不会发送到链上执行,只会在 rpc
节点本地模拟执行
From
: 交易的构造地址To
: 交易的接收方。EOA
或者合约地址Gas
:0
表示在模拟执行过程中,gas
不限量,用来模拟出最终的花销Value
:交易附加的Value
,转账需要花费额外的gas
,所以需要表明Data
:交易附加的data
,data
数据上链或者发送到合约处理,都需要额外的gas
// CallMsg contains parameters for contract calls.
type CallMsg struct {
From common.Address // the sender of the 'transaction'
To *common.Address // the destination contract (nil for contract creation)
Gas uint64 // if 0, the call executes with near-infinite gas
GasPrice *big.Int // wei <-> gas exchange ratio
GasFeeCap *big.Int // EIP-1559 fee cap per gas.
GasTipCap *big.Int // EIP-1559 tip per gas.
Value *big.Int // amount of wei sent along with the call
Data []byte // input data, usually an ABI-encoded contract method invocation
AccessList types.AccessList // EIP-2930 access list.
// For BlobTxType
BlobGasFeeCap *big.Int
BlobHashes []common.Hash
}
RPC
节点在本地基于当前 Pending
的链状态模拟执行该交易,但是不能保证和真实链上 gas
消耗完全一致,仅能作为参考
因为,在模拟执行和真实上链的空隙,有可能存在其他交易更新了和当前交易相关地址的数据,造成 gas
消耗的增加或减少
代码
func EstimateGas(from, to string, data []interface{}, value uint64) uint64 {
var ctx = context.Background()
var err error
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.
bytesData = encodeData(data)
// 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
}
WriteTokenTransferRawTx
to = SmartComtract
, 表示合约的调用交易,data
整体发送到合于地址按照合约逻辑执行
合约函数的调用由两部分组成:selector + 传参编码
- 代币转账函数为
transfer(address,uint256)
- 首先计算
selector = sha3.NewLegacyKeccak256()[:4]
- 首先计算
- 函数需要的两个传参按照 abi.encode 编码向左补全 256bit
data = append(data, common.LeftPadBytes(receiver, 32)...)
完整代码
package main
import (
"bytes"
"context"
"crypto/ecdsa"
"encoding/hex"
"errors"
"fmt"
"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"
"golang.org/x/crypto/sha3"
"log"
"math/big"
"strconv"
"strings"
)
var (
client *ethclient.Client
err error
)
// wss://ethereum.callstaticrpc.com wss://mainnet.gateway.tenderly.co wss://ws-rpc.graffiti.farm wss://ethereum-rpc.publicnode.com
func init() {
client, err = ethclient.Dial("wss://sepolia.drpc.org")
if err != nil {
checkError(errors.New(fmt.Sprintf("subclient failed to dial: %v", err)))
}
}
func checkError(err error) {
if err != nil {
log.Fatalf("error = %v", err)
}
}
func main() {
data := []interface{}{"transfer(address,uint256)", "0x604427A2d0805F7037d2747c2B4D882116616cb9", "200"}
writeTokenTransferRawTx("0x779877A7B0D9E8603169DdbD7836e478b4624789", data, 0, 60000)
}
func writeTokenTransferRawTx(to string, data []interface{}, value, gasLimit uint64) {
var ctx = context.Background()
// Import the from address
//0x96216849c49358B10257cb55b28eA603c874b05E
key := "fad9c8855b740a0b7ed4c221dbad0f33a83a49cad6b3fe8d5817ac83d38b6a19"
// Decode the provided private key.
if ok := strings.HasPrefix(key, "0x"); ok {
key, _ = strings.CutPrefix(key, "0x")
}
ecdsaPrivateKey, err := crypto.HexToECDSA(key)
if err != nil {
log.Fatalf("Errors in parsing key %v", err)
}
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)
}
bytesData := encodeData(data)
// Set up the transaction fields, including the recipient address, value, and gas parameters.
toAddr := common.HexToAddress(to)
amount := new(big.Int).SetUint64(value)
chainID, err := client.ChainID(ctx)
if err != nil {
log.Fatalln(err)
}
gasPrice := SuggestedGasPrice(ctx)
txData := types.LegacyTx{
Nonce: nonce,
GasPrice: gasPrice,
Gas: gasLimit,
To: &toAddr,
Value: amount,
Data: bytesData,
}
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())
//fmt.Printf("tx details: %s, build hash: %s", rawTxRLPHex, signedTx.Hash().Hex())
err = client.SendTransaction(ctx, signedTx)
if err != nil {
log.Fatal(err)
} else {
fmt.Printf("tx details: %s, build hash: %s", rawTxRLPHex, signedTx.Hash().Hex())
}
}
func SuggestedGasPrice(ctx context.Context) *big.Int {
// Retrieve the currently suggested gas price for a new transaction.
gasPrice, err := client.SuggestGasPrice(ctx)
if err != nil {
log.Fatalf("Failed to suggest gas price: %v", err)
}
return gasPrice
}
func encodeData(datas []interface{}) []byte {
// Prepare data payload.
funcs := datas[0].(string)
receipt := datas[1].(string)
tokenValue := datas[2].(string)
hash := sha3.NewLegacyKeccak256()
hash.Write([]byte(funcs))
methodID := hash.Sum(nil)[:4]
var data []byte
data = append(data, methodID...)
rece, _ := common.ParseHexOrString(strings.ToLower(receipt))
data = append(data, common.LeftPadBytes(rece, 32)...)
number, _ := strconv.ParseInt(tokenValue, 10, 64)
s := hexutil.EncodeBig(big.NewInt(number))
byteD, err := hexutil.Decode(s)
if err != nil {
checkError(err)
}
data = append(data, common.LeftPadBytes(byteD, 32)...)
//res := hexutil.Encode(data)
//if ok := strings.HasPrefix(res, "0x"); !ok {
// res = hexutil.Encode([]byte(res))
//}
return data
}
ReadContract
solc
合约编译时会编译成两部分: 字节码(bin
) + 合约函数的二进制接口(abi
)
bin
跟随交易部署到合约地址abi
文件用来调用合约函数
Solc工具用于编译合约
solc --abi xx.sol
, 生成合约abi
文件solc --bin xx.sol
,生成合约的bin
文件
abigen工具可以基于 abi
和 bin
文件创建合约部署和调用的 Golang
文件
abigen --abi=xxx.abi --bin=xxx.bin --pkg=xxx --out=xxx.go
- 仅仅提供
abi
文件的话,只能生成合约的调用文件,不能部署合约
- 仅仅提供
代码
package milestone2
import (
"github.com/ethereum/go-ethereum/common"
"math/big"
"strconv"
)
func StakingInfo(contract, nft, nftId string) (common.Address, int64, error) {
var staker common.Address
stakingEndTime := int64(0)
address := common.HexToAddress(contract)
instance, err := NewStakeCaller(address, client)
if err != nil {
return staker, stakingEndTime, err
}
nftid, err := strconv.ParseInt(nftId, 10, 64)
if err != nil {
return staker, stakingEndTime, err
}
staker, endts, err := instance.RegisterData(nil, common.HexToAddress(nft), big.NewInt(nftid))
if err != nil {
return staker, stakingEndTime, err
}
return staker, endts.Int64(), nil
}
WriteContract
链上写入交易会更新链上数据状态,因此需要发送方签名和付费,保证交易的完整性
- 将合约 abi 调用程序和 RPC 节点绑定
- 构建交易
- 从私钥导出账户
- 获取账户 nonce
- 配置交易 gasLimit、gasPrice
- 私钥签名并发送交易
package milestone2
import (
"context"
"crypto/ecdsa"
"fmt"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"math/big"
)
func WriteExtend(registerContract, nftContract common.Address, NFTIds []*big.Int, time uint64, pk *ecdsa.PrivateKey) (error, string) {
instance, err := NewStake(registerContract, client)
if err != nil {
return fmt.Errorf("WriteExtend构建延期交易Instance失败"), ""
}
publicKey := pk.Public()
publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
if !ok {
return fmt.Errorf("WriteExtend导出公钥地址失败"), ""
}
fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA)
chainId, err := client.ChainID(context.Background())
opts, err := bind.NewKeyedTransactorWithChainID(pk, chainId)
if err != nil {
return fmt.Errorf("WriteExtend绑定链数据失败"), ""
}
nonce, err := client.PendingNonceAt(context.Background(), fromAddress)
if err != nil {
return fmt.Errorf("WriteExtend获取地址nonce失败"), ""
}
opts.Nonce = big.NewInt(int64(nonce))
limit := len(NFTIds) * 50000
opts.GasLimit = uint64(limit)
//opts.GasPrice, err = config.Client.SuggestGasPrice(context.Background())
suggestPrice, err := client.SuggestGasPrice(context.Background())
if err != nil {
return fmt.Errorf("WriteExtend获取GasPrice失败"), ""
}
opts.GasPrice = new(big.Int).Mul(new(big.Int).Div(suggestPrice, big.NewInt(10)), big.NewInt(13))
//opts.GasPrice = suggestPrice
trans, err := instance.RegistrationRenewal(opts, nftContract, NFTIds, time)
if err != nil {
return fmt.Errorf("WriteExtend发送交易失败"), ""
}
//fmt.Printf("---No.%d 交易成功 %s Hash: %s \n", index, fromAddress, trans.Hash().Hex())
return nil, trans.Hash().Hex()
}
MerkleProof
简介链接:
https://yuhuajing.github.io/solidity-book/milestone_6/merkle-proof-validation.html
https://github.com/yuhuajing/solidity-book/tree/main/src/ContractsHub/merkle_tree_prove
merkleSecurity
为保证默克尔树的安全性,防止攻击者仅传递非叶子节点的数据跳过校验
叶子节点和验证节点的采用不用的 hash
校验:
叶子节点双 hash
待验证的叶子节点采用双 hash
的方式,区别叶子节点和验证节点的数据
func abiPackLeafHash(leafEncodings []string, values ...interface{}) ([]byte, error) {
data, err := AbiPack(leafEncodings, values...)
if err != nil {
return nil, err
}
hash, err := standardLeafHash(data)
return hash, err
}
func standardLeafHash(value []byte) ([]byte, error) {
k1, err := Keccak256(value)
if err != nil {
return nil, err
}
k2, err := Keccak256(k1)
return k2, err
}
单类型叶子节点
叶子节点仅采用单一数据类型,匹配的验证合约能够获取的数据有限
func MerkleOnlyOneArgOZ() {
leaf1 := []interface{}{
smt.SolAddress("0x0000000000000000000000000000000000000000"),
}
leaf2 := []interface{}{
smt.SolAddress("0x0000000000000000000000000000000000000000"),
}
leaf3 := []interface{}{
smt.SolAddress("0x0000000000000000000000000000000000000000"),
}
leaf4 := []interface{}{
smt.SolAddress("0x0000000000000000000000000000000000000000"),
}
leaf5 := []interface{}{
smt.SolAddress("0x0000000000000000000000000000000000000000"),
}
leaves := [][]interface{}{
leaf1,
leaf2,
leaf3,
leaf4,
leaf5,
}
tree, err := smt.Of(
leaves,
[]string{
smt.SOL_ADDRESS,
//smt.SOL_UINT256,
})
if err != nil {
fmt.Println("Of ERR", err)
}
root := hexutil.Encode(tree.GetRoot())
fmt.Println("Merkle Root: ", root)
proof, err := tree.GetProof(leaf1)
strProof := make([]string, len(proof))
if err != nil {
fmt.Println("GetProof ERR", err)
}
for _, v := range proof {
strProof = append(strProof, hexutil.Encode(v))
}
fmt.Println("02 proof: ", strProof)
}
多类型叶子节点
叶子节点拼接数据类型,可以将各种类型拼接(各种类型、类型数组等),匹配的验证合约能够获取足额数据,进行额外数据的处理
示例代码将地址和书来给你拼接,合约中通过额外的 mapping
记录,可以验证当前地址的剩余额度,
func MerkleWithMultiArgOZ() {
leaf1 := []interface{}{
smt.SolAddress("0x0000000000000000000000000000000000000000"),
smt.SolNumber("5000000000000000000"),
}
leaf2 := []interface{}{
smt.SolAddress("0x0000000000000000000000000000000000000000"),
smt.SolNumber("2500000000000000000"),
}
leaf3 := []interface{}{
smt.SolAddress("0x0000000000000000000000000000000000000000"),
smt.SolNumber("5000000000000000000"),
}
leaf4 := []interface{}{
smt.SolAddress("0x0000000000000000000000000000000000000000"),
smt.SolNumber("2500000000000000000"),
}
leaf5 := []interface{}{
smt.SolAddress("0x0000000000000000000000000000000000000000"),
smt.SolNumber("2500000000000000000"),
}
leaves := [][]interface{}{
leaf1,
leaf2,
leaf3,
leaf4,
leaf5,
}
tree, err := smt.Of(
leaves,
[]string{
smt.SOL_ADDRESS,
smt.SOL_UINT256,
})
if err != nil {
fmt.Println("Of ERR", err)
}
root := hexutil.Encode(tree.GetRoot())
fmt.Println("Merkle Root: ", root)
proof, err := tree.GetProof(leaf1)
strProof := make([]string, len(proof))
if err != nil {
fmt.Println("GetProof ERR", err)
}
for _, v := range proof {
strProof = append(strProof, hexutil.Encode(v))
}
fmt.Println("02 proof: ", strProof)
}
Signature
简介:
https://yuhuajing.github.io/solidity-book/milestone_6/signature-ECDSA-validation.html
sign_sha256
预编译合约地址 0x2 https://yuhuajing.github.io/solidity-book/milestone_5/contracts-precompile.html实现 sha256
哈希校验
package sign
import (
"crypto/sha256"
"fmt"
"github.com/ethereum/go-ethereum/common/hexutil"
"math/big"
)
func Sha256EncodeNumber(number int64) {
s := hexutil.EncodeBig(big.NewInt(number))
prefix := ""
num := 64 - len(s[2:])
for index := 0; index < num; index++ {
prefix += "0"
}
s = s[:2] + prefix + s[2:]
byteD, err := hexutil.Decode(s)
if err != nil {
fmt.Println(err)
}
h := sha256.New()
h.Write(byteD)
bs := h.Sum(nil)
fmt.Println(hexutil.Encode(bs))
}
sign_ecdsa
- 编码
hash
待签名数据
prefix := []byte(fmt.Sprintf("\x19Ethereum Signed Message:\n%d", len(message)))
messageBytes := milestone4.UnsafeBytes(message)
// Hash the prefix and message using Keccak-256
hash := crypto.Keccak256Hash(prefix, messageBytes)
编码多种类型的代签数据,基于开源库solsha3 "github.com/miguelmota/go-solidity-sha3"
mes := solsha3.SoliditySHA3(
[]string{"uint32[]", "uint32", "uint64", "uint64", "uint64", "address", "address", "address"},
[]interface{}{
nftId,
chainId,
timestamp,
uuid,
signId,
nft,
sender,
contract,
},
)
hash := solsha3.SoliditySHA3WithPrefix(mes)
- 私钥签名数据hash
// 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
- 校验数据,根据待签数据和签名逆向反推签名地址
- 签名不匹配的话,会返回错误的签名地址
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
}
GetSlotData
合约数据全部按照Solidity Slot 存储规则存储在区块链上,因此只要上链的数据就能通过 slot 键获取值
GetSlocByKey
合约数据按照声明顺序和编码规则存储在链上空间
读取区块内slot值
func GetStorageAtBlock(ctx context.Context, address common.Address, slot common.Hash, blockNum *big.Int) (*big.Int, error) {
//t := common.BigToHash(big.NewInt(int64(slot)))
int256 := new(big.Int)
res, err := client.StorageAt(ctx, address, slot, blockNum) // nil is the latest blockNum
if err != nil {
return int256, err
}
int256.SetBytes(res)
return int256, nil
}
读取区块slot值
基于区块 hash 锁定区块,读取截止区块高度的合约数据的 slot 数值
- hash 不存在:报错
error = header for hash not found
func GetStorageAtHash(ctx context.Context, address common.Address, slot common.Hash, hash common.Hash) (*big.Int, error) {
int256 := new(big.Int)
res, err := client.StorageAtHash(ctx, address, slot, hash)
if err != nil {
return int256, err
}
int256.SetBytes(res)
return int256, nil
}
PendingStorage
func GetPendingStorage(ctx context.Context, address common.Address, slot common.Hash) (*big.Int, error) {
int256 := new(big.Int)
res, err := client.PendingStorageAt(ctx, address, slot)
if err != nil {
return int256, err
}
int256.SetBytes(res)
return int256, nil
}
ContractSlotParser
合约内部的存储结构通过标准的 json
请求可以获取
solc --storage-layout --pretty-json -o $PWD/tempDirForSolc --overwrite ./xxx.sol
Json
对象包含两个键值: storage
和 types
Storage
{
"astId": 2,
"contract": "fileA:A",
"label": "x",
"offset": 0,
"slot": "0",
"type": "t_uint256"
}
astId
:状态变量声明的AST
节点的ID
contract
: 当前合约名称label
:状态变量的名称offset
:字节偏移量,表示在当前slot
中的偏移量slot
:存储的插槽位置type
:标识符,表示具体的数据存储,在types
中存在对用的结构体数据
Type
{
"base": "t_bool",
"encoding": "inplace",
"label": "uint256",
"numberOfBytes": "32",
"key": "t_string_memory_ptr",
"value": "t_uint256",
"members": `Type` 数组
}
基础数据结构:
encoding
:数据编码方式inplace
:数据能够在插槽中连续存储的数据mapping
:基于Keccak-256
寻址dynamic_array
:基于Keccak-256
寻址bytes
:单槽或基于Keccak-256
哈希值,取决于数据大小
label
:类型名称numberOfBytes
: 数据存储占据的字节数,如果 大于32
,表示使用一个以上的插槽存储数据
其中, mapping
类型额外包含
key
: 键值类型value
:值类型
"t_mapping(t_uint256,t_mapping(t_address,t_uint256))": {
"encoding": "mapping",
"key": "t_uint256",
"label": "mapping(uint256 => mapping(address => uint256))",
"numberOfBytes": "32",
"value": "t_mapping(t_address,t_uint256)"
},
数组包含:
base
: 数组成员的数据类型
"t_array(t_bool)5_storage": {
"base": "t_bool",
"encoding": "inplace",
"label": "bool[5]",
"numberOfBytes": "32"
},
结构体包含:
members
: 数组类型,表示结构体内部每个值的类型
"t_struct(Entity)62_storage": {
"encoding": "inplace",
"label": "struct StorageScan.Entity",
"members": [
{
"astId": 57,
"contract": "StorageScan.sol:StorageScan",
"label": "age",
"offset": 0,
"slot": "0",
"type": "t_uint64"
},
{
"astId": 59,
"contract": "StorageScan.sol:StorageScan",
"label": "id",
"offset": 8,
"slot": "0",
"type": "t_uint128"
},
{
"astId": 61,
"contract": "StorageScan.sol:StorageScan",
"label": "value",
"offset": 0,
"slot": "1",
"type": "t_string_storage"
}
],
"numberOfBytes": "64"
},
preference
https://github.com/yuhuajing/getSCSlotData/tree/main
https://github.com/yuhuajing/EVMSlotScan/tree/main
ParserUintSlot
数据能够在 EVM
栈空间连续存储的数据在 json storage
的编码格式: encoding:inplace
uint
类型没有负值,因此直接按照 类型长度和偏移量获取 slot
数据
enum
类型固定占位 8 bit
,采用 uint8
类型表示
// enum 固定占 8 位 ,采用 uint8
type SolidityUint struct {
SlotIndex common.Hash
Length uint
Offset uint
}
- 首先获取
baseSlot
的全部数值 - 根据偏移量和数据
length
类型判断是否独占一个slot
- 低位存在别的参数值的话,偏移量不为
0
,直接右移去掉偏移值 - 高位也可能存储别的参数值,因此要根据自身类型获取特定长度的值
- 直接按照类型长度,将高位全置
0
,length
长度的数据位置1
- 直接相与,去除高位数据
- 直接按照类型长度,将高位全置
golang
数据仅支持到int64
- 数值超限的话,转为
string
输出 - 数值不超限,直接输出
- 数值超限的话,转为
func (s SolidityUint) Value(f GetValueStorageAtFunc) interface{} {
v := f(s.SlotIndex)
vb := common.BytesToHash(v).Big()
vb.Rsh(vb, s.Offset)
mask := new(big.Int)
mask.SetBit(mask, int(s.Length), 1).Sub(mask, big.NewInt(1))
vb.And(vb, mask)
// if vb > uint64 max, return string, else return uint64
if vb.Cmp(big.NewInt(0).SetUint64(1<<64-1)) > 0 {
return vb.String()
} else {
return vb.Uint64()
}
}
ParserIntSlot
数据能够在 EVM
栈空间连续存储的数据在 json storage
的编码格式: encoding:inplace
int
类型存在负值,因此直接按照类型长度和偏移量获取 slot
数据后,还需要根据最高位的符号位判断正负
type SolidityInt struct {
SlotIndex common.Hash
Length uint
Offset uint
}
- 首先获取
baseSlot
的全部数值 - 根据偏移量和数据
length
类型判断是否独占一个slot
- 低位存在别的参数值的话,偏移量不为
0
,直接右移去掉偏移值 - 高位也可能存储别的参数值,因此要根据自身类型获取特定长度的值
- 直接按照类型长度,将高位全置
0
,length
长度的数据位置1
- 直接相与,去除高位数据
- 直接按照类型长度,将高位全置
- 判断符号位:
- 符号位为0(表示正数)
- 符号位为1(表示负数)
- 数值转为
string
输出
// Int 类型的数据按照顺序存储在slot中
// slot 栈宽不满256bit 时,数据放在同一个slot中存储
func (s SolidityInt) Value(f GetValueStorageAtFunc) interface{} {
v := f(s.SlotIndex)
// 获取当前slot的数据
// 根据 length 和 offset 判断是否当前数据独占一个slot 还是和 别的数据共享 slot
vb := common.BytesToHash(v).Big()
vb.Rsh(vb, s.Offset) // 直接右移,去掉 offset的数据
//下一步就是根据长度,去掉前面被挤占的数据
// get mask for length
mask := new(big.Int)
mask.SetBit(mask, int(s.Length), 1).Sub(mask, big.NewInt(1))
// 只保留 length 长度的 1,高位全是0
// get value by mask
vb.And(vb, mask)
// Int类型的数据由符号
// 通过最高位的符号位判断正负
// signBit is 0 if the value is positive and 1 if it is negative
signBit := new(big.Int)
signBit.Rsh(vb, s.Length-1)
if signBit.Uint64() == 0 {
//return vb.Uint64()
return vb.String()
} else {
//负数的处理
// flip the bits
vb.Sub(vb, big.NewInt(1))
r := make([]byte, 0)
for _, b := range vb.Bytes() {
r = append(r, ^b)
}
// convert back to big int
//return -new(big.Int).SetBytes(r).Int64()
return "-" + new(big.Int).SetBytes(r).String()
}
}
ParserBoolSlot
数据能够在 EVM
栈空间连续存储的数据在 json storage
的编码格式: encoding:inplace
bool
类型固定占位 8bit
,可以采用 uint8
类型或者 bool
类型解析
// bool 固定占 8 位
type SolidityBool struct {
SlotIndex common.Hash
Offset uint
}
- 首先获取
baseSlot
的全部数值 - 根据偏移量和数据
length == 8
类型判断是否独占一个slot
- 低位存在别的参数值的话,偏移量不为
0
,直接右移去掉偏移值 - 高位也可能存储别的参数值,因此要根据自身类型获取特定长度的值
- 直接按照类型长度,将高位全置
0
,length
长度的数据位置1
- 直接相与,去除高位数据
- 直接按照类型长度,将高位全置
- 数值转为
bool
类型输出true = 1, false = 0
func (s SolidityBool) Value(f GetValueStorageAtFunc) interface{} {
v := f(s.SlotIndex)
vb := common.BytesToHash(v).Big()
vb.Rsh(vb, s.Offset)
lengthOffset := new(big.Int)
lengthOffset.SetBit(lengthOffset, 8, 1).Sub(lengthOffset, big.NewInt(1))
vb.And(vb, lengthOffset)
return vb.Uint64() == 1
}
ParserAddressSlot
数据能够在 EVM
栈空间连续存储的数据在 json storage
的编码格式: encoding:inplace
address
类型固定占位 160bit
// address 固定占 160 位
type SolidityAddress struct {
SlotIndex common.Hash
Offset uint
}
- 首先获取
baseSlot
的全部数值 - 根据偏移量和数据
length == 160
类型判断是否独占一个slot
- 低位存在别的参数值的话,偏移量不为
0
,直接右移去掉偏移值 - 高位也可能存储别的参数值,因此要根据自身类型获取特定长度的值
- 直接按照类型长度,将高位全置
0
,length
长度的数据位置1
- 直接相与,去除高位数据
- 直接按照类型长度,将高位全置
- 数值转为地址类型输出
func (s SolidityAddress) Value(f GetValueStorageAtFunc) interface{} {
v := f(s.SlotIndex)
vb := common.BytesToHash(v).Big()
vb.Rsh(vb, s.Offset)
lengthOffset := new(big.Int)
lengthOffset.SetBit(lengthOffset, 160, 1).Sub(lengthOffset, big.NewInt(1))
vb.And(vb, lengthOffset)
return common.BytesToAddress(vb.Bytes())
}
ParserBytesSlot
数据能够在 EVM
栈空间连续存储的数据在 json storage
的编码格式: encoding:inplace
bytes
类型表现为固定长度的数组,数据从 bytes1~bytes32, 有长度的区别
type SolidityBytes struct {
SlotIndex common.Hash
Length uint
Offset uint
}
- 首先获取
baseSlot
的全部数值 - 根据偏移量和数据
length == 160
类型判断是否独占一个slot
- 低位存在别的参数值的话,偏移量不为
0
,直接右移去掉偏移值 - 高位也可能存储别的参数值,因此要根据自身类型获取特定长度的值
- 直接按照类型长度,将高位全置
0
,length
长度的数据位置1
- 直接相与,去除高位数据
- 直接按照类型长度,将高位全置
- 数值转为
string
类型输出
func (s SolidityBytes) Value(f GetValueStorageAtFunc) interface{} {
v := f(s.SlotIndex)
vb := common.BytesToHash(v).Big()
vb.Rsh(vb, s.Offset)
lengthOffset := new(big.Int)
lengthOffset.SetBit(lengthOffset, int(s.Length), 1).Sub(lengthOffset, big.NewInt(1))
vb.And(vb, lengthOffset)
return string(common.TrimRightZeroes(vb.Bytes()))
}
ParserArraySlot
数据能够在 EVM
栈空间连续存储的数据在 json storage
的编码格式: encoding:inplace
array
类型为固定长度的数组,同时具备维度的区别
array
类型在 storage base
中表明 子集数据的类型
第一维数值表示当前数组子集的长度 最后维度表示当前数组的长度
uint[8], 一维数组,表示 8 个uint8类型的数据
uin8[2][3],二维数组,第一维的 2 表示每个数组子集的数据长度为2,最后维度 3 表示当前数组的长度是3
// [[1,2],[3,4],[5,6]]
数组的每个子集从新的slot开始编码存储数据
- 因此,array 类型需要记录每维的数据长度
- 第一维决定了子集参数数量
- 第二维度决定了数据子集个数
type SolidityArray struct {
SlotIndex common.Hash
UnitLength uint64 `json:"unit_length"` // 第二维度的数据子集个数
UnitTyp Variable `json:"unit_typ"` // 子集数据类型
}
- 首先获取当前数据类型
- 一维数据就是当前数值的类型
- 多维数据就是数组类型
- 根据数据类型获取slot数据
- 一维数据类型,直接根据数据长度和偏移量获取数值
- 多维数据
- 首先根据baseSlot 和 子集参数类型以及子集数量 确定 数组占位
- 逐层解析每一维度
- 先获取当前维度中子集参数类型和子集个数,确定子集参数占据的slot长度
- 按照子集类型,直接递归获取数值
func (s SolidityArray) Value(f GetValueStorageAtFunc) interface{} {
switch s.UnitTyp.Typ() {
case IntTy:
si := s.UnitTyp.(*SolidityInt)
return IntSliceValue{
slotIndex: s.SlotIndex,
length: s.UnitLength,
uintBitLength: si.Length,
f: f,
}
case UintTy:
su := s.UnitTyp.(*SolidityUint)
return UintSliceValue{
slotIndex: s.SlotIndex,
length: s.UnitLength,
uintBitLength: su.Length,
f: f,
}
case BytesTy:
sb := s.UnitTyp.(*SolidityBytes)
return BytesSliceValue{
slotIndex: s.SlotIndex,
length: s.UnitLength,
uintBitLength: sb.Length,
f: f,
}
case StructTy:
ss := s.UnitTyp.(*SolidityStruct)
return StructSliceValue{
slotIndex: s.SlotIndex,
length: s.UnitLength,
filedValueMap: ss.FiledValueMap,
f: f,
}
case BoolTy:
return BoolSliceValue{
length: s.UnitLength,
slotIndex: s.SlotIndex,
f: f,
}
case StringTy:
return StringSliceValue{
length: s.UnitLength,
slotIndex: s.SlotIndex,
f: f,
}
case AddressTy:
return AddressSliceValue{
length: s.UnitLength,
slotIndex: s.SlotIndex,
f: f,
}
case ArrayTy:
lens := s.UnitLen()
//fmt.Println(lens) // 第一层数组大小
arrayLen := s.UnitTyp.(*SolidityArray).UnitLength
//fmt.Println(arrayLen)
dataTypeLen := s.UnitTyp.(*SolidityArray).UnitTyp.Len()
//fmt.Println(dataTypeLen)
var factor uint
h := uint(arrayLen) * dataTypeLen % 256
if h == 0 {
factor += uint(arrayLen) * dataTypeLen / 256
} else {
factor += uint(arrayLen)*dataTypeLen/256 + 1
}
lens *= factor
res := make([]interface{}, 0)
for i := uint(0); i < lens; i++ {
var loc int64 = 1
if i == 0 {
loc = 0
}
t := s.SlotIndex.Big().Int64() + loc
sb := new(big.Int)
sb.SetInt64(t)
s.SlotIndex = common.BigToHash(sb)
if i == 0 {
s.UnitTyp = s.UnitTyp.(*SolidityArray).UnitTyp //uint8
if dataTypeLen < 128 {
s.UnitLength = uint64(dataTypeLen * uint(arrayLen))
}
}
res = append(res, s.Value(f))
//fmt.Println(s.Value(f))
}
return res
}
return nil
}
ParserStructSlot
数据能够在 EVM
栈空间连续存储的数据在 json storage
的编码格式: encoding:inplace
struct
内部参数按照声明顺序依次存储入栈
struct
在 storage members[]
中表明每个数据类型
"t_struct(Entity)62_storage": {
"encoding": "inplace",
"label": "struct StorageScan.Entity",
"members": [
{
"astId": 57,
"contract": "StorageScan.sol:StorageScan",
"label": "age",
"offset": 0,
"slot": "0",
"type": "t_uint64"
},
{
"astId": 59,
"contract": "StorageScan.sol:StorageScan",
"label": "id",
"offset": 8,
"slot": "0",
"type": "t_uint128"
},
{
"astId": 61,
"contract": "StorageScan.sol:StorageScan",
"label": "value",
"offset": 0,
"slot": "1",
"type": "t_string_storage"
}
],
"numberOfBytes": "64"
}
结构体中每个变量都是独立的数据类型,因此,结构体通过 Field
字段定义每个变量的数据类型
type StructValueI interface {
Field(f string) interface{}
String() string
}
- 首先获取当前
Field
的数据类型 - 获取结构体内部变量的每个起始
slot
存储位置,基于baseSlot
和 声明顺序 - 按照每个变量类型和
slot
起始位置获取具体的数值
func (s StructValue) Field(fd string) interface{} {
filedValue, ok := s.filedValueMap[fd]
if !ok {
return nil
}
oldSlot := filedValue.Slot()
slotIndex := new(big.Int)
slotIndex.Add(s.baseSlotIndex.Big(), filedValue.Slot().Big())
// convert the slotIndex to common.Hash and assign it to the SlotIndex field of filed Value.V, using reflection
reflect.ValueOf(filedValue).Elem().FieldByName("SlotIndex").Set(reflect.ValueOf(common.BigToHash(slotIndex)))
value := filedValue.Value(s.f)
reflect.ValueOf(filedValue).Elem().FieldByName("SlotIndex").Set(reflect.ValueOf(oldSlot))
return value
}
ParserDynamicArraySlot
动态数组数据在 json storage
的编码格式: encoding:dynamic_array
array
类型在 storage base
中表明子集数据的类型
数组的每个子集参数基于当前的 baseSlot 和 参数顺序 存储数据
type SoliditySlice struct {
SlotIndex common.Hash
UnitTyp Variable `json:"unit_typ"`
}
- 首先获取当前参数的
baseSlot
- 获取当前数据类型,根据数据类型获取slot数据
func (s SoliditySlice) Value(f GetValueStorageAtFunc) interface{} {
length := common.BytesToHash(f(s.SlotIndex)).Big().Uint64()
valueSlotIndex := crypto.Keccak256Hash(s.SlotIndex.Bytes())
switch s.UnitTyp.Typ() {
case IntTy:
si := s.UnitTyp.(*SolidityInt)
return IntSliceValue{
slotIndex: valueSlotIndex,
length: length,
uintBitLength: si.Length,
f: f,
}
case UintTy:
su := s.UnitTyp.(*SolidityUint)
return UintSliceValue{
slotIndex: valueSlotIndex,
length: length,
uintBitLength: su.Length,
f: f,
}
case BytesTy:
sb := s.UnitTyp.(*SolidityBytes)
return BytesSliceValue{
slotIndex: valueSlotIndex,
length: length,
uintBitLength: sb.Length,
f: f,
}
case StructTy:
ss := s.UnitTyp.(*SolidityStruct)
return StructSliceValue{
slotIndex: valueSlotIndex,
length: length,
filedValueMap: ss.FiledValueMap,
f: f,
}
case BoolTy:
return BoolSliceValue{
slotIndex: valueSlotIndex,
length: length,
f: f,
}
case StringTy:
return StringSliceValue{
slotIndex: valueSlotIndex,
length: length,
f: f,
}
case AddressTy:
return AddressSliceValue{
slotIndex: valueSlotIndex,
length: length,
f: f,
}
case SliceTy:
{
ss := s.UnitTyp.(*SoliditySlice)
return ss.Value(f)
}
}
return nil
}
ParserDynamicStringSlot
动态数组数据在 json storage
的编码格式: encoding:bytes
- 首先获取当前参数的
baseSlot
- 查询
baseSlot
中存储的值
- 根据末尾数值判断
string
数据存储在当前slot
或者仅存储了当前string
数据的长度- 末尾数值为
0
,表示短string
- 数据存储在当前
slot
, 数据编码格式为string + length
- 数据存储在当前
- 末尾数值为
1
,表示长string
- 数据长度存储在当前
slot
, 数据编码格式为len(string) + length
- 具体数据从
keccak256(slot)
开始存储- 数据长度取余
256
为0
的话,数据存储在整数个slot
,直接相除获取具体slot
个数 - 取余为
1
的话,表示有额外slot
- 数据长度取余
- 数据长度存储在当前
- 末尾数值为
length
占位8 bit
- 获取当前数据类型,根据数据类型获取
slot
数据
// Value calculate the string length of the current slot record
// the length of the string exceeds 31 bytes (0x1f), and the entire slot stores the length of the string*2+1
// the length of the string does not exceed 31 bytes, the rightmost bit of the entire slot stores the character length*2, and the leftmost stores the string content
// if the last digit is odd then it is a long string, otherwise it is a short string
func (s SolidityString) Value(f GetValueStorageAtFunc) interface{} {
data := f(s.SlotIndex)
v := common.BytesToHash(data).Big()
// get the last digit of v
lastDigit := v.Bit(0)
// equal to 1 means it is a long string
if lastDigit == 1 {
// get the current string length bit
length := new(big.Int)
length.Sub(v, big.NewInt(1)).Div(length, big.NewInt(2)).Mul(length, big.NewInt(8))
remainB := new(big.Int)
remainB.Mod(length, big.NewInt(256))
slotNum := new(big.Int)
if remainB.Uint64() == 0 {
slotNum.Div(length, big.NewInt(256))
} else {
slotNum.Div(length, big.NewInt(256)).Add(slotNum, big.NewInt(1))
}
firstSlotIndex := crypto.Keccak256Hash(s.SlotIndex.Bytes())
value := f(firstSlotIndex)
for i := int64(0); i < slotNum.Int64()-1; i++ {
nextSlot := new(big.Int)
nextSlot.Add(firstSlotIndex.Big(), big.NewInt(i))
nextValue := f(common.BigToHash(nextSlot))
value = append(value, nextValue...)
}
lastSlotIndex := new(big.Int)
lastSlotIndex.Add(firstSlotIndex.Big(), big.NewInt(slotNum.Int64()-1))
lastSlotValue := f(common.BigToHash(lastSlotIndex))
if remainB.Uint64() == 0 {
value = append(value, lastSlotValue...)
} else {
// move right to get the final value
lastValueBig := common.BytesToHash(lastSlotValue).Big()
lastValueBig.Rsh(lastValueBig, 256-uint(remainB.Uint64()))
value = append(value, lastValueBig.Bytes()...)
}
return string(value)
} else {
length := new(big.Int)
length.And(v, big.NewInt(0xff))
length.Div(length, big.NewInt(2)).Mul(length, big.NewInt(8))
v.Rsh(v, 256-uint(length.Uint64()))
return string(v.Bytes())
}
}
ParserMappingSlot
数据在 json storage
的编码格式: encoding:mapping
array
类型为固定长度的数组,同时具备维度的区别
array
类型在 storage
中 通过 key,value
表明数据的类型
"t_mapping(t_int256,t_uint256)": {
"encoding": "mapping",
"key": "t_int256",
"label": "mapping(int256 => uint256)",
"numberOfBytes": "32",
"value": "t_uint256"
}
mapping
数据的具体存储位置和 mapping
键值和层数相关
- 因此,
mapping
类型需要记录当前数据的baseSlot
- 每层键值决定了下一层数据的起始
slot
- 键值类型决定了键值的编码格式
type SolidityMapping struct {
SlotIndex common.Hash
KeyTyp SolidityTyp
ValueTyp Variable `json:"value_typ"`
}
- 根据键值传参判断当前
mapping
的层数 - 计算最外层数据的起始
slot
存储位置 - 每进入一层,就进入新的
mapping
数据解析过程
- 重新计算当前层的数据起始存储
slot
- 根据数据类型获取具体的数值
// slotIndex = abi.encode(key,slot)
func (m MappingValue) Keys(ks []string) interface{} {
var slotIndex = m.baseSlotIndex
//k := ks[0]
var keyByte []byte
for index, k := range ks {
if index != 0 && index+1 <= len(ks) {
m.keyTyp = m.valueTyp.(*SolidityMapping).KeyTyp
m.valueTyp = m.valueTyp.(*SolidityMapping).ValueTyp
}
switch m.keyTyp {
case UintTy:
keyByte = encodeUintString(k)
case IntTy:
keyByte = encodeIntString(k)
case BytesTy:
keyByte = encodeByteString(k)
case StringTy:
keyByte = []byte(k)
case AddressTy:
keyByte = encodeHexString(k)
default:
panic("invalid key type")
}
slotIndex = crypto.Keccak256Hash(keyByte, slotIndex.Bytes())
}
reflect.ValueOf(m.valueTyp).Elem().FieldByName("SlotIndex").Set(reflect.ValueOf(slotIndex))
return m.valueTyp.Value(m.f)
}
PrivatePOAEthereum
正式使用硬件需求
Hardware Requirements Minimum:
CPU with 2+ cores 4GB RAM 1TB free storage space to sync the Mainnet 8 MBit/sec download Internet service Recommended:
Fast CPU with 4+ cores 16GB+ RAM High-performance SSD with at least 1TB of free space 25+ MBit/sec download Internet service
出块节点: 开放30303 (测试可以开放8545,后续需要关闭) 同步节点:开放30303 8545
安装golang gcc
sudo apt install build-essential
两种方式编译Geth工具
1- 从源码编译
git clone https://github.com/ethereum/go-ethereum.git
编译geth工具
cd go-ethereum && make geth
2- 直接下载geth工具
wget https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.12.1-9c216bd6.tar.gz
解压文件夹
tar -xzf geth-linux-amd64-1.12.1-9c216bd6.tar.gz
写入geth环境变量
vi ~/.bashrc
export ETHPATH=YouPathToGeth
export PATH=$ETHPATH:$PATH
source ~/.bashrc
生成账户地址
私钥通过用户输入的密码加密存储,可以通过golang解析出账户地址和私钥:
geth account new --datadir /opt/etherData
Address: 0x430CbEEffa18BD7ad0Ae5BAc062f130b6c8129B6
geth account new --datadir /opt/etherData
Address: 0x413E129dD6b217E4a8702821ee069e1929D17c6a
geth account new --datadir /opt/etherData
Address: 0x9449202f3E28Dd4595b0BE3c1736922Ba5aAce71
构造创世区块
1. chainID:自定义的链ID
2. homesteadBlock、eip150Block、eip155Block、eip158Block、byzantiumBlock、constantinopleBlock、petersburgBlock:各项提案和升级的区块高度
3. period:出块时间间隔,0为不允许出空交易块,会等待有交易才出块
4. epoch:更新出块节点列表的周期
5. difficulty:POA下无作用
6. gasLimit:gasLimit限制
7. extradata:POA模式下用来指定验证者地址,账户地址去掉0x后加上64个前缀0和130个后缀0,比如0x+(64个前缀0)+5534F5024146D16a5C1ce60A9f5a2e9794e3F981+(130个后缀0)
8. alloc:用来预置账号以及账号的以太币数量,比如预置0x0B587FFD0BBa122fb5ddc19AD6eEcEB1D2dBbff7地址拥有1000ETH(1000*10^18WEI)
genesis.json
{
"config":{
"chainId":12345,
"homesteadBlock":0,
"eip150Block":0,
"eip155Block":0,
"eip158Block":0,
"byzantiumBlock":0,
"constantinopleBlock":0,
"petersburgBlock":0,
"istanbulBlock":0,
"berlinBlock":0,
"londonBlock": 0,
"clique":{
"period":5,
"epoch":300
}
},
"alloc":{
"0x6593B47be3F4Bd1154c2faFb8Ad4aC4EFddD618f":{
"balance":"1000000000000000000000"
},
"0x6C345f0771a2f2B2694f97522D3371bF87b6BDF9":{
"balance":"1000000000000000000000"
},
"0xab6bbb89eFd62dF605C881E692960a4951238D71":{
"balance":"1000000000000000000000"
}
},
"coinbase": "0x6593B47be3F4Bd1154c2faFb8Ad4aC4EFddD618f",
"difficulty": "1",
"extraData": "0x0000000000000000000000000000000000000000000000000000000000000000验证者地址0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"gasLimit": "80000000",
"nonce": "0x0000000000000000",
"mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"timestamp": "0x00"
}
启动节点
创建创世区块
geth --datadir /opt/etherData init ./genesis.json
节点启动
节点正常启动
nohup geth --identity "myethereum" --datadir /opt/etherData --allow-insecure-unlock --networkid 12345 --http --http.addr 0.0.0.0 --http.corsdomain "*" --ws --ws.addr 0.0.0.0 --ws.origins "*" --http.api "eth,net,debug,txpool,web3,personal,admin,miner" --rpc.enabledeprecatedpersonal --miner.gaslimit 80000000 --syncmode "full" --nodiscover --rpc.enabledeprecatedpersonal >> geth.log 2>&1 &
连接控制台
geth attach http://localhost:8545
或者ipc连接
geth attach /opt/etherData/node0/geth.ipc
查询链上状态
- 查看所有账户列表
eth.accounts
- 根据节点私钥导入账号,提供节点私钥、加密节点私钥的对称密钥
personal.importRawKey("","")
- 查看账户余额
eth.getBalance(eth.accounts[0])
balanse=web3.fromWei(eth.getBalance(eth.accounts[0]),'ether')
- 查询区块高度
eth.blockNumber
- 根据交易hash查询交易数据
eth.getTransaction("TxHash")
- 查看节点信息
admin.nodeInfo.enode
发送链上交易
- 发送链上交易时,需要先解锁账户。
eth.sendTransaction({from:eth.accounts[0],to:eth.accounts[1],value:web3.toWei(4,'ether')})
- 解锁账户–需要指定时间,默认解锁300s personal.unlockAccount(address, passphrase, duration),密码和解锁时长都是可选的。如果密码为null,控制台将提示交互输密码。解密的密钥将保存在内存中直到解锁周期超时,默认的解锁周期为300秒。将解锁周期设置为0秒将解锁该密钥直到退出geth程序。
personal.unlockAccount(eth.accounts[0],'passward',0)
启动出块
- 设置矿工地址(验证者地址)
miner.setEtherbase(eth.accounts[0])
- 查看矿工账户
eth.coinbase
- 设置区块GasLimit
miner.setGasLimit(80000000)
- 启动出块(start() 的参数表示出块使用的线程数)/关闭出块 必须先解锁矿工账户,否则不会启动出块
miner.start()
miner.stop()
关闭节点
ps aux | grep geth | grep -v grep | awk '{print $2}'| xargs kill -15
清除链数据
geth removedb --datadir "/opt/etherData/"
启动节点2
新机器操作
数据备份节点
- 拷贝 genesis.json,创建相同的0号区块
- 节点正常启动
nohup geth --identity "myethereum" --datadir /opt/etherData --allow-insecure-unlock --networkid 12345 --http --http.addr 0.0.0.0 --http.corsdomain "*" --ws --ws.addr 0.0.0.0 --ws.origins "*" --http.api "eth,net,debug,txpool,web3,personal,admin,miner" --rpc.enabledeprecatedpersonal --miner.gaslimit 80000000 --syncmode "full" --nodiscover --rpc.enabledeprecatedpersonal >> geth.log 2>&1 &
连接控制台
geth attach http://localhost:8545
或者ipc连接
geth attach /opt/etherData/node0/geth.ipc
- 建立连接 查询节点信息
admin.nodeInfo.enode
通过addPeer命令添加节点.
admin.addPeer("节点信息")
出块节点(新的验证者地址)
新机器执行以下操作
- 生成节点
geth account new --datadir /opt/etherData
Address: 0x68d866baAfa993bc002cd35218c13f10aC54221d
- 连接控制台
- 在已连接的节点上执行以下操作:(需要半数以上的验证者同意)
目前的验证节点通过发起提案增加出块节点,增加后的节点和当前的验证者轮流出块。
clique.propose("新机器上生成的新验证者地址",true)
回到新服务器的终端:
- 在新服务器上设置矿工地址(新的验证者地址)
miner.setEtherbase(eth.accounts[0])
- 查看矿工账户
eth.coinbase
- 解锁账户
personal.unlockAccount(eth.accounts[0],'passward',0)
- 启动挖矿(start() 的参数表示挖矿使用的线程数)/关闭挖矿
miner.start()
连接钱包
钱包中创建新的网络
当前服务器已经启动区块链,对外开放 8545 接口
通过 ip:8545 查询链上信息,指定链Id唯一标识链身份
导出节点私钥
通过 geth account new
创建的地址保存在 keystore
文件夹中
通过 keystore 和 密码 可以解密导出私钥数据
//--file /opt/etherAccount/node1/keystore/UTC--2023-07-18T06-10-46.187084661Z--erer --password xxxx
package main
import (
"encoding/hex"
"flag"
"fmt"
"io/ioutil"
"os"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/crypto"
)
var (
file = flag.String("file", "", "file")
password = flag.String("password", "", "password")
)
func init() {
flag.Parse()
}
func main() {
if _, err := os.Stat(*file); os.IsNotExist(err) {
flag.Usage()
os.Exit(1)
}
keyjson, err := ioutil.ReadFile(*file)
if err != nil {
panic(err)
}
key, err := keystore.DecryptKey(keyjson, *password)
if err != nil {
panic(err)
}
address := key.Address.Hex()
privateKey := hex.EncodeToString(crypto.FromECDSA(key.PrivateKey))
fmt.Printf("Address: %s\nPrivateKey: %s\n",
address,
privateKey,
)
}
//--file /opt/etherAccount/node1/keystore/UTC--2023-07-18T06-10-46.187084661Z--0b587ffd0bba122fb5ddc19ad6eeceb1d2dbbff7 --password xxxx