mynotes/ETH笔记/ETH笔记.md
2019-10-17 11:40:32 +08:00

14 KiB
Raw Permalink Blame History

ETH笔记

以太坊架构图

1557107319034

以太坊区块数据

由以下三部分组成

  1. 区块头
  2. 交易列表
  3. 叔区块头

区块头组成

  1. 父块的散列值( Prev Hash
  2. 叔区块的散列值( Uncles Hash
  3. 状态树根散列值( stateRoot
  4. 交易树根散列值( Transaction Root
  5. 收据树根散列值( Receipt Root
  6. 时间戳( Timestamp
  7. 随机数( Nonce
  8. ……

以太坊账户

一个以太坊的账户包含四个部分

  • 该地址交易的次数( nonce ),它是用于保障每笔交易能且只能被处理一次的计数器,有效避免重放( replay )攻击;
  • 账户目前的以太币余额
  • 账户的合约二进制代码(合约账户才有)
  • 账户的存储(默认为空) 。

重放攻击

以太坊硬分叉后产生了两条链,分别是 ETH chain 和 ETH Classic chain。 这两条链上的地址利私钥生成算法相同,交易格式也完全相同,导致其中一条链上的合法交易在另一条链上很可能也是合法的 。 所谓的以太坊“重放攻击”指的是如在一条链上合法的转账交易被广播到另一条链上也是合法的,从而导致转账两次的情形 。

nonce值是如何解决的

TODO……

生成外部账户的步骤

用户可以使用 Geth 指令创建一个外部账户 。 生成一个账户地址的过程主要有三步 。

  1. 设置账户的私钥,也就是通常意义的用户密码 。
  2. 使用加密算法由私钥生成对应的公钥 。
  3. 根据公钥得出相应的账户地址 。

第 2 步中使用的加密算法是 secp256kl 椭圆曲线密码算法,而不是 RSA 加密算法;第 3 步公钥计算地址,以太坊中使用 SHA3 方法 。

合约账户

合约账户地址是由合约创建时 创建者的地址 和 改地址发出的交易 共同计算得出的

私钥

目前常见的私钥有三种形态: Private key、 Keystore & Password 以及 Memonic code 。

  • **Private key**是私钥最初始的状态 ,是一份随机生成的 256 位二进制数字
  • **Keystore & Password**在以太坊官方钱包中,私钥和公钥将会以加密的方式保存一份 JSON 文件,存储在 keystore 子目录下 。 这份 JSON 文件就是 Keystore ,所以用户需要同时备份 Keystore 和对应的 Password (创建钱包时设置的密码);
  • **Memonic code**是随机生成 12 24 个比较容易记住的单词,该单词序列通过 PBKDF2 与 HMAC-SHA512 函数创建出随机种子,该种子通过 BIP-0032 提案的方式生成确定性钱包 。

以太坊源码解读 - RLP模块

/rlp目录下

大体框架

decode.go // 解码器

encode.go // 编码器

raw.go // 未解码的 rlp 数据

typecache.go // 用map缓存类型和解码器|编码器)的映射关系
  • typecache.go 维护了一个map缓存并实现了该map的 增 和 查 的操作;

RLP原理

RLP实际只给以下两种类型数据编码

  • byte数组
  • byte数组的数组称之为列表

byte数组

  • 规则1对于值在[0, 127]之间的单个字节,其编码是其本身。

    例: a 的编码是 97 。

  • 规则2 如果byte数组⻓度 l <= 55编码的结果是数组本身再加上128+l 作为前缀。

    例: abc 编码结果是131 97 98 99其中131=128+len("abc")97 98 99 依次是a b c

  • 规则3 如果数组⻓度⼤于55 编码结果第⼀个是183加上字符串⻓度所占⽤的字节数然后是数组⻓度的本身的编码最后是byte数组的编码。

    例如:编码下⾯这段字符串: The length of this sentence is more than 55 bytes, I know it because I pre-designed it 这段字符串共86个字节⽽86的编码只需要⼀个字节那就是它⾃⼰因此 编码的结 果如下: 184 86 84 104 101 32 108 101 110 103 116 104 32 111 102 32 116 104 105 115 32 115 101 110 116 101 110 99 101 32 105 115 32 109 111 114 101 32 116 104 97 110 32 53 53 32 98 121 116 101 115 44 32 73 32 107 110 111 119 32 105 116 32 98 101 99 97 117 115 101 32 73 32 112 114 101 45 100 101 115 105 103 110 101 100 32 105 116 其中前三个字节的计算⽅式如下: 184 = 183 + 1因为数组⻓度86编码后仅占⽤⼀个字节。 86即数组⻓度 8684是T的编码 编码⼀个重复1024次"a"的字符串,其结果为: 185 4 0 97 97 97 97 97 97 ...。 1024 ⼆进制 00000100 00000000 ⼀个字节为8位所以 1024 的字节数为 2 4 0 是数组⻓度的编码列表

列表

  • 规则4如果列表⻓度⼩于55编码结果第⼀位是192加列表⻓度的编码的⻓度然后依次连接各⼦列表的编码。

    例6["abc", "def"]的编码结果是 200 131 97 98 99 131 100 101 102。其中 abc 的编码为 131 97 98 99 , def 的编码为131 100 101 102。两个⼦字符串的编码后总⻓度是8因此编码结果第⼀位计算得出 192 + 8 =200。

  • 规则5如果列表⻓度超过55编码结果第⼀位是247加列表⻓度的编码⻓度所占⽤的字节数然后是列表⻓度本身的编码最后依次连接各⼦列表的编码。

    ["The length of this sentence is more than 55 bytes, ", "Iknow it because I pre-designed it"] 的编码结果是: 248 88 179 84 104 101 32 108 101 110 103 116 104 32 111 102 32 116 104 105 115 32 115 101 110 116 101 110 99 101 32 105 115 32 109 111 114 101 32 116 104 97 110 32 53 53 32 98 121 116 101 115 44 32 163 73 32 107 110 111 119 32 105 116 32 98 101 99 97 117 115 101 32 73 32 112 114 101 45 100 101 115 105 103 110 101 100 32 105 116 其中前两个字节的计算⽅式如下: 列表⻓度 88 = 86 + 2在规则3的示例中⻓度为86⽽在此例中由于有两 个⼦字符串每个⼦字符串本身的⻓度的编码各占1字节因此总共占2字节。 列表⻓度的编码⻓度为 1第1个字节为248 = 247 +1第2个字节为 88 第3个字节179依据规则2得出179 = 128 + 51第55个字节163同样依据规则2得出 163 = 128 + 35

以太坊源码解读 - MPT树模块

MPT是借鉴了以下三种树的思想设计出来的Merkle TreePatriciaTrie

Trie

  • 又被称为 “字典树”
  • 每个分叉记录一个可能存在的情况
  • 用于快速查找单词
  • 存储空间消耗较大
  • 下图为例:
    • 查找 to 和 tea 只需要根据字母顺序遍历树快速找到7和3节点

1548948924190

Patricia Trie

  • 基于 Trie 根据实际需求缩减查询范围
  • 如图如果只需要存储下图中的7个单词只需要在r后边加入两个分支而且不再是一个字母一个字母遍历
  • 这样将会缩减许多的存储空间,简化树的数据结构,加快搜索效率

1548949367962

MPT

MPT树结合了 字典树,帕特丽夏树,和梅克尔树 的特色

作用

它能在一次插入、更新、删除操作后,从所改变节点快速地计算到树根,而不需要重新计算整棵树的 Hash 。

具体含义

  • 由于账户都是由16进制的hash值构成所以这里是16叉树

  • 假如有三个值:[123 1ETH][12345 2ETH] [123789, 3ETH]

    • 首先结合帕特丽夏树的思想找出账户的共同字母123
    • 然后再利用字典树的思想分16个分支。字典树是索引单词所以有26个分支以太坊是索引16进制的账户地址所以有16个分支
    • MPT定义4种结点类型
      • 空结点NULL
      • 分⽀结点branch node包含16个分⽀以及1个value
      • 扩展结点extension node只有1个⼦结点
      • 叶⼦结点leaf node没有⼦结点包含⼀个value

    形成的结构如下:

    1557195460413

    **注意:**上图的extension node 应该指向的是分支节点并不是分支节点的5号分支。

融入梅克尔树
  • 将每个要存储的数据分成 shortNode 和 valueNode 两部分shortNode记录账户地址余额记录在valueNode中。例如下图的 [123 1ETH]
  • 由于梅克尔树的特性,首先计算 [123 1ETH] 的shortNode的hash值生成hashNode这个hashNode就可以唯一标识 shortNode也是整个树的根hash
  • 再接下来 [123 1ETH] 的shortNode的 Val 字段存储的是 fullNode的 hashNode。fullNode的Children字段中有17个子节点是因为还有一个子节点存储 [123 1ETH] 的 ValueNode
  • [12345 2ETH]只需要在4的位置分叉出来5shortNode只存储了5计算出他的hashNode存储在fullNode的Children字段中
  • [12345 2ETH]的shortNode的Val字段存储的是自身的 valueNode
  • 这样数据库只要以 hashNode 为KeyshortNode为value就能够存储所有的数据。

1548949604202

源码:

路径:/trie/node.go

// node是一个接口既可以指向valueNode又可以指向hashNode 
type node interface {
	fstring(string) string
	cache() (hashNode, bool)
	canUnload(cachegen, cachelimit uint16) bool
}
type (
	fullNode struct {
		Children [17]node 
		flags    nodeFlag
	}
	shortNode struct {
		Key   []byte
        // 当Val字段存储的是valueNode时代表该shortNode节点是叶节点
		Val   node
		flags nodeFlag
	}
	hashNode  []byte
	valueNode []byte
)

以太坊交易

交易nonce值

作用

防止双花和重放攻击

以太坊幽灵GHost协议

以太坊幽灵协议是以太坊网络处理分叉的,让整个网络达成共识的协议。

  • 叔块同样会被记录在子块中,并且给予奖励;
  • 子块最多纪录两个叔块;
  • 取区块数量最大链为主链;如图: img
    • 首先1A和1B分叉出两条链。若按照最长链协议那么1A的链很容易成为最长链而从1B生出的链的区块数远大于1A所以按照Ghost协议以1B为主链1A为叔块
    • 1B后边又出现了分叉还是取数量最多的链所以取2C最终取到4B3E和3C是4B的叔块
  • 由于子块只会记录2个叔块所以多出来的叔块就会由后边的子块来记录并且给予奖励但是奖励的金额会根据相隔的代数递减1/8同时给予叔块奖励的子块每奖励一个叔块获得1/32的出块奖励。 img

以太坊的问题

  1. 交易拥堵
  2. DAPP交易费用太高

项目

1. 技术点

  1. gas节省细节 **TODO**如何计算gas消耗

    • **结构封装:**只有在结构体内部使用uint16等才会节省gas消耗它会把结构体中的uint16加起来凑成一个uint256只消耗一个uint256的gas如果不在结构体内一个uint16就要消耗uint256的gas
    • **view**view修饰的函数不会消耗gas被web3调用不会消耗gas被合约内部其他函数调用调用会消耗
    • 使用memory修饰数组数组存储在内存中把这种数组放在view里用完全不消耗gas即使是执行for循环
  2. 与其他合约进行交互

    • 定义合约接口
    • 实例化,绑定合约地址
    • 调用外部合约方法
  3. 通过地址获取他拥有的所有僵尸的方法

    • 原本的思路:
      • 建立一个映射 address -> []uint 僵尸ID集合
      • 新生成,就加一
      • 卖掉了就抽出,位置前移,将切片长度减一
      • **问题:**这样做gas消耗太高需要进行多次读写操作
    • 新思路:
      • 建立一个长度为 用户拥有的僵尸数量 的 memory 切片用来临时存储僵尸的ID
      • for循环 遍历僵尸池获得僵尸ID遍历 ID指向address的map获得该用户对应的僵尸ID
      • 每找到一个写入memory切片中最后再返回该切片
        • 这里写入切片不是使用push长度已经定好了
        • 只需要引入一个变量作为切片的索引,然后依次赋值
  4. 支付系统:

    • 升级需要的费用
    • 合约创建者从合约中提现
  5. 采用ERC721 代币 系统

    • 每个代币都被认为是唯一且不可分割的,你只能以整个单位交易它们

    • 每个单位都有唯一的 ID

    • 有5个方法需要在代码中实现

      • 两种交易方式:拥有者直接转账;拥有者放在中间处指定谁可以取,然后接收者再取
      • 两种交易方式都会产生Transfer事件
      contract ERC721 {
        // 两个事件,第一个只要发生交易就需要监控,第二个对应 approve
        event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);
        event Approval(address indexed _owner, address indexed _approved, uint256 _tokenId);
      
        // 查询余额
        function balanceOf(address _owner) public view returns (uint256 _balance);
        // 查询拥有者
        function ownerOf(uint256 _tokenId) public view returns (address _owner);
        // 拥有者卖出
        function transfer(address _to, uint256 _tokenId) public;
        // 拥有者指定谁可以获得该僵尸
        function approve(address _to, uint256 _tokenId) public;
        // 接收者获取该僵尸
        function takeOwnership(uint256 _tokenId) public;
      }
      
  6. 确保数据不会溢出

    • 使用Zippline的safemath包确保 uint 类型不会溢出
    • uint16 uint32 等都不一样,需要在 safemath.sol 中专门定义他两的防溢出的 library
    • 将所有的++--操作都替换成 safemath 中的方法