区块链版本
- 区块链1.0,以BTC公链为代表:不具备只能合约功能,是一条支持电子货币转账的完整区块链。
- 区块链2.0,以ETH公链为代表:具备智能合约功能,共识机制是PoW向PoS过去,但是目前共识机制还是使用PoW,但是此PoW算法经过改进,性能优于BTC的PoW。
- 区块链3.0,以EOS公链为代表:性能高,大吞吐量,支持智能合约功能,共识算法是DPoS,目前向BFT-DPoW发展。
ETH框架
ETH的技术栈分为6个层级:分别为应用层,网络层,合约层,共识层,激励层和数据层。
- 应用层:主要是以ETH公链衍生出来的应用,如:DApp,Geth控制台,Web3.js,钱包等。
- 网络层:主要是以ETH的P2P通讯和RPC(远程过程调用)接口服务。
- 合约层:主要是基于EVM的智能合约模块开发的智能合约。
- 共识层:主要是节点使用的共识机制。
- 激励层:主要提现在对minter的奖励。
- 数据层:用于整体的数据管理,包含但不限于区块数据,交易数据,事件数据和levelDB存储技术模块等。
- DApp应用通过web3.js,web3.py或者其他SDK的以太坊接口访问代码,来访问以太坊的RPC接口或者对应的数据。接口细分可分智能合约相关接口和区块相关接口。
- Whisper是P2P通讯模块中的协议,通讯消息都是经过它转发,所有转发的消息都经过加密传输。
- Swarm是ETH实现类似于IPFS的分布式文件存储系统,在P2P模块中结合Whisper协议使用。
- HttpClient是Http服务请求方法的实现模块。
- Crypto是ETH的加密模块,内部包含SHA3、SECP256k1等加密算法。
- RLP是ETH所使用的一种数据编码方式,包含数据序列化和反序列化,除了常见的编码方式外,还包含了base16,base32,base64等。
- Solidity是ETH的高级计算机编程语言,由EVM载入字节码运行。
- LevelDB是ETH所使用的键值对数据库,区块和交易数据都采用该数据库存储。在ETH中,作为key的一般是数据的hash,而value则是数据的RLP编码。
- Logger是日志模块,主要分为两类:一类是智能合约的Event日志,这类日志被存储到区块链中,可以通过调用相关的RPC接口获取;另一类是代码级别的运行日志,这类日志会被保存到本地的日志文件。
DApp(Decentralized Application)介绍
顾名思义:去中心化应用。
一般应用都是C/S(Client/Server,即客户端/服务端)模式,多个C,一个S,但是传统的分布式应用是多个C,多个S,但是多个S公用一个数据库(可能有多个数据库,但是数据库的关系是主从关系,整体可以理解为一个数据库,这里提现还是中心化的),去中心化应用和传统的分布式应用相同的地方还是多个S多个C,但是不同的是S对应的数据库,相互独立,互不干扰(即可以理解为每个节点都有它自己的数据库)。
ETH的DApp
ETH具备图灵完备,一般在ETH上部署智能合约,然后通过web3.js,web3.py或者其他SDK的以太坊接口访问链上的智能合约,最后得到输出。
DApp一般含有多个角色,每个角色各司其职。
- 智能合约应用,负责链上的数据处理。
- 中继服务负责接收用户的请求和访问链上的智能合约应用,再将链上输出的数据结果返回给用户。(举个例子,开发一个翻译APP的中继服务,用户输入单词,中继服务接收用户输入的单词,然后把用户输入的单词,通过API发送给翻译平台,翻译平台翻译该单词,并给回结果到中继服务器,中继服务器再把该结果返回给用户)
区块的组成
以下是ETH源码对区块结构体(Block)的定义
// Header表示ETH区块链中的区块报头
type Header struct {
// 父区块的哈希值
ParentHash common.Hash `json:"parentHash" gencodec:"required"`
// 叔块hash
UncleHash common.Hash `json:"sha3Uncles" gencodec:"required"`
// 挖出这个块的minter地址,因为mint出块所奖励的ETH就会发放到这个地址。
Coinbase common.Address `json:"miner"`
// 全局状态MPT树的根哈希,这个全局状态树包含了以太坊网络中每一个账户的一组键值对,stateDB的RLP编码后的哈希值
Root common.Hash `json:"stateRoot" gencodec:"required"`
// 交易MPT树的根哈希,由本区块所有交易的交易哈希算出
TxHash common.Hash `json:"transactionsRoot" gencodec:"required"`
// 收据MPT树的哈希
ReceiptHash common.Hash `json:"receiptsRoot" gencodec:"required"`
// 布隆过滤器,快速定位日志是否在这个区块中。
Bloom Bloom `json:"logsBloom" gencodec:"required"`
// 当前工作量证明(Pow)算法的复杂度。
Difficulty *big.Int `json:"difficulty" gencodec:"required"`
// 区块号
Number *big.Int `json:"number" gencodec:"required"`
// 每个区块Gas的消耗上限。
GasLimit uint64 `json:"gasLimit" gencodec:"required"`
// 当前区块所有交易使用的Gas之和。
GasUsed uint64 `json:"gasUsed" gencodec:"required"`
// 区块产生出来的Unix时间戳
Time uint64 `json:"timestamp" gencodec:"required"`
// 该变量用于为当前区块的建造者保留的附属信息
Extra []byte `json:"extraData" gencodec:"required"`
// 区块产生出来的Unix时间戳
MixDigest common.Hash `json:"mixHash"`
// mint找到的满足条件的值。
Nonce BlockNonce `json:"nonce"`
// BaseFee是由EIP-1559添加的,在遗留头文件中被忽略
BaseFee *big.Int `json:"baseFeePerGas" rlp:"optional"`
/*
TODO (MariusVanDerWijden)需要时添加此字段
// Random是在合并过程中添加的,包含BeaconState随机性
Random common.Hash `json:"random" rlp:"optional"`
*/
}
// Block表示以太坊区块链中的一个完整的区块。
type Block struct {
// block的header指向的是一个的结构体,其中带有该block的所有属性信息
header *Header
// 叔块block的数组
uncles []*Header
// 当前该区块所有打包的交易记录
transactions Transactions
// 缓存,应该是该区块的的hash及区块大小
hash atomic.Value
size atomic.Value
// total difficulty,当前区块总共的难度值=前一个区块的td+当前区块header中的difficulty。
td *big.Int
// 区块被接受的时间
ReceivedAt time.Time
// 记录区块是从哪个P2P网络传过来的
ReceivedFrom interface{}
}
ETH Address
在ETH中,每个账户,其中包含用户账户和智能合约账户,都是一个字符长度为42的十六进制地址值,该值作为唯一标识自己的地址值,通过ETH的RPC接口可以查询到该地址的相关信息。
地址值规则如下3点:
- 0x开头。
- 除0x这两个字符外,剩下的部分(即40个字符)必须由数字(0-9)和字母(a-f)组成,其中,不分大小写。
- 整体是一个十六进制字符串。
地址的作用
- 唯一标识一个账户或合约
- 作为标识,用于查询地址账户信息
- 进行ETH交易时,充当交易双方的唯一标识
地址的生成
地址又分为非合约地址和合约地址(又称外部地址和内部地址),生成地址的方式不同,具体如下。
非合约地址(外部地址)的生成流程
- 随机产生一个私钥(大整数),32个字节。
- 计算得到的私钥在ECDSA-secp256k1椭圆曲线上对应的公钥。
- 对公钥进行SHA3计算,得到一个哈希值,取这个哈希值的后20个字节来作为非合约地址(即外部账户地址)。
合约地址(内部地址)的生成流程
- 使用RLP算法将(合约创建者地址+当前创建合约交易的序列号Nonce)进行序列化。
- 使用Keccak256将步骤1的序列化数据进行哈希运算,得出一个哈希值。
- 取第(2)步的哈希值的前12字节之后的所有字节生成地址,即后20个字节。
TIP
- 合约地址和椭圆曲线加密无关,因为合约地址是基于用户地址和交易序列号的,所以不会出现雷同情况。
- 私钥是通过伪随机算法(PRNG)产生的,是一个256位(32个字节)的二进制数。 2 256 2 ^ {256} 2256非常大,能够保证在一个十分的安全范围内。
Header中Nonce的作用
区块Header中是的Nonce主要用于PoW共识情况下的mint,是一个随机数,minter在不断尝试Nonce,直到找到合适的Nonce即可出块。
燃料费(Gas)
计算公式
Gas = GasUsed * GasPrice
其中:
GasPrice:单价
GasUsed:计算数量
GasLimit:基础量
GasLimit有两个基础量:
1.创建智能合约的基础量:53000
2.非创建智能合约的基础量:21000
在交易函数中设置的GasLimit比基础量小时,就会导致交易失败。这也是之前在部署智能合约的时候出现Gas估计错误的原因。
计算的方式按照ETH设置的规则计算
1.0字节的收费为4,没发现一个0字节,基础量累加4
2.非0字节的收费68,每发现一个非0字节,基础量累加68
ETH的EVM计算Gas的源码
更加具体,to read yellowpaper,please!
// TransitionDb将通过应用当前消息并返回带有以下字段的evm执行结果来转换状态。
// - used gas:
// 总使用的gas(包括已退还的用气量)
// - returndata:
// 从evm返回的数据
// - concrete execution error:
// 各种**EVM**错误终止执行,
// 例如:ErrOutOfGas, ErrExecutionReverted
//
// 但是,如果遇到任何共识问题,直接返回错误
// 无evm执行结果。
func (st *StateTransition) TransitionDb() (*ExecutionResult, error) {
//首先检查此消息是否满足之前的所有共识规则
//应用消息。规则包括这些条款
// 1。消息调用者的nonce是正确的
// 2。来电者有足够余款支付交易费用(汽油限价*汽油价格)
// 3。所需的天然气量在该区块内可用
// 4。购买的汽油足够支付内在使用量
// 5。计算内禀气体时无溢出现象
// 6。来电者有足够的余额来支付**最高**呼叫的资产转移
// 检查条款1-3,如果一切正确,就 buy gas
if err := st.preCheck(); err != nil {
return nil, err
}
if st.evm.Config.Debug {
st.evm.Config.Tracer.CaptureTxStart(st.initialGas)
defer func() {
st.evm.Config.Tracer.CaptureTxEnd(st.gas)
}()
}
var (
msg = st.msg
sender = vm.AccountRef(msg.From())
rules = st.evm.ChainConfig().Rules(st.evm.Context.BlockNumber, st.evm.Context.Random != nil)
contractCreation = msg.To() == nil
)
// 检查条款4-5,如果一切正确,减去 内部的 gas
gas, err := IntrinsicGas(st.data, st.msg.AccessList(), contractCreation, rules.IsHomestead, rules.IsIstanbul)
if err != nil {
return nil, err
}
if st.gas < gas {
return nil, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, st.gas, gas)
}
st.gas -= gas
// 检查条款6
if msg.Value().Sign() > 0 && !st.evm.Context.CanTransfer(st.state, msg.From(), msg.Value()) {
return nil, fmt.Errorf("%w: address %v", ErrInsufficientFundsForTransfer, msg.From().Hex())
}
// 设置初始访问列表。
if rules.IsBerlin {
st.state.PrepareAccessList(msg.From(), msg.To(), vm.ActivePrecompiles(rules), msg.AccessList())
}
var (
ret []byte
vmerr error // vm错误不影响共识,因此不分配给err
)
if contractCreation {
ret, _, st.gas, vmerr = st.evm.Create(sender, st.data, st.gas, st.value)
} else {
// 为下一个事务增加nonce
st.state.SetNonce(msg.From(), st.state.GetNonce(sender.Address())+1)
ret, st.gas, vmerr = st.evm.Call(sender, st.to(), st.data, st.gas, st.value)
}
if !rules.IsLondon {
// EIP-3529之前:退款上限为gasUsed / 2
st.refundGas(params.RefundQuotient)
} else {
// EIP-3529之后:退款上限为gasUsed / 5
st.refundGas(params.RefundQuotientEIP3529)
}
effectiveTip := st.gasPrice
if rules.IsLondon {
effectiveTip = cmath.BigMin(st.gasTipCap, new(big.Int).Sub(st.gasFeeCap, st.evm.Context.BaseFee))
}
st.state.AddBalance(st.evm.Context.Coinbase, new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), effectiveTip))
return &ExecutionResult{
UsedGas: st.gasUsed(),
Err: vmerr,
ReturnData: ret,
}, nil
}
叔块奖励规则
激励计算共识
reward = (uncleNumber + 8 - headerNumber) * blockReward / 8
-- uncleNumber: 叔块的高度,即区块号
-- headerNumber: 当前正在打包的区块高度
-- blockReward: mint出区块时,给minter的基础奖励,目前是 3 ETH
-- 满足关系: headerNumber - 6 < uncleNumber < headerNumber - 1
间隔层数 | 报酬比例 | 报酬(ETH) |
---|---|---|
1 | 7/8 | 2.625 |
2 | 6/8 | 2.25 |
3 | 5/8 | 1.875 |
4 | 4/8 | 1.5 |
5 | 3/8 | 1.125 |
6 | 2/8 | 0.75 |
Mint Reward
ETH mint 出的区块有三种:
- 普通的成功进入主链的区块,有ETH奖励
- 被主链区块打包的叔块的分叉区块(叔块),有ETH奖励
- 孤块,没有任何奖励
普通的成功进入主链的区块的奖励构成:共 3 部分
- 固定的基础奖励,Block Reward
- mint的区块打包了的所有交易的gas总和,即Header结构体的GasUsed字段的值
- 当前区块打包叔块的奖励,一个区块最多只能打包 2 个叔块。奖励计算公式:reward = N * Block Reward / 32。N即是当前区块打包了叔块的数量值。
看一个例子:
默克尔数
ETH区块的Herder结构的Root,Txhash,ReceiptHash代表的都是ETH默克尔前缀树(MPT)的根节点哈希值(Hash)。
默克尔树(也称作哈希树),它满足树的数据结构特点。默克尔树满足下面的条件:
- 树的数据结构,包含不局限于二叉树,也可以是多叉树,具有树结构的全部特点。
- 基础数据不是固定的,节点所存储的数据值是具体的数据值经过哈希运算后所得到的哈希值。
- 哈希的计算是从下往上逐层进行的,就是说每个中间节点根据相邻的两个叶子节点组合计算得出,跟节点的哈希值根据其左右孩子节点组合计算得出。
- 最底层的节点包含基础的数据
ETH钱包地址存储余额的方式
ETH区块Header结构体中,Root变量的真实含义是ETH区块账户MPT树根节点的哈希值,区块账户MPT树中每个叶子节点的Key中存放的是ETH钱包的地址值,叶子节点的Value对应的是ETH的状态存储对象stateObject。stateObject中包含stateAccount对象,在stateAccount对象中有一个指针变量Balance,指向ETH存放余额的内存地址。
ETH源码定义的stateObject和stateAccount结构体如下:
// stateObject表示一个正在被修改的ETH账户。
// 使用模式如下:
// 首先你需要获得一个state_object。
// 帐户值可以通过对象访问和修改。
// 最后,调用CommitTrie将修改后的存储trie写入数据库。
type stateObject struct {
address common.Address
addrHash common.Hash // ETH钱包地址的hash值
data types.StateAccount // StateAccount对象
db *StateDB
// 数据库错误。
// stateObject会被共识算法的核心和VM使用,在这些代码内部无法处理数据库级别的错误。
// 在数据库读取期间发生的任何错误都会在这里被存储,最终将由StateDB.Commit返回。
dbErr error
// 写缓存
trie Trie // 用户的存储trie ,在第一次访问的时候变得非空
code Code // 合约字节码,当代码被加载的时候被设置
originStorage Storage // 要重复数据删除的原始条目的存储缓存会重写,对每个事务进行重置
pendingStorage Storage // 在整个块的末尾需要刷新到磁盘的存储条目
dirtyStorage Storage // 在当前事务执行中被修改的存储项
fakeStorage Storage // 由调用者为调试而构造的伪存储。
// 缓存的标记
// 当一个对象被标记为自杀时,它将在状态转换的“更新”阶段从try中删除。
dirtyCode bool // 如果更新了代码,则为true
suicided bool
deleted bool
}
// StateAccount是ETH账户的共识表示。
// 这些对象存储在主帐户trie中。
type StateAccount struct {
Nonce uint64
// 如果账户是用户钱包账户,Nonce表示的是该账户发出当前交易时的交易序列号
// 如果账户是智能合约账户,Nonce表示的是此账户创建的合约序列号
Balance *big.Int
// 该账户目前存放ETH余额的内存地址,TIP:ETH Coin
Root common.Hash
// 存储树的默克尔根的哈希值
CodeHash []byte
// 如果账户是用户钱包账户,该值为空
// 如果是智能合约账户,该值对应于当前初始发布智能合约的十六进制哈希值
}
余额的查询顺序
账户数据会持久化保存到<k,v>数据库(键值对数据库)中,但是查询余额时并不是直接去查<k,v>数据库,这是因为ETH钱包地址中Token的余额查询上设置了三层缓存机制。
在stateObject结构体中,有一个StateDB类型的db指针对象,该db指针对象就是存储了基于内存的缓存Map。其ETH源码的StateDB结构体定义如下:
// 以太坊协议中的StateDB结构用于存储merkle try中的任何内容。
// statedb负责缓存和存储嵌套状态。
// 它是用于检索的通用查询接口:
// * 合约
// * 账户
type StateDB struct {
db Database
prefetcher *triePrefetcher
originalRoot common.Hash // 状态前根,在做出任何改变之前
trie Trie
hasher crypto.KeccakState
snaps *snapshot.Tree
snap snapshot.Snapshot
snapDestructs map[common.Hash]struct{}
snapAccounts map[common.Hash][]byte
snapStorage map[common.Hash]map[common.Hash][]byte
// 这个映射保存着“活动”对象,它将在处理状态转换时被修改。
stateObjects map[common.Address]*stateObject
stateObjectsPending map[common.Address]struct{} // 状态对象已完成但尚未写入 trie
stateObjectsDirty map[common.Address]struct{} // 状态对象在当前执行中被修改
// DB error.
// 状态对象被共识核心和VM使用,它们无法处理数据库级别的错误。
// 在读取数据库期间发生的任何错误都将被存储在这里,
// 并最终由StateDB.Commit返回。
dbErr error
// 这个退还计数器, 也用于状态转换。
refund uint64
thash common.Hash
txIndex int
logs map[common.Hash][]*types.Log
logSize uint
preimages map[common.Hash][]byte
// 外加访问列表
accessList *accessList
// 日志状态修改.
// 这是Snapshot和RevertToSnapshot的主干。
journal *journal
validRevisions []revision
nextRevisionId int
// 在执行期间为调试目的收集的度量
AccountReads time.Duration
AccountHashes time.Duration
AccountUpdates time.Duration
AccountCommits time.Duration
StorageReads time.Duration
StorageHashes time.Duration
StorageUpdates time.Duration
StorageCommits time.Duration
SnapshotAccountReads time.Duration
SnapshotStorageReads time.Duration
SnapshotCommits time.Duration
AccountUpdated int
StorageUpdated int
AccountDeleted int
StorageDeleted int
}
余额的查找顺序如下:
- 第一级查找基于内存中的stateObject对象,这里保留了近期比较活跃的账号信息
- 第二级查找基于内存中的trie树
- 第三级查找基于leveldb,即<k,v>数据库层
第一、二级查找都是基于内存,第二级的Trie提现在diamagnetic上的一个接口,在stateObject中,trie变量最终是一棵MPT树,它被用于在检验某个钱包地址的stateObject数据是否真的存在于某个区块中,其验证方式就是验证默克尔树的数据校验的方式。