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!

  1. 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:

  1. 先执行全部变量的初始化
  2. 在执行 init 函数
  3. 最后执行 主函数
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

  • []runegolang 的基本数据类型
    • string 是只读的 byte 数据,string 字符使用 utf-8 编码,每个字符占位 1~3 bytes
    • rune 占位 4 bytes
    • 对于英文字符,stringrune 类型没有区别
    • 对于中文字符,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:

  1. hello你好 占位 11 bytes(5 * 1 + 2 * 3 = 11)
  2. stringrune 时,一共 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 分配内存,返回的是指向类型的指针,并且内存置为0
      • new 可以申请任何类型变量内存块并返回一个指针指向它
    • 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 时没必要初始化新切片的 lenappend 时会自动将数据依次加到新切片末尾
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

  1. 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为了提供更容易使用的并发方法,使用了 goroutinechannel

  • 协程被称为 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 队列是有互斥锁进行保护的

  1. 创建、销毁、调度 G 都需要每个 M 获取锁,这就形成了激烈的锁竞争
  2. M 转移 G 会造成延迟和额外的系统负载。比如当 G 中包含创建新协程的时候,M 创建了 G’,为了继续执行 G,需要把 G’ 交给 M’ 执行,也造成了很差的局部性,因为 G’G 是相关的,最好放在 M 上执行,而不是其他 M'
  3. 系统调用(CPUM 之间的切换)导致频繁的线程阻塞和取消阻塞操作增加了系统开销。

二、Goroutine调度器的GMP模型的设计思想

面对之前调度器的问题,Go 设计了新的调度器。

在新调度器中,除了M(thread)G(goroutine),又引进了P(Processor)

** Processor,它包含了运行 goroutine 的资源**,如果线程 M 想运行 goroutine ,必须先获取 PP 中还包含了可运行的 G 队列。

(1)GMP模型

Go 中,线程 M 是运行 goroutine 的实体,调度器的功能是把用户态中可运行的 goroutine 分配到工作线程上

  1. 全局队列Global Queue):存放等待运行的G(goroutine)
  2. P的本地队列:同全局队列类似,存放的也是等待运行的 G,存的数量有限,不超过256个。新建G'时,G'优先加入到 P 的本地队列,如果队列满了,则会把本地队列中一半的 G 移动到全局队列
  3. P列表:所有的P都在程序启动时创建,并保存在数组中,最多有GOMAXPROCS(可配置)个。
  4. M:线程想运行任务就得获取P,从P的本地队列获取GP队列为空时,M也会尝试从全局队列取一批G放到P的本地队列,或从其他P的本地队列拿出一半放到自己P的本地队列。M运行GG执行之后,M会从P获取下一个G,不断重复下去

Goroutine 调度器和OS调度器是通过线程M结合起来的,每个M都代表了1个内核线程,OS调度器负责把内核线程分配到CPU的核上执行

有关P和M的个数问题

  1. P 的数量:
  • 由启动时环境变量$GOMAXPROCS或者是由runtime的方法GOMAXPROCS()决定。这意味着在程序执行的任意时刻都只有$GOMAXPROCSgoroutine 在同时运行。
  1. M线程的数量:
  • go 程序启动时,会设置 M 的最大数量,默认 10000.但是内核很难支持这么多的线程数,所以这个限制可以忽略。
  • runtime/debug 中的 SetMaxThreads 函数,设置 M 的最大数量
  • 一个 M 阻塞了,会创建新的 M

MP 的数量没有绝对关系,一个 M 线程阻塞,P 就会去创建或者切换另一个 M,所以,即使 P 的默认数量是1,也有可能会创建很多个 M 出来。

P和M何时会被创建

  1. P 何时创建:在确定了 P 的最大数量 n 后,运行时系统会根据这个数量创建 n个P
  2. M 何时创建:古国没有足够的 M 线程来关联 P 并运行其中的可运行的 G
  • 比如所有的 M 线程此时都阻塞住了,而 P 中还有很多就绪任务
    • 先去就会去寻找空闲的 M 执行协程任务
    • 如果没有空闲的,就会去创建新的 M

(2)调度器的设计策略

复用线程:避免频繁的创建、销毁线程,而是对线程的复用

  1. work stealing机制

当本线程无可运行的 G 时,尝试从全局队列或者其他线程绑定的 P 获取待执行的任务 G,而不是销毁线程

  1. hand off机制

