1. 交易的签名
  2. 理解收据receipt
  3. 理解区块
  4. 理解交易
  5. blockchain核心
  6. forkId 解读
  7. oracle 原理和实现
  8. 布隆过滤器原理
  9. TxList 解读
  10. 交易池分析
  11. MPT树
  12. 区块同步
  13. geth源码学习——介绍
  14. How Geth starts its server

交易的入门介绍

这里不作额外说明,可以学习官方文档、以及仓库中提供的《区块链基础》

交易的结构

类型

传统交易(Legacy)是以太坊最初的交易类型,但是后来逐渐出现了 AccessList 类型的交易和 DynamicFee 类型的交易,他们分别由 EIP-2930 和 EIP-1559 定义

1
2
3
4
5
const (
LegacyTxType = iota //传统交易
AccessListTxType //EIP-2930 定义的访问列表
DynamicFeeTxType //EIP-1559 定义的动态交易费
)

关于这两个 EIP 的详细说明请看后文的『影响交易的 EIP』

下面是交易的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type Transaction struct {
inner TxData // Consensus contents of a transaction
//time 包的特点是时间分成显示和计算两部分
time time.Time // Time first seen locally (spam avoidance)

/*atomic 是 Golang 中底层硬件的原子操作的封装,可以提高并发的效率
atomic.Value 是容器,用来“原子地”存储(Store)和加载(Load)任意类型的值。

一个或者多个操作在 CPU 执行的过程中不被中断的特性,称为原子性,
对外表现成一个不可分割的整体,他们要么都执行,要么都不执行,外界不会看到他们只执行到一半的状态。
更多见:https://studygolang.com/articles/23242
*/

// 使用频次高且CPU计算量大,因此下面缓存下面三个

// caches
hash atomic.Value
size atomic.Value
from atomic.Value
}

TxData 是交易自身的核心数据,包括了所有需要的信息,这些字段都很基础,请读者自行阅读其他书籍或者博客了解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type TxData interface {
txType() byte // returns the type ID
copy() TxData // creates a deep copy and initializes all fields

chainID() *big.Int
accessList() AccessList
data() []byte
gas() uint64
gasPrice() *big.Int
gasTipCap() *big.Int // EIP-1559 单位 gas 最大超过基础费用的溢价
gasFeeCap() *big.Int //EIP-1559 单位 gas 的最大价格
value() *big.Int
nonce() uint64
to() *common.Address

rawSignatureValues() (v, r, s *big.Int)
setSignatureValues(chainID, v, r, s *big.Int)
}
  • gasTipCap() 最高的小费单价。
  • gasFeeCap() 最高的 gas 费。
  • AccessList 里面是由包含地址和存储的 Key 的元组构成的切片,Key 是哈希类型。
  • data 是调用合约时的 ABI 编码后的函数签名和参数。
  • v, s, R 是签名,它们和 TxData 相匹配,可以根据签名和 TxData 恢复出地址,作为验证信息。

编码

交易也需要编码后才能写入数据库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// EncodeRLP implements rlp.Encoder
func (tx *Transaction) EncodeRLP(w io.Writer) error {
//对于传统的无类型的交易,直接编码 data 字段就可以了
if tx.Type() == LegacyTxType {
return rlp.Encode(w, tx.inner) //第二个参数编码后写入 rlp 缓冲区
}

// EIP-2718 将交易类型封装到了交易对象中,可以方便地处理对应操作,
//减少因匹配交易所有字段造成的不必要消耗

// It's an EIP-2718 typed TX envelope.
buf := encodeBufferPool.Get().(*bytes.Buffer)
defer encodeBufferPool.Put(buf)
buf.Reset()

//序列化后存储在 buf 中
if err := tx.encodeTyped(buf); err != nil {
return err
}
//为了保证向后兼容性再进行了一次封装,
return rlp.Encode(w, buf.Bytes())
}

// encodeTyped writes the canonical encoding of a typed transaction to w.
func (tx *Transaction) encodeTyped(w *bytes.Buffer) error {
w.WriteByte(tx.Type()) //先写入类型,第一个字节
return rlp.Encode(w, tx.inner)
}

当然也有配套的解码函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// DecodeRLP implements rlp.Decoder
func (tx *Transaction) DecodeRLP(s *rlp.Stream) error {
kind, size, err := s.Kind()
switch {
case err != nil:
return err
case kind == rlp.List:
// It's a legacy transaction.
var inner LegacyTx
err := s.Decode(&inner)
if err == nil {
tx.setDecoded(&inner, int(rlp.ListSize(size)))
}
return err
case kind == rlp.String:
// It's an EIP-2718 typed TX envelope.
var b []byte
if b, err = s.Bytes(); err != nil {
return err
}
inner, err := tx.decodeTyped(b)
if err == nil {
tx.setDecoded(inner, len(b))
}
return err
default:
return rlp.ErrExpectedList
}
}

