从cannon的角度理解Layer2 - 3:代码才是最好的老师
上一次,我们通过一个实际例子梳理了cannon的运行过程,更细节的部分,让我们使用代码的形式进行了解,由于业务流程已经连贯并且完整了,所以,下面的代码部分我将采用知识点的形式进行记录,可能会较为零散,但结合业务进行理解,应该也是轻而易举的
让我们从项目目录开始入手
在开始了解代码之前,让我们先了解一下cannon的目录结构:
-
minigeth:是Go Ethereum针对cannon进行裁剪后的精简版本
-
mipigo:minigeth是一个golang项目,可以由golang编译器直接编译到MIPS平台上,mipigo的作用,就是编译minigeth后,加入启动参数,并输出为可执行文件
-
unicorn:CPU架构模拟引擎,在cannon中提供了MIPS平台模拟功能
-
mipsevm:将mipigo输出的MIPS平台下的minigeth可执行文件,输入到unicorn引擎中,并通过回调,为unicorn的模拟环境提供数据的加载与输出功能
-
contracts:在EVM中,用solidity模拟了MIPS的操作码运行过程
- Challenge.sol:L1智能合约的API
- MIPS.sol:核心为
function stepPC(bytes32 stateHash, uint32 pc, uint32 nextPC) internal returns (bytes32)
,用于在EVM中单步模拟MIPS操作码运行 - MIPSMemory.sol:实现了一颗MPT树,用于映射mipsevm中内存快照的MPT(引用minigeth的MPT),并使用
mapping(bytes32 => Preimage) public preimage
,存储contracts/Challenge.sol/callWithTrieNodes
方法所提供的链上历史数据(数据原像)
mipsevm
// mipsevm/main.go
func main() {
...
mu := GetHookedUnicorn(root, ram, func(step int, mu uc.Unicorn, ram map[uint32](uint32)) {
if step == regfault {
fmt.Printf("regfault at step %d\n", step)
mu.RegWrite(uc.MIPS_REG_V0, 0xbabababa)
}
if step == target {
SyncRegs(mu, ram)
fn := fmt.Sprintf("%s/checkpoint_%d.json", root, step)
WriteCheckpoint(ram, fn, step)
if step == target {
// done
mu.RegWrite(uc.MIPS_REG_PC, 0x5ead0004)
}
}
lastStep = step + 1
})
...
}
// mipsevm/run_unicorn.go
import (
...
uc "github.com/unicorn-engine/unicorn/bindings/go/unicorn"
)
func GetHookedUnicorn(root string, ram map[uint32](uint32), callback func(int, uc.Unicorn, map[uint32](uint32))) uc.Unicorn {
mu, err := uc.NewUnicorn(uc.ARCH_MIPS, uc.MODE_32|uc.MODE_BIG_ENDIAN)
check(err)
...
check(mu.MemMap(0, 0x80000000))
return mu
}
我们可以看到,mipsevm通过 import uc "github.com/unicorn-engine/unicorn/bindings/go/unicorn"
引入了unicorn的golang sdk,并设置了 (uc.ARCH_MIPS, uc.MODE_32|uc.MODE_BIG_ENDIAN
),也就是32位的MIPS架构,并使用了大端模式
// mipsevm/run_unicorn.go
func GetHookedUnicorn(root string, ram map[uint32](uint32), callback func(int, uc.Unicorn, map[uint32](uint32))) uc.Unicorn {
mu, err := uc.NewUnicorn(uc.ARCH_MIPS, uc.MODE_32|uc.MODE_BIG_ENDIAN)
check(err)
_, outputfault := os.LookupEnv("OUTPUTFAULT")
mu.HookAdd(uc.HOOK_INTR, func(mu uc.Unicorn, intno uint32) {
if intno != 17 {
log.Fatal("invalid interrupt ", intno, " at step ", steps)
}
syscall_no, _ := mu.RegRead(uc.MIPS_REG_V0)
v0 := uint64(0)
if syscall_no == 4020 {
oracle_hash, _ := mu.MemRead(0x30001000, 0x20)
hash := common.BytesToHash(oracle_hash)
key := fmt.Sprintf("%s/%s", root, hash)
value, err := ioutil.ReadFile(key)
check(err)
tmp := []byte{0, 0, 0, 0}
binary.BigEndian.PutUint32(tmp, uint32(len(value)))
mu.MemWrite(0x31000000, tmp)
mu.MemWrite(0x31000004, value)
WriteRam(ram, 0x31000000, uint32(len(value)))
value = append(value, 0, 0, 0)
for i := uint32(0); i < ram[0x31000000]; i += 4 {
WriteRam(ram, 0x31000004+i, binary.BigEndian.Uint32(value[i:i+4]))
}
}
...
}, 0, 0)
if callback != nil {
mu.HookAdd(uc.HOOK_MEM_WRITE, func(mu uc.Unicorn, access int, addr64 uint64, size int, value int64) {
rt := value
rs := addr64 & 3
addr := uint32(addr64 & 0xFFFFFFFC)
if outputfault && addr == 0x30000804 {
fmt.Printf("injecting output fault over %x\n", rt)
rt = 0xbabababa
}
//fmt.Printf("%X(%d) = %x (at step %d)\n", addr, size, value, steps)
if size == 1 {
mem := ram[addr]
val := uint32((rt & 0xFF) << (24 - (rs&3)*8))
mask := 0xFFFFFFFF ^ uint32(0xFF<<(24-(rs&3)*8))
WriteRam(ram, uint32(addr), (mem&mask)|val)
} else if size == 2 {
mem := ram[addr]
val := uint32((rt & 0xFFFF) << (16 - (rs&2)*8))
mask := 0xFFFFFFFF ^ uint32(0xFFFF<<(16-(rs&2)*8))
WriteRam(ram, uint32(addr), (mem&mask)|val)
} else if size == 4 {
WriteRam(ram, uint32(addr), uint32(rt))
} else {
log.Fatal("bad size write to ram")
}
}, 0, 0x80000000)
mu.HookAdd(uc.HOOK_CODE, func(mu uc.Unicorn, addr uint64, size uint32) {
callback(steps, mu, ram)
steps += 1
}, 0, 0x80000000)
}
...
}
unicorn的sdk提供了hook方式,作为模拟引擎与外部交互的通道,mipsevm通过3个hook函数完成了加载、内存映射和单步执行的功能:
- uc.HOOK_INTR:系统中断时的回调,通过读取寄存器uc.MIPS_REG_V0中syscall_no(系统中断码),例如
syscall_no == 4020
,会将预生成(minigeth/main.go中会详细介绍)的Preimage(原像,block的历史数据)通过mu.MemWrite
写入到unicorn引擎的内存中,用于支持minigeth在MIPS中的运行过程(即,重跑L1的交易) - uc.HOOK_MEM_WRITE:写内存时的回调,将unicorn引擎中的内存状态映射出来,mipsevm使用
ram map[uint32](uint32)
全程记录,可以将ram理解为MIPS的内存状态 - uc.HOOK_CODE:操作码运行的回调,每个操作码运行完毕后都会回调一次,这是mipsevm的单步运行基础
以上HOOK的注册是OS级别的,就是这个特性支持了cannon与主链的解藕,可以将minigeth替换成其他主链的终端,运行逻辑依然成立
// mipsevm/main.go
func main() {
...
LoadMappedFileUnicorn(mu, "mipigo/minigeth.bin", ram, 0)
if root == "" {
WriteCheckpoint(ram, fmt.Sprintf("%s/golden.json", basedir), -1)
fmt.Println("exiting early without a block number")
os.Exit(0)
}
LoadMappedFileUnicorn(mu, fmt.Sprintf("%s/input", root), ram, 0x30000000)
mu.Start(0, 0x5ead0004)
SyncRegs(mu, ram)
}
// minigeth/main.go
func main() {
...
// init secp256k1BytePoints
crypto.S256()
// get inputs
inputBytes := oracle.Preimage(oracle.InputHash())
var inputs [6]common.Hash
for i := 0; i < len(inputs); i++ {
inputs[i] = common.BytesToHash(inputBytes[i*0x20 : i*0x20+0x20])
}
...
}
通过 LoadMappedFileUnicorn
方法,将mipigo输出的MIPS平台下的miniget可执行文件加载到MIPS内存的开头中,并将预生成的input(minigeth/main.go中会详细介绍)加载到特定位置,由于 mipigo/minigeth.bin
是不跟参数的,所以,minigeth/main.go
在MIPS中运行时是直接从上述代码 crypto.S256
开始的,而 oracle.InputHash()
得到的值,便是刚才加载到特定位置的input值,并且使用 orcal.Preimage()
获取Preimage的时候,系统会触发 syscall == 4020
,导致Preimage文件被加载到MIPS的内存中
// mipsevm/main.go
func WriteCheckpoint(ram map[uint32](uint32), fn string, step int) {
trieroot := RamToTrie(ram)
dat := TrieToJson(trieroot, step)
fmt.Printf("writing %s len %d with root %s\n", fn, len(dat), trieroot)
ioutil.WriteFile(fn, dat, 0644)
}
func main() {
...
if step == target {
SyncRegs(mu, ram)
fn := fmt.Sprintf("%s/checkpoint_%d.json", root, step)
WriteCheckpoint(ram, fn, step)
if step == target {
// done
mu.RegWrite(uc.MIPS_REG_PC, 0x5ead0004)
}
}
lastStep = step + 1
})
...
}
通过uc.HOOK_CODE的HOOK逻辑,mipsevm具备在任何MIPS操作码运行后,输出MIPS内存快照的能力,而输出的快照其实是 ram map[uint32]uint32
MPT化后的Json文件
minigeth
// minigeth/oracle/prefetch.go
...
func PrefetchStorage(blockNumber *big.Int, addr common.Address, skey common.Hash, postProcess func(map[common.Hash][]byte)) {
...
}
func PrefetchAccount(blockNumber *big.Int, addr common.Address, postProcess func(map[common.Hash][]byte)) {
...
}
func PrefetchCode(blockNumber *big.Int, addrHash common.Hash) {
...
}
...
func prefetchUncles(blockHash common.Hash, uncleHash common.Hash, hasher types.TrieHasher) {
...
}
func PrefetchBlock(blockNumber *big.Int, startBlock bool, hasher types.TrieHasher) {
...
}
func getProofAccount(blockNumber *big.Int, addr common.Address, skey common.Hash, storage bool) []string {
...
}
...
prefetch.go
中的prefetch*
函数可以从远端主链上得到所需要的运行时数据,包括完整block信息、account数据、account验证数据等等,minigeth在重放区块交易时,所使用到的区块历史数据等信息便是依赖 prefetch.go
得到的
// minigeth/main.go
func main() {
...
if len(os.Args) > 1 {
...
blockNumber, _ := strconv.Atoi(os.Args[1])
// TODO: get the chainid
oracle.SetRoot(fmt.Sprintf("%s/0_%d", basedir, blockNumber))
oracle.PrefetchBlock(big.NewInt(int64(blockNumber)), true, nil)
oracle.PrefetchBlock(big.NewInt(int64(blockNumber)+1), false, pkwtrie)
hash, err := pkwtrie.Commit()
check(err)
fmt.Println("committed transactions", hash, err)
}
// init secp256k1BytePoints
crypto.S256()
// get inputs
inputBytes := oracle.Preimage(oracle.InputHash())
var inputs [6]common.Hash
for i := 0; i < len(inputs); i++ {
inputs[i] = common.BytesToHash(inputBytes[i*0x20 : i*0x20+0x20])
}
...
}
// minigeth/oracle/prefetch.go
func PrefetchBlock(blockNumber *big.Int, startBlock bool, hasher types.TrieHasher) {
r := jsonreq{Jsonrpc: "2.0", Method: "eth_getBlockByNumber", Id: 1}
r.Params = make([]interface{}, 2)
r.Params[0] = fmt.Sprintf("0x%x", blockNumber.Int64())
r.Params[1] = true
jsonData, err := json.Marshal(r)
check(err)
/*dat, _ := ioutil.ReadAll(getAPI(jsonData))
fmt.Println(string(dat))*/
jr := jsonrespt{}
check(json.NewDecoder(getAPI(jsonData)).Decode(&jr))
//fmt.Println(jr.Result)
blockHeader := jr.Result.ToHeader()
// put in the start block header
if startBlock {
blockHeaderRlp, err := rlp.EncodeToBytes(&blockHeader)
check(err)
hash := crypto.Keccak256Hash(blockHeaderRlp)
preimages[hash] = blockHeaderRlp
emptyHash := common.Hash{}
if inputs[0] == emptyHash {
inputs[0] = hash
}
return
}
// second block
if blockHeader.ParentHash != inputs[0] {
fmt.Println(blockHeader.ParentHash, inputs[0])
panic("block transition isn't correct")
}
inputs[1] = blockHeader.TxHash
inputs[2] = blockHeader.Coinbase.Hash()
inputs[3] = blockHeader.UncleHash
inputs[4] = common.BigToHash(big.NewInt(int64(blockHeader.GasLimit)))
inputs[5] = common.BigToHash(big.NewInt(int64(blockHeader.Time)))
// save the inputs
saveinput := make([]byte, 0)
for i := 0; i < len(inputs); i++ {
saveinput = append(saveinput, inputs[i].Bytes()[:]...)
}
inputhash = crypto.Keccak256Hash(saveinput)
preimages[inputhash] = saveinput
ioutil.WriteFile(fmt.Sprintf("%s/input", root), inputhash.Bytes(), 0644)
//ioutil.WriteFile(fmt.Sprintf("%s/input", root), saveinput, 0644)
...
// save the uncles
prefetchUncles(blockHeader.Hash(), blockHeader.UncleHash, hasher)
}
我们应该可以理解到,不管是在MIPS平台下,还是在linux平台下,使用历史的blockNumber在minigeth中,运行顺序相同的交易后,所得出的结果都是相同的,所以,cannon在linux平台下预生成了一些文件,作为缓存使用:
- Preimage文件:链上的历史数据,使用
prefetch*
得到 - input文件:blockHeader的
Keccak256Hash
,用于索引一个唯一的block数据
通过缓存这些数据,minigeth在MIPS下多次运行时,就不必再次远程到链上了
// minigeth/main.go
func main() {
...
bc := core.NewBlockChain(&parent)
database := state.NewDatabase(parent)
statedb, _ := state.New(parent.Root, database, nil)
vmconfig := vm.Config{}
processor := core.NewStateProcessor(params.MainnetChainConfig, bc, bc.Engine())
...
// read txs
//traverseStackTrie(newheader.TxHash)
//fmt.Println(fn)
//fmt.Println(txTrieRoot)
var txs []*types.Transaction
triedb := trie.NewDatabase(parent)
tt, _ := trie.New(newheader.TxHash, &triedb)
tni := tt.NodeIterator([]byte{})
for tni.Next(true) {
//fmt.Println(tni.Hash(), tni.Leaf(), tni.Path(), tni.Error())
if tni.Leaf() {
tx := types.Transaction{}
var rlpKey uint64
check(rlp.DecodeBytes(tni.LeafKey(), &rlpKey))
check(tx.UnmarshalBinary(tni.LeafBlob()))
// TODO: resize an array in go?
for uint64(len(txs)) <= rlpKey {
txs = append(txs, nil)
}
txs[rlpKey] = &tx
}
}
var uncles []*types.Header
check(rlp.DecodeBytes(oracle.Preimage(newheader.UncleHash), &uncles))
var receipts []*types.Receipt
block := types.NewBlock(&newheader, txs, uncles, receipts, trie.NewStackTrie(nil))
...
// validateState is more complete, gas used + bloom also
receipts, _, _, err := processor.Process(block, statedb, vmconfig)
...
}
minigeth在将blockNumber推进到blockNumber+1之前,会做一些准备:
- 使用
database := state.NewDatabase(parent)
构建起运行的数据基线,其内部使用PrefetchAccount
- 找出需要重放的交易
var txs []*types.Transaction
并将其封装到block中
做好准备后,便可以使用 processor.Process
进行交易重放了,如果该过程是在MIPS中进行的,minigeth的运行逻辑最终会被转译为MIPS操作码,从而,其运行过程和数据会被mipsevm的HOOK所捕获
Challenge.sol
// contracts/Challenge.sol
contract Challenge {
...
struct ChallengeData {
uint256 L;
uint256 R;
mapping(uint256 => bytes32) assertedState;
mapping(uint256 => bytes32) defendedState;
address payable challenger;
uint256 blockNumberN;
}
...
function initiateChallenge(
uint blockNumberN, bytes calldata blockHeaderNp1, bytes32 assertionRoot,
bytes32 finalSystemState, uint256 stepCount)
external
returns (uint256)
{
...
}
function callWithTrieNodes(address target, bytes calldata dat, bytes[] calldata nodes) public {
for (uint i = 0; i < nodes.length; i++) {
mem.AddTrieNode(nodes[i]);
}
...
}
function proposeState(uint256 challengeId, bytes32 stateHash) external {
ChallengeData storage c = challenges[challengeId];
...
uint256 stepNumber = getStepNumber(challengeId);
require(c.assertedState[stepNumber] == bytes32(0), "state already proposed");
c.assertedState[stepNumber] = stateHash;
}
function respondState(uint256 challengeId, bytes32 stateHash) external {
ChallengeData storage c = challenges[challengeId];
...
uint256 stepNumber = getStepNumber(challengeId);
require(c.assertedState[stepNumber] != bytes32(0), "challenger state not proposed");
require(c.defendedState[stepNumber] == bytes32(0), "state already proposed");
c.defendedState[stepNumber] = stateHash;
if (c.assertedState[stepNumber] == c.defendedState[stepNumber]) {
c.L = stepNumber; // agree
} else {
c.R = stepNumber; // disagree
}
}
function confirmStateTransition(uint256 challengeId) external {
ChallengeData storage c = challenges[challengeId];
...
bytes32 stepState = mips.Step(c.assertedState[c.L]);
require(stepState == c.assertedState[c.R], "wrong asserted state for challenger");
// pay out bounty!!
(bool sent, ) = c.challenger.call{value: address(this).balance}("");
require(sent, "Failed to send Ether");
emit ChallengerWins(challengeId);
}
function denyStateTransition(uint256 challengeId) external {
ChallengeData storage c = challenges[challengeId];
...
bytes32 stepState = mips.Step(c.defendedState[c.L]);
if (c.defendedState[c.R] == bytes32(0)) {
emit ChallengerLosesByDefault(challengeId);
return;
}
require(stepState == c.defendedState[c.R], "wrong asserted state for defender");
...
}
...
}
Challenge.sol的API设计的清晰明了,在链上的storage上会存储 ChallengeData
数据结构,用于支持后续的一系列挑战过程,这些过程包括:
initiateChallenge
:用于Challenger发起挑战,其中会生成challengeData
以及与其对应的索引Id(challengeId
)callWithTrieNodes
:是一个调用代理,其中dat是真正要调用的方法,而代理的作用是将MIPS的内存快照(bytes[] calldata nodes
)加载到MIPSMemory.sol
中,这里需要注意,MIPS的内存快照是很大的(测试时有10MB~30MB),在scripts/lib.getTrieNodesForCall
看到的思路是用到时加载proposeState
与respondState
:Challenger与Defender的攻防过程,使用c.L与c.R的二分查找模式,可以最终确定争议的stepNumberconfirmStateTransition
与denyStateTransition
:Challenger与Defender获取仲裁结果的过程,该过程需要调用callWithTrieNodes
导入MIPS的内存快照后才可以运行,其使用mips.Step
方法单步生成stepNumber+1的内存快照RootHash,以支持最终的仲裁过程
上述过程也就是:发起挑战 -> 来回攻防 -> 发现确定争议步骤 -> 做出仲裁