当本线程 M 因为 G 进行系统调用阻塞时,线程 M 释放绑定的 P ,把 P 转移给其他空闲的线程执行

  1. 抢占
  • goroutine 中要等待一个协程主动让出 CPU 才执行下一个协程
  • 一个 goroutine 最多占用 CPU 10ms,防止其他 goroutine 被阻塞饿死

(3) go func() 调度流程

  1. go 关键字来创建一个程序调用的 goroutine
  2. 有两个存储 G 的队列,一个是局部调度器 P 的本地队列、一个是全局 G 队列。
  • 新创建的 G 会先保存在 P 的本地队列中
  • 如果 P 的本地队列已经满了就会保存在全局的队列中
  1. G 只能运行在 M 中,一个 M 必须持有一个 PM与P是1:1 的关系。
  • M 会从 P 的本地队列弹出一个可执行状态的 G 来执行
  • 如果 P 的本地队列为空,就会从其他的 MP 组合获取一个可执行的 G 来执行或者从全局 G 队列获取任务
  1. 一个 M 调度 G 执行的过程是一个循环机制;
  • M 执行某一个 G 时候如果发生了 syscall 或则其余阻塞操作引起的 M 阻塞
    • 如果当前 P 中有一些 G 在执行,runtime 会把这个线程 MP 中摘除(detach),然后再创建一个新的操作系统的线程(如果有空闲的线程可用就复用空闲线程)来服务于这个 P
  • M 系统调用结束时候
    • G 会尝试获取空闲 P 任务,并放入到这个 P 的本地队列
    • 如果 G 获取不到空闲 P,那么这个线程 M 变成休眠状态,G 放回到全局队列

(4)调度器的生命周期

特殊的 M0和G0

  1. M0是启动程序后的编号为0的主线程,这个 M 对应的实例会在全局变量 runtime.m0,不需要在 heap 上分配,M0 负责执行初始化操作和启动第一个 G, 在之后 M0 就和其他的 M 一样
  2. G0是每次启动一个 M 都会第一个创建的 goroutineG0 仅用于负责调度的 GG0 不指向任何可执行的函数, 每个 M 都会有一个自己的 G0。在调度或系统调用时会使用 G0 的栈空间, 全局变量的 G0M0G0
package main

import "fmt"

func main() {
    fmt.Println("Hello world")
}

分析:

  1. runtime 创建最初的线程 m0goroutine g0,并把2者关联。
  2. 调度器初始化:初始化 m0、栈、垃圾回收,以及创建和初始化由 GOMAXPROCSP 构成的 P 列表。
  3. 示例代码中的 main 函数是main.main
  • runtime中也有1个 main 函数——runtime.main
  • 代码经过编译后,runtime.main会调用main.main
  • 程序启动时会为runtime.main创建 goroutine
  • 然后把 main goroutine 加入到 P 的本地队列。
  1. 启动 m0m0 已经绑定了 P,会从 P 的本地队列获取 G,获取到 main goroutine
  2. G 拥有栈,M 根据 G 中的栈信息和调度信息设置运行环境
  3. M 运行 G
  4. G 退出,再次回到 M 获取可运行的 G
  • 重复直到main.main退出
    • runtime.main执行 DeferPanic 处理,或调用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 g18G1 运行在 P1 上,G18 运行在 P0 上。

这里有两个 P,我们知道,一个 P 必须绑定一个 M 才能调度 G

G18P0 上被运行的时候,确实在 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的数量;通过 gomaxprocsidleprocs 的差值,我们就可知道执行 go 代码的 P 的数量;
  • threads: os threads/M的数量,包含 scheduler 使用 m 数量,加上 runtime 自用的类似 sysmon 这样的 thread 的数量;
  • spinningthreads: 处于自旋状态的 os thread 数量;
  • idlethread: 处 idle 状态的 os thread 的数量;
  • runqueue=0Scheduler 全局队列中 G 的数量;
  • [0 0]: 分别为2个 Plocal queue 中的 G 的数量。

三、Go调度器调度场景过程全解析

(1)场景1

P 拥有代运行的协程任务 G1M1 线程获取 P 后开始运行 G1

G1 使用go func()创建了 G2G2 优先加入到 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,如果 GG2 之后就执行的,会被保存在本地队列,

利用某个老的 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 会尝试唤醒其他空闲的 PM 组合去执行

假定 G2 唤醒了 M2M2 绑定了 P2,并运行 G0