为了方便封装和解封装,有对应的 MarshalUnmarshal ,这在源码中还是比较常见的,这里暂时不做进一步的分析。具体需要研究编码时可深入学习

签名

这是检查签名合法性的主要函数,isProtectedV 是 EIP-155 的内容,可见后文。如果通过了对 v 的检查,那么可以从 v 中获取 chainid,最后检查签名的三个参数是否合法,这涉及密码学内容,暂时不作深入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//检查签名的合法性
func sanityCheckSignature(v *big.Int, r *big.Int, s *big.Int, maybeProtected bool) error {
if isProtectedV(v) && !maybeProtected {
return ErrUnexpectedProtection
}

var plainV byte
if isProtectedV(v) {
chainID := deriveChainId(v).Uint64()
plainV = byte(v.Uint64() - 35 - 2*chainID)
} else if maybeProtected {
// Only EIP-155 signatures can be optionally protected. Since
// we determined this v value is not protected, it must be a
// raw 27 or 28.
plainV = byte(v.Uint64() - 27)
} else {
// If the signature is not optionally protected, we assume it
// must already be equal to the recovery id.
plainV = byte(v.Uint64())
}
if !crypto.ValidateSignatureValues(plainV, r, s, false) {
return ErrInvalidSig
}

return nil
}

//是否开启了防止重放攻击
func isProtectedV(V *big.Int) bool {
if V.BitLen() <= 8 {
v := V.Uint64()
return v != 27 && v != 28 && v != 1 && v != 0
}
// anything not 27 or 28 is considered protected
return true
}

关于手续费

首先判断是否采用了 EIP-1559,如果 baseFee 不存在的话,就返回最高的小费。然后手续费的单价需要高于基础费用,采取最大小费单价或者是最大手续费单价减去基础费用,这是矿工可盈利的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// EffectiveGasTip returns the effective miner gasTipCap for the given base fee.
// Note: if the effective gasTipCap is negative, this method returns both error
// the actual negative value, _and_ ErrGasFeeCapTooLow
func (tx *Transaction) EffectiveGasTip(baseFee *big.Int) (*big.Int, error) {
if baseFee == nil {
return tx.GasTipCap(), nil
}
var err error
gasFeeCap := tx.GasFeeCap()

if gasFeeCap.Cmp(baseFee) == -1 {
err = ErrGasFeeCapTooLow
}
// 最大值 - 基础费和消费最大值中的小者
return math.BigMin(tx.GasTipCap(), gasFeeCap.Sub(gasFeeCap, baseFee)), err
}

交易哈希

交易哈希是交易参数哈希之后的值,对于类型化的交易需要额外考虑前八位的交易类型。具体密码学算法,不作探讨。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Hash returns the transaction hash.
func (tx *Transaction) Hash() common.Hash {
if hash := tx.hash.Load(); hash != nil {
return hash.(common.Hash)
}

var h common.Hash
if tx.Type() == LegacyTxType {
//根据 Txdata 计算交易哈希
h = rlpHash(tx.inner)
} else {
h = prefixedRlpHash(tx.Type(), tx.inner)
}
tx.hash.Store(h)
return h
}

矿工对交易的封装和排序

首先封装交易和对应矿工费,矿工费为上文分析的手续费。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// TxWithMinerFee wraps a transaction with its gas price or effective miner gasTipCap
type TxWithMinerFee struct {
tx *Transaction
minerFee *big.Int
}

// NewTxWithMinerFee creates a wrapped transaction, calculating the effective
// miner gasTipCap if a base fee is provided.
// Returns error in case of a negative effective miner gasTipCap.
func NewTxWithMinerFee(tx *Transaction, baseFee *big.Int) (*TxWithMinerFee, error) {
minerFee, err := tx.EffectiveGasTip(baseFee)
if err != nil {
return nil, err
}
return &TxWithMinerFee{
tx: tx,
minerFee: minerFee,
}, nil
}

接着需要根据 nonce 和手续费对交易排序

1
2
3
4
5
6
type TransactionsByPriceAndNonce struct {
txs map[common.Address]Transactions // Per account nonce-sorted list of transactions
heads TxByPriceAndTime // Next transaction for each unique account (price heap)
signer Signer // Signer for the set of transactions
baseFee *big.Int // Current base fee
}

