ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

从cannon的角度理解Layer2 - 3:代码才是最好的老师

2022-07-15 10:04:20  阅读:170  来源: 互联网

标签:Layer2 ... cannon 代码 mu minigeth MIPS uint32 uc


上一次,我们通过一个实际例子梳理了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 看到的思路是用到时加载
  • proposeStaterespondState:Challenger与Defender的攻防过程,使用c.L与c.R的二分查找模式,可以最终确定争议的stepNumber
  • confirmStateTransitiondenyStateTransition:Challenger与Defender获取仲裁结果的过程,该过程需要调用 callWithTrieNodes 导入MIPS的内存快照后才可以运行,其使用 mips.Step 方法单步生成stepNumber+1的内存快照RootHash,以支持最终的仲裁过程

上述过程也就是:发起挑战 -> 来回攻防 -> 发现确定争议步骤 -> 做出仲裁

标签:Layer2,...,cannon,代码,mu,minigeth,MIPS,uint32,uc
来源: https://www.cnblogs.com/chenkaihong/p/16480264.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有