P2 本地队列没有 GM2 此时为自旋线程(没有 G 但为运行状态的线程,不断寻找 G

M2 首先尝试从全局队列(简称“GQ”)取一批 G 放到 P2 的本地队列(函数:findrunnable()

M2 从全局队列取的 G 数量符合下面的公式:

n = min(len(GQ)/GOMAXPROCS + 1, len(GQ)/2)

至少从全局队列取 1个G,但每次不要从全局队列移动太多的 GP 本地队列,给其他 P 留点

这是从全局队列到 P 本地队列的负载均衡

(4)场景4

假设 G2 一直在 M1 上运行

经过2轮后,M2 已经把 G7、G4 从全局队列获取到了 P2 的本地队列并完成运行

全局队列和 P2 的本地队列都空了,如场景8图的左半部分。

**全局队列已经没有 G,那m就要执行 work stealing(借取)

从其他有 GP 哪里借取一半 G 过来,放到自己的 P 本地队列

P2P1 的本地队列尾部取一半的 G,本例中一半则只有 1个G8,放到 P2 的本地队列并执行

G1 本地队列 G5、G6 已经被其他 M 运行完成

当前 M1M2 分别在运行 G2G8

M3M4 没有 goroutine 可以运行,M3M4 处于自旋状态,它们不断寻找 goroutine

为什么不销毁线程,来节约CPU资源?

因为创建和销毁CPU也会浪费时间

希望当有新 goroutine 创建时,立刻能有 M 运行它

如果销毁再新建就增加了时延,降低了效率。当然也考虑了过多的自旋线程是浪费CPU,所以系统中最多有GOMAXPROCS个自旋的线程

(5)场景5

假定当前除了 M3M4 为自旋线程

还有 M5M6 为空闲的线程(P 的数量应该永远是 M>=P, 大部分都是 M 在抢占需要运行的 P)

G8 创建了 G9G8 进行了阻塞的系统调用

  • M2P2 立即解绑
    • 如果 P2 本地队列有 G、全局队列有 G 或有空闲的 MP2 会立马唤醒1个 M 和它绑定
    • 否则 P2 则会加入到空闲 P 列表,等待 M 来获取可用的 P

本场景中,P2 本地队列有 G9,可以和其他空闲的线程 M5 绑定。

G8 创建了 G9G8 进行了阻塞的系统调用

本场景中,P2 本地队列有 G9,可以和其他空闲的线程 M5 绑定。

M2P2 会解绑,但 M2 会记住 P2,然后 G8M2 进入系统调用状态

G8M2 退出系统调用时

  • 尝试获取 P2
    • 如果无法获取,则获取空闲的 P
    • 如果没有空闲 PG8 会被记为可运行状态,并加入到全局队列,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 的时间

七、总结

以上便是 GolangGC 全部的标记-清除逻辑及场景演示全过程。

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:

MySQLGormCase

StoreChainTxsInMySQL

GormDoc

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:

MongoDBCase

MongoDoc

初始化区块链连接

建立连接

Go 初始化以太坊客户端是和区块链交互所需的基本步骤。

首先,导入 go-etheremethclient 包并通过调用接收区块链服务提供者 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 ContractEOA 创建的合约地址,账户下存储特定的合约逻辑
    • 合约账户不允许直接构造交易
    • EIP4337介绍如何实现合约账户的交易代付
  • 合约地址和EOA地址的区别在于合约地址在账户中存储合约代码,通过判断地址数据长度可以判断当前地址是否是合约地址
  • eth_getCode 支持读取传参地址在特定区块高度、特定区块 hash、最新区块中、Pending 池中的账户 contract codes

读取最新区块高度的账户contract codes

将区块号设置为 nil 将使用最新的区块高度

  1. 先使用简单的正则表达式来检查以太坊地址是否有效
  2. 获取地址存储的代码
    1. 如果长度为空,表示目前该地址时 EOA 地址
    2. 如果长度不为空,表明该地址是合约地址
  3. 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 方法

  1. 生成全新的私钥,并将私钥存储在本地文件
  2. 基于私钥导出公钥信息
  3. 基于公钥导出账户地址
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
}

生成助记词,从助记词导出新地址

地址胜场方式:

  1. 生成助记词
  2. 拼接助记词和 secret, 构造哈希种子 seed
  3. 基于 seed 逐步生成 ecdsa 私钥
  4. 基于私钥导出钱包地址

导出账户密钥

  1. 基于助记词导出密钥时:
  • 必须提供相匹配的 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导出地址

  1. 创建新的钱包地址,账户信息通过 secret 加密存储并导出 keystore 文件
  2. 基于 secret 解密 keystore 文件

私钥签名

  1. keystore 直接基于 secret 签名数据
  2. keystore 可以先基于 secret TimedUnlock 解锁一段时间后可以直接用于签名
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 查询区块体中交易总数量

传参区块哈希值,查询该区块体中的交易数量

  1. 传参无效的 hash
    1. 无法找到有效的区块,因此在解析数据时会报错
    2. 报错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获取交易收据

  1. 先获取交易数据,判断当前交易是否存在以及当前交易是否仍然处在 pending 状态
  2. 只有当前交易 hash 有效以及被打包出块的情况下,才能有效的获取该交易 hash 的收据状态
    1. 收据树中记录该交易执行完毕触发的全部 logs 信息
    2. 收据树记录当前交易的执行状态,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))
}