对于同一个区块,基础费用是相同的,heads 是每个账户最小的未打包的交易的切片,它会从小到大排序。排序算法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func NewTransactionsByPriceAndNonce(signer Signer, txs map[common.Address]Transactions, baseFee *big.Int) *TransactionsByPriceAndNonce {
// Initialize a price and received time based heap with the head transactions
heads := make(TxByPriceAndTime, 0, len(txs))
for from, accTxs := range txs {
acc, _ := Sender(signer, accTxs[0]) //通过签名和对应交易恢复地址
wrapped, err := NewTxWithMinerFee(accTxs[0], baseFee) //带矿工费的交易
// Remove transaction if sender doesn't match from, or if wrapping fails.
if acc != from || err != nil {
delete(txs, from) //删除 map 中对应 Key
continue
}
heads = append(heads, wrapped)
txs[from] = accTxs[1:] //更新账户的交易列表
}
heap.Init(&heads) //堆排序,小堆

// Assemble and return the transaction set
return &TransactionsByPriceAndNonce{
txs: txs,
heads: heads,
signer: signer,
baseFee: baseFee,
}
}

因为是每个账户都提取一个交易,因此新建 heads。接着验证每个账户的签名,必须签名通过后才能加入 heads,同时更新原来每个账户的交易列表。

需要更新排序的交易时,会把 heads[0] 删除。

1
2
3
4
5
6
7
8
9
10
11
12
13
// Shift replaces the current best head with the next one from the same account.
func (t *TransactionsByPriceAndNonce) Shift() {
acc, _ := Sender(t.signer, t.heads[0].tx) //根据签名和交易获取的地址
if txs, ok := t.txs[acc]; ok && len(txs) > 0 {
//封装交易和矿工费,取出矿工费最小的交易
if wrapped, err := NewTxWithMinerFee(txs[0], t.baseFee); err == nil {
t.heads[0], t.txs[acc] = wrapped, txs[1:]
heap.Fix(&t.heads, 0) //Fix 在索引 i 处的元素更改其值后重新建立堆排序。
return
}
}
heap.Pop(&t.heads)//删除第一个元素
}

Message 对象

message 会封装合约,方便后面执行代码,日后将会移除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Message is a fully derived transaction and implements core.Message
//
// NOTE: In a future PR this will be removed.
type Message struct {
to *common.Address
from common.Address
nonce uint64
amount *big.Int
gasLimit uint64
gasPrice *big.Int
gasFeeCap *big.Int
gasTipCap *big.Int
data []byte
accessList AccessList
isFake bool
}

影响交易的 EIP

EIP-155

重放攻击保护——防止了在一个以太坊链上的交易被重复广播到另外一条链。从区块高度 2,675,000 开始,为了签名计算交易哈希时应该加入 chainid,并且签名的 v 需要为 {0,1} + CHAIN_ID * 2 + 35,如果是不加入 chainid,那么 v{0,1} + 27 也可以。

EIP-1559

改变交易的手续费机制,并且通过动态的区块大小避免短时间的拥塞。

首选对于单位 gas 都有基础费用,它是由父区块使用的 gas(gasused)和 gas 上限决定。这个机制导致当父区块的 gas 上限过大时,单位 gas 的基础费用就增加,反之则减少。基础费用会销毁而不是分给矿工。交易可以指定最高的 gas 单价,它包括了基础的区块费用和为交易优先打包的小费。

EIP-2718

定义新的交易类型,它可以作为未来的交易类型的封装。未来可能会通过底层的交易类型来指定转移支付 gas 的交易、多签名的交易,而不用通过合约实现复杂且高消耗的逻辑。封装一层,将来出现不同的 Txdata 时能保证向后兼容。

具体地,需要在原来的 Txdata 编码之前加上 8 位的类型标识,并且对于类型化的交易将会采用新的解释方式。例如 TransactionType || TransactionPayload, TransactionPayload 是难懂的字节数组,和交易的类型有关,将来可能采用不同的编码方式。

EIP-2929

提高部分操作 storage 的操作码在交易中第一次执行时消耗的 gas。代码为 0x54SLOAD 的 gas 成本增加到 2100,将*CALL操作码族 ( 0xf1, f2, f4, fA)、BALANCE 0x31EXT*操作码族 ( 0x3b, 0x3c, 0x3f) 增加到 2600。

EIP-2930

添加新的带有 access list 的交易类型,访问列表里是地址列表和交易将访问的关键字。