根据交易索引获取交易信息

区块体中的交易是有序的,保证全部验证者执行顺序的一致,从而有效校验区块

  1. 根据区块信息和交易顺序,能够有效获取当前交易
  2. 提供无效的区块hash时,报错ethereum.NotFound
  3. 提供无效的区块Index值时:
    1. 提供的值 < 0, 报错 constant -1 overflows uint
    2. 提供的值 > 最大值,报错 ethereum.NotFound
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 节点从网络订阅新区块信息,基于 websocketchannel 实现。

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}} 当前交易触发事件中,只获取第一位是 Alog
      • {{},{B}} 当前交易触发事件中,只获取第二位是 Blog
      • {{A},{B}} 当前交易触发事件中,只获取第一位是 A 并且 第二位是 Blog
      • {{A,B},{C,D}} 当前交易触发事件中,只获取第一位是 A或者B 并且 第二位是 C或者Dlog
// 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/sstoreslot 键值
// 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"`
}

构建转账交易

  1. 从私钥导出交易发送方
  2. 获取发送方的 Nonce
  3. 获取 gasPrice (两种转账类型获取不同的 gasPrice )
  4. 基于 Nonce,To,Value,gasPrice,data 构建交易
  5. 私钥签名数据
  6. 通过 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工具可以基于 abibin 文件创建合约部署和调用的 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

链上写入交易会更新链上数据状态,因此需要发送方签名和付费,保证交易的完整性

  1. 将合约 abi 调用程序和 RPC 节点绑定
  2. 构建交易
  • 从私钥导出账户
  • 获取账户 nonce
  • 配置交易 gasLimit、gasPrice
  1. 私钥签名并发送交易
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

  1. 编码 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)
  1. 私钥签名数据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
  1. 校验数据,根据待签数据和签名逆向反推签名地址
  • 签名不匹配的话,会返回错误的签名地址
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 对象包含两个键值: storagetypes

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
}
  1. 首先获取 baseSlot 的全部数值
  2. 根据偏移量和数据 length 类型判断是否独占一个 slot
  • 低位存在别的参数值的话,偏移量不为 0,直接右移去掉偏移值
  • 高位也可能存储别的参数值,因此要根据自身类型获取特定长度的值
    • 直接按照类型长度,将高位全置 0length 长度的数据位置 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
}
  1. 首先获取 baseSlot 的全部数值
  2. 根据偏移量和数据 length 类型判断是否独占一个 slot
  • 低位存在别的参数值的话,偏移量不为 0,直接右移去掉偏移值
  • 高位也可能存储别的参数值,因此要根据自身类型获取特定长度的值
    • 直接按照类型长度,将高位全置 0length 长度的数据位置 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
}
  1. 首先获取 baseSlot 的全部数值
  2. 根据偏移量和数据 length == 8 类型判断是否独占一个 slot
  • 低位存在别的参数值的话,偏移量不为 0,直接右移去掉偏移值
  • 高位也可能存储别的参数值,因此要根据自身类型获取特定长度的值
    • 直接按照类型长度,将高位全置 0length 长度的数据位置 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
}
  1. 首先获取 baseSlot 的全部数值
  2. 根据偏移量和数据 length == 160 类型判断是否独占一个 slot
  • 低位存在别的参数值的话,偏移量不为 0,直接右移去掉偏移值
  • 高位也可能存储别的参数值,因此要根据自身类型获取特定长度的值
    • 直接按照类型长度,将高位全置 0length 长度的数据位置 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
}
  1. 首先获取 baseSlot 的全部数值
  2. 根据偏移量和数据 length == 160 类型判断是否独占一个 slot
  • 低位存在别的参数值的话,偏移量不为 0,直接右移去掉偏移值
  • 高位也可能存储别的参数值,因此要根据自身类型获取特定长度的值
    • 直接按照类型长度,将高位全置 0length 长度的数据位置 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"` // 子集数据类型
}
  1. 首先获取当前数据类型
  • 一维数据就是当前数值的类型
  • 多维数据就是数组类型
  1. 根据数据类型获取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 内部参数按照声明顺序依次存储入栈

structstorage 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
}
  1. 首先获取当前 Field 的数据类型
  2. 获取结构体内部变量的每个起始 slot 存储位置,基于 baseSlot 和 声明顺序
  3. 按照每个变量类型和 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"`
}
  1. 首先获取当前参数的 baseSlot
  2. 获取当前数据类型,根据数据类型获取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

  1. 首先获取当前参数的 baseSlot
  2. 查询 baseSlot 中存储的值
  • 根据末尾数值判断 string 数据存储在当前 slot 或者仅存储了当前 string 数据的长度
    • 末尾数值为 0,表示短 string
      • 数据存储在当前 slot, 数据编码格式为 string + length
    • 末尾数值为 1,表示长 string
      • 数据长度存储在当前 slot, 数据编码格式为 len(string) + length
      • 具体数据从 keccak256(slot) 开始存储
        • 数据长度取余 2560 的话,数据存储在整数个 slot,直接相除获取具体 slot 个数
        • 取余为 1 的话,表示有额外 slot
  • length 占位 8 bit
  1. 获取当前数据类型,根据数据类型获取 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"`
}
  1. 根据键值传参判断当前 mapping 的层数
  2. 计算最外层数据的起始 slot 存储位置
  3. 每进入一层,就进入新的 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

查询链上状态

  1. 查看所有账户列表
eth.accounts
  1. 根据节点私钥导入账号,提供节点私钥、加密节点私钥的对称密钥
personal.importRawKey("","")
  1. 查看账户余额
eth.getBalance(eth.accounts[0])
balanse=web3.fromWei(eth.getBalance(eth.accounts[0]),'ether')
  1. 查询区块高度
eth.blockNumber
  1. 根据交易hash查询交易数据
eth.getTransaction("TxHash")
  1. 查看节点信息
admin.nodeInfo.enode

发送链上交易

  1. 发送链上交易时,需要先解锁账户。
eth.sendTransaction({from:eth.accounts[0],to:eth.accounts[1],value:web3.toWei(4,'ether')})
  1. 解锁账户–需要指定时间,默认解锁300s personal.unlockAccount(address, passphrase, duration),密码和解锁时长都是可选的。如果密码为null,控制台将提示交互输密码。解密的密钥将保存在内存中直到解锁周期超时,默认的解锁周期为300秒。将解锁周期设置为0秒将解锁该密钥直到退出geth程序。
personal.unlockAccount(eth.accounts[0],'passward',0)

启动出块

  1. 设置矿工地址(验证者地址)
miner.setEtherbase(eth.accounts[0])
  1. 查看矿工账户
eth.coinbase
  1. 设置区块GasLimit
miner.setGasLimit(80000000)
  1. 启动出块(start() 的参数表示出块使用的线程数)/关闭出块 必须先解锁矿工账户,否则不会启动出块
miner.start()
miner.stop()

关闭节点

ps aux | grep geth | grep -v grep | awk '{print $2}'| xargs kill -15

清除链数据

geth removedb --datadir "/opt/etherData/"

启动节点2

新机器操作

数据备份节点

  1. 拷贝 genesis.json,创建相同的0号区块
  2. 节点正常启动
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
  1. 建立连接 查询节点信息
admin.nodeInfo.enode

通过addPeer命令添加节点.

admin.addPeer("节点信息")

出块节点(新的验证者地址)

新机器执行以下操作

  1. 生成节点
geth account new --datadir /opt/etherData

Address: 0x68d866baAfa993bc002cd35218c13f10aC54221d

  1. 连接控制台
  2. 在已连接的节点上执行以下操作:(需要半数以上的验证者同意)

目前的验证节点通过发起提案增加出块节点,增加后的节点和当前的验证者轮流出块。

clique.propose("新机器上生成的新验证者地址",true)

回到新服务器的终端:

  1. 在新服务器上设置矿工地址(新的验证者地址)
miner.setEtherbase(eth.accounts[0])
  1. 查看矿工账户
eth.coinbase
  1. 解锁账户
personal.unlockAccount(eth.accounts[0],'passward',0)
  1. 启动挖矿(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