├── .gitignore ├── Blockchain源码分析.md ├── LICENSE ├── Mempool源码分析.md ├── README.md ├── abci接口调用.md ├── crypto模块源码分析.md ├── epbft ├── pbft论文.md └── tendermint拜占庭共识算法.md ├── evm移植 ├── evm之实战.md ├── evm之总结.md ├── evm之操作码分析.md ├── evm之智能合约详解.md ├── evm之源码分析.md └── index.md ├── google38470afd3c3e46d1.html ├── img ├── 0FD1604CB898965C908B8D08C36D7EBC.png ├── 149BC9339512B878285EF7D9AA4F7207.png ├── 1F50374E4165D3F086F83F28A4027EC4.jpg ├── 2046B1E1C9D205D48070AD7A62FD3624.jpg ├── 3D5F11F23D9D284B81181ABEB555081A.png ├── 5E0910FA138824DDAD13A23C77E18193.png ├── 601D23F3E76830F3E0C55C7127B146A1.png ├── 6F3F6C7F4E2BE800C0A1C2E4D2CB64A8.png ├── 7C4BA46600C364D522BBB519B09F7689.jpg ├── 91F9F25EA7BC8AE39E9A3E1D7FC0783B.jpg ├── CA37F867C576AA2AE4B2BE82F3FEE28D.png ├── ED9351B67F09465C702F11BEBD85BC07.png ├── evm-bytes.jpg ├── evm-code.jpg └── source-code.jpg ├── node启动流程分析.md ├── p2p源码分析.md └── state源码分析.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | -------------------------------------------------------------------------------- /Blockchain源码分析.md: -------------------------------------------------------------------------------- 1 | 2 | 老规矩,先上类图。 (虽然我知道看上去啥也看不出来) 3 | ![WX20180919-162356.png](img/3D5F11F23D9D284B81181ABEB555081A.png) 4 | 5 | 然后顺便看一下blockchain模块的文件目录 6 | 7 | ![WX20180919-164456.png](img/149BC9339512B878285EF7D9AA4F7207.png) 8 | 9 | 也就是说blockchain模块我们只需要看pool.go store.go和reactor.go模块 根据名字猜功能,pool 猜想是存储区块的区块池,对多个区块进行管理的? store.go应该是和数据库进行相关操作的代码。 reactor.go就显而易见就是和Peer进行通信实现Reactor接口的代码了。 10 | 是的当我第一次看这个模块的代码时,就是这样想的。 可是当我仔细看到pool.go的源码时,我真的迷惑了。 代码不到600行,可就是不知道要干什么。 用许三多的话说就是这都写的是啥啥啥? 11 | 12 | 13 | ### pool.go的代码分析 14 | 先上一个简单的框图 15 | 16 | ![WX20180919-162430.png](img/CA37F867C576AA2AE4B2BE82F3FEE28D.png) 17 | 18 | 当你看到上面我画的框图可能也会说这都画的啥啥啥。 是的,我现在真的是在一个只可意会不可言传的阶段。 我尝试去描述整个blockchain模块的流程和功能。 如果到最后您还没有看的太明白, 没关系,你可以再看一遍。😁或许会对上面这个框图有了深一些的理解。 19 | 20 | 我们先看创建BlockPool做了哪些动作 21 | ```go 22 | func NewBlockPool(start int64, requestsCh chan<- BlockRequest, errorsCh chan<- peerError) *BlockPool { 23 | // 初始化一个实例 实现基本NewBaseService接口 24 | // 构造requesters和peers的容器 用map来保存 25 | bp := &BlockPool{ 26 | peers: make(map[p2p.ID]*bpPeer), 27 | 28 | requesters: make(map[int64]*bpRequester), 29 | // height字段表示当前应该获取的区块高度 30 | height: start, 31 | // numPending 表示当前正在进行区块请求的request 同时也代表了启动的requesters.requestRoutine()的个数 32 | numPending: 0, 33 | // 下面这两个channel是和Reactor进行通信的关键 34 | requestsCh: requestsCh, 35 | errorsCh: errorsCh, 36 | } 37 | bp.BaseService = *cmn.NewBaseService(nil, "BlockPool", bp) 38 | return bp 39 | } 40 | ``` 41 | 接着来看看启动`OnStart`的时候都做了哪些动作? 42 | ```go 43 | func (pool *BlockPool) OnStart() error { 44 | go pool.makeRequestersRoutine() 45 | pool.startTime = time.Now() 46 | return nil 47 | } 48 | 49 | func (pool *BlockPool) makeRequestersRoutine() { 50 | for { 51 | if !pool.IsRunning() { 52 | break 53 | } 54 | // 这个函数是获取当前需要下载的最小的块高度, 进行请求的个数, 已经开启的请求的线程数 55 | // numPeding 表示当前request正则请求的块,但是还未返回块的reqeust lenRequester是所有已经启动的request 可能一部分已经返回了块内容 56 | // 也就是lenRequester>=numPeding 57 | _, numPending, lenRequesters := pool.GetStatus() 58 | if numPending >= maxPendingRequests { 59 | // 如果现在请求的个数大于600个 暂时先不开启新的请求了 并尝试移除哪些被标记为超时的peer 60 | time.Sleep(requestIntervalMS * time.Millisecond) 61 | // 注意这个函数 名义上只是移除超时的peer 实际上这个函数处理的时候需要特别小心 因为peer是和request关联的 62 | // 移除掉它之后 就要把关联的request给取消掉 让request去重新找新的peer去绑定 63 | // 一会我们追踪它在分析 64 | pool.removeTimedoutPeers() 65 | } else if lenRequesters >= maxTotalRequesters { 66 | // 这个地方感觉和上面类似 lenRequesters其实就是len(requesters)的个数 67 | time.Sleep(requestIntervalMS * time.Millisecond) 68 | // check for timed out peers 69 | pool.removeTimedoutPeers() 70 | } else { 71 | // 如果挂起的请求数量不足600个 那么我们就创建一个routine进行区块请求 72 | // 从这个我们可以看出来 tendermint是同时默认进行600个区块的下载。 73 | pool.makeNextRequester() 74 | } 75 | } 76 | } 77 | // 也就是说BlockPool启动之后 会一直循环 然后看看是不是有600个routine在进行块请求 78 | // 如果有了 就尝试移除那些被标记为超时的peer,如果没有超过600个routine则继续创建一个请求。 79 | 80 | // 我们先追踪makeNextRequester看看它做了什么 81 | func (pool *BlockPool) makeNextRequester() { 82 | pool.mtx.Lock() 83 | defer pool.mtx.Unlock() 84 | 85 | //这个很容易理解 就是接着之前块高度后面继续创建请求 一个请求对应一个块高度 86 | nextHeight := pool.height + pool.requestersLen() 87 | request := newBPRequester(pool, nextHeight) 88 | pool.requesters[nextHeight] = request 89 | // 注意这句 表明创建一个request pool.numPending就会加一 90 | atomic.AddInt32(&pool.numPending, 1) 91 | 92 | // 启动request任务 这个任务 我下面再说。 93 | err := request.Start() 94 | if err != nil { 95 | request.Logger.Error("Error starting request", "err", err) 96 | } 97 | } 98 | ``` 99 | 接着我们再看几个BlockPool的重要的成员函数 为了避免代码太长隐藏了主线 我只用文字说明函数的功能。 100 | * `removeTimedoutPeers` 遍历容器中的所有peer 如果已经超时了 则移除掉这个peer 同时将绑定这个peer的所有request进行撤销请求。 101 | * `PeekTwoBlocks`从pool.height和pool.height+1对应的request取出块内容 102 | * `PopRequest` 此时删除pool.height对应的request的routine(通过调用request.Stop()), 更新pool.height+1 也就是说这个函数调用的时候 poo.height这个块高度已经被接收到并处理完成了 103 | * `RedoRequest` 撤销某个块高度对应请求的结果 如果这个request已经绑定了某个peer, 通知绑定这个peer下的所有request均进行撤销请求,然后将这个peer从容器中删除。 这个函数是因为区块交易没通过才会被调用的。 后面会分析到。 request的撤销是通过这个通道置位来标识。后面分析request的routine来解释它是怎么和这个通道进行联系的。 104 | * `AddBlock(peerID p2p.ID, block *types.Block, blockSize int)` 添加一个区块到对应的高度的request。同时将对应的peer超时时间置位。当对应高度的request被添加一个块内容, 说明这个请求的块已经拿到, 这个时候将pool.numPending-1 表示这个请求已经不用挂起了, 同时置位这个通道表示request已经接受到块内容。 注意现在我们已经提到request的两个通道了。 显而易见这个函数应该是在BlockChian的Reactor的Receive函数中会直接或者间接调用的。 105 | * `SetPeerHeight(peerID p2p.ID, height int64)` 其实是更新某个peer对应的最高的区块高度。 这个函数的调用应该也是在Rector的Receive中被调用。 设想一下场景, 本节点向连接的所有peer发送了一个块高度请求, 然后有一些peer回应了自己当前所属的最高块高度。这个时候调用这个函数。 106 | * `RemovePeer` 移除维护的peer 在peer通信出错的时候调用 和RedoRequest做的内容差不多 只是这个是通过peerID来移除对应的peer和撤销所有绑定的request。上面那个函数是根据request来移除对应的peer和撤销内容。 107 | * `pickIncrAvailablePeer` 这个函数就是给一个request找一个合适的peer进行绑定。 同时增加这个peer的numpending值(相当于是引用值)。这个引用值啥用呢,当引用值从0到1 则启动定时器。 当引用值每次减少一个(未减少到0)这个重置定时器。 这个定时器的作用就是为了体现peer是否超时。 也即是表示对于peer的一次块请求是否超时了, 如果超时了我们就在前面的`makeRequestersRoutine`函数中看到了就是把这个peer给移除掉(removeTimedoutPeers)。一会我们分析一下这个peer找超时回调都做了啥。 108 | 109 | 我们来看看request的启动进程一直在做什么? 110 | ```go 111 | func (bpr *bpRequester) requestRoutine() { 112 | OUTER_LOOP: 113 | for { 114 | var peer *bpPeer 115 | PICK_PEER_LOOP: 116 | for { 117 | if !bpr.IsRunning() || !bpr.pool.IsRunning() { 118 | return 119 | } 120 | // 这个函数 我们刚才提到了 尝试进行request和peer进行绑定 121 | peer = bpr.pool.pickIncrAvailablePeer(bpr.height) 122 | if peer == nil { 123 | time.Sleep(requestIntervalMS * time.Millisecond) 124 | continue PICK_PEER_LOOP 125 | } 126 | break PICK_PEER_LOOP 127 | } 128 | bpr.mtx.Lock() 129 | bpr.peerID = peer.id 130 | bpr.mtx.Unlock() 131 | // 看到最上面的那个框图了吗 就是把{ BlockRequest{height, peerID} 通过requestsCh这个通道发给Reactor 告诉Reactor 找个peer去要块内容 } 132 | bpr.pool.sendRequest(bpr.height, peer.id) 133 | WAIT_LOOP: 134 | for { 135 | select { 136 | // 一般这个函数不会调用 137 | case <-bpr.pool.Quit(): 138 | bpr.Stop() 139 | return 140 | // 这个函数就是在PopRequest中调用 说明这个请求的块已经获取并且被验证完保存到数据库中了 141 | case <-bpr.Quit(): 142 | return 143 | // 这个 就是对撤销请求的处理 144 | case <-bpr.redoCh: 145 | // 把之前绑定的peer取消掉 pool的numPending加一 146 | bpr.reset() 147 | // 会到循环起始处继续上面的流程 148 | continue OUTER_LOOP 149 | case <-bpr.gotBlockCh: 150 | // 这个通道的置位我上面提到了 说明这个时候request已经添加对呀的区块了 这个时候就是继续等待这个块被处理 然后看是否要撤销请求然后重新进行 151 | // peer绑定还是说只需要关闭routine就行了。 152 | continue WAIT_LOOP 153 | } 154 | } 155 | } 156 | } 157 | ``` 158 | 我想如果仔细看到这里,或多或少对这个BlockPool和Peer, request之间的关系稍微有了一些了解。 159 | 160 | 我们再看一下peer的超时回调做了些什么内容。 161 | ```go 162 | // 主要2个事情 一个是向通道发送了 peerError{err, peerID} 对应的错误信息 由Reactor的来读取 Reactor读到这个内容然后告诉P2P的Switch删除这个peer 163 | // 另一个是pool的routine根据didTimeout将其从容器中移除。 164 | func (peer *bpPeer) onTimeout() { 165 | peer.pool.mtx.Lock() 166 | defer peer.pool.mtx.Unlock() 167 | 168 | err := errors.New("peer did not send us anything") 169 | peer.pool.sendError(err, peer.id) 170 | peer.logger.Error("SendTimeout", "reason", err, "timeout", peerTimeout) 171 | peer.didTimeout = true 172 | } 173 | ``` 174 | pool.go中的内容展示就只分析到这里,如果实在还是不清楚。 没关系, 最后我会以一个场景来说明他们的流转。 175 | 176 | 177 | ### reactor.go源码 178 | 先看创建Reactor的代码`NewBlockchainReactor` 179 | ```go 180 | func NewBlockchainReactor(state sm.State, blockExec *sm.BlockExecutor, store *BlockStore, 181 | fastSync bool) *BlockchainReactor { 182 | // 接收了三个参数 state是状态组件的状态表示 blockExec 是状态组件中的区块执行器 store 是Blockchain中的store模块 183 | // state组件我们下次分析 这里暂时人为就是更新区块最新状态 store的功能一会分析 184 | 185 | // 判断当前节点最新的区块高度是否和从数据库中加载的一致 186 | if state.LastBlockHeight != store.Height() { 187 | panic(fmt.Sprintf("state (%v) and store (%v) height mismatch", state.LastBlockHeight, 188 | store.Height())) 189 | } 190 | 191 | // 创建请求通道 和BlockPool的request进行通信 192 | requestsCh := make(chan BlockRequest, maxTotalRequesters) 193 | 194 | const capacity = 1000 // must be bigger than peers count 195 | // 创建和peer出错进行通信的通道 196 | errorsCh := make(chan peerError, capacity) // so we don't block in #Receive#pool.AddBlock 197 | 198 | pool := NewBlockPool( 199 | store.Height()+1, // 从未知的块开始下载新的块内容 200 | requestsCh, 201 | errorsCh, 202 | ) 203 | 204 | bcR := &BlockchainReactor{ 205 | initialState: state, 206 | blockExec: blockExec, 207 | store: store, 208 | pool: pool, 209 | fastSync: fastSync, 210 | requestsCh: requestsCh, 211 | errorsCh: errorsCh, 212 | } 213 | bcR.BaseReactor = *p2p.NewBaseReactor("BlockchainReactor", bcR) 214 | return bcR 215 | } 216 | ``` 217 | // 看启动内容 `OnStart` 218 | ```go 219 | func (bcR *BlockchainReactor) OnStart() error { 220 | if bcR.fastSync { 221 | // 启动创建的BlockPool 222 | err := bcR.pool.Start() 223 | if err != nil { 224 | return err 225 | } 226 | // 开启poolRoutine 227 | go bcR.poolRoutine() 228 | } 229 | return nil 230 | } 231 | ``` 232 | 接下来我们看看Reactor的这个主任务在做什么`poolRoutine` 233 | ```go 234 | func (bcR *BlockchainReactor) poolRoutine() { 235 | // 尝试同步时间 10MS 236 | trySyncTicker := time.NewTicker(trySyncIntervalMS * time.Millisecond) 237 | // 状态更新时间 10S 238 | statusUpdateTicker := time.NewTicker(statusUpdateIntervalSeconds * time.Second) 239 | // 转换到共识时间间隔 1S 240 | switchToConsensusTicker := time.NewTicker(switchToConsensusIntervalSeconds * time.Second) 241 | 242 | blocksSynced := 0 243 | 244 | chainID := bcR.initialState.ChainID 245 | state := bcR.initialState 246 | 247 | lastHundred := time.Now() 248 | lastRate := 0.0 249 | 250 | didProcessCh := make(chan struct{}, 1) 251 | 252 | FOR_LOOP: 253 | for { 254 | // 开始进行循环 255 | select { 256 | case request := <-bcR.requestsCh: 257 | // 说明有请求过来了 查看这个请求想通过那个peer发出 258 | peer := bcR.Switch.Peers().Get(request.PeerID) 259 | if peer == nil { 260 | continue FOR_LOOP // Peer has since been disconnected. 261 | } 262 | // 尝试向这个peer发送指定块的内容请求 263 | msgBytes := cdc.MustMarshalBinaryBare(&bcBlockRequestMessage{request.Height}) 264 | queued := peer.TrySend(BlockchainChannel, msgBytes) 265 | if !queued { 266 | // We couldn't make the request, send-queue full. 267 | // The pool handles timeouts, just let it go. 268 | continue FOR_LOOP 269 | } 270 | 271 | case err := <-bcR.errorsCh: 272 | // 说明有某个peer回应超时错误了 273 | peer := bcR.Switch.Peers().Get(err.peerID) 274 | if peer != nil { 275 | // 告诉Switch 移除这个peer 代码里默认超时是40S 276 | // 这个40s不是开始请求之后的时间 而是从request和peer绑定之后就开始算起 277 | // 如果40S 仍然木有回应(回应是在Receive中有回调) 就把这个peer标记出错了 278 | // 如果不是负载过于严重 绑定之后就会立刻读取到requestsCh的内容进行请求了 279 | bcR.Switch.StopPeerForError(peer, err) 280 | } 281 | 282 | case <-statusUpdateTicker.C: 283 | // 每隔10S 向所有的已知peer发送一次 区块高度状态的请求 如果有peer回复了 284 | // 自己的当前区块高度 就会把高度和对应的peer加入BlockPool的peer容器中 285 | // 调用SetPeerHeight 回想一下上面我写的这个函数的功能 286 | go bcR.BroadcastStatusRequest() // nolint: errcheck 287 | 288 | case <-switchToConsensusTicker.C: 289 | // 判断是否已经追上了最高块 290 | if bcR.pool.IsCaughtUp() { 291 | bcR.Logger.Info("Time to switch to consensus reactor!", "height", height) 292 | // 不要被stop迷惑了 BlockPool的Stop啥也没做 之前的routine会依然继续运行 293 | bcR.pool.Stop() 294 | // 如果追上最高快 获取共识模块的Reactor 这个SwitchToConsensus 暂时不知做什么 等到阅读共识 295 | // 模块时在去讨论 296 | conR := bcR.Switch.Reactor("CONSENSUS").(consensusReactor) 297 | conR.SwitchToConsensus(state, blocksSynced) 298 | 299 | break FOR_LOOP 300 | } 301 | 302 | case <-trySyncTicker.C: // chan time 303 | select { 304 | case didProcessCh <- struct{}{}: 305 | default: 306 | } 307 | 308 | case <-didProcessCh: 309 | // 几乎是每10MS就要进入此处 这里才是实际区块处理的地方 310 | // 先取出请求的最低的两个区块 也就是当前状态保存的区块下一个和下下一个 311 | first, second := bcR.pool.PeekTwoBlocks() 312 | if first == nil || second == nil { 313 | // 如果没拿到 这继续重新开始 314 | continue FOR_LOOP 315 | } else { 316 | didProcessCh <- struct{}{} 317 | } 318 | // 这个函数在types/block.go中 主要实现到的功能就是把Block这个结构序列化 319 | // 然后把序列化的内容分隔成多个部分 对分隔的多个部分做默克尔树校验 返回生成的集合对象。 320 | firstParts := first.MakePartSet(state.ConsensusParams.BlockPartSizeBytes) 321 | firstPartsHeader := firstParts.Header() 322 | firstID := types.BlockID{first.Hash(), firstPartsHeader} 323 | // 对区块进行校验 324 | err := state.Validators.VerifyCommit( 325 | chainID, firstID, first.Height, second.LastCommit) 326 | if err != nil { 327 | // 如果校验失败了 撤销之前的请求块 RedoRequest这个函数上文已有说明 328 | peerID := bcR.pool.RedoRequest(first.Height) 329 | peer := bcR.Switch.Peers().Get(peerID) 330 | if peer != nil { 331 | // NOTE: we've already removed the peer's request, but we 332 | // still need to clean up the rest. 333 | bcR.Switch.StopPeerForError(peer, fmt.Errorf("BlockchainReactor validation error: %v", err)) 334 | } 335 | continue FOR_LOOP 336 | } else { 337 | // 如果能执行到这里 说明这个块已经被校验通过了 我们需要移除这个块的请求 将块内容进行保存 关于保存的格式和Block的数据结构的定义在store.go分析的时候在去细细研究 338 | bcR.pool.PopRequest() 339 | bcR.store.SaveBlock(first, firstParts, second.LastCommit) 340 | 341 | var err error 342 | // 将这个块提交给blockExec进行重放 猜测这个函数应该会将块的信息给拆解然后通过ABCI提交给APP 343 | state, err = bcR.blockExec.ApplyBlock(state, firstID, first) 344 | if err != nil { 345 | // TODO This is bad, are we zombie? 346 | cmn.PanicQ(cmn.Fmt("Failed to process committed block (%d:%X): %v", 347 | first.Height, first.Hash(), err)) 348 | } 349 | blocksSynced++ 350 | 351 | if blocksSynced%100 == 0 { 352 | lastRate = 0.9*lastRate + 0.1*(100/time.Since(lastHundred).Seconds()) 353 | bcR.Logger.Info("Fast Sync Rate", "height", bcR.pool.height, 354 | "max_peer_height", bcR.pool.MaxPeerHeight(), "blocks/s", lastRate) 355 | lastHundred = time.Now() 356 | } 357 | } 358 | continue FOR_LOOP 359 | 360 | case <-bcR.Quit(): 361 | break FOR_LOOP 362 | } 363 | } 364 | } 365 | ``` 366 | 总结一下Reactor的主任务应该就是读取区块请求, 向指定的peer发送区块下载, 查询下一个区块是否已经下载,如果已经下载则处理完后进行校验。 如果校验成功则保存到数据库中,同时提交给state组件进行区块重复(猜测它会做一些和ABCI进行交互的事情)。 367 | 368 | 接着我们分析BlockChain的Reactor的接口其他函数的实现 369 | `GetChannels`返回通道描述 ID为0x40 优先级为10 370 | `AddPeer` 向加入的peer发送一次块高度的请求 371 | `RemovePeer` 调用BlockPool.RemovePeer移除容器中对应的peer 同时撤销绑定的request的请求 需要重新请求 上文有描述过这个函数功能 372 | 仔细分析`Receive` 373 | ```go 374 | func (bcR *BlockchainReactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) { 375 | msg, err := decodeMsg(msgBytes) 376 | if err != nil { 377 | bcR.Switch.StopPeerForError(src, err) 378 | return 379 | } 380 | switch msg := msg.(type) { 381 | case *bcBlockRequestMessage: 382 | // 有人向我发送了块具体的请求 那我就把我当前保存的块具体内容回应回去 383 | if queued := bcR.respondToPeer(msg, src); !queued { 384 | // Unfortunately not queued since the queue is full. 385 | } 386 | case *bcBlockResponseMessage: 387 | // 说明有人把一个具体的块消息回复了 这个时候就是要调用pool.AddBlock了 388 | // 看来我们上文的关于这个函数的分析很正确 389 | bcR.pool.AddBlock(src.ID(), msg.Block, len(msgBytes)) 390 | case *bcStatusRequestMessage: 391 | // 有人想问问我们当前的块高度 我们把我们当前块高度告诉别人 392 | msgBytes := cdc.MustMarshalBinaryBare(&bcStatusResponseMessage{bcR.store.Height()}) 393 | queued := src.TrySend(BlockchainChannel, msgBytes) 394 | if !queued { 395 | // sorry 396 | } 397 | case *bcStatusResponseMessage: 398 | // 有peer回应了它自己当前的块高度 我们把它加入我们的peer容器表中进行维护。 399 | bcR.pool.SetPeerHeight(src.ID(), msg.Height) 400 | default: 401 | bcR.Logger.Error(cmn.Fmt("Unknown message type %v", reflect.TypeOf(msg))) 402 | } 403 | } 404 | ``` 405 | 406 | 是的到了这里我就算是把BlockChian的主要功能说完了, 可能读到此处依然不是很明白。 下面我准备以具体场景来描述这个流程。 407 | 408 | 现在假设有三个peer 分别是张三 李四 王五。 我们用张三的视角来描述问题。 409 | 410 | 1. 张三在启动的时候会从数据库中加载自己现在保存的最高区块高度为n=100, 然后张三开始创建了从n+1,n2....n+600个请求任务去请求每一个区块。每个任务标号表示为区块号。 411 | 这个时候张三向所有拥有的邻居李四和王五广播请求了。 请求内容就是请大家告诉我你们当前最高的区块高度是多少? 412 | 413 | 2. 过了一会王五回他了,跟他说自己保存的最高区块高度时600, 这个时候张三就在自己的邻居池中记下了王五高度为600 414 | 又过了一会李四也回应他了 高度是500。 然后张三邻居池中记下了李四的高度为500 415 | 416 | 3. 张三的所有请求任务准备尝试找个邻居进行绑定。 101任务发现自己请求高度在邻居池中的李四的高度下面,符合条件,那就绑定李四。 501号任务发现王五符合条件那就绑定王五。 601号任务发现没有符合条件的邻居,那就过一会再去找找。 417 | 418 | 4. 经过上面的一些任务绑定, 这个时候张三就收到了一些任务发来的请求。 101号任务请求向李四要101的区块内容。 102号任务向李四要102的区块内容。等等。。。 419 | 420 | 5. 张三按着上述要求向对应的邻居发送的请求。 可能在接下来的某个时间点, 李四回复了102号区块高度。 那张三就把102的内容放在102号的任务下。 也就是说回复了那个块的内容就放在那个任务下面。 421 | 422 | 6. 张三时不时的从101号和102号查一查是不是下面有数据了 如果有了 把两个区块内容拿出来, 为啥拿出来两个呢 是因为需要第二个来验证前一个区块。经过一系列的校验发现101这个块没毛病, 那就保存到自己的数据库。提交给相关的组件去做其他操作。此时可以销毁101号任务了。 423 | 424 | 7. 这个时候张三就可以更新最高的区块高度n为101了。尝试在任务池中创建n+601这个请求任务, 让区块下载能够往前推进。 这样周而复始的继续下去。 425 | 426 | 上面的描述可能不够严谨, 但是为了描述清楚这已经是我尽的最大努力了。 我想把文章仔细读一遍,在看看我写的这个场景描述, 我猜应该看得明白的了。 427 | 428 | 429 | ### store.go的源码 430 | store.go中主要就是将区块数据保存到数据库以及从数据库中读取之前的区块内容。 虽然说后端存储使用的是KV数据库,但是保存的时候并不是把区块内容整体序列化后直接放到value中的。这里涉及到几个数据结构,他们在types/目录下面, 我准备列举出来。 431 | ```go 432 | /* 为了方便我进行了一些处理*/ 433 | type Block struct { 434 | Header `json:"header"` 435 | Data { 436 | Txs Txs `json:"txs"` 437 | hash cmn.HexBytes 438 | } `json:"data"` 439 | Evidence EvidenceData `json:"evidence"` 440 | LastCommit *Commit `json:"last_commit"` 441 | } 442 | type Header struct{ 443 | ChainID string `json:"chain_id"` 444 | Height int64 `json:"height"` 445 | Time time.Time `json:"time"` 446 | NumTxs int64 `json:"num_txs"` 447 | 448 | // prev block info 449 | LastBlockID BlockID `json:"last_block_id"` 450 | TotalTxs int64 `json:"total_txs"` 451 | 452 | // hashes of block data 453 | LastCommitHash cmn.HexBytes `json:"last_commit_hash"` // commit from validators from the last block 454 | DataHash cmn.HexBytes `json:"data_hash"` // transactions 455 | 456 | // hashes from the app output from the prev block 457 | ValidatorsHash cmn.HexBytes `json:"validators_hash"` // validators for the current block 458 | ConsensusHash cmn.HexBytes `json:"consensus_hash"` // consensus params for current block 459 | AppHash cmn.HexBytes `json:"app_hash"` // state after txs from the previous block 460 | LastResultsHash cmn.HexBytes `json:"last_results_hash"` // root hash of all results from the txs from the previous block 461 | // consensus info 462 | EvidenceHash cmn.HexBytes `json:"evidence_hash"` // evidence included in the block 463 | } 464 | type Commit struct { 465 | BlockID BlockID `json:"block_id"` 466 | Precommits []*Vote `json:"precommits"` 467 | firstPrecommit *Vote 468 | hash cmn.HexBytes 469 | bitArray *cmn.BitArray 470 | } 471 | type BlockMeta struct { 472 | BlockID BlockID `json:"block_id"` // the block hash and partsethash 473 | Header Header `json:"header"` // The block's Header 474 | } 475 | 476 | type BlockID struct { 477 | Hash cmn.HexBytes `json:"hash"` 478 | PartsHeader PartSetHeader `json:"parts"` 479 | } 480 | 481 | type PartSet struct { 482 | total int 483 | hash []byte 484 | mtx sync.Mutex 485 | parts []*Part 486 | partsBitArray *cmn.BitArray 487 | count int 488 | } 489 | type Part struct { 490 | Index int `json:"index"` 491 | Bytes cmn.HexBytes `json:"bytes"` 492 | Proof merkle.SimpleProof `json:"proof"` 493 | 494 | // Cache 495 | hash []byte 496 | } 497 | 498 | type PartSetHeader struct { 499 | Total int `json:"total"` 500 | Hash cmn.HexBytes `json:"hash"` 501 | } 502 | ``` 503 | 504 | 先分析从Block数据生成PartSet的函数`NewPartSetFromData` 505 | ```go 506 | // data是Block进行序列化后的内容 partSize代表每个部分几个字节 507 | func NewPartSetFromData(data []byte, partSize int) *PartSet { 508 | // divide data into 4kb parts. 509 | total := (len(data) + partSize - 1) / partSize 510 | parts := make([]*Part, total) 511 | parts_ := make([]merkle.Hasher, total) 512 | partsBitArray := cmn.NewBitArray(total) 513 | 514 | // 将data分隔成total份 放入part中 515 | for i := 0; i < total; i++ { 516 | part := &Part{ 517 | Index: i, 518 | Bytes: data[i*partSize : cmn.MinInt(len(data), (i+1)*partSize)], 519 | } 520 | parts[i] = part 521 | parts_[i] = part 522 | partsBitArray.SetIndex(i, true) 523 | } 524 | 将所有的part进行一次默克尔计算 525 | // Compute merkle proofs 526 | root, proofs := merkle.SimpleProofsFromHashers(parts_) 527 | for i := 0; i < total; i++ { 528 | parts[i].Proof = *proofs[i] 529 | } 530 | // 返回分隔处理后的集合 531 | return &PartSet{ 532 | // total表示分隔个数 hash为默克尔计算的结果 这两个字段和Header内容组成了BlockMeta 533 | total: total, 534 | hash: root, 535 | parts: parts, 536 | partsBitArray: partsBitArray, 537 | count: total, 538 | } 539 | } 540 | ``` 541 | 看一看如何将区块内容保存到数据库中 542 | ```go 543 | func (bs *BlockStore) SaveBlock(block *types.Block, blockParts *types.PartSet, seenCommit *types.Commit) { 544 | 545 | height := block.Height 546 | // bs.Height() 表示数据库中已经保存的区块高度 保证连续性 没有毛病 547 | if g, w := height, bs.Height()+1; g != w { 548 | cmn.PanicSanity(cmn.Fmt("BlockStore can only save contiguous blocks. Wanted %v, got %v", w, g)) 549 | } 550 | if !blockParts.IsComplete() { 551 | cmn.PanicSanity(cmn.Fmt("BlockStore can only save complete block part sets")) 552 | } 553 | 554 | // 首先保存元数据 BlockMeta上面说了就是有PartSet.total PartSet.hash Header 组成 555 | blockMeta := types.NewBlockMeta(block, blockParts) 556 | metaBytes := cdc.MustMarshalBinaryBare(blockMeta) 557 | bs.db.Set(calcBlockMetaKey(height), metaBytes) 对应的key格式为 H:height 558 | 559 | // 开始保存分隔成的部分key格式为P:height:index 内容就是Part的序列化结果 560 | for i := 0; i < blockParts.Total(); i++ { 561 | part := blockParts.GetPart(i) 562 | bs.saveBlockPart(height, i, part) 563 | } 564 | 565 | // 保存对上一个区块的确认 key格式为C:height-1 566 | blockCommitBytes := cdc.MustMarshalBinaryBare(block.LastCommit) 567 | bs.db.Set(calcBlockCommitKey(height-1), blockCommitBytes) 568 | 569 | // 保存预确认的Commit 因为每一个区块的确认是在下一个区块中的 570 | seenCommitBytes := cdc.MustMarshalBinaryBare(seenCommit) 571 | bs.db.Set(calcSeenCommitKey(height), seenCommitBytes) 572 | 573 | // 可以更新数据库最新的区块高度了 574 | BlockStoreStateJSON{Height: height}.Save(bs.db) 575 | 576 | // Done! 577 | bs.mtx.Lock() 578 | bs.height = height 579 | bs.mtx.Unlock() 580 | 581 | // Flush 582 | bs.db.SetSync(nil, nil) 583 | } 584 | ``` 585 | 我准备简单列一下一个区块内容在数据中保存的所有信息 假设区块高度为100 586 | 587 | ![WX20180920-195513.png](img/ED9351B67F09465C702F11BEBD85BC07.png) 588 | 589 | 590 | 同理从数据库中加载一个区块就比较明白了, 先加载BlockMeta, 根据BlockMeta加载多个Part,将Part的内容拼装完成后直接进行反序列化既是Block的结构内容。 591 | 函数是`LoadBlock` 代码就不贴了。 592 | 593 | 594 | 大致到这里, 关于BlockChain的内容就算分析完了。 总结起来BlockChian功能就是向其他peer下载新区块,当然也给别的peer提供下载功能。 然后验证区块,保存到数据库中。 里面和其他组件有交互的地方就是state组件和p2p了。 和P2P之间的交互应该比较清楚了, 和State之间的交互等到分析state组件的源码时再进行分析。 595 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 wupengxin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Mempool源码分析.md: -------------------------------------------------------------------------------- 1 | 老规矩,先上图。 2 | ![WX20180917-135413.png](img/601D23F3E76830F3E0C55C7127B146A1.png) 3 | 4 | 内存池的作用简而言之就是为了保存从其他peer或者自身受到的还未被打包的交易。 5 | 6 | 我们看一下mempool的文件夹。 7 | 8 | ![WX20180917-135520.png](img/0FD1604CB898965C908B8D08C36D7EBC.png) 9 | 10 | 所以我们关注的内存池的源码其实只有mempool.go和reactor.go文件。 从源文件名称应该可以看出来MemPool的成员方法是在mempool.go文件中, 和peer信息信息的交互应该是在reactor.go文件中的。 11 | 12 | 在mempool.go文件中看到这样的注释: 13 | The mempool pushes new txs onto the proxyAppConn. 14 | It gets a stream of (req, res) tuples from the proxy. 15 | The mempool stores good txs in a concurrent linked-list. 16 | 17 | Multiple concurrent go-routines can traverse this linked-list 18 | safely by calling .NextWait() on each element. 19 | 20 | So we have several go-routines: 21 | 1. Consensus calling Update() and Reap() synchronously 22 | 2. Many mempool reactor's peer routines calling CheckTx() 23 | 3. Many mempool reactor's peer routines traversing the txs linked list 24 | 4. Another goroutine calling GarbageCollectTxs() periodically 25 | 26 | To manage these goroutines, there are three methods of locking. 27 | 1. Mutations to the linked-list is protected by an internal mtx (CList is goroutine-safe) 28 | 2. Mutations to the linked-list elements are atomic 29 | 3. CheckTx() calls can be paused upon Update() and Reap(), protected by .proxyMtx 30 | 31 | 总结一下: 32 | 1.共识引擎会调用mempool的Update()和Reap()方法去更新内存池中的交易 33 | 2.内存池在每次收到交易会首先放到交易cache中, 然后将交易提交给应用(通过ABCI), 决定交易是否可以放入交易池 34 | 3.内存池的Reactor在通过OnReceive回调函数接收到交易时,会调用CheckTx() 35 | 4.交易池是使用链表进行保存的. 36 | 37 | 先从创建内存池`NewMempool`开始。 38 | ```go 39 | func NewMempool( 40 | config *cfg.MempoolConfig, 41 | proxyAppConn proxy.AppConnMempool, 42 | height int64, 43 | options ...MempoolOption, 44 | ) *Mempool { 45 | // 初始化相关的成员变量 46 | mempool := &Mempool{ 47 | config: config, 48 | // 应用层连接 49 | proxyAppConn: proxyAppConn, 50 | txs: clist.New(), // 创建一个双向链表 用来保存交易 51 | counter: 0, 52 | height: height, 53 | rechecking: 0, 54 | recheckCursor: nil, 55 | recheckEnd: nil, 56 | logger: log.NewNopLogger(), 57 | metrics: NopMetrics(), 58 | } 59 | if config.CacheSize > 0 { 60 | // 内存池缓存 61 | mempool.cache = newMapTxCache(config.CacheSize) 62 | } else { 63 | mempool.cache = nopTxCache{} 64 | } 65 | // 注意这个函数很重要 设置了代理连接的回调函数为resCb(req *abci.Request, res *abci.Response) 66 | // 可能当你看到这个不是很理解 可以先只有这个印象。 67 | // 因为交易池在收到交易后会把交易提交给APP 根据APP的返回来决定后续这个交易 68 | // 如何处理 所以在APP处理完提交的交易后回调mempool.resCb进而让mempool来继续决定当前交易如何处理 69 | proxyAppConn.SetResponseCallback(mempool.resCb) 70 | for _, option := range options { 71 | option(mempool) 72 | } 73 | return mempool 74 | } 75 | ``` 76 | 看一看被共识引擎调用的两个方法`Reap`和`Update`. 先入为主想一想共识引擎和内存池的关系,应该是从内存池取出交易-->执行交易--->打包交易--->告诉内存池应该移除的交易。 77 | 所以`Reap`作用应该就是从内存池中取出交易。 78 | 看代码: 79 | ```go 80 | func (mem *Mempool) Reap(maxTxs int) types.Txs { 81 | // 设置并发控制 按理说此方法应该只能被串行调用 82 | mem.proxyMtx.Lock() 83 | defer mem.proxyMtx.Unlock() 84 | 85 | for atomic.LoadInt32(&mem.rechecking) > 0 { 86 | // 内存是否在重新检查 如果是 则延时一段时间继续查看 87 | time.Sleep(time.Millisecond * 10) 88 | } 89 | // -1 表示不限制, 0表示没有 90 | txs := mem.collectTxs(maxTxs) 91 | return txs 92 | } 93 | 94 | func (mem *Mempool) collectTxs(maxTxs int) types.Txs { 95 | if maxTxs == 0 { 96 | return []types.Tx{} 97 | } else if maxTxs < 0 { 98 | maxTxs = mem.txs.Len() 99 | } 100 | txs := make([]types.Tx, 0, cmn.MinInt(mem.txs.Len(), maxTxs)) 101 | // 遍历mempool.txs的链表 102 | for e := mem.txs.Front(); e != nil && len(txs) < maxTxs; e = e.Next() { 103 | memTx := e.Value.(*mempoolTx) 104 | txs = append(txs, memTx.tx) 105 | } 106 | return txs 107 | } 108 | 109 | ``` 110 | 接下来看一看更新内存池中的交易`Update` 111 | ```go 112 | func (mem *Mempool) Update(height int64, txs types.Txs) error { 113 | // 这一步不用多解释 114 | txsMap := make(map[string]struct{}) 115 | for _, tx := range txs { 116 | txsMap[string(tx)] = struct{}{} 117 | } 118 | 119 | // Set height 120 | mem.height = height 121 | mem.notifiedTxsAvailable = false 122 | 123 | //一会跳到此函数看一下做了什么 124 | // 其实就是把共识引擎提交的交易列表中把在本地内存池的全部移除掉 125 | // 并返回移除的内存池 126 | goodTxs := mem.filterTxs(txsMap) 127 | 128 | // Recheck mempool txs if any txs were committed in the block 129 | // NOTE/XXX: in some apps a tx could be invalidated due to EndBlock, 130 | // so we really still do need to recheck, but this is for debugging 131 | // 下面这个好像是为了重新检查一遍交易 但是目前只是调试状态 暂且忽略。 132 | if mem.config.Recheck && (mem.config.RecheckEmpty || len(goodTxs) > 0) { 133 | mem.logger.Info("Recheck txs", "numtxs", len(goodTxs), "height", height) 134 | mem.recheckTxs(goodTxs) 135 | // At this point, mem.txs are being rechecked. 136 | // mem.recheckCursor re-scans mem.txs and possibly removes some txs. 137 | // Before mem.Reap(), we should wait for mem.recheckCursor to be nil. 138 | } 139 | mem.metrics.Size.Set(float64(mem.Size())) 140 | return nil 141 | } 142 | ``` 143 | 所以上面两个函数的功能很符合我们之前的猜想。 144 | 下面我们看一看一个非常重要的函数`CheckTx` 它把新的交易提交给APP, 然后决定是否被加入内存池中。 145 | ```go 146 | func (mem *Mempool) CheckTx(tx types.Tx, cb func(*abci.Response)) (err error) { 147 | // 并发控制 也就是说当共识引擎在进行交易池读取和更新的时候 此函数应该是阻塞的。 148 | mem.proxyMtx.Lock() 149 | defer mem.proxyMtx.Unlock() 150 | 151 | // 如果已经超过了设置的内存池则放弃加入 152 | if mem.Size() >= mem.config.Size { 153 | return ErrMempoolIsFull 154 | } 155 | 156 | // 先加入内存池cache 如果cache中存在此交易则返回false 157 | if !mem.cache.Push(tx) { 158 | return ErrTxInCache 159 | } 160 | 161 | // 写入预写式日志中 关于cache和WAL后面在说明 162 | if mem.wal != nil { 163 | // TODO: Notify administrators when WAL fails 164 | _, err := mem.wal.Write([]byte(tx)) 165 | if err != nil { 166 | mem.logger.Error("Error writing to WAL", "err", err) 167 | } 168 | _, err = mem.wal.Write([]byte("\n")) 169 | if err != nil { 170 | mem.logger.Error("Error writing to WAL", "err", err) 171 | } 172 | } 173 | // END WAL 174 | 175 | // NOTE: proxyAppConn may error if tx buffer is full 176 | if err = mem.proxyAppConn.Error(); err != nil { 177 | return err 178 | } 179 | // 此时把交易传给proxyAppConn 180 | reqRes := mem.proxyAppConn.CheckTxAsync(tx) 181 | if cb != nil { 182 | reqRes.SetCallback(cb) 183 | } 184 | 185 | return nil 186 | } 187 | ``` 188 | 上面这个函数我们看到交易被简单判断之后加入了cache,然后提交给你APP, 但是这个交易啥时候放入内存池中供共识引擎获取最终被打包的呢。 189 | 我们继续追踪当tx被传递给proxyAppConn做了哪些动作。 proxyAppConn是一个接口, golang中的接口其实有利有弊,实现接口很简单,可是对于阅读源码却真心有时候很困难, 因为你不知道最终是调用了那个实例,只能从最初的传递参数开始找起来。这里我们就不找了,就是前面框图里的appConnMempool, 这个对象有一个唯一的实例就是abcicli.Client。 而abcicli.Client也是一个接口, 现在我们不打算深入去追踪了,不然越追越远都忘记初衷了。 这里只说结论。 190 | abcicli.Client的一些实例会调用types.Application而type.Application就是需要用户自己实现的接口。 也被tendermint称之为ABCI(抽象二进制接口)。 也是就是只要实现了这几个接口, 你就创造了一个属于自己的区块链了。我想大家了解tendermint,可能最先了解的就是abci,只有极少的人会去研究代码, 然后才有更少的人看到这篇文章吧。 说远了,我们来看看appConnMempool.CheckTxAsync()做了什么。 191 | ```go 192 | type appConnMempool struct { 193 | appConn abcicli.Client 194 | } 195 | // 直接调用abcicli.Client的CheckTxAsync 196 | func (app *appConnMempool) CheckTxAsync(tx []byte) *abcicli.ReqRes { 197 | return app.appConn.CheckTxAsync(tx) 198 | } 199 | // abcicli.Client是一个接口, 有好几个实例实现。代码在abci文件夹下。 我们关心localClient实例,它是一个直接和tendermint交互不需要创建socket的客户端。 200 | type localClient struct { 201 | cmn.BaseService 202 | mtx *sync.Mutex 203 | // 注意types.Application 就是用户要做的内容 204 | types.Application 205 | Callback 206 | } 207 | 208 | func (app *localClient) CheckTxAsync(tx []byte) *ReqRes { 209 | app.mtx.Lock() 210 | // 看到没 终于找到你还好没放弃。 咳咳 211 | // 把交易传递给用户要实现的CheckTx函数 212 | res := app.Application.CheckTx(tx) 213 | app.mtx.Unlock() 214 | // 注意此处 app.callback 也就是说当交易被APP处理之后 会把交易和返回的内容封装一下传递给回调函数。 215 | // 还记得我在内存池示例创建时说的那个非常重要的函数不? 不记得了 没关系 我在写一下 216 | // proxyAppConn.SetResponseCallback(mempool.resCb) 217 | // 所以说结果会被再次传递给mempool的resCb 218 | return app.callback( 219 | types.ToRequestCheckTx(tx), 220 | types.ToResponseCheckTx(res), 221 | ) 222 | } 223 | ``` 224 | 显而易见我们该追踪`resCb`了 225 | ```go 226 | func (mem *Mempool) resCb(req *abci.Request, res *abci.Response) { 227 | if mem.recheckCursor == nil { 228 | //我们只最终resCbNormal 正常情况只会到这里 229 | mem.resCbNormal(req, res) 230 | } else { 231 | mem.resCbRecheck(req, res) 232 | } 233 | mem.metrics.Size.Set(float64(mem.Size())) 234 | } 235 | 236 | func (mem *Mempool) resCbNormal(req *abci.Request, res *abci.Response) { 237 | switch r := res.Value.(type) { 238 | case *abci.Response_CheckTx: 239 | // 这里进行一次转换 是因为回调之时进行了封装 240 | tx := req.GetCheckTx().Tx 241 | if r.CheckTx.Code == abci.CodeTypeOK { 242 | // 总之只有我们自己实现的ABIC返回结果的CODE为0才会进入这里 243 | mem.counter++ 244 | memTx := &mempoolTx{ 245 | counter: mem.counter, 246 | height: mem.height, 247 | tx: tx, 248 | } 249 | // 这个流程应该比较清楚了 把刚才提交的交易加入交易池中 250 | mem.txs.PushBack(memTx) 251 | mem.logger.Info("Added good transaction", "tx", TxID(tx), "res", r, "total", mem.Size()) 252 | // 同时设置为通知交易有效 这个功能好像是在共识引擎上有用 在分析consensus源码时候说明 253 | mem.notifyTxsAvailable() 254 | } else { 255 | // 如果APP任务交易错误 那么就把这个交易从cache中移除掉。 256 | mem.logger.Info("Rejected bad transaction", "tx", TxID(tx), "res", r) 257 | 258 | // remove from cache (it might be good later) 259 | mem.cache.Remove(tx) 260 | } 261 | default: 262 | // ignore other messages 263 | } 264 | } 265 | ``` 266 | 到了这里我们看看mempool实现的功能应该已经满足了条件, 那这个CheckTx会在哪里被调用呢。 一个是我们调用接口时通过API接口广播交易还有一个就是在自身Reactor中接收到其他peer的广播。 267 | 我们在看看mempool实现的Reactor。Reactor在P2P源码分析的时候已经进行说明, mempool的Reactor的实现代码很明朗,我们只需要看看`AddPeer`和`Receive`做了什么。 268 | ```go 269 | // 从P2P源码分析里面我们知道了这个函数是在P2P的MConnecttion中当收到peer发送的消息之后被调用的。 270 | func (memR *MempoolReactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) { 271 | // 所以流程就是先解码消息 看能否正确解码 272 | msg, err := decodeMsg(msgBytes) 273 | if err != nil { 274 | // 275 | memR.Logger.Error("Error decoding message", "src", src, "chId", chID, "msg", msg, "err", err, "bytes", msgBytes) 276 | // 出错了 告诉Switch 移除掉这个peer 277 | memR.Switch.StopPeerForError(src, err) 278 | return 279 | } 280 | memR.Logger.Debug("Receive", "src", src, "chId", chID, "msg", msg) 281 | 282 | switch msg := msg.(type) { 283 | case *TxMessage: 284 | // 确认消息格式正确 调用CheckTx 试图将交易加入本地交易池 285 | err := memR.Mempool.CheckTx(msg.Tx, nil) 286 | if err != nil { 287 | memR.Logger.Info("Could not check tx", "tx", TxID(msg.Tx), "err", err) 288 | } 289 | // broadcasting happens from go routines per peer 290 | default: 291 | memR.Logger.Error(fmt.Sprintf("Unknown message type %v", reflect.TypeOf(msg))) 292 | } 293 | } 294 | ``` 295 | 在看看添加一个peer时做哪些事情 296 | ```go 297 | func (memR *MempoolReactor) AddPeer(peer p2p.Peer) { 298 | // 启动一个goroutine 尝试把内存池中的交易实时广播到对应的peer 299 | go memR.broadcastTxRoutine(peer) 300 | } 301 | 302 | func (memR *MempoolReactor) broadcastTxRoutine(peer p2p.Peer) { 303 | if !memR.config.Broadcast { 304 | return 305 | } 306 | 307 | var next *clist.CElement 308 | for { 309 | // This happens because the CElement we were looking at got garbage 310 | // collected (removed). That is, .NextWait() returned nil. Go ahead and 311 | // start from the beginning. 312 | if next == nil { 313 | select { 314 | case <-memR.Mempool.TxsWaitChan(): // Wait until a tx is available 315 | if next = memR.Mempool.TxsFront(); next == nil { 316 | continue 317 | } 318 | case <-peer.Quit(): 319 | return 320 | case <-memR.Quit(): 321 | return 322 | } 323 | } 324 | // 执行到此处 说明已经从内存池中读取到一个交易 325 | memTx := next.Value.(*mempoolTx) 326 | // make sure the peer is up to date 327 | height := memTx.Height() 328 | if peerState_i := peer.Get(types.PeerStateKey); peerState_i != nil { 329 | peerState := peerState_i.(PeerState) 330 | peerHeight := peerState.GetHeight() 331 | if peerHeight < height-1 { // Allow for a lag of 1 block 332 | time.Sleep(peerCatchupSleepIntervalMS * time.Millisecond) 333 | continue 334 | } 335 | } 336 | // 发送交易给peer 337 | msg := &TxMessage{Tx: memTx.tx} 338 | success := peer.Send(MempoolChannel, cdc.MustMarshalBinaryBare(msg)) 339 | if !success { 340 | time.Sleep(peerCatchupSleepIntervalMS * time.Millisecond) 341 | continue 342 | } 343 | 344 | select { 345 | case <-next.NextWaitChan(): 346 | // see the start of the for loop for nil check 347 | next = next.Next() 348 | case <-peer.Quit(): 349 | return 350 | case <-memR.Quit(): 351 | return 352 | } 353 | } 354 | } 355 | ``` 356 | 我们在看看之前立的flag。 357 | ``` 358 | type mapTxCache struct { 359 | mtx sync.Mutex 360 | size int 361 | map_ map[string]struct{} 362 | list *list.List 363 | } 364 | 所以内存池使用列表和map在维护cache。 365 | Push: 通过map_判断是否存在 如果不存在则加入list和map_中 366 | Remove: 从map_中移除成员 367 | ``` 368 | 这里我觉得这个cache实现的不是很优雅, 从目前来看cache的唯一作用就是判断这个交易是否是重复提交。 没必要使用map和list组合。 也许是我考虑少了。反正目前代码就是这样写的。 369 | 370 | 在看看WAL(Write Ahead File)预写入日志, 一般是在事务时使用 就是在事务之前先写日志。 tendermint的WAL实现其实比较简单, 创建file句柄, 写入文件。 371 | 代码在libs/autofile/ 372 | ```go 373 | type AutoFile struct { 374 | ID string 375 | Path string 376 | ticker *time.Ticker 377 | tickerStopped chan struct{} // closed when ticker is stopped 378 | mtx sync.Mutex 379 | file *os.File 380 | } 381 | // 创建一个AutoFile的对象 382 | func OpenAutoFile(path string) (af *AutoFile, err error) { 383 | af = &AutoFile{ 384 | ID: cmn.RandStr(12) + ":" + path, 385 | Path: path, 386 | // 1S 一个周期 387 | ticker: time.NewTicker(autoFileOpenDuration), 388 | tickerStopped: make(chan struct{}), 389 | } 390 | // openFile 打开一个文件句柄 391 | if err = af.openFile(); err != nil { 392 | return 393 | } 394 | // 启动processTicks任务 这个任务其实只做了一件事 395 | // 每隔1S尝试关闭关闭打开的文件句柄 396 | go af.processTicks() 397 | // 注册信号监控任务 目的就是当有SIGHUP信息时 尝试关闭所有的文件句柄 防止文件丢失 398 | sighupWatchers.addAutoFile(af) 399 | return 400 | } 401 | 402 | AutoFile:Write函数就是对外的接口 尝试写入内容 当文件句柄被关闭时重新打开在写入文件。 403 | ``` 404 | 到了这里内存池的主要内容差不多就分析完成了。 在tendermint中的内存池其实算是比较简单的。 因为它为了提供一个通用的区块链平台,所以"内存池就真的是一个内存池".可能这话说起来比较绕, 因为在整个源码分析的过程中, 我们只看到了交易其实就是[]byte 在各个组件之间之间进行流转。 什么时候把这块数组放入内存池什么时候从内存池中移除。这些都很符合我们的想法和预期,不像在以太坊的内存池中要考虑各个账户的nonce,还要考虑pending和queue的不同状态。 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Tendermint源码分析 2 | 3 | ## 缘由 4 | 最近工作时间稍微空闲一些, 本来是想写一些关于以太坊的源码分析,一来ethereum的实现过于复杂, 二来网上的资源也是比较丰富的。 有段时间在研究数据如何上链的问题是接触到了一个叫做[bigchaindb][1]的项目。 发现此项目是基于tendermint引擎的。 逐渐接触到了[tendermint][2]。 我想每一个区块链行业的从业者应该都有实现一条公链的想法。 tendermint正好满足了所有的功能。 不用去自己写P2P网络, 不用去实现复杂的共识算法, 不用研究如何对区块打包和存储。 只需要实现几个特定的接口就可以实现一个全新的链。 5 | 6 | 在基于tendermint实现了一个简(无)单(用)的公链之后, 愈发想研究一下tendermint的技术细节。 所以就有了现在这个源码分析的文章。目前已经通读和理解了大部分的代码, 我是按着模块来阅读的。目前已经看完了P2P, Mempool, Blockchain, State, Consensue。 很多模块的代码注释和文档都比较全面对于阅读源码非常有帮助。当然也有些模块注释很不明确需要自己琢磨许久才能明确其功能。我会逐渐将其落实为文档, 期望能给看到这篇文章的同学提供一些帮助。 7 | 8 | ## 分析计划 9 | 10 | - [x] [P2P模块源码分析][3] 11 | - [x] [Mempool模块源码分析][5] 12 | - [x] [BlockCain模块源码分析][6] 13 | - [x] [State模块源码分析][9] 14 | - [ ] Consensus模块源码分析 15 | - [x] [pbft论文简述][121] 16 | - [x] [tendermint共识流程][122] 17 | - [ ] Evidence模块源码分析 18 | - [x] [Crypto加密包功能分析][7] 19 | - [x] [Tendermint的启动流程分析][8] 20 | - [x] [分析Tendermint的ABCI接口实现自己的区块链][11] 21 | - [x] [移植以太坊虚拟机到Tendermint][10] 22 | - [x] [移植evm虚拟机之智能合约详解][101] 23 | - [x] [移植evm虚拟机之分析操作码][102] 24 | - [x] [移植evm虚拟机之源码分析][103] 25 | - [x] [移植evm虚拟机之实战][104] 26 | - [x] [移植evm虚拟机总结][105] 27 | 28 | 29 | ## 进度(90%) 30 | 31 | 目前自己已经看完的模块有P2P, MemPool, Blockchain, State, Crypto, Node, 实现一个[Demo][4]版本的区块链。 同时完成了移植evm到此项目中。 但是这些工作均没有落实为文档, 因此接下来我会利用空余时间按着上面的计划将已经做的内容落实到文档。 未实现的部分边分析边记录。 32 | 33 | 34 | ## 关于 35 | 36 | 如果有缘人看到这些文章有想法或建议想和我沟通可以email。 37 | 38 | 最近在做一个钱包功能的APP,使用flutter来构建跨平台的APP。 使用golang作为后端语言。 感兴趣的可以查看 39 | [此项目][12] 40 | 41 | [1]: https://github.com/bigchaindb/bigchaindb 42 | [2]: https://github.com/tendermint/tendermint 43 | [3]: p2p源码分析.md 44 | [4]: https://github.com/blockchainworkers/conch 45 | [5]: Mempool源码分析.md 46 | [6]: Blockchain源码分析.md 47 | [7]: crypto模块源码分析.md 48 | [8]: node启动流程分析.md 49 | [9]: state源码分析.md 50 | [10]: ./evm移植/index.md 51 | [11]: ./abci接口调用.md 52 | [12]: https://github.com/wupeaking/vechain_helper 53 | 54 | [101]: ./evm移植/evm之智能合约详解.md 55 | [102]: ./evm移植/evm之操作码分析.md 56 | [103]: ./evm移植/evm之源码分析.md 57 | [104]: ./evm移植/evm之实战.md 58 | [105]: ./evm移植/evm之总结.md 59 | 60 | [121]: ./epbft/pbft论文.md 61 | [122]: ./epbft/tendermint拜占庭共识算法.md 62 | -------------------------------------------------------------------------------- /abci接口调用.md: -------------------------------------------------------------------------------- 1 | 一般在开始使用tendermint之前, 作为开发者应该最关心的就是abci接口了, 因为这个是和tendermint进行交互的关键。 个人觉得这个也是tendermint的优势之一。 有了这个接口定义才有了实现通用区块链平台的可能。 所以说如果作为一个开发者, 不想了解整个tendermint的流转流程, 只想实现自己特定功能的区块链, 那么这一篇文章至少是应该看得。 我会从客户端创建开始说起, 到每个接口调用的时机, 应该注意的小细节等等。 现在开始吧。 2 | 3 | 4 | 先从创建Node实例开始说, 在前面的文章中我们说到最开始创建abci客户端的地方就是在node启动中。在node/node.go 5 | ```go 6 | // 这个函数是用于重放区块之前已经保存的区块 7 | handshaker := cs.NewHandshaker(stateDB, state, blockStore, genDoc) 8 | 9 | // 此处是创建abci客户端的关键 创建一个代理APP 然后管理 10 | //多个模块的连接 (consensus, mempool, query) 11 | // 也即是说我们今天分析的入口就是从这个函数开始 12 | proxyApp := proxy.NewAppConns(clientCreator, handshaker) 13 | if err := proxyApp.Start(); err != nil { 14 | return nil, fmt.Errorf("Error starting proxy app connections: %v", err) 15 | } 16 | ``` 17 | ```proxy.NewAppConns``` 函数位于proxy/multi_app_con.go文件中。 18 | 19 | 我们先看一下proxyAPP的数据结构 20 | ```go 21 | type multiAppConn struct { 22 | // 这个已经直线说过 就是为了方便标准调用 23 | cmn.BaseService 24 | 25 | handshaker Handshaker 26 | 27 | // 下面三个变量就是proxyapp用于管理的连接 28 | // 也就是说返回给内存池模块的客户端连接被封装在mempoolConn中 29 | // 返回给共识模块的客户端连接封装在了consensusConn 30 | // 返回给查询模块的连接封装在queryConn 31 | // 但是最终调用的地方还是abci.Client客户端的实现上。 32 | 33 | mempoolConn *appConnMempool 34 | consensusConn *appConnConsensus 35 | queryConn *appConnQuery 36 | 37 | clientCreator ClientCreator 38 | } 39 | ``` 40 | 41 | ```go 42 | func NewAppConns(clientCreator ClientCreator, handshaker Handshaker) AppConns { 43 | return NewMultiAppConn(clientCreator, handshaker) 44 | } 45 | 46 | func NewMultiAppConn(clientCreator ClientCreator, handshaker Handshaker) *multiAppConn { 47 | multiAppConn := &multiAppConn{ 48 | handshaker: handshaker, 49 | clientCreator: clientCreator, 50 | } 51 | multiAppConn.BaseService = *cmn.NewBaseService(nil, "multiAppConn", multiAppConn) 52 | return multiAppConn 53 | } 54 | 55 | // 注意传递的参数clientCreator和handshaker handshaker就是上面我们说的用于重放之前的区块 该函数是在共识模块提供的, 56 | // clientCreator默认情况下调用的为此包下的```DefaultClientCreator```这个函数。 注意这个函数非常关键。一会我们会着重分析它。 57 | ``` 58 | 59 | 创建完ProxyAPP 下一步就是启动它看看启动流程。 60 | ``` 61 | func (app *multiAppConn) OnStart() error { 62 | // 现在为了便于理解 我们就认为clientCreator是DefaultClientCreator这个函数 当调用clientCreator 63 | // 就是调用DefaultClientCreator函数 然后返回了abci接口的实例。 64 | 65 | // 可以明显的发现 tendermint为mempoolConn, consensusConn, queryConn均创建了abci接口的实例。 66 | 67 | querycli, err := app.clientCreator.NewABCIClient() 68 | if err != nil { 69 | return errors.Wrap(err, "Error creating ABCI client (query connection)") 70 | } 71 | querycli.SetLogger(app.Logger.With("module", "abci-client", "connection", "query")) 72 | if err := querycli.Start(); err != nil { 73 | return errors.Wrap(err, "Error starting ABCI client (query connection)") 74 | } 75 | app.queryConn = NewAppConnQuery(querycli) 76 | 77 | // mempool connection 78 | memcli, err := app.clientCreator.NewABCIClient() 79 | if err != nil { 80 | return errors.Wrap(err, "Error creating ABCI client (mempool connection)") 81 | } 82 | memcli.SetLogger(app.Logger.With("module", "abci-client", "connection", "mempool")) 83 | if err := memcli.Start(); err != nil { 84 | return errors.Wrap(err, "Error starting ABCI client (mempool connection)") 85 | } 86 | app.mempoolConn = NewAppConnMempool(memcli) 87 | 88 | // consensus connection 89 | concli, err := app.clientCreator.NewABCIClient() 90 | if err != nil { 91 | return errors.Wrap(err, "Error creating ABCI client (consensus connection)") 92 | } 93 | concli.SetLogger(app.Logger.With("module", "abci-client", "connection", "consensus")) 94 | if err := concli.Start(); err != nil { 95 | return errors.Wrap(err, "Error starting ABCI client (consensus connection)") 96 | } 97 | app.consensusConn = NewAppConnConsensus(concli) 98 | 99 | // 看这里 这个地方就是重复之前保存的区块的入口 100 | if app.handshaker != nil { 101 | return app.handshaker.Handshake(app) 102 | } 103 | 104 | return nil 105 | } 106 | 107 | ``` 108 | 109 | 所以分析完这个, 重心就被转移到abci的接口实例上面来了。 我们先看看abci.Client的接口都定义了哪些函数。 110 | ```go 111 | type Client interface { 112 | cmn.Service 113 | 114 | SetResponseCallback(Callback) 115 | Error() error 116 | 117 | FlushAsync() *ReqRes 118 | EchoAsync(msg string) *ReqRes 119 | InfoAsync(types.RequestInfo) *ReqRes 120 | SetOptionAsync(types.RequestSetOption) *ReqRes 121 | DeliverTxAsync(tx []byte) *ReqRes 122 | CheckTxAsync(tx []byte) *ReqRes 123 | QueryAsync(types.RequestQuery) *ReqRes 124 | CommitAsync() *ReqRes 125 | InitChainAsync(types.RequestInitChain) *ReqRes 126 | BeginBlockAsync(types.RequestBeginBlock) *ReqRes 127 | EndBlockAsync(types.RequestEndBlock) *ReqRes 128 | 129 | FlushSync() error 130 | EchoSync(msg string) (*types.ResponseEcho, error) 131 | InfoSync(types.RequestInfo) (*types.ResponseInfo, error) 132 | SetOptionSync(types.RequestSetOption) (*types.ResponseSetOption, error) 133 | DeliverTxSync(tx []byte) (*types.ResponseDeliverTx, error) 134 | CheckTxSync(tx []byte) (*types.ResponseCheckTx, error) 135 | QuerySync(types.RequestQuery) (*types.ResponseQuery, error) 136 | CommitSync() (*types.ResponseCommit, error) 137 | InitChainSync(types.RequestInitChain) (*types.ResponseInitChain, error) 138 | BeginBlockSync(types.RequestBeginBlock) (*types.ResponseBeginBlock, error) 139 | EndBlockSync(types.RequestEndBlock) (*types.ResponseEndBlock, error) 140 | } 141 | ``` 142 | 是不是觉得有些奇怪, 怎么定义的接口这么多, 但是需要我们实现的接口比这个要少一些呢。 主要是tendermint对abci.Client又进行了一次封装, 上面这些接口供给 mempoolConn consensusConn 143 | queryConn 使用的, 然后具体的调用再由abci.Client的实例调用我们实现的必不可少的那几个接口。 144 | 所以我们最后要最终到```DefaultClientCreator```看一看实现的abci实例对象。 145 | 146 | ```go 147 | func DefaultClientCreator(addr, transport, dbDir string) ClientCreator { 148 | switch addr { 149 | case "kvstore": 150 | fallthrough 151 | case "dummy": 152 | // 这个abci的实例 就是tendermint在文档中提到的创建的KV数据的实例客户端 153 | // 我们在tendermint的文档上是可以看到tendermint为了支持其他语言 所有可以使用非go的版本 其实非go的版本就是 154 | // 通过socket或者grpc进行通信。 155 | // 也就是最后的默认分支创建的abci实例 156 | // 但是作为一个gopher当然是选择直接集成的tendermint中了。 所以我们一会主要分析NewLocalClientCreator函数 157 | // 其实其他语言的扩展是类似。分析完NewLocalClientCreator函数 就明白了。 158 | return NewLocalClientCreator(kvstore.NewKVStoreApplication()) 159 | case "persistent_kvstore": 160 | fallthrough 161 | case "persistent_dummy": 162 | return NewLocalClientCreator(kvstore.NewPersistentKVStoreApplication(dbDir)) 163 | case "nilapp": 164 | return NewLocalClientCreator(types.NewBaseApplication()) 165 | 166 | // 这个分支是我扩展的 167 | case "conchapp": 168 | return NewLocalClientCreator(conchapp.NewConchApplication(dbDir)) 169 | default: 170 | mustConnect := false // loop retrying 171 | return NewRemoteClientCreator(addr, transport, mustConnect) 172 | } 173 | } 174 | ``` 175 | 176 | 我们注意一下NewLocalClientCreator需要传递的参数types.Application 是的, 你没看错, 就是这个接口, 这个接口才是开发者最终要实现的接口。 我们列一下这个接口。 177 | ```go 178 | type Application interface { 179 | // Info/Query Connection 180 | Info(RequestInfo) ResponseInfo // Return application info 181 | SetOption(RequestSetOption) ResponseSetOption // Set application option 182 | Query(RequestQuery) ResponseQuery // Query for state 183 | 184 | // Mempool Connection 185 | CheckTx(tx []byte) ResponseCheckTx // Validate a tx for the mempool 186 | 187 | // Consensus Connection 188 | InitChain(RequestInitChain) ResponseInitChain // Initialize blockchain with validators and other info from TendermintCore 189 | BeginBlock(RequestBeginBlock) ResponseBeginBlock // Signals the beginning of a block 190 | DeliverTx(tx []byte) ResponseDeliverTx // Deliver a tx for full processing 191 | EndBlock(RequestEndBlock) ResponseEndBlock // Signals the end of a block, returns changes to the validator set 192 | Commit() ResponseCommit // Commit the state and return the application Merkle root hash 193 | } 194 | ``` 195 | 我们来具体看看NewLocalClientCreator返回的localClient这个对象。由于篇幅问题我只列出localClient实现的abci.Client部分函数。 196 | ``` 197 | func (app *localClient) InfoAsync(req types.RequestInfo) *ReqRes { 198 | app.mtx.Lock() 199 | // 此处最终调用了开发者要实现的Info函数 200 | res := app.Application.Info(req) 201 | app.mtx.Unlock() 202 | return app.callback( 203 | types.ToRequestInfo(req), 204 | types.ToResponseInfo(res), 205 | ) 206 | } 207 | 208 | func (app *localClient) DeliverTxAsync(tx []byte) *ReqRes { 209 | app.mtx.Lock() 210 | // 此处调用了开发者要实现的DeliverTx函数。 211 | res := app.Application.DeliverTx(tx) 212 | app.mtx.Unlock() 213 | return app.callback( 214 | types.ToRequestDeliverTx(tx), 215 | types.ToResponseDeliverTx(res), 216 | ) 217 | } 218 | 219 | ``` 220 | 221 | 在之前的State模块分析中我们分析```ApplyBlock```函数时, 注意到从proxyApp中得到consensusConn 然后调用了BeginBlockSync(), CheckTxSync(), EndBlockSync(), CommitSync()其实质就是调用了 222 | localClient的BeginBlockSync(), CheckTxSync(), EndBlockSync(), CommitSync()。 最终又调用开发者实现的BeginBlock,DeliverTx, EndBlock, Commit。 223 | 224 | 225 | 到了这里, abci客户端的流程差不多就分析完成了。也许你觉得好像还是意犹未尽。 没有关系, 我们接下来具体分析需要实现的每一个函数。 226 | ```GO 227 | Info(RequestInfo) ResponseInfo // Return application info 228 | SetOption(RequestSetOption) ResponseSetOption // Set application option 229 | Query(RequestQuery) ResponseQuery // Query for state 230 | ``` 231 | 这三个函数主要用在查询上。 Info主要是返回APP的信息。 我以之前写的demo来举例。 232 | ```go 233 | // 注意 如果你实现的区块链应用中有APPHASH 一定要保存 它们非常关键 否则 234 | // 下次再启动的时候 重放之前的区块会出现异常的。 235 | func (app *ConchApplication) Info(req types.RequestInfo) types.ResponseInfo { 236 | // load state from 237 | err := app.state.HeadSt.LoadHeaderState() 238 | if err != nil { 239 | app.logger.Error("load state from db failed", "err", err.Error()) 240 | panic(err) 241 | } 242 | 243 | var res types.ResponseInfo 244 | res.LastBlockAppHash, _ = hex.DecodeString(app.state.HeadSt.CurAPPHash) 245 | res.LastBlockHeight = app.state.HeadSt.CurBlockNum 246 | res.Version = "v0.01" 247 | res.Data = "conch is an virtual currency" 248 | return res 249 | } 250 | ``` 251 | 252 | ```SetOption``` 函数 可以直接返回空 一般情况可以不用设置。 253 | ```Query``` 函数是当我们使用tendermint的查询接口时, 最终会调用到我们实现的这个函数。当我们进行类似下面的Get请求。 254 | 255 | ```shell 256 | curl localhost:26657/abci_query?path=""&data="abcd"&trusted=false 257 | ``` 258 | 最终就会调用到我们的Query函数。 该函数会收到的参数有path和data 然后根据这两个参数进行特别的处理。tendermin已经进行了一些接口的路由实现。 具体的rpc请求流程在rpc包中。 这里我们不具体分析。 259 | 如果你想扩展rpc函数, 只需在rpc/core/route.go中注册自己的函数就可以了。 260 | 261 | ```CheckTx```函数函数的作用是来决定是否将受到的交易放到交易池中。 之前我们在内存池模块分析的时候说到, tendermint最初是将受到的交易先放到缓存区中的, 最终调用开发者实现的CheckTx函数来决定是否把在缓存区中的交易放到内存池中。 因为只有在内存池中的交易才会打包入区块。 这样做目的很明确,可以让应用层来决定此交易是否是合法的。 262 | 263 | ```InitChain```这个函数只在tendermint第一次初始化时才调用的, 主要功能就是给应用层提交了初始的区块验证者和链ID。 这样你可以在应用层初始化时增加新的验证者, 修改共识参数。 此函数只有在tendermint第一次启动时才会调用。 默认情况下, 也可以直接返回空, 直接在配置文件中增加共识参数和验证者即可。 264 | 265 | ```go 266 | BeginBlock(RequestBeginBlock) ResponseBeginBlock // Signals the beginning of a block 267 | DeliverTx(tx []byte) ResponseDeliverTx // Deliver a tx for full processing 268 | EndBlock(RequestEndBlock) ResponseEndBlock // Signals the end of a block, returns changes to the validator set 269 | Commit() ResponseCommit 270 | ``` 271 | 上面这是个函数其实是关键。 BeginBlock表示tendermint告诉应用层准备ApplyBlock了, DeliverTx函数会被循环调用, 将打包的区块中的交易循环提交给了应用层, 也就是说应用层的主要状态流程其实是从此次开始的, 比如说进行账户金额变动, 执行虚拟机等。 大部分业务扩展都在于此。 272 | EndBlock表示tendermint一个区块的数据已经提交完成了。Commit是告诉应用层做一次整个区块的提交。 比如应用层进行数据状态确认, 生成merkelhash等等返回最终的APPHASH。 273 | 274 | 所以分析到这里的第一个注意点就是, 一个区块的BeginBlock->DeliverTx->EndBlock->Commit是有序和单线的。 大可放心不会有并发问题,整个应用层状态的更新不会出现并发导致的任何异常情况。 275 | 需要注意的另一个点就是 我们之前看到tendermint是创建多个客户端实例进行调用的。 所以在编写接口时一定不要在实现客户端实例中设计到全局信息。 比如区块高度, 账户信息等。 这些应该放到一个全局状态对象中处理。 大可放心, 不会有竞争问题。 276 | 277 | 278 | 到这里, 整个tendermint的abci流程就分析差不多了。参考一下tendermint的创建的KV示例, 已经这篇文档, 我相信创建一个简易版的区块链应该已经不成问题了。 279 | 280 | 281 | 282 | -------------------------------------------------------------------------------- /crypto模块源码分析.md: -------------------------------------------------------------------------------- 1 | 先看一下crypto的文件夹: 2 | 3 | ![WechatIMG1.jpeg](img/91F9F25EA7BC8AE39E9A3E1D7FC0783B.jpg) 4 | 5 | 6 | 简单列一下各个目录的功能: 7 | 8 | * armor 这是一个数据编码包 主要用在电子邮件加密中 9 | * ed25519 这个是EdDSA加密算法的一种实现 10 | * encoding 这个是Tendermint使用go-amino包对公钥和私钥进行序列化 go-amino类似于以太坊的RLP的一种二进制序列化和反序列化工具 11 | * merkle merkle的实现包 12 | * secp256k1 这个是ECDSA加密算法的一种实现 关于EdDSA, ECDSA, SHA等和加密相关的术语 下面我会简单说明一下 13 | * tmhash 这个是对sha256hash的封装 14 | * xchacha20poly1305 对称加密aead算法的一种实现 15 | * xsalsa20symmetric 暂时不知作用是什么 也没发现Tendermint有其他模块调用 16 | 17 | 18 | ### armor包 19 | armor中只有两个函数`EncodeArmor`和`DecodeArmor` 20 | 21 | OpenPGP Armor 22 | OpenPGP是使用最广泛的电子邮件加密标准。它由Internet工程任务组(IETF)的OpenPGP工作组定义为RFC 4880中的建议标准.OpenPGP最初源自由Phil Zimmermann创建的PGP软件。 23 | 24 | 虽然OpenPGP的主要目的是端到端加密电子邮件通信,但它也用于加密消息传递和其他用例,如密码管理器。 25 | 26 | OpenPGP的加密消息,签名证书和密钥的基本描述是八位的字节流。为了通过不能保障安全的网络通道传输OpenPGP的二进制八位字节,需要编码为可打印的二进制字符。OpenPGP提供将原始8位二进制八位字节流转换为可打印ASCII字符流,称为Radix-64编码或ASCII Armor。 27 | 28 | ASCII Armor是OpenPGP的可选功能。当OpenPGP将数据编码为ASCII Armor时,它会在Radix-64编码数据中放置特定的Header。OpenPGP可以使用ASCII Armor来保护原始二进制数据。OpenPGP通过使用Header告知用户在ASCII Armor中编码了什么类型的数据。 29 | ASCII Armor的数据结构如下: 30 | * Armor标题行,匹配数据类型 31 | * Armor Headers 32 | * A Blank(零长度或仅包含空格)行 33 | * The ASCII-Armored data 34 | * An Armor Checksum 35 | * The Armor Tail,取决于护甲标题线 36 | 37 | 具体格式: 38 | ```shell 39 | -----BEGIN PGP MESSAGE----- 40 | 41 | Version: OpenPrivacy 0.99 42 | 43 | 44 | yDgBO22WxBHv7O8X7O/jygAEzol56iUKiXmV+XmpCtmpqQUKiQrFqclFqUDBovzSvBSFjNSiVHsuAA== 45 | 46 | =njUN 47 | 48 | -----END PGP MESSAGE----- 49 | ``` 50 | 51 | ### encoding 包 52 | ```go 53 | func PrivKeyFromBytes(privKeyBytes []byte) (privKey crypto.PrivKey, err error) { 54 | err = cdc.UnmarshalBinaryBare(privKeyBytes, &privKey) 55 | return 56 | } 57 | 58 | func PubKeyFromBytes(pubKeyBytes []byte) (pubKey crypto.PubKey, err error) { 59 | err = cdc.UnmarshalBinaryBare(pubKeyBytes, &pubKey) 60 | return 61 | } 62 | ``` 63 | encoding主要就是这两个比较重要的函数, 分别是反序列化私钥和反序列化公钥`crypto.PrivKey`和`crypto.PubKey`是两个接口 我们下面说一下这两个接口 64 | 65 | ### crypto.go文件 66 | 当前文件定义了两个重要的接口 67 | ```go 68 | type PrivKey interface { 69 | Bytes() []byte 70 | Sign(msg []byte) ([]byte, error) 71 | PubKey() PubKey 72 | Equals(PrivKey) bool 73 | } 74 | 75 | // An address is a []byte, but hex-encoded even in JSON. 76 | // []byte leaves us the option to change the address length. 77 | // Use an alias so Unmarshal methods (with ptr receivers) are available too. 78 | type Address = cmn.HexBytes 79 | 80 | type PubKey interface { 81 | Address() Address 82 | Bytes() []byte 83 | ByteArray() []byte 84 | VerifyBytes(msg []byte, sig []byte) bool 85 | Equals(PubKey) bool 86 | } 87 | ``` 88 | 这两个接口很明显能看出是想对非对称加密的公钥和私钥进行统一。 目前crypto模块中实现这两个接口的签名算法有两个一个是ed25519一个是secp256k1。 也就是说在Tendermint中。 可以使用这两种加密算法进行加密,验签。 89 | 90 | 91 | ### 加密的一些术语说明 92 | 93 | 加密可以分为非对称加密和对称加密两种。 对称加密就是通过密码进行加密和解密。 比如AES。 但是因为需要密码才能解密, 就会涉及到密码流转问题。 94 | 非对称加密会有公钥和私钥。 我们可以用私钥去对一串内容进行签名, 那么公钥的拥有者就可以验证这串内容是否确定是私钥拥有者发出的。 这个过程被称为加签和验签。 95 | 同时公钥方可以对一串内容进行加密。 这样只有私钥拥有者才能解开加密的内容。这个过程被称为加密和解密。 96 | 非对称加密的使用领域特别广泛, CA证书,API支付,到现在我们在区块链上的使用等等。 这里我不打算详细讲解这些东西, 因为我也不是专业的。 只是大致介绍一下。 97 | 非对称加密主要有RSA和ECC两类。 目前在各个实现的区块链中主要是ECC这种, 关于RAS我在此处就不在说明了。 98 | 99 | 100 | * ECC (Elliptic curve crypto) 椭圆曲线加密 101 | * ECDH(Elliptical Curve Diffle-Hellmen) 椭圆曲线秘钥交换算法 102 | * ECDSA (Elliptical Curve Digital Signature Algorithm)椭圆曲线数字签名算法 103 | * EdDSA(Edwards-Cure Digital SIgnature Algorithm) 104 | 105 | - Ed25519是EdDSA签名簇方案中的一个实现。它被描述在RFC8032中。 该签名家族方案中还有Ed448。 Ed25519使用的椭圆曲线为curve25519 106 | 107 | - ECDSA是和EdDSA没有太大关系的另一种签名方案。ECDSA的所有实例均是不兼容的。 如scep256k1算法。 scep256k1确定了椭圆曲线的轨迹,scep256k1描述的椭圆曲线为y^2 = x^3 + 7 108 | 109 | ### ed25519与secp256k1包 110 | 111 | 这两个包中是对上述PrivKey和PubKey的具体实现。ed25519是Tendermint自己代码实现的, secp256k1调用了btcsuit相关的代码。 112 | 113 | 114 | ### tmhash 包 115 | ```go 116 | // Sum returns the first 20 bytes of SHA256 of the bz. 117 | func Sum(bz []byte) []byte { 118 | hash := sha256.Sum256(bz) 119 | return hash[:Size] 120 | } 121 | 122 | ``` 123 | 124 | 这个包只有一个重要的函数, 只是封装了go自带的sha256的hash函数而已 125 | 126 | ### merkle包 127 | 128 | ``` 129 | * 130 | / \ 131 | / \ 132 | / \ 133 | / \ 134 | * * 135 | / \ / \ 136 | / \ / \ 137 | / \ / \ 138 | * * * h6 139 | / \ / \ / \ 140 | h0 h1 h2 h3 h4 h5 141 | ``` 142 | 默克尔数在各种公链中都有应用。主要是进行校验数据。 在以太坊中使用的是更复杂的默克尔前缀数。 除了有校验功能还可以加快查询速度。 143 | 144 | Tendermint中的merkle包中核心函数如下 145 | ```go 146 | func simpleHashFromHashes(hashes [][]byte) []byte { 147 | // Recursive impl. 148 | switch len(hashes) { 149 | case 0: 150 | return nil 151 | case 1: 152 | return hashes[0] 153 | default: 154 | left := simpleHashFromHashes(hashes[:(len(hashes)+1)/2]) 155 | right := simpleHashFromHashes(hashes[(len(hashes)+1)/2:]) 156 | return SimpleHashFromTwoHashes(left, right) 157 | } 158 | } 159 | 160 | func SimpleHashFromTwoHashes(left, right []byte) []byte { 161 | var hasher = tmhash.New() 162 | err := encodeByteSlice(hasher, left) 163 | if err != nil { 164 | panic(err) 165 | } 166 | err = encodeByteSlice(hasher, right) 167 | if err != nil { 168 | panic(err) 169 | } 170 | return hasher.Sum(nil) 171 | } 172 | ``` 173 | 进行递归hash运算知道生成最终的根hash。 这个包中还有一个对map求merkle的方法, 将map中的key根据byte比较`bytes.Compare`排序生成对应的value数组再进行hash运算得到最终的根hash。 174 | 175 | ### 加密扩展 176 | 177 | 整个模块差不多就是这么多内容, 可能用的比较多的就是签名,加解密以及默克尔树这个地方。 但是在我使用加密包进行交易签名和验证的时候, 发现这个包的功能可能达不到我的要求。 178 | 179 | 先说为什么: 180 | > 我们在这个包中可以看到如果要验签是需要公钥的。所以说如果我创建一个自己的链就需要保存地址对应的公钥才能对交易进行交易验证。 181 | 182 | 那可不可以不保存这种对应关系呢? 183 | 184 | > 在以太坊源码中我们可以看到以太坊是不需要保存账户的公钥就可以判断这笔交易是否有效的? 这是如何做到的呢? 在以太坊进行交易校验时, 根据交易序列化的内容, 签名的内容是可以反推出公钥的,然后根据公钥进行地址生成,将生成的地址和交易的from地址相比较是否一致就可以判定出此交易是否有效了。 185 | 说到这里也说一说自己曾经在以太坊中遇到的一个小问题, 有一次用私钥离线签名进行转账时geth节点总是会报余额不足的问题, 当时很纳闷, 自己的from地址明明余额是足够的, 后来追踪源码的时候才发现,节点根本不会直接用你发送来的交易中的from来验证余额, 而是根据签名自己推导出地址, 由于自己私钥和地址不匹配。所有就出现了这个问题。 186 | 187 | 既然可以根据签名反推出公钥, 然后阅读了一下btcsuite的代码发现可以扩展一下这个包。 我只扩展了secp256k1的加密关于ed25519我没有具体分析,不知道是否可以反推出公钥。 188 | ``` 189 | // 类似下面这个函数: 190 | func RecoverPublicKey(sign, hash []byte) (crypto.PubKey, error) { 191 | pubkeyObj, _, err := secp256k1.RecoverCompact(secp256k1.S256(), sign, hash) 192 | if err != nil { 193 | return nil, err 194 | } 195 | var pubkeyBytes PubKeySecp256k1 196 | copy(pubkeyBytes[:], pubkeyObj.SerializeCompressed()) 197 | return pubkeyBytes, nil 198 | } 199 | ``` 200 | 201 | ## 总结 202 | 203 | 关于密码学, 我觉得其实没有大家想的那么简单。 有很多数学原理在其中, 对于我本人也只是知道一些名词, 知道如何使用一些代码包。 吾生也有涯,而知也无涯。唉。 204 | 205 | 206 | -------------------------------------------------------------------------------- /epbft/pbft论文.md: -------------------------------------------------------------------------------- 1 | 2 | 论文地址:[http://pmg.csail.mit.edu/papers/osdi99.pdf](http://pmg.csail.mit.edu/papers/osdi99.pdf) 3 | PBFT 是 Practical Byzantine Fault Tolerance 的缩写,意为实用拜占庭容错算法。该算法是 Miguel Castro (卡斯特罗) 和 Barbara Liskov(利斯科夫)在 1999 年提出来的,解决了原始拜占庭容错算法效率不高的问题,将算法复杂度由指数级降低到多项式级,使得拜占庭容错算法在实际系统应用中变得可行。该论文发表在 1999 年的操作系统设计与实现国际会议上(OSDI99)。没错,这个 Loskov 就是提出著名的里氏替换原则(LSP)的人,2008 年图灵奖得主。 4 | 5 | # 摘要部分 6 | 7 | OSDI99 这篇论文描述了一种副本复制(replication)算法解决拜占庭容错问题。作者认为拜占庭容错算法将会变得更加重要,因为恶意攻击和软件错误的发生将会越来越多,并且导致失效的节点产生任意行为。(拜占庭节点的任意行为有可能误导其他副本节点产生更大的危害,而不仅仅是宕机失去响应。)而早期的拜占庭容错算法或者基于同步系统的假设,或者由于性能太低而不能在实际系统中运作。这篇论文中描述的算法是实用的,因为该算法可以工作在异步环境中,并且通过优化在早期算法的基础上把响应性能提升了一个数量级以上。作者使用这个算法实现了拜占庭容错的网络文件系统(NFS),性能测试证明了该系统仅比无副本复制的标准 NFS 慢了 3%。 8 | 9 | # 1\. 概要介绍 10 | 11 | 这篇论文提出解决拜占庭容错的状态机副本复制(state machine replication)算法。这个算法在保证活性和安全性(liveness & safety)的前提下提供了 (n-1)/3 的容错性。从 Lamport 教授在 1982 年提出拜占庭问题开始,已经有一大堆算法去解决拜占庭容错了。PBFT 在之上进行了优化使得能够应用在实际场景,在只读操作中只使用 1 次消息往返(message round trip),在只写操作中只使用 2 次消息往返,并且在正常操作中使用了消息验证编码(Message Authentication Code, 简称 MAC),而造成妖艳贱货性能低下的公钥加密(public-key cryptography)只在发生失效的情况下使用(viewchange 12 | and new-view messages)。作者不仅提出算法,而且使用这个算法实现了一个拜占庭容错的 NFS 服务。 13 | 作者列举一下这边论文的贡献: 14 | 15 | 1)首次提出在异步网络环境下使用状态机副本复制协议 16 | 2)使用多种优化使性能显著提升 17 | 3)实现了一种拜占庭容错的分布式文件系统 18 | 4)为副本复制的性能损耗提供试验数据支持 19 | 20 | # 2\. 系统模型 21 | 22 | 系统假设为异步分布式的,通过网络传输的消息可能丢失、延迟、重复或者乱序。作者假设节点的失效必须是独立发生的,也就是说代码、操作系统和管理员密码这些东西在各个节点上是不一样的。 23 | 24 | 作者使用了加密技术来防止欺骗攻击和重播攻击,以及检测被破坏的消息。消息包含了公钥签名(其实就是 RSA 算法)、消息验证编码(MAC)和无碰撞哈希函数生成的消息摘要(message digest)。使用 m 表示消息,mi 表示由节点 i 签名的消息,D(m) 表示消息 m 的摘要。按照惯例,只对消息的摘要签名,并且附在消息文本的后面。并且假设所有的节点都知道其他节点的公钥以进行签名验证。 25 | 26 | 系统允许攻击者可以操纵多个失效节点、延迟通讯、甚至延迟正确节点。但是不能无限期地延迟正确的节点,并且算力有限不能破解加密算法。例如,不能伪造正确节点的有效签名,不能从摘要数据反向计算出消息内容,或者找到两个有同样摘要的消息。 27 | message digests:[https://www.techopedia.com/definition/4024/message-digest](https://www.techopedia.com/definition/4024/message-digest) 28 | 29 | 可以理解为 checksum, 对文件内容进行 hash, 如果文件内容被修改则 hash 改变, 用以验证文件是否损坏 / 被修改. MD5, SHA 等算法都可以用作生成 message digest 的算法) 30 | 31 | [Cryptographic hash function](https://en.wikipedia.org/wiki/Cryptographic_hash_function) 32 | 33 | 密码学中将输入数据作为 message, 输出数据, 即 hash value, 记做 message digest = 消息摘要. 34 | 仅对 message digest 进行签名, 而不是对整个 message 进行签名. 35 | **什么是签名** 36 | 非对称加密中使用私钥对消息 + 自己的身份进行加密, 外部节点使用公钥进行解密, 可以看到加密方的身份, 即为签名 37 | 38 | # 3\. 服务属性 39 | 40 | 这部分描述了副本复制服务的特性 41 | 42 | 论文算法实现的是一个具有确定性的副本复制服务,这个服务包括了一个状态(state)和多个操作(operations)。这些操作不仅能够进行简单读写,而且能够基于状态和操作参数进行任意确定性的计算。客户端向副本复制服务发起请求来执行操作,并且阻塞以等待回复。副本复制服务由 n 个节点组成。 43 | 44 | ``` 45 | 针对安全性 46 | 47 | ``` 48 | 49 | 算法在失效节点数量不超过(n-1)/3 的情况下同时保证安全性和活性(safety & liveness)。安全性是指副本复制服务满足线性一致性(linearizability), 就像中心化系统一样原子化执行操作。安全性要求失效副本的数量不超过上限,但是对客户端失效的数量和是否与副本串谋不做限制。系统通过访问控制来限制失效客户端可能造成的破坏,审核客户端并阻止客户端发起无权执行的操作。同时,服务可以提供操作来改变一个客户端的访问权限。因为算法保证了权限撤销操作可以被所有客户端观察到,这种方法可以提供强大的机制从失效的客户端攻击中恢复。 50 | 51 | ``` 52 | 针对活性 53 | 54 | ``` 55 | 56 | 算法不依赖同步提供安全性,因此必须依靠同步提供活性。否则,这个算法就可以被用来在异步系统中实现共识,而这是不可能的(由 Fischer1985 的论文证明)。本文的算法保证活性,即所有客户端最终都会收到针对他们请求的回复,只要失效副本的数量不超过(n-1)/3,并且延迟 delay(t) 不会无限增长。这个 delay(t) 表示 t 时刻发出的消息到它被目标最终接收的时间间隔,假设发送者持续重传直到消息被接收。这时一个相当弱的同步假设,因为在真实系统中网络失效最终都会被修复。但是这就规避了 Fischer1985 提出的异步系统无法达成共识的问题。 57 | 58 | ``` 59 | 下面这段话是关键 60 | 61 | ``` 62 | 63 | 本文的算法是最优的:当存在 f 个失效节点时必须保证存在至少 3f+1 64 | 个副本数量,这样才能保证在异步系统中提供安全性和活性。这么多数量的副本是需要的,因为在同 n-f 个节点通讯后系统必须做出正确判断,由于 f 个副本有可能失效而不发回响应。但是,有可能 f 个失效的节点是好节点,f 个坏节点依然发送请求。尽管如此,系统仍旧需要好节点的返回数量大于坏节点的返回数量,即 n-2f>f,因此得到 n>3f。 65 | 66 | 算法不能解决信息保密的问题,失效的副本有可能将信息泄露给攻击者。在一般情况下不可能提供信息保密,因为服务操作需要使用参数和服务状态处理任意的计算,所有的副本都需要这些信息来有效执行操作。当然,还是有可能在存在恶意副本的情况下通过秘密分享模式(secret sharing scheme)来实现私密性,因为参数和部分状态对服务操作来说是不可见的。 67 | **推理** 68 | 由于有 f 个坏节点, 它们可能完全不回复消息, 所以我们要确保客户端只要接收到 n-f 个消息就足以做出判断. 不能依赖于更多的信息, 否则等待永不回复坏节点就意味着 liveness 被破坏. 69 | 考虑一个最差的情况: 那 f 个还未回复的节点是好节点, 收到的 n-f 个消息中有 f 个坏节点消息. 那么接到的好节点的消息实际上是 n-2f 个, 坏节点消息是 f 个. 根据少数服从多数, 需要 n-2f>f, 即 n>3f. 70 | 综上, 全网总节点数 n 至少是 3f+1 才能确保安全性和 Liveness. 71 | 72 | # 4\. 算法 73 | 74 | PBFT 是一种状态机副本复制算法,即服务作为状态机进行建模,状态机在分布式系统的不同节点进行副本复制。每个状态机的副本都保存了服务的状态,同时也实现了服务的操作。将所有的副本组成的集合使用大写字母 R 表示,使用 0 到 | R|-1 的整数表示每一个副本。为了描述方便,假设 | R|=3f+1,这里 f 是有可能失效的副本的最大个数。尽管可以存在多于 3f+1 个副本,但是额外的副本除了降低性能之外不能提高可靠性。 75 | 76 | 所有的副本在一个被称为视图(View)的轮换过程(succession of configuration)中运作。在某个视图中,一个副本作为主节点(primary),其他的副本作为备份(backups)。视图是连续编号的整数。主节点由公式 p = v mod |R | 计算得到,这里 v 是视图编号,p 是副本编号,|R | 是副本集合的个数。当主节点失效的时候就需要启动视图更换(view change)过程。Viewstamped Replication 算法和 Paxos 算法就是使用类似方法解决良性容错的。 77 | **重要概念: View.** 78 | 节点运行过程中, 全网络的配置 (configuration) 是在不断变化的. 比方说现在的配置是 A 节点是主节点, 其余节点都是从节点, 那么一段时间后, 这个配置可能改变为 B 节点为主节点, 其余从节点. 79 | 我们将每一次的配置状态称为一个 View. 整个网络就是不断在不同的 View 之间切换的. 80 | 记当前 View 的编号为 v, 编号为 p = v % n 的节点就作为主节点, 其余作为从节点. 当主节点失效时, 进行 View 的切换. 81 | 82 | PBFT 算法如下: 83 | 1\. 客户端向主节点发送请求调用服务操作 84 | 2\. 主节点通过广播将请求发送给其他副本 85 | 3\. 所有副本都执行请求并将结果发回客户端 86 | 4\. 客户端需要等待 f+1 个不同副本节点发回相同的结果,作为整个操作的最终结果。 87 | 88 | 同所有的状态机副本复制技术一样,PBFT 对每个副本节点提出了两个限定条件:(1)所有节点必须是确定性的。也就是说,在给定状态和参数相同的情况下,操作执行的结果必须相同;(2)所有节点必须从相同的状态开始执行。在这两个限定条件下,即使失效的副本节点存在,PBFT 算法对所有非失效副本节点的请求执行总顺序达成一致,从而保证安全性。 89 | 90 | 接下去描述简化版本的 PBFT 算法,忽略磁盘空间不足和消息重传等细节内容。并且,本文假设消息验证过程是通过数字签名方法实现的,而不是更加高效的基于消息验证编码(MAC)的方法。 91 | 92 | ## 4.1 客户端 93 | 94 | 客户端 c 向主节点发送 请求执行状态机操作 o,这里时间戳 t 用来保证客户端请求只会执行一次。客户端 c 发出请求的时间戳是全序排列的,后续发出的请求比早先发出的请求拥有更高的时间戳。例如,请求发起时的本地时钟值可以作为时间戳。 95 | 96 | 每个由副本节点发给客户端的消息都包含了当前的视图编号,使得客户端能够跟踪视图编号,从而进一步推算出当前主节点的编号。客户端通过点对点消息向它自己认为的主节点发送请求,然后主节点自动将该请求向所有备份节点进行广播。 97 | 98 | 副本发给客户端的响应为 ,v 是视图编号,t 是时间戳,i 是副本的编号,r 是请求执行的结果。 99 | 100 | 客户端等待 f+1 个从不同副本得到的同样响应,同样响应需要保证签名正确,并且具有同样的时间戳 t 和执行结果 r。这样客户端才能把 r 作为正确的执行结果,因为失效的副本节点不超过 f 个,所以 f+1 个副本的一致响应必定能够保证结果是正确有效的。 101 | 102 | 如果客户端没有在有限时间内收到回复,请求将向所有副本节点进行广播。如果请求已经在副本节点处理过了,副本就向客户端重发一遍执行结果。如果请求没有在副本节点处理过,该副本节点将把请求转发给主节点。如果主节点没有将该请求进行广播,那么就有认为主节点失效,如果有足够多的副本节点认为主节点失效,则会触发一次视图变更。 103 | 104 | 本文假设客户端会等待上一个请求完成才会发起下一个请求,但是只要能够保证请求顺序,可以允许请求是异步的。 105 | 106 | ## 4.2 PBFT 算法主线流程(正常情况) 107 | 108 | 每个副本节点的状态都包含了: 109 | 110 | * 服务的整体状态 111 | * 副本节点上的消息日志 (message log) 包含了该副本节点接受 (accepted) 的消息 112 | * 当前视图编号 v。 113 | 114 | ``` 115 | 请求开始 116 | 117 | ``` 118 | 119 | 当主节点 p 收到客户端的请求 m,主节点将该请求向所有副本节点进行广播,然后展开一个三阶段协议(three-phase protocol)。在这里,如果请求太多, 主节点会将请求 buffer 起来稍后再处理。 120 | 121 | ``` 122 | 三阶段协议 123 | 124 | ``` 125 | 126 | 我们重点讨论预准备(pre-prepare)、准备 (prepare) 和确认 (commit) 这三个阶段。pre-prepare 和 prepare 两个阶段用对同一个视图中的 requests 进行排序(即使对请求进行排序的主节点失效了),prepare 和 commit 两个阶段用来确保在不同的 view 之间的 requests 是严格排序的。 127 | 128 | ``` 129 | 预准备阶段 130 | 131 | ``` 132 | 133 | 在预准备阶段,主节点 (primary) 分配一个序列号 n 给 request,然后向所有 backup node 发送 PRE-PREPARE 消息,PRE-PREPARE 消息的格式为<,m>,这里 v 是视图编号,m 是客户端发送的请求消息,d 是请求消息 m 的摘要。 134 | 135 | 请求 m 本身是不包含在预准备的消息里面的,这样就能使预准备消息足够小,因为预准备消息的目的是作为一种证明,确定该请求是在视图 v 中被赋予了序号 n,从而在视图变更的过程中可以追索。另外一个层面,将 “将排序的协议” 和“广播请求的协议”进行解耦,有利于对消息传输的效率进行深度优化。 136 | 137 | 只有满足以下条件,各个 backup node 才会接受一个 PRE-PREPARE 消息: 138 | 139 | 1. 请求和 pre-prepare 消息的签名都正确, d 是 m 的 digest 140 | 2. 当前视图编号是 v。 141 | 3. 该备份节点从未在视图 v 中接受过序号为 n 但是摘要 d 不同的消息 m。 142 | 4. 编号 n 在下限 h 和上限 H 之间. (这是为了避免坏的主节点通过设置一个超大的 n 来穷尽编号) 143 | 144 | ``` 145 | 进入准备阶段 146 | 147 | ``` 148 | 149 | 如果备份节点 i 接受了预准备消息 <,m>,则进入准备阶段。在准备阶段的同时,该节点向所有 backup node 发送准备消息 < PREPARE,v,n,d,i>,并且将 PRE-PREPARE 消息和 PREPARE 消息写入自己的消息日志。backup node 若不接受 pre-prepare 消息就什么都不做。 150 | 151 | ``` 152 | 接受准备消息需要满足的条件 153 | 154 | ``` 155 | 156 | 包括主节点在内的所有副本节点在收到准备消息之后,1、对消息的签名是否正确,2、视图编号 v 是否一致,3 消息序号 n 是否满限制这三个条件进行验证,如果验证通过则把这个准备消息写入消息日志中。 157 | 158 | ``` 159 | 准备阶段完成的标志 160 | 161 | ``` 162 | 163 | 当且仅当节点 i 将如下消息插入到 message log 后, 我们才认为节点进入准备完毕状态, 记做 prepared(m, v, n, i). 164 | 165 | * 请求 m 166 | * 针对 m, v, n 的 pre-prepare 消息 167 | * 2f 个来自不同从节点 (只有 backup) 的对应于 pre-prepare 的 prepare 消息(算上自己的那个 prepare 消息). 如何验证对应关系? 通过视图编号 v、消息序号 n 和摘要 d。 168 | 169 | **为啥要 2f 个**: 因为 prepare 消息主节点不参与发送, 这样全网 3f+1 个节点刨去主节点和恶意节点 (f+1) 个, 剩下的是 2f 个好节点. 170 | 171 | 预准备阶段和准备阶段确保所有正常节点对同一个视图中的请求排序达成一致。接下去是对这个结论的形式化证明:如果 prepared(m,v,n,i) 为真,则 prepared(m',v,n,j) 必不成立 (i 和 j 表示副本编号,i 可以等于 j),这就意味着至少 f+1 个正常节点在视图 v 的预准备或者准备阶段发送了序号为 n 的消息 m。 172 | **证明:** 173 | 反证法:假如有 prepared(m,v,n,i) 为真且 prepared(m',v,n,j) 也为真,那么可以又接受 prepared 的条件可以看出,有 f+1 个好节点(包括他本身)发出了 prepare 消息接受 m,另有 f+1 个好节点发出了 prepare 接受 m', 这样好节点为 2f+2 但是 N=3f+1, 好节点只有 N-f=2f+1 个,有矛盾。 174 | 175 | ``` 176 | 进入确认阶段 177 | 178 | ``` 179 | 180 | 当 prepared(m,v,n,i) 条件为真的时候,副本 i 将 < COMMIT,v,n,D(m),i > 向其他副本节点广播,于是就进入了确认阶段。每个副本接受确认消息的条件是:1)签名正确;2)消息的视图编号与节点的当前视图编号一致;3)消息的序号 n 满足水线条件,在 h 和 H 之间。一旦确认消息的接受条件满足了,则该副本节点将确认消息写入消息日志中。(补充:需要将针对某个请求的所有接受的消息写入日志,这个日志可以是在内存中的)。 181 | 182 | ``` 183 | committed && committed-local 184 | 185 | ``` 186 | 187 | 我们定义 committed(m,v,n) 为真得条件为:**任意 f+1 个好节点进入 prepared(m,v,n,i) 为真的时候**;(不带节点编号 i 表示是一个整体状态) 188 | committed-local(m,v,n,i) 为真的条件为:**prepared(m,v,n,i) 为真,并且 i 已经接受了 2f+1 个 commit 消息(包括自身在内)和与之对应一致的 pre-prepare 消息**。commit 与 pre-prepare 消息一致的条件是具有相同的视图编号 v、消息序号 n 和消息摘要 d。 189 | 190 | ``` 191 | 确认被接受的形式化描述 192 | 193 | ``` 194 | 195 | commit 阶段保证了以下这个不变式(invariant):**对某个好节点 i 来说,如果 committed-local(m,v,n,i) 为真则 committed(m,v,n) 也为真。**(也就是说只要有一个好节点到了 committed-local 状态则整体就达到了 committed 状态) 196 | **证明:** 197 | 某个好节点 i 达到 committed-local 说明其至少接收了 2f+1 个节点的 commit 消息,所以至少有 f+1 个好节点的 commit 消息。好节点发送 commit 需要保证是已经进入 prepared 状态才行。这样就至少有 f+1 个好节点进入了 prepared 状态,也意味着整体进入了 committed 状态。 198 | 199 | 这个不变式和视图变更协议保证了所有正常节点对本地确认的请求的序号达成一致,即使这些请求在每个节点的确认处于不同的视图。更进一步地讲,只要有一个号节点到达 committed 状态,那么至少有 f+1 个好节点也会到达 committed 状态。 200 | 201 | ``` 202 | 故事的终结 203 | 204 | ``` 205 | 206 | 每个副本节点 i 在 committed-local(m,v,n,i) 为真之后执行 m 的请求,并且 i 的状态反映了所有编号小于 n 的请求依次顺序执行。这就确保了所有正常节点以同样的顺序执行所有请求,这样就保证了算法的正确性(safety)。在完成请求的操作之后,每个副本节点都向客户端发送回复。副本节点会把时间戳比已回复时间戳更小的请求丢弃,以保证请求只会被执行一次。 207 | 208 | 我们不依赖于消息的顺序传递,因此某个副本节点可能乱序确认请求。因为每个副本节点在请求执行之前已经将预准备、准备和确认这三个消息记录到了日志中,这样乱序就不成问题了。 209 | 210 | 下图展示了在没有发生主节点失效的情况下算法的正常执行流程,其中副本 0 是主节点,副本 3 是失效节点,而 C 是客户端。 211 | 212 | ``` 213 | PBFT算法流程 214 | 215 | ``` 216 | 217 | ![](https://upload-images.jianshu.io/upload_images/14844132-8896dea5d525524c) 218 | 219 | 4.3 垃圾回收 220 | = 221 | 为了节省内存,系统需要一种将日志中的无异议消息记录删除的机制。为了保证系统的安全性,副本节点在删除自己的消息日志前,需要确保至少 f+1 个正常副本节点执行了消息对应的请求,并且可以在视图变更时向其他副本节点证明。另外,如果一些副本节点错过部分消息,但是这些消息已经被所有正常副本节点删除了(例如故障恢复),这就需要通过传输部分或者全部服务状态实现该副本节点的同步。因此,副本节点同样需要证明状态的正确性。 222 | 223 | 在每一个操作执行后都生成这样的证明是非常消耗资源的。因此,证明过程只有在请求序号可以被某个常数(比如 100)整除的时候才会周期性地进行。我们将这些请求执行后得到的状态称作检查点(checkpoint),并且将具有证明的检查点称作稳定检查点(stable checkpoint)。 224 | 225 | 副本节点保存了服务状态的多个逻辑拷贝,包括最新的稳定检查点,零个或者多个非稳定的检查点,以及一个当前状态。写时复制技术可以被用来减少存储额外状态拷贝的空间开销。 226 | 227 | 检查点的正确性证明的生成过程如下:当副本节点 i 生成一个检查点后,向其他副本节点广播检查点消息 ,这里 n 是最近一个影响状态的请求序号,d 是状态的摘要。每个副本节点都默默地在各自的日志中收集并记录其他节点发过来的检查点消息,直到收到来自 2f+1 个不同副本节点的具有相同序号 n 和摘要 d 的检查点消息。这 2f+1 个消息就是这个检查点的正确性证明。 228 | 229 | 具有证明的检查点成为稳定检查点,然后副本节点就可以将所有序号小于等于 n 的预准备、准备和确认消息从日志中删除。同时也可以将之前的检查点和检查点消息一并删除。 230 | 231 | 检查点协议可以用来更新水线(watermark)的高低值(h 和 H),这两个高低值限定了可以被接受的消息。水线的低值 h 与最近稳定检查点的序列号相同,而水线的高值 H=h+k,k 需要足够大才能使副本不至于为了等待稳定检查点而停顿。加入检查点每 100 个请求产生一次,k 的取值可以是 200。 232 | 233 | # 4.4 view-change 视图变更 234 | 235 | view-change 存在的意义: 主节点坏掉了, 更换 view(即更换主节点 primary), 以此来保证全网的 liveness. 使用计时器的超时机制触发 view-change。 236 | 237 | ## 从节点触发 View Change 238 | 239 | 视图变更可以由从节点 timeout 触发,以防止从节点无期限地等待请求的执行。从节点等待一个请求,就是该节点接收到一个有效请求,但是还没有执行它。当从节点接收到一个请求但是计时器还未运行,那么它就启动计时器;当它不再等待请求的执行就把计时器停止,但是当它等待其他请求执行的时候再次情动计时器。 240 | 241 | ## 从节点广播 View Change 242 | 243 | 当计时器超时的时候,会触发 view-change,使 v 变为 v+1,并停止服务(除了接收 checkpoint, view-change, and new-view messages)并广播 消息到所有节点(n 为最近的一个 stable checkpoint 对应的请求号,C 表示 2f+1 个有效的 checkpoint 的证明,P 是一个包含若干个 Pm 的集合. 其中 Pm 的定义: 对于编号大于 n 且已经在节点 i prepared 的消息 m, Pm 包含一个合法的 pre-prepare 消息 (不包括对应的 client message) 和 2f 个对应的, 由不同节点签名的 prepare 消息 (v, n, D(m) 要一样))。 244 | 245 | ## 新的主节点上位 246 | 247 | View v+1 的主节点 p 接收到 2f 个来自不同其他节点的 view-change 消息后, 就广播 _sigma_p 到所有节点. 248 | V : 集合 {p 接收到的 view-change 消息, p 发送(或者应该发送) 的对于 v+1 的 view-change 消息} 249 | O: 一个包含若干 pre-prepare 消息的集合 (不顺带请求), 包含如下内容: 250 | 251 | * 主节点从 V 中找到 252 | * min-s, 即最近一次 stable checkpoint 的编号. 253 | * V 中所有 prepare 消息的最大的编号 max-s. 254 | * 对 min-s 和 max-s 之间的每个编号 n, 主节点创建一个新的 pre-prepare 消息, 用于 View v+1\. 两种情况: 255 | * V 中的某个编号为 n 的 view-change 消息, 其 P 集合不为空. 这种情况下, 主节点创建一个新的 pre-prepare 消息 _sigma_p 256 | * 其 P 集合为空. 主节点创建一个新的 pre-prepare 消息 _sigma_p, 其中 d^null 是一个特殊的 null 请求的 digest. 该操作什么都不做. 257 | 258 | 然后, 主节点将 O 中的信息插入到 log 中. 如果 min-s 大于它最近一个 checkpoint 的编号, 主节点要计算编号为 min-s 的 checkpoint 的 proof, 并将其插入到 log 中, 然后按照 4.3 中所讲进行垃圾回收. 259 | 至此, 主节点正式进入 View v+1, 可以接受关于 v+1 的消息. 260 | 261 | ## 从节点接受新的主节点 262 | 263 | 如果从节点接受的 new-view 消息满足如下条件 264 | 265 | * 签名合法 266 | * V 中的 view-change 消息合法. 267 | * O 合法 (算法类似于主节点创建 O 的算法) 268 | 269 | 从节点会将这些消息加入到 log 中 (跟上一段的主节点操作一样), 然后对于 O 中的每一个 pre-prepare 消息, 广播对应的 prepare 消息到所有节点并插入 log, 正是进入 View v+1. -------------------------------------------------------------------------------- /epbft/tendermint拜占庭共识算法.md: -------------------------------------------------------------------------------- 1 | ### 状态机概述 2 | 在每一个区块高度上, 基于多轮协议来决定下一个区块。每一轮有三个步骤(提案(`Propose`), 预投票(`Provote`), 预提交(`Precommit`)), 以及两个特殊的步骤Commit和NewHeight 3 | 所以在tendermint的区块打包始终是按着下面的顺序进行的: 4 | ``` 5 | NewHeight->(Propose -> Prevote -> Precommit)[一次区块确认可能需要多轮] -> Commit ->NewHeight 6 | ``` 7 | 8 | (Propose -> Prevote -> Precommit)被称为一轮。 所以说在tendermint中如果想要确认一个新的区块, 必须经历至少一轮这样的顺序。 一些情况下可能需要多轮才能决定出一个区块: 9 | 10 | - 指定的提议者(指轮到其进行提案的验证者)不在线 11 | - 被提议者提议的区块时无效的 12 | - 被提议者提议的区块没有按时广播 13 | - 提议的区块时有效的, 但是没有在规定时间内收到大于2/3个节点进行的预投票。 14 | - 提议的区块时有效的, 但是没有在规定时间内收到大于2/3个节点进行的预提交。 15 | 16 | #### PoLC 17 | 超过2/3的预投票为一个提议的区块或者nill, 将会进行锁定, proof-of-lock-change 。 也就是说当一个验证者在正常提议正常投票之后, 当它收到了超过2/3的投票他就会进入PoLC的锁定。 18 | 19 | ### 框图 20 | ```shell 21 | (Wait til `CommmitTime+timeoutCommit`) 22 | +-------------------------------------+ 23 | v | 24 | +-----------+ +-----+-----+ 25 | +----------> | Propose +--------------+ | NewHeight | 26 | | +-----------+ | +-----------+ 27 | | | ^ 28 | |(Else, after timeoutPrecommit) v | 29 | +-----+-----+ +-----------+ | 30 | | Precommit | <------------------------+ Prevote | | 31 | +-----+-----+ +-----------+ | 32 | |(When +2/3 Precommits for block found) | 33 | v | 34 | +--------------------------------------------------------------------+ 35 | | Commit | 36 | | | 37 | | * Set CommitTime = now; | 38 | | * Wait for block, then stage/save/commit block; | 39 | +--------------------------------------------------------------------+ 40 | ``` 41 | 42 | ### 背景知识 43 | 44 | #### 验证节点 45 | tendermint中并不是所有节点都是验证节点(如果均是验证节点也不可能达到那么高的TPS), 只有那些参与上述区块打包过程的节点才会成为验证节点。 但是这并不表示其他节点没有作用。 其他非验证节点可以对相关的区块数据, 提议, 投票进行广播。 保证了网络的健壮性。 46 | 47 | #### 共识广播的策略 48 | 49 | > 节点会广播一个提议者在当前轮提交的区块。 但是它只会广播区块数据的Parset部分(tendermint的区块数据结构在之前文档有提到过) 50 | > 节点会广播预投票和预提交阶段的投票。 51 | > 如果一个节点处于PoLC(proof-of-lock-change: 指当前验证者已经锁定了自己的改变, 表示在下一轮投票时, 它依然会坚持目前的投票)阶段。 他会在预投票阶段继续投票之前锁定的提议的区块。 52 | > 节点会向那些之后的节点广播已经提交的区块 53 | > 节点会找机会广播HasVote指令用于获取对方已经拥有的投票 54 | > 节点会广播当前它的状态。 55 | 56 | 57 | #### 何为tendermint提案 58 | 提案是有特定的提议者在每一轮进行的将要打包的提议, 对其进行签名和广播。 提案者是确定性的。 一个在(H,R)(H高度R轮)的提案是由一个区块和可选的PoLC组成。 如果有PoLC说明之前此提案已经有超过2/3的节点进行了投票通过。 59 | 60 | ### 状态机步骤说明 61 | 62 | 通用的退出情况 63 | 1. 超过2/3节点预提交投票在(H, R) 跳到Commit(H, R) --- 表示在本轮已经收到了2/3的节点预投票 那么直接可以进行Commit了 64 | 2. 超过2/3节点进行了预投票在(H, R+X) 跳到Prevote(H, R+x) --- 表示有更新的轮进入到了预投票阶段了 65 | 3. 超过2/3的预提交投票在(H, R+X) 跳到PreCommit(H, R+x) -- 表示更新的一轮已经进入到预提交阶段 66 | 67 | #### Propose Step(H, R) 68 | 提议者提出一个区块。 根据下面几种情况进入下个步骤: 69 | 1. 提案超时 直接跳到PreVote(H, R) 70 | 2. 已经接收到提议的区块, 并且此提案已经锁定(PoLC) 跳到PreVote(H, R) 71 | 3. 通用的退出情况 72 | 73 | 74 | #### Prevote Step (H, R) 75 | 每一个验证者进入PreVote阶段时, 会广播自己的预投票。 76 | 如果验证者目前处于锁定阶段, 而且现在有一个新的锁定出现, 并且新的锁定比之前的锁定轮数大并且小于当前轮, 那么此验证者解锁 77 | 如果验证者处于锁定, 但是不满足上面的情况, 则会直接投票当前锁定的区块 78 | 如果验证者不处于锁定阶段,并且当前提案的区块是有效的 则投票给此处收到的提议区块。 79 | 如果验证者不处于锁定阶段,并且当前提案的区块是有效的 则投票给nill。 80 | 81 | 预投票阶段根据下面几种情况进入下一个步骤: 82 | 1. 超过2/3的预投票为此提案区块或者nill 则进入PreCommit(H,R)阶段 83 | 2. 预投票超时 进入PreCommit(H,R)阶段 ---也就是说在规定时间没有收到超过2/3的预投票 84 | 3. 通用退出情况 85 | 86 | #### Precommit Step (height:H,round:R) 87 | 在PreCommit阶段, 验证者广播预提交的选票: 88 | if 验证者在此轮锁定了区块, 那么它就会进行预提交投票此区块。并且设置LastLockRound=R 89 | else if 验证者在此轮锁定了nill, 那么它会解锁并且预提交投票给nill 90 | else 保持之前锁定不变, 投票nill 91 | 92 | 根据情况进入下一个步骤: 93 | 1. 超过2/3的节点预提交为nill 进入ProPose(H, R+1) 94 | 2. 规定时间没有收到足够多的预提交 进入ProPose(H, R+1) 95 | 3. 通用退出情况 96 | 97 | #### Commit Step (height:H) 98 | 设置CommitTime = now() 99 | 等待这个区块内容接收到, 进入NewHeight(H+1) -------------------------------------------------------------------------------- /evm移植/evm之实战.md: -------------------------------------------------------------------------------- 1 | 2 | ### 简述 3 | 前面我们从一个智能合约的部署流程,到智能合约的字节码流程分析,再到evm虚拟机的源码分析。 整个分析其实就是为了移植虚拟机做基础。 如果看了前面几篇文章在来进行代码移植就会跟得心应手一些。 4 | 5 | ### 说明 6 | 因为涉及到的代码会比较多, 不可能把所有代码都列举出来。 所以也只是挑关键的部分进行讲解说明。 整个移植的代码我已经合到之前的那个简单(无用)[demo](https://github.com/blockchainworkers/conch)版本的公链项目上了。 7 | 移植的以太坊版本为v1.8.12. 8 | 9 | ### 开始移植 10 | 首先先创建一个go的新项目, 将go-ethereum项目下core/vm文件夹下的代码全部拷贝到新项目下,我们为新的文件夹名称为cvm。 保存之后, 假设你用的是vscode(带上了go的周边插件)或者goland,这个时候你会发现有大量的报错。 没有关系, 因为很多以太坊包还没有被导入进来。 但是呢, 既然我们只想移植虚拟机部分,又不引入以太坊的其他模块。 这个时候我们就把需要的包直接拷贝到我们的项目中。 彻底分离和go-ethereum的关系。这里需要说明一下, 虽然是开源项目, 拷贝和使用别人的开源代码也要注意license的。 11 | 12 | 1. 主要需要拷贝的包如下 13 | * go-ethereum/common这个文件夹, 我们也将其这个内容均拷贝到cvm这个文件夹下。 14 | * go-ethereum/params这个文件夹中的gas_tables.go, protocol_params.go两个文件拷贝到cvm/params文件夹下。 15 | * 创建log.go文件在cvm/types/下, 该文件中主要是智能合约emit提交事件时使用的log对象。 16 | 内容如下 17 | ```go 18 | type Log struct { 19 | // Consensus fields: 20 | // address of the contract that generated the event 21 | Address common.Address `json:"address" gencodec:"required"` 22 | // list of topics provided by the contract. 23 | Topics []common.Hash `json:"topics" gencodec:"required"` 24 | // supplied by the contract, usually ABI-encoded 25 | Data []byte `json:"data" gencodec:"required"` 26 | 27 | // Derived fields. These fields are filled in by the node 28 | // but not secured by consensus. 29 | // block in which the transaction was included 30 | BlockNumber uint64 `json:"blockNumber"` 31 | // hash of the transaction 32 | TxHash common.Hash `json:"transactionHash" gencodec:"required"` 33 | // index of the transaction in the block 34 | TxIndex uint `json:"transactionIndex" gencodec:"required"` 35 | // hash of the block in which the transaction was included 36 | BlockHash common.Hash `json:"blockHash"` 37 | // index of the log in the receipt 38 | Index uint `json:"logIndex" gencodec:"required"` 39 | 40 | // The Removed field is true if this log was reverted due to a chain reorganisation. 41 | // You must pay attention to this field if you receive logs through a filter query. 42 | Removed bool `json:"removed"` 43 | } 44 | ``` 45 | 46 | 2. 在cvm目录下创建vm.go文件, 主要是生成evm的上下文对象。 47 | ``` 48 | // NewEVMContext creates a new context for use in the EVM. 49 | func NewEVMContext(from common.Address, blockNum, timeStamp, difficulty int64) vm.Context { 50 | // If we don't have an explicit author (i.e. not mining), extract from the header 51 | return vm.Context{ 52 | CanTransfer: CanTransfer, 53 | Transfer: Transfer, 54 | GetHash: GetHashFn(), 55 | Origin: from, 56 | Coinbase: common.Address{}, 57 | BlockNumber: new(big.Int).Set(big.NewInt(blockNum)), 58 | Time: new(big.Int).Set(big.NewInt(timeStamp)), 59 | Difficulty: new(big.Int).Set(big.NewInt(difficulty)), 60 | GasLimit: 0xfffffffffffffff, //header.GasLimit, 61 | GasPrice: new(big.Int).Set(big.NewInt(10)), 62 | } 63 | } 64 | 65 | // GetHashFn returns a GetHashFunc which retrieves header hashes by number 获取块号码对于的块hash 66 | func GetHashFn() func(n uint64) common.Hash { 67 | 68 | return func(n uint64) common.Hash { 69 | // If there's no hash cache yet, make one 70 | // if cache == nil { 71 | // cache = map[uint64]common.Hash{ 72 | // ref.Number.Uint64() - 1: ref.ParentHash, 73 | // } 74 | // } 75 | // // Try to fulfill the request from the cache 76 | // if hash, ok := cache[n]; ok { 77 | // return hash 78 | // } 79 | // // Not cached, iterate the blocks and cache the hashes 80 | // for header := chain.GetHeader(ref.ParentHash, ref.Number.Uint64()-1); header != nil; header = chain.GetHeader(header.ParentHash, header.Number.Uint64()-1) { 81 | // cache[header.Number.Uint64()-1] = header.ParentHash 82 | // if n == header.Number.Uint64()-1 { 83 | // return header.ParentHash 84 | // } 85 | // } 86 | return common.Hash{} 87 | } 88 | } 89 | 90 | // CanTransfer checks wether there are enough funds in the address' account to make a transfer. 91 | // This does not take the necessary gas in to account to make the transfer valid. 92 | func CanTransfer(db vm.StateDB, addr common.Address, amount *big.Int) bool { 93 | return db.GetBalance(addr).Cmp(amount) >= 0 94 | } 95 | 96 | // Transfer subtracts amount from sender and adds amount to recipient using the given Db 97 | func Transfer(db vm.StateDB, sender, recipient common.Address, amount *big.Int) { 98 | db.SubBalance(sender, amount) 99 | db.AddBalance(recipient, amount) 100 | } 101 | ``` 102 | 103 | 3. 接着我们把go-ethereum/account文件夹下的abi内容拷贝到cvm文件夹下。 104 | 105 | > abi/bind文件内容可以直接删除掉。此文件夹下是对智能合约进行函数调用进行编码的包。换句话调用智能合约构建的交易中的input内容就是需要此包中函数来生成的。当然如果你看了前面的文章,对智能合约调用了解的话,此处自然就理解这个包的作用了。 106 | 107 | 到了这里整个需要拷贝的文件就齐全了, 目录结构如下: 108 | 109 | ![WechatIMG5.jpeg](../img/7C4BA46600C364D522BBB519B09F7689.jpg) 110 | 111 | 4. 接下来我们需要修改evm的部分代码了。 112 | 113 | > vm/contracts.go文件我们直接删除掉。 这个是自带的智能合约, 内部主要是一些内置函数, 注意实际使用的时候记得还是要实现的, 114 | evm.go文件中run函数忽略掉所有内置的合约函数。 115 | 116 | 117 | ![WechatIMG6.jpeg](../img/1F50374E4165D3F086F83F28A4027EC4.jpg) 118 | 119 | 120 | 121 | 修改evm.go文件中的Call函数 当地址不存在时我们直接认为是创建地址, 忽略掉掉内置合约。 122 | ![WechatIMG7.jpeg](../img/2046B1E1C9D205D48070AD7A62FD3624.jpg) 123 | 124 | 125 | 5. 接着我们要实现evm.StateDB接口的内容了, 因为此接口涉及涉及的主要是个账户状态相关的内容, 也即是说可整个区块的存储是有关联的, 暂时我也只能以一个示例来说明是如何简单的实现这些接口。 126 | 127 | 我们在cvm文件夹下创建一个account_state.go的文件。 定义的数据结构格式如下: 128 | ```go 129 | type accountObject struct { 130 | Address common.Address `json:"address,omitempty"` 131 | AddrHash common.Hash `json:"addr_hash,omitempty"` // hash of ethereum address of the account 132 | ByteCode []byte `json:"byte_code,omitempty"` 133 | Data accountData `json:"data,omitempty"` 134 | CacheStorage map[common.Hash]common.Hash `json:"cache_storage,omitempty"` // 用于缓存存储的变量 135 | } 136 | 137 | type accountData struct { 138 | Nonce uint64 `json:"nonce,omitempty"` 139 | Balance *big.Int `json:"balance,omitempty"` 140 | Root common.Hash `json:"root,omitempty"` // merkle root of the storage trie 141 | CodeHash []byte `json:"code_hash,omitempty"` 142 | } 143 | 144 | //AccountState 实现vm的StateDB的接口 用于进行测试 145 | type AccountState struct { 146 | Accounts map[common.Address]*accountObject `json:"accounts,omitempty"` 147 | } 148 | 149 | 150 | ``` 151 | 152 | 6. 接下来我们实现StateDB接口: 153 | 154 | ```go 155 | // CreateAccount 创建账户接口 156 | func (accSt *AccountState) CreateAccount(addr common.Address) { 157 | if accSt.getAccountObject(addr) != nil { 158 | return 159 | } 160 | obj := newAccountObject(addr, accountData{}) 161 | accSt.setAccountObject(obj) 162 | } 163 | 164 | // SubBalance 减去某个账户的余额 165 | func (accSt *AccountState) SubBalance(addr common.Address, amount *big.Int) { 166 | stateObject := accSt.getOrsetAccountObject(addr) 167 | if stateObject != nil { 168 | stateObject.SubBalance(amount) 169 | } 170 | } 171 | 172 | // AddBalance 增加某个账户的余额 173 | func (accSt *AccountState) AddBalance(addr common.Address, amount *big.Int) { 174 | stateObject := accSt.getOrsetAccountObject(addr) 175 | if stateObject != nil { 176 | stateObject.AddBalance(amount) 177 | } 178 | } 179 | 180 | //// GetBalance 获取某个账户的余额 181 | func (accSt *AccountState) GetBalance(addr common.Address) *big.Int { 182 | stateObject := accSt.getOrsetAccountObject(addr) 183 | if stateObject != nil { 184 | return stateObject.Balance() 185 | } 186 | return new(big.Int).SetInt64(0) 187 | } 188 | //GetNonce 获取nonce 189 | func (accSt *AccountState) GetNonce(addr common.Address) uint64 { 190 | stateObject := accSt.getAccountObject(addr) 191 | if stateObject != nil { 192 | return stateObject.Nonce() 193 | } 194 | return 0 195 | } 196 | 197 | // SetNonce 设置nonce 198 | func (accSt *AccountState) SetNonce(addr common.Address, nonce uint64) { 199 | stateObject := accSt.getOrsetAccountObject(addr) 200 | if stateObject != nil { 201 | stateObject.SetNonce(nonce) 202 | } 203 | } 204 | 205 | // GetCodeHash 获取代码的hash值 206 | func (accSt *AccountState) GetCodeHash(addr common.Address) common.Hash { 207 | stateObject := accSt.getAccountObject(addr) 208 | if stateObject == nil { 209 | return common.Hash{} 210 | } 211 | return common.BytesToHash(stateObject.CodeHash()) 212 | } 213 | 214 | //GetCode 获取智能合约的代码 215 | func (accSt *AccountState) GetCode(addr common.Address) []byte { 216 | stateObject := accSt.getAccountObject(addr) 217 | if stateObject != nil { 218 | return stateObject.Code() 219 | } 220 | return nil 221 | } 222 | 223 | //SetCode 设置智能合约的code 224 | func (accSt *AccountState) SetCode(addr common.Address, code []byte) { 225 | stateObject := accSt.getOrsetAccountObject(addr) 226 | if stateObject != nil { 227 | stateObject.SetCode(crypto.Sha256(code), code) 228 | } 229 | } 230 | 231 | // GetCodeSize 获取code的大小 232 | func (accSt *AccountState) GetCodeSize(addr common.Address) int { 233 | stateObject := accSt.getAccountObject(addr) 234 | if stateObject == nil { 235 | return 0 236 | } 237 | if stateObject.ByteCode != nil { 238 | return len(stateObject.ByteCode) 239 | } 240 | return 0 241 | } 242 | 243 | // AddRefund 暂时先忽略补偿 244 | func (accSt *AccountState) AddRefund(uint64) { 245 | return 246 | } 247 | 248 | //GetRefund ... 249 | func (accSt *AccountState) GetRefund() uint64 { 250 | return 0 251 | } 252 | 253 | // GetState 和SetState 是用于保存合约执行时 存储的变量是否发生变化 evm对变量存储的改变消耗的gas是有区别的 254 | func (accSt *AccountState) GetState(addr common.Address, key common.Hash) common.Hash { 255 | stateObject := accSt.getAccountObject(addr) 256 | if stateObject != nil { 257 | return stateObject.GetStorageState(key) 258 | } 259 | return common.Hash{} 260 | } 261 | 262 | // SetState 设置变量的状态 263 | func (accSt *AccountState) SetState(addr common.Address, key common.Hash, value common.Hash) { 264 | stateObject := accSt.getOrsetAccountObject(addr) 265 | if stateObject != nil { 266 | fmt.Printf("SetState key: %x value: %s", key, new(big.Int).SetBytes(value[:]).String()) 267 | stateObject.SetStorageState(key, value) 268 | } 269 | } 270 | 271 | // Suicide 暂时禁止自杀 272 | func (accSt *AccountState) Suicide(common.Address) bool { 273 | return false 274 | } 275 | 276 | // HasSuicided ... 277 | func (accSt *AccountState) HasSuicided(common.Address) bool { 278 | return false 279 | } 280 | 281 | // Exist 检查账户是否存在 282 | func (accSt *AccountState) Exist(addr common.Address) bool { 283 | return accSt.getAccountObject(addr) != nil 284 | } 285 | 286 | //Empty 是否是空账户 287 | func (accSt *AccountState) Empty(addr common.Address) bool { 288 | so := accSt.getAccountObject(addr) 289 | return so == nil || so.Empty() 290 | } 291 | 292 | // RevertToSnapshot ... 293 | func (accSt *AccountState) RevertToSnapshot(int) { 294 | 295 | } 296 | 297 | // Snapshot ... 298 | func (accSt *AccountState) Snapshot() int { 299 | return 0 300 | } 301 | 302 | // AddLog 添加事件触发日志 303 | func (accSt *AccountState) AddLog(log *types.Log) { 304 | fmt.Printf("log: %v", log) 305 | } 306 | 307 | // AddPreimage 308 | func (accSt *AccountState) AddPreimage(common.Hash, []byte) { 309 | 310 | } 311 | 312 | // ForEachStorage 暂时没发现vm调用这个接口 313 | func (accSt *AccountState) ForEachStorage(common.Address, func(common.Hash, common.Hash) bool) { 314 | 315 | } 316 | 317 | // Commit 进行持久存储 这里我们只将其简单的json话之后保存到本地磁盘中。 318 | func (accSt *AccountState) Commit() error { 319 | // 将bincode写入文件 320 | file, err := os.Create("./account_sate.db") 321 | if err != nil { 322 | return err 323 | } 324 | err = json.NewEncoder(file).Encode(accSt) 325 | //fmt.Println("len(binCode): ", len(binCode), " code: ", binCode) 326 | // bufW := bufio.NewWriter(file) 327 | // bufW.Write(binCode) 328 | // // bufW.WriteByte('\n') 329 | // bufW.Flush() 330 | file.Close() 331 | return err 332 | } 333 | 334 | //TryLoadFromDisk 尝试从磁盘加载AccountState 335 | func TryLoadFromDisk() (*AccountState, error) { 336 | file, err := os.Open("./account_sate.db") 337 | if err != nil && os.IsNotExist(err) { 338 | return NewAccountStateDb(), nil 339 | } 340 | if err != nil { 341 | return nil, err 342 | } 343 | 344 | // stat, _ := file.Stat() 345 | // // buf := stat.Size() 346 | var accStat AccountState 347 | 348 | err = json.NewDecoder(file).Decode(&accStat) 349 | return &accStat, err 350 | } 351 | 352 | ``` 353 | 354 | 355 | 7. 接下来尝试部署两份智能合约进行测试: 356 | ```js 357 | pragma solidity ^0.4.21; 358 | interface BaseInterface { 359 | function CurrentVersion() external view returns(string); 360 | } 361 | 362 | contract Helloworld { 363 | uint256 balance; 364 | event Triggle(address, string); 365 | mapping(address=>uint256) _mapamount; 366 | 367 | constructor() public { 368 | balance = 6000000000; 369 | _mapamount[0] = 100; 370 | _mapamount[1] = 200; 371 | } 372 | 373 | function getbalance() public returns (address, uint256) { 374 | emit Triggle(msg.sender, "funck"); 375 | return (msg.sender, balance--); 376 | } 377 | 378 | function onlytest() public{ 379 | _mapamount[1] = 100; 380 | emit Triggle(msg.sender, "onlytest"); 381 | } 382 | 383 | function setBalance(uint256 tmp) public { 384 | balance = tmp; 385 | } 386 | 387 | function getVersion(address contractAddr) public view returns (string) { 388 | BaseInterface baseClass = BaseInterface(contractAddr); 389 | return baseClass.CurrentVersion(); 390 | } 391 | 392 | } 393 | 394 | pragma solidity ^0.4.21; 395 | 396 | contract BaseContract { 397 | address public owner; 398 | // 399 | function CurrentVersion() pure public returns(string) { 400 | return "BaseContractV0.1"; 401 | } 402 | } 403 | ``` 404 | 405 | 通过这两个合约我们就可以测试到一些view 类型的函数调用, 一些对数据状态有修改的合约调用, 和跨合约的调用。 406 | 我们可以将上面的两个合约通过ethereum官方出品的remix进行编译,得到字节码。因为BaseContract没有涉及初始化的内容 所以我们可以直接使用runtime的bytecode。 407 | 不过我们直接使用Create函数去部署合约。 408 | ``` 409 | runtimeBytecode, contractAddr, leftgas, err := vmenv.Create(vm.AccountRef(normalAccount), helloCode, 10000000000, big.NewInt(0)) 410 | ``` 411 | 第一个返回值其实就是需要部署的runtime字节码 , 我们调用```stateDb.SetCode(helloWorldcontactAccont, runtimeBytecode)```将其部署。 412 | 413 | ```go 414 | var normalAddress, _ = hex.DecodeString("123456abc") 415 | var hellWorldcontractAddress, _ = hex.DecodeString("987654321") 416 | var baseContractAddress, _ = hex.DecodeString("038f160ad632409bfb18582241d9fd88c1a072ba") 417 | var normalAccount = common.BytesToAddress(normalAddress) 418 | var helloWorldcontactAccont = common.BytesToAddress(hellWorldcontractAddress) 419 | var baseContractAccont = common.BytesToAddress(baseContractAddress) 420 | 421 | // 基本账户字节码 422 | var baseCodeStr = "608060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632b225f29146100675780638afc3605146100f75780638da5cb5b1461010e578063f2fde38b14610165575b600080fd5b34801561007357600080fd5b5061007c6101a8565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156100bc5780820151818401526020810190506100a1565b50505050905090810190601f1680156100e95780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561010357600080fd5b5061010c6101e5565b005b34801561011a57600080fd5b50610123610227565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b34801561017157600080fd5b506101a6600480360381019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919050505061024c565b005b60606040805190810160405280601081526020017f42617365436f6e747261637456302e3100000000000000000000000000000000815250905090565b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415156102a757600080fd5b600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff16141515156102e357600080fd5b806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055508073ffffffffffffffffffffffffffffffffffffffff166000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e060405160405180910390a3505600a165627a7a723058208c3064096245894122f6bcf5e2ee12e30d4775a3b8dca0b21f10d5a5bc386e8b0029" 423 | 424 | // hellworld 账户字节码 425 | var hellCodeStr = "6080604052600436106100615763ffffffff7c01000000000000000000000000000000000000000000000000000000006000350416634d9b3d5d81146100665780637e8800a7146100ab578063c3f82bc3146100c2578063fb1669ca14610165575b600080fd5b34801561007257600080fd5b5061007b61017d565b6040805173ffffffffffffffffffffffffffffffffffffffff909316835260208301919091528051918290030190f35b3480156100b757600080fd5b506100c06101fa565b005b3480156100ce57600080fd5b506100f073ffffffffffffffffffffffffffffffffffffffff6004351661028f565b6040805160208082528351818301528351919283929083019185019080838360005b8381101561012a578181015183820152602001610112565b50505050905090810190601f1680156101575780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561017157600080fd5b506100c0600435610389565b60408051338152602081018290526005818301527f66756e636b0000000000000000000000000000000000000000000000000000006060820152905160009182917f08c31d20d5c3a5f2cfe0adf83909e6411f43fe97eb091e15c12f3e5a203e8fde9181900360800190a150506000805460001981019091553391565b600080526001602090815260647fa6eef7e35abe7026729641147f7915573c7e97b47efa546f5f6e3230263bcb4955604080513381529182018190526008828201527f6f6e6c79746573740000000000000000000000000000000000000000000000006060830152517f08c31d20d5c3a5f2cfe0adf83909e6411f43fe97eb091e15c12f3e5a203e8fde9181900360800190a1565b606060008290508073ffffffffffffffffffffffffffffffffffffffff16632b225f296040518163ffffffff167c0100000000000000000000000000000000000000000000000000000000028152600401600060405180830381600087803b1580156102fa57600080fd5b505af115801561030e573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f19168201604052602081101561033757600080fd5b81019080805164010000000081111561034f57600080fd5b8201602081018481111561036257600080fd5b815164010000000081118282018710171561037c57600080fd5b5090979650505050505050565b6000555600a165627a7a72305820c63a859d93a3512b52ccaec75bb9aa146648c41b21c8a0cd0cd2e2c1aede35ed0029" 426 | 427 | var helloCode, _ = hex.DecodeString(hellCodeStr) 428 | var baseCode, _ = hex.DecodeString(baseCodeStr) 429 | 430 | func updateContract() { 431 | // 加载账户State 432 | stateDb, err := cvm.TryLoadFromDisk() 433 | if err != nil { 434 | panic(err) 435 | } 436 | stateDb.SetCode(helloWorldcontactAccont, helloCode) 437 | stateDb.SetCode(baseContractAccont, baseCode) 438 | fmt.Println(stateDb.Commit()) 439 | } 440 | ``` 441 | 442 | 当我们调用一个智能合约比如getbalance函数, 代码类似下面这样: 443 | 444 | ```go 445 | // 4d9b3d5d : getbalance 7e8800a7: onlytest fb1669ca000000000000000000000000000000000000000000000000000000000000029a: setbalance 666 446 | var input, _ = hex.DecodeString("7e8800a7") 447 | 448 | func main() { 449 | // updateContract() 450 | // return 451 | // 创建账户State 452 | stateDb, err := cvm.TryLoadFromDisk() 453 | if err != nil { 454 | panic(err) 455 | } 456 | 457 | evmCtx := cvm.NewEVMContext(normalAccount, 100, 1200000, 1) 458 | vmenv := vm.NewEVM(evmCtx, stateDb, vm.Config{}) 459 | 460 | ret, leftgas, err := vmenv.Call(vm.AccountRef(normalAccount), helloWorldcontactAccont, input, 1000000, big.NewInt(0)) 461 | fmt.Printf("ret: %v, usedGas: %v, err: %v, len(ret): %v, hexret: %v, ", ret, 1000000-leftgas, err, len(ret), hex.EncodeToString(ret)) 462 | 463 | abiObjet, _ := abi.JSON(strings.NewReader(hellWorldContractABIJson)) 464 | 465 | // begin, length, _ := lengthPrefixPointsTo(0, ret) 466 | addr := new(common.Address) 467 | 468 | value := big.NewInt(0) //new(*big.Int) 469 | restult := []interface{}{addr, &value} 470 | fmt.Println(abiObjet.Unpack(&restult, "getbalance", ret)) 471 | //fmt.Println(unpackAtomic(&restult, string(ret[begin:begin+length]))) 472 | println(restult[0].(*common.Address).String(), (*restult[1].(**big.Int)).String()) 473 | fmt.Println(stateDb.Commit()) 474 | 475 | } 476 | ``` 477 | 478 | ### 最后 479 | 480 | 到了这里, evm移植流程就算完成了。 如果理解evm执行的原理, 大部分的工作其实就是拷贝, 出错的任务。 当然这个移植后的代码肯定是不能在生产中使用的, 但是需要修改和添加的代码主要也就是上文提到的内容。 最后还是想说明白原理和流程就是成功了一大半, 后面的部分主要就是调试和排错的过程了。 481 | 482 | 483 | 484 | -------------------------------------------------------------------------------- /evm移植/evm之总结.md: -------------------------------------------------------------------------------- 1 | ### 为什么会写这个系列的文章 2 | 虽说是作为一个coder, 其实大部分时间在做学习和研究工作,然后一小部分时间是在写代码来实现想要的功能。 回顾自己的工作, 发现除了留下一堆代码好像可视化的文档少之又少。 留意身边的同事大都很反感写文档。 其实文档是一个很好的锻炼自己的方式, 一来可以将之间学习的到知识再一次梳理和巩固一遍。 二来以后需要的时候可以随时查看,比去翻代码强多了。三来纵观各位程序员大佬都有写文章的习惯, 虽说不能达到一些大神的高度,学习其习惯总是没有错的。 3 | 4 | ### 这个系列的文章的收获有哪些 5 | 6 | 1. 对编译原理和计算机结构有更深入的了解。 7 | > 在看evm的源码之前,其实也有做过很多功课。 当时也有想法去实现一个新的智能合约虚拟机。 只是难度太大, 个人精力和时间都有限, 遂放弃。但是编译原理的知识对阅读整个evm的帮助很大。在这里推荐一本书叫做<> 算是真正的使用Go写一个解释器的实战项目。从token解析, 到语义分析到生成抽象语法树以及最终解释器的实现都有详细的说明。 真的是实战派的好帮手。 8 | 9 | 2. 对整个以太坊的智能合约理解更透彻 10 | 在之前使用以太坊的大部分工作都是查看RPC接口, 调用接口或者官方封装的方法。 很多过程都不是很理解, 比如调用合约到底是如何一个流程, 创建的input的数据是怎么打包的, abi到底是个什么东西。 在整个代码看完之后,其实这些自然就不在是问题。 11 | 12 | 3. 善用调试工具 13 | 在前在使用go的过程中,很少会用到调试工具, 主要是golang的goroutine模型会导致调试的时候程序莫名其妙就跑飞了。 gdb在其上的支持并不是很完善。 后来的dlv依然有这些问题, 久而久之打日志倒是使用最频繁的调试方法了。 但是evm的整个流程并不涉及任何并发, 所以非常适合使用调试工具进行调试。整个调试的时候你会明显的看到整个PC计数器,栈的状态。对于理解evm非常有帮助。 14 | 15 | 4. 对知识更深一步巩固 16 | 很多时候我们理解一个知识点是一方面,但是将其用文章描述出来又是另外一方面。 写文章的过程其实更是一种巩固和消化的过程。 整个过程就好像你要把你知道的东西尽最大能力通俗准确的解释给其他人。 17 | 18 | ### 是否还会有后续 19 | 20 | evm的移植文章暂时也就写到这里,一来自己大部分的工作并不是围绕着智能合约展开的。 二来好像大部分人对这个方面的关注也比较少好像并部分给别人提供到帮助, 三来自己也已经了解整个过程也自己动手实践过算是已达到目的。 21 | 22 | ### 最后 23 | 如果你能看到这篇文章, 那真的应该是有缘人。 区块链寒冬, 期望能耐得住寂寞好好磨练功力。大家共勉之! 24 | 25 | -------------------------------------------------------------------------------- /evm移植/evm之操作码分析.md: -------------------------------------------------------------------------------- 1 | ### evm概述 2 | evm的操作码和其他汇编语言的指令码类似。 只是一般的CPU是哈弗架构或者冯诺依曼架构。 evm是基于栈式结构, 大端序的256bit的虚拟机。 每一个字节码是一个字节。也即是说evm的操作码指令集不会超过256个。 [这个网站](https://ethervm.io/)列出了evm的所有操作码,和相关的栈操作。 3 | 我们找出几个操作码来看一下。 4 | ![WechatIMG1.jpeg](../img/evm-code.jpg) 5 | 6 | ADD指令的十六进制数字表示为0x01 需要操作的栈的数量为两个。 最终入栈的数据剩下一个。 7 | 这样看起来好像没什么感觉, 我们来用一个简单的智能合约编译之后分析一下其运行流程。 8 | 9 | ### 一段简单的solidity合约流程分析 10 | 11 | ```solidity 12 | pragma solidity ^0.4.25; 13 | contract Helloworld { 14 | function add(uint256 a, uint256 b) pure public returns (uint256) { 15 | return a+b; 16 | } 17 | 18 | } 19 | ``` 20 | 使用remix进行编译之后, 查看具体合约详情。我们贴一下汇编信息: 21 | 22 | ``` 23 | PUSH1 0x80 24 | PUSH1 0x40 25 | MSTORE // memory[0x80:0x80+32]=0x40 26 | CALLVALUE // 将msg.value压栈 此时栈中的数据为 [msg.value] 27 | DUP1 // 复制一份 [msg.value, msg,value] 28 | ISZERO PUSH2 0x10 JUMPI // 如果为0跳转到0x10的位置 0x10也就是tag1的位置 我们这个合约是不支持payable的 所以msg.value必须是0 29 | PUSH1 0x0 DUP1 REVERT 30 | 31 | tag1: 32 | JUMPDEST POP ;清空栈 33 | PUSH1 0xC5 34 | DUP1 35 | PUSH2 0x1F 36 | PUSH1 0x0 ; 此时栈的内容为[0xc5, 0xc5, 0x1f, 0x0] 37 | CODECOPY 从内存地址0开始拷贝code便宜为0x1f之后的0xc5个字节的内容 也就是下面runtime的位置 38 | PUSH1 0x0 此时栈的内容为[0xc5, 0x0] 39 | RETURN 返回内存地址0到0xc5的内容 返回了runtime后面的智能合约代码 40 | STOP 41 | 42 | runtime(0x1f的位置): 43 | PUSH1 0x80 PUSH1 0x40 MSTORE PUSH1 0x4 CALLDATASIZE LT PUSH1 0x3F JUMPI PUSH1 0x0 CALLDATALOAD PUSH29 0x100000000000000000000000000000000000000000000000000000000 SWAP1 DIV PUSH4 0xFFFFFFFF AND DUP1 PUSH4 0x771602F7 EQ PUSH1 0x44 JUMPI JUMPDEST PUSH1 0x0 DUP1 REVERT JUMPDEST CALLVALUE DUP1 ISZERO PUSH1 0x4F JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0x76 PUSH1 0x4 DUP1 CALLDATASIZE SUB DUP2 ADD SWAP1 DUP1 DUP1 CALLDATALOAD SWAP1 PUSH1 0x20 ADD SWAP1 SWAP3 SWAP2 SWAP1 DUP1 CALLDATALOAD SWAP1 PUSH1 0x20 ADD SWAP1 SWAP3 SWAP2 SWAP1 POP POP POP PUSH1 0x8C JUMP JUMPDEST PUSH1 0x40 MLOAD DUP1 DUP3 DUP2 MSTORE PUSH1 0x20 ADD SWAP2 POP POP PUSH1 0x40 MLOAD DUP1 SWAP2 SUB SWAP1 RETURN JUMPDEST PUSH1 0x0 DUP2 DUP4 ADD SWAP1 POP SWAP3 SWAP2 POP POP JUMP STOP LOG1 PUSH6 0x627A7A723058 KECCAK256 0xc8 0xab CREATE2 0xdc PUSH2 0xA37F DUP16 CALLCODE 0x2d SLOAD SWAP1 PUSH22 0xD6F7217B771B9A268A2A299F7CFBFE20FE435B002900 44 | ``` 45 | 46 | 当我们部署一个智能合约的时候, 首先是应该对合约进行一系列的初始化 最后写入到以太坊数据库的智能合约代码是runtime之后的内容。 47 | 为了验证我们分析的正确性, 我们把直接分析的runtime之后的代码和remix返回的runtime的字节码对比发现是一致的。 48 | ![WechatIMG2.jpeg](../img/evm-bytes.jpg) 49 | 也就是当合约部署完成后, 调用智能合约是从runtime之后开始的。 50 | 51 | ### 调用智能合约 52 | ``` 53 | // 假设input的内容为 0x771602f700000000000000000000000000000000000000000000000000000000000004580000000000000000000000000000000000000000000000000000000000002f59 54 | PUSH1 0x80 PUSH1 0x40 MSTORE 55 | PUSH1 0x4 // [0x4] 56 | CALLDATASIZE //[0x4, 68] 57 | LT PUSH1 0x3F [0, 0x3f] 58 | JUMPI 情况为0 不跳转到0x3f 继续PC+1 59 | PUSH1 0x0 60 | CALLDATALOAD 取出input[0:32]的内容 61 | PUSH29 0x100000000000000000000000000000000000000000000000000000000 SWAP1 DIV PUSH4 0xFFFFFFFF AND DUP1 PUSH4 0x771602F7 EQ 这几步是将input[0:32]进行处理取出前四个字节判断是否和0x771602F7相等 如果相等跳转到0x44偏移的位置 62 | PUSH1 0x44 JUMPI 63 | JUMPDEST PUSH1 0x0 DUP1 REVERT 64 | 65 | tag1(0x44的位置): 66 | JUMPDEST CALLVALUE ;[msg.value] 67 | DUP1 ;[0, 0] 68 | ISZERO PUSH1 0x4F JUMPI ; 如果为0跳转到0x4f的偏移 69 | PUSH1 0x0 DUP1 REVERT 70 | 71 | tag2(0x4f偏移的位置): 72 | JUMPDEST POP 73 | PUSH1 0x76 PUSH1 0x4 DUP1 ; 栈中的数据[0x76, 0x04, 0x04] 74 | CALLDATASIZE [0x76, 0x04, 0x04, 0x44] 75 | SUB ; [0x76, 0x04, 0x40] 76 | DUP2; [0x76, 0x04, 0x40, 0x04] 77 | ADD ; [0x76, 0x04, 0x44] 78 | SWAP1; [0x76, 0x44, 0x04] 79 | DUP1; [0x76, 0x44, 0x04, 0x04] 80 | DUP1; [0x76, 0x44, 0x04, 0x04, 0x04] 81 | CALLDATALOAD ; 取出input[0x04: 0x04+32] 内容 此时栈中的数据为[0x76, 0x44, 0x04, 0x04, 0x458] 82 | SWAP1 ; [0x76, 0x44, 0x04, 0x458, 0x04] 83 | PUSH1 0x20 ; [0x76, 0x44, 0x04, 0x458, 0x04, 0x20] 84 | ADD ; [0x76, 0x44, 0x04, 0x458, 0x24] 85 | SWAP1; [0x76, 0x44, 0x04, 0x24, 0x458] 86 | SWAP3 ;[0x76, 0x458, 0x04, 0x24, 0x44] 87 | SWAP2 ;[0x76, 0x458, 0x44, 0x24, 0x04] 88 | SWAP1 ;[0x76, 0x458, 0x44, 0x04, 0x24] 89 | DUP1 ;[0x76, 0x458, 0x44, 0x04, 0x24, 0x24] 90 | CALLDATALOAD ; 取出input[0x24:0x24+32内容] [0x76, 0x458, 0x44, 0x04, 0x24, 0x2f59] 91 | SWAP1 ; [0x76, 0x458, 0x44, 0x04, 0x2f59, 0x24] 92 | PUSH1 0x20; [0x76, 0x458, 0x44, 0x04, 0x2f59, 0x24, 0x20] 93 | ADD ;[0x76, 0x458, 0x44, 0x04, 0x2f59, 0x44] 94 | SWAP1 ;[0x76, 0x458, 0x44, 0x04, 0x44, 0x2f59] 95 | SWAP3 ;[0x76, 0x458, 0x2f59, 0x04, 0x44, 0x44] 96 | SWAP2 ;[0x76, 0x458, 0x2f59, 0x44, 0x44, 0x04] 97 | SWAP1 ;[0x76, 0x458, 0x2f59, 0x44, 0x04, 0x44] 98 | POP POP POP ;[0x76, 0x458, 0x2f59] 99 | PUSH1 0x8C ;[0x76, 0x458, 0x2f59, 0x8C] 100 | JUMP ; 跳转到0x8c偏移位置 [0x76, 0x458, 0x2f59] 101 | 102 | tag4(0x76偏移的位置) 103 | JUMPDEST PUSH1 0x40 ; [0x33b1, 0x40] 104 | MLOAD; [0x33b1, 0x80] 105 | DUP1 ; [0x33b1, 0x80, 0x80] 106 | DUP3 ; [0x33b1, 0x80, 0x80, 0x33b1] 107 | DUP2 ; [0x33b1, 0x80, 0x80, 0x33b1; 0x80] 108 | MSTORE ;[0x33b1, 0x80, 0x80] // memory[0x80:0x80+32]=0x33b1 109 | PUSH1 0x20; [0x33b1, 0x80, 0x80, 0x20] 110 | ADD ;[0x33b1, 0x80, 0xa0] 111 | SWAP2 ;[0xa0, 0x80, 0x33b1] 112 | POP POP ;[0xa0] 113 | PUSH1 0x40;[0xa0, 0x40] 114 | MLOAD ;[0xa0, 0x80] 115 | DUP1 ;[0xa0, 0x80, 0x80] 116 | SWAP2 ;[0x80, 0x80, 0xa0] 117 | SUB ;[0x80, 0x20] 118 | SWAP1 ;[0x20, 0x80] 119 | RETURN ; 返回memory[0x80:0x80+32] 也就是0x33b1的值 120 | 121 | tag5(0x8c偏移的位置): 122 | JUMPDEST PUSH1 0x0 ; [0x76, 0x458, 0x2f59, 0x0] 123 | DUP2 ; [0x76, 0x458, 0x2f59, 0x0, 0x2f59] 124 | DUP4 ; [0x76, 0x458, 0x2f59, 0x0, 0x2f59, 0x458] 125 | ADD ; [0x76, 0x458, 0x2f59, 0x0, 0x33b1] 126 | SWAP1 ;[0x76, 0x458, 0x2f59, 0x33b1, 0x0] 127 | POP ; [0x76, 0x458, 0x2f59, 0x33b1] 128 | SWAP3 ;[0x33b1, 0x458, 0x2f59, 0x76] 129 | SWAP2 ;[0x33b1, 0x76, 0x2f59, 0x458] 130 | POP POP; [0x33b1, 0x76] 131 | JUMP;跳转到0x76偏移的位置 [0x33b1] 132 | STOP LOG1 PUSH6 0x627A7A723058 KECCAK256 0xc8 0xab CREATE2 0xdc PUSH2 0xA37F DUP16 CALLCODE 0x2d SLOAD SWAP1 PUSH22 0xD6F7217B771B9A268A2A299F7CFBFE20FE435B002900 133 | ``` 134 | 135 | 上述就是当调用add函数, 实参a=0x458, b=0x2f59 构造的交易input为0x771602f700000000000000000000000000000000000000000000000000000000000004580000000000000000000000000000000000000000000000000000000000002f59时, 整个的调用流程。 136 | 总结一下流程: 137 | 138 | 1. 判断input是否大于4个字节 139 | 2. 取出32个字节内容经过转换判断前4个字节是否和hash(add(uint256, uint256))的值是否相同 140 | 3. 跳到tag1 判断交易的以太坊值是否为0 如果不为0直接中断 因为此合约不支持payable 141 | 4. 跳到tag2 整个tag2一直在进行各种变换 最终取出input的两个实参的值 然后跳到tag5 142 | 5. 在tag5又是一顿变换计算出结果 然后跳到tag4 143 | 6. 将tag5计算的结果保存到内存中 最后返回。 144 | 145 | 146 | ### 总结 147 | 整个汇编的流程特别复杂, 真的不是适合人类可读的。 只是为了分析一下字节码是怎么流转的, 每个操作码又是如何操作栈的。 这样我们接下来分析以太坊虚拟机就会有了基础, 分析起来就更容易一些。 148 | -------------------------------------------------------------------------------- /evm移植/evm之智能合约详解.md: -------------------------------------------------------------------------------- 1 | ## 一些术语理解 2 | 3 | ### 什么是智能合约 4 | 智能合约(smart code)其实质就是一串代码,目的很明确期望用代码来代替一些需要公信力的地方, 代码的执行不会受人为意志而转移。 只要代码被公开,所有执行的结果就是可预知的, 不会出现黑幕, 不会出现暗箱操作等。 5 | 6 | ### 什么是evm 7 | 既然智能合约是一串代码, 那它就需要有执行的宿主环境, 因此evm(以太坊虚拟机)就是执行智能合约的宿主机环境。 8 | 9 | ### 什么是solidity 10 | solidity是一种编程语言, 编写代码有很多种语言, C, C++, 而solidity就是一种用于编写以太坊智能合约的语言, 以太坊官方之前推出了多种智能合约编程语言, 目前看来使用最广泛和支持最好的既是solidity。 11 | 12 | ### 什么是solc 13 | 如果我们写过C/C++ 我们肯定知道gcc/g++, solc就是类似于gcc编译器的东西。 14 | 15 | ### 一段solidity的代码示例 16 | ```solidity 17 | pragma solidity ^0.4.21; 18 | 19 | interface BaseInterface { 20 | function CurrentVersion() external view returns(string); 21 | } 22 | 23 | contract Helloworld { 24 | uint256 balance; 25 | event Triggle(address, string); 26 | mapping(address=>uint256) _mapamount; 27 | 28 | constructor() public { 29 | balance = 6000000000; 30 | _mapamount[0] = 100; 31 | _mapamount[1] = 200; 32 | } 33 | 34 | function getbalance() public returns (address, uint256) { 35 | emit Triggle(msg.sender, "funck"); 36 | return (msg.sender, balance--); 37 | } 38 | 39 | function onlytest() public{ 40 | _mapamount[1] = 100; 41 | emit Triggle(msg.sender, "onlytest"); 42 | } 43 | 44 | function setBalance(uint256 tmp) public { 45 | balance = tmp; 46 | } 47 | 48 | function getVersion(address contractAddr) public view returns (string) { 49 | BaseInterface baseClass = BaseInterface(contractAddr); 50 | return baseClass.CurrentVersion(); 51 | } 52 | 53 | } 54 | 55 | ``` 56 | 57 | ### 什么是以太坊账户体系 58 | 账户可以类比我们现实中的银行账户的概念, 一个银行账户至少有卡号, 密码, 你的所有交易流水等信息。 59 | 以太坊是有两种账户, 一种称为普通的账户, 类似于我们的银行账户, 还有一种类似于合约账户, 代表着智能合约代码。 60 | 61 | 先说普通账户: 62 | 账户是有地址的, 地址对应着我们的银行卡号, 有了地址我们就能查询到所有与其相关的交易。 63 | 除了账户还有私钥,私钥类似于银行卡的密码, 唯一不同的地方在于一个银行卡我们可以随意修改它的密码, 但是对于以太坊普通账户地址(注意是普通账户), 不能修改私钥, 地址是根据私钥推导出来的(反之则不行)。 私钥是不可泄露不可更改的。 64 | 除了账户还有nonce值, nonce值代表当前账户执行交易的次数。 每次执行交易加1. 65 | 地址通过私钥-->公钥-->公钥SHA3-->取前20个字节的16进制字符串表示形式 66 | 67 | 再说智能合约账户: 68 | 智能合约账户的地址格式和普通账户是一样的, 只根据地址是区分不出它是智能合约还是普通账户地址。 69 | 但是智能合约的地址是没有私钥的,为什么没有私钥也能创建出地址呢? 70 | 试想一下,一串智能合约代码如果要发送到以太坊节点上, 必然需要有一个普通账户作为发起方, 当一个普通账户发起一笔发布智能合约的交易时, 根据from地址及其当前nonce进行hash运算取前20个字节的16进制字符串即为智能合约账户的地址。 71 | 所以说地址格式一样,只能根据账户地址去节点中查询才能知道代表的是何种账户类型。 72 | 73 | 注意: 74 | 智能合约账户不仅包含了智能合约编译后的字节码, 它依然可以进行以太坊收款和转账。 只是实现方式是通过智能合约代码来实现而已。 75 | 76 | 77 | 78 | 79 | ## 智能合约的创建流程 80 | 81 | 有了上面的术语理解, 会对我们理解以太坊智能合约有很大的帮助, 至少会让我们知道智能合约是什么, 它的作用是干嘛的。 可是对于它到底是如何工作的。 它是如何会被发布到所谓的各个以太坊节点之中的。 又是如何去调用的等等需要问题都是不了解的。 下面一步步去说明这些过程。 82 | 83 | 为了更容易的说明一个问题, 我们举例来描述其过程。 84 | 85 | 假设小明最近在和他的2个兄弟(小米和小刚)一起玩石头剪刀布的游戏, 赢的人可以继承老爷子上亿资产。 三个人现在都怕对方使诈。 在出招的最后一刻变卦赢得了比赛。 86 | 那如果这个时候我们用智能合约代码来决定胜负似乎就没有问题了。 87 | 我们先实现这个简单功能的代码: 88 | 89 | ``` 90 | pragma solidity ^0.4.21; 91 | 92 | contract Winner { 93 | mapping(string=>uint8) _mapActions; 94 | mapping(address=>bool) _AllAccounts; 95 | mapping(address=>uint8) _AccountsActions; 96 | bool start; 97 | 98 | // 这个是构造函数 在合约第一次创建时执行 99 | constructor() public { 100 | _mapActions["scissors"] = 1; 101 | _mapActions["hammer"] = 2; 102 | _mapActions["cloth"] = 3; 103 | 104 | _AllAccounts[0x763418009b636593e86256ffa32bef1b0218a1e1] = true; // xiaomi 105 | _AllAccounts[0x14723a09acff6d2a60dcdf7aa4aff308fddc160c] = true; // xiaoming 106 | _AllAccounts[0x583031d1113ad414f02576bd6afabfb302140225] = true; // xiaogang 107 | 108 | _AccountsActions[0x763418009b636593e86256ffa32bef1b0218a1e1] = 0; 109 | _AccountsActions[0x14723a09acff6d2a60dcdf7aa4aff308fddc160c] = 0; 110 | _AccountsActions[0x583031d1113ad414f02576bd6afabfb302140225] = 0; 111 | } 112 | 113 | // 设置执行动作 要求只能是scissors hammer cloth 114 | // 并且要求只能是上述要求的三个以太坊地址 115 | function setAction( string action) public returns (bool) { 116 | if (_mapActions[action] == 0 ) { 117 | return false; 118 | } 119 | 120 | if (!_AllAccounts[msg.sender]) { 121 | return false; 122 | } 123 | _AccountsActions[msg.sender] = _mapActions[action]; 124 | return true; 125 | } 126 | 127 | function reset() private { 128 | _AccountsActions[0x763418009b636593e86256ffa32bef1b0218a1e1] = 0; 129 | _AccountsActions[0x14723a09acff6d2a60dcdf7aa4aff308fddc160c] = 0; 130 | _AccountsActions[0x583031d1113ad414f02576bd6afabfb302140225] = 0; 131 | } 132 | 133 | function whoIsWinner() public returns (string, bool) { 134 | if ( 135 | _AccountsActions[0x763418009b636593e86256ffa32bef1b0218a1e1] == 0 || 136 | _AccountsActions[0x14723a09acff6d2a60dcdf7aa4aff308fddc160c] == 0 || 137 | _AccountsActions[0x583031d1113ad414f02576bd6afabfb302140225] == 0 138 | ) { 139 | reset(); 140 | return ("", false); 141 | } 142 | uint8 xiaomi = _AccountsActions[0x763418009b636593e86256ffa32bef1b0218a1e1]; 143 | uint8 xiaoming = _AccountsActions[0x14723a09acff6d2a60dcdf7aa4aff308fddc160c]; 144 | uint8 xiaogang = _AccountsActions[0x583031d1113ad414f02576bd6afabfb302140225]; 145 | if (xiaomi != xiaoming && xiaomi != xiaogang && xiaoming != xiaogang) { 146 | reset(); 147 | return ("", false); 148 | } 149 | 150 | if (xiaomi == xiaoming) { 151 | if (winCheck(xiaomi, xiaogang)) { 152 | return ("小刚", true); 153 | }else{ 154 | reset(); 155 | return ("", false); 156 | } 157 | } 158 | if (xiaomi == xiaogang) { 159 | if (winCheck(xiaomi, xiaoming)) { 160 | return ("小明", true); 161 | }else{ 162 | reset(); 163 | return ("", false); 164 | } 165 | } 166 | if (xiaoming == xiaogang) { 167 | if (winCheck(xiaoming, xiaomi)) { 168 | return ("小米", true); 169 | }else{ 170 | reset(); 171 | return ("", false); 172 | } 173 | } 174 | reset(); 175 | return ("", false); 176 | } 177 | 178 | function winCheck(uint8 a, uint8 b ) private returns( bool) { 179 | if(a == 1 && b==3) { 180 | return true; 181 | }else if (a==2 && b==1) { 182 | return true; 183 | }else if (a==3 && b==2) { 184 | return true; 185 | } 186 | return false; 187 | } 188 | 189 | } 190 | 191 | ``` 192 | 想象一下, 如果小明把这个代码发送到了以太坊上, 然后大家都调用智能合约的setAction函数设置自己想出的动作, 当大家都设置完成后, 最后再调用whoIsWinner函数来决定谁是胜负。 这个方案可是比葛优老师的分歧终端机逼格搞多了。 193 | 194 | 写到这里我并不是只是给大家讲一个段子, 只是期望以一个类似的生活分歧场景来引出智能合约在其中的解决方案。 作为技术研究者, 到了这里也只是刚刚开始, 下面我们要分析整个流程是如何实现的。 195 | 196 | ### 智能合约代码编写 197 | 198 | 假设小明联合两个兄弟一起写了上述的代码, 大家一起分析了代码, 并且认为其中没有漏洞,比如当有第四个人也想调用这个函数如何剔除它。 比如三个人一次没有决定胜负时要重新开始,比如兄弟中有一个人在一轮比赛期间没有给出自己的动作。 199 | 最后三个人对上述代码的认同一致。 认为代码没有漏洞。 200 | 201 | ### 智能合约编译 202 | 代码如果想被虚拟机执行, 那么它必须应该是字节码。 这个时候就需要将上述代码进行编译了。 可以使用以太坊官方的solc进行编译。 或者使用truffle框架进行编译。最后获取到字节码。 203 | 其字节码的形式类似于下面这个样子: 204 | "608060405234801561001057600080fd5b506001600060405180807f73636973736f72730....." 205 | 206 | ### 发布智能合约到以太坊各个节点 207 | 208 | 这个时候假设小刚(0x583031d1113ad414f02576bd6afabfb302140225)想主动把此合约发布到节点上, 然后他构建了类似交易 209 | { 210 | "from": 0x583031d1113ad414f02576bd6afabfb302140225, 211 | "nonce": 100, 212 | "input": "608060405234801561001057600080fd5b506001600060405180807f73636973736f72730.....", 213 | "to": nil 214 | "timestamp": 2018-11-27 12:32:48, 215 | "sign": "" 216 | ... 217 | } 218 | 然后他用自己的私钥对上述内容进行了签名, 并将整个消息编码之后发给了一个以太坊节点。 219 | 220 | ### 以太坊节点部署智能合约 221 | 222 | 我们假设有一个以太坊节点对接收到的编码消息进行了解码还原了上述的交易内容。 223 | 验证发起方是否签名正确, 验证发起方余额是否足够, 验证发起方nonce是否是发起方最后一次持续交易的nonce加一。 然后开始执行交易内容。同时广播这个交易到它知道的所有相邻节点。 224 | 当它发现to为nil, 那么它认为这就是要部署一个智能合约的节奏。 225 | 部署智能合约前要创建一个智能合约账户。 所以此智能合约的账户地址为 hex(hash(from, nonce)[0:20]) 假设创建的智能合约地址为0xfc713aab72f97671badcb14669248c4e922fe2bb 226 | 合约的字节码为执行evm执行完input字节码返回的内容。 这里可能大家会有疑虑, 为什么合约字节码不直接是input, 我们在编写智能合约代码时会在构造函数进行一些初始化的内容。 那么在部署合约之前这些初始化的动作应该也要初始化。所以在input执行之后除了返回可执行的字节码之外, 一些初始化动作也就被执行完成了。 227 | 228 | 这样一来, 所有的节点均会执行此交易。 智能合约代码在所有的以太坊节点都会部署一份。 229 | 230 | ### 智能合约调用 231 | 232 | 当合约被部署完成之后, 接下来就是要调用合约了。 比如小明(0x763418009b636593e86256ffa32bef1b0218a1e1)想调用合约设置自己出剪刀的动作。 整个流程如下: 233 | 234 | 小明也会发起一笔交易交易, 交易结构类似下面: 235 | { 236 | "from": "0x763418009b636593e86256ffa32bef1b0218a1e1", 237 | "to": "0xfc713aab72f97671badcb14669248c4e922fe2bb", // 合约地址 238 | "value": 0, 239 | "input": "0x3e0e455a0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000668616d6d65720000000000000000000000000000000000000000000000000000", 240 | "gas": 300000000, 241 | "sign": "" 242 | "nonce" 101 243 | } 244 | 245 | 注意上面的交易内容, from就是合约的调用者, to就是合约地址。 value表示向往此合约账户转账的以太坊金额。 我们现在专门关注一下input的内容。 246 | input初看是一串数字。 其实其前四个字节是调用函数的hash值也即是'setAction(string)' hash运算之后返回的内容。 后面的字符串就是以太坊对输入的参数编码之后的内容。 247 | 248 | 当此交易被广播到节点之后, 校验流程和上述部署合约流程一致。 249 | 校验通过之后会被此节点中继到所有相邻的节点 进而全网收到此交易。 250 | 251 | 首先节点发现to是一个合约地址, 进而加载合约地址, 解析input之后发现是调用setAction("hammer") 于是evm会执行setAction函数。 同时把执行结果放入交易收据详情中。 252 | 253 | 这样一次智能合约调用就算完成了。 254 | 255 | 256 | ### 总结 257 | 写到这里整个智能合约的概述也就说完了。 从以太坊节点看来, 不论是合约部署还是调用合约函数对其来说都是一次交易的过程。 只要有evm, 有图灵完备的语言。 就能实现想要的功能。 258 | 259 | ### 关于ERC20代币 260 | 261 | 如果问以太坊的智能合约应用最广泛的地方在哪里, 肯定就是发币了。 虽然可能V神也没想到世界还没被改变, ICO(圈钱)的方式倒是又多出了一个。 262 | 263 | 既然是数字token, 代码实现起来就可以有各种各样的方案。 实质无外乎就是记录下每个用户拥有的数字token数量, 具有转账,查询余额等功能就行了。 上面我们说到只要调用对应相关的函数去执行即可完成相关的功能。 可是这个时候假如某个ICO方A 写了代币转账功能, 它的转账函数叫做tx(uint256). 264 | 那么如果想调用它的转账就要调用tx这个函数, 我们知道只要调用函数不相同, input的内容就不会一样。 如果所有的ICO方写的这些函数名称均不一样。 调用者就要查看每一个ICO方的合约代码。 265 | 于是这个时候ERC20就来了, 它定义了一些发币(圈钱)的规范。也即是如果你想发行ICO, 最好按着我的规范来, 这样大家用起来就跟方便了。ERC20简而言之就是定义了下面几个接口 266 | 267 | ```solidity 268 | contract ERC20 { 269 | 270 | uint256 public totalSupply; 271 | 272 | event Transfer(address indexed from, address indexed to, uint256 value); // 转账触发 273 | event Approval(address indexed owner, address indexed spender, uint256 value); // 容许提取触发 274 | 275 | function balanceOf(address who) public view returns (uint256); // 查询余额函数 276 | function transfer(address to, uint256 value) public returns (bool); // 进行转账函数 277 | 278 | function approve(address spender, uint256 value) public returns (bool); // 容许某个地址提款 279 | function transferFrom(address from, address to, uint256 value) public returns (bool); // 从一方向另一方转账的余额 280 | 281 | } 282 | ``` 283 | 有了这个规范, 各个ICO方在发行token时都实现上面的接口, 这样无论任何的ERC20代币, 均可以用一套方法实现所有代币转账,查询余额等功能。 284 | 285 | 286 | 287 | 288 | 289 | ### 分析一下PAX稳定币的智能合约代码 290 | 291 | 292 | ```solidity 293 | pragma solidity ^0.4.24; 294 | pragma experimental "v0.5.0"; 295 | 296 | // 导入外部包 此包中主要是一些 297 | // 安全的数学运算 298 | // 因为在一些场景 出现了数据溢出没有考虑的问题导致了 299 | // 一些ico币直接归零 300 | import "./zeppelin/SafeMath.sol"; 301 | 302 | contract PAXImplementation { 303 | 304 | using SafeMath for uint256; 305 | bool private initialized = false; 306 | 307 | // 定义了ERC20规定的代币名称 符号 精度 308 | mapping(address => uint256) internal balances; 309 | uint256 internal totalSupply_; 310 | string public constant name = "PAX"; // solium-disable-line uppercase 311 | string public constant symbol = "PAX"; // solium-disable-line uppercase 312 | uint8 public constant decimals = 18; // solium-disable-line uppercase 313 | 314 | // ERC20 DATA 315 | mapping (address => mapping (address => uint256)) internal allowed; 316 | 317 | // OWNER DATA 318 | address public owner; 319 | 320 | // PAUSABILITY DATA 321 | bool public paused = false; 322 | 323 | // LAW ENFORCEMENT DATA 324 | address public lawEnforcementRole; 325 | mapping(address => bool) internal frozen; 326 | 327 | // SUPPLY CONTROL DATA 328 | address public supplyController; 329 | 330 | // 定义触发时间 当转账或者授权别人转账时 调用此事件 当调用时 其实质会在以太坊节点区块上写入日志。 331 | event Transfer(address indexed from, address indexed to, uint256 value); 332 | event Approval( 333 | address indexed owner, 334 | address indexed spender, 335 | uint256 value 336 | ); 337 | 338 | // OWNABLE EVENTS 339 | event OwnershipTransferred( 340 | address indexed oldOwner, 341 | address indexed newOwner 342 | ); 343 | 344 | // PAUSABLE EVENTS 345 | event Pause(); 346 | event Unpause(); 347 | 348 | // LAW ENFORCEMENT EVENTS 349 | event AddressFrozen(address indexed addr); 350 | event AddressUnfrozen(address indexed addr); 351 | event FrozenAddressWiped(address indexed addr); 352 | event LawEnforcementRoleSet ( 353 | address indexed oldLawEnforcementRole, 354 | address indexed newLawEnforcementRole 355 | ); 356 | 357 | // SUPPLY CONTROL EVENTS 358 | event SupplyIncreased(address indexed to, uint256 value); 359 | event SupplyDecreased(address indexed from, uint256 value); 360 | event SupplyControllerSet( 361 | address indexed oldSupplyController, 362 | address indexed newSupplyController 363 | ); 364 | 365 | /** 366 | * FUNCTIONALITY 367 | */ 368 | 369 | // INITIALIZATION FUNCTIONALITY 370 | 371 | /** 372 | 合约部署时的初始化过程 373 | 设置合约拥有者为部署合约的账户 374 | 设置总供应量为0 375 | 并保证此函数只会被调用一次 376 | */ 377 | function initialize() public { 378 | require(!initialized, "already initialized"); 379 | owner = msg.sender; 380 | lawEnforcementRole = address(0); 381 | totalSupply_ = 0; 382 | supplyController = msg.sender; 383 | initialized = true; 384 | } 385 | 386 | /** 387 | 合约的构造函数 调用上面的初始化函数 并且设置暂停交易 388 | */ 389 | constructor() public { 390 | initialize(); 391 | pause(); 392 | } 393 | 394 | // ERC20 BASIC FUNCTIONALITY 395 | 396 | /** 397 | ERC20接口 返回总的供应量 398 | */ 399 | function totalSupply() public view returns (uint256) { 400 | return totalSupply_; 401 | } 402 | 403 | /* 404 | 转账函数 实现将调用者的token转给其他人 405 | msg.sender 即为合约的调用者 406 | 并且此函数要求必须是非暂停状态 即whenNotPaused返回真 407 | 408 | 这个函数有需要验证条件 409 | 1.交易没有被暂停 410 | 2.接收方地址不能是0 411 | 3.接收方和发起方均不可以是冻结地址 412 | 4.转账的token余额要足够。 413 | */ 414 | function transfer(address _to, uint256 _value) public whenNotPaused returns (bool) { 415 | require(_to != address(0), "cannot transfer to address zero"); 416 | require(!frozen[_to] && !frozen[msg.sender], "address frozen"); 417 | require(_value <= balances[msg.sender], "insufficient funds"); 418 | 419 | balances[msg.sender] = balances[msg.sender].sub(_value); 420 | balances[_to] = balances[_to].add(_value); 421 | emit Transfer(msg.sender, _to, _value); 422 | return true; 423 | } 424 | 425 | /** 426 | ERC20接口 返回某个账户的token余额 427 | */ 428 | function balanceOf(address _addr) public view returns (uint256) { 429 | return balances[_addr]; 430 | } 431 | 432 | // ERC20 FUNCTIONALITY 433 | 434 | /* 435 | ERC20接口 实现了 _from地址下容许调用方可以转出金额到其他_to 436 | 此函数要求必须是非暂停状态 437 | */ 438 | function transferFrom( 439 | address _from, 440 | address _to, 441 | uint256 _value 442 | ) 443 | public 444 | whenNotPaused 445 | returns (bool) 446 | { 447 | require(_to != address(0), "cannot transfer to address zero"); 448 | require(!frozen[_to] && !frozen[_from] && !frozen[msg.sender], "address frozen"); 449 | require(_value <= balances[_from], "insufficient funds"); 450 | require(_value <= allowed[_from][msg.sender], "insufficient allowance"); 451 | 452 | balances[_from] = balances[_from].sub(_value); 453 | balances[_to] = balances[_to].add(_value); 454 | allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value); 455 | emit Transfer(_from, _to, _value); 456 | return true; 457 | } 458 | 459 | /** 460 | ERC20接口 实现调用方容许_spender 可以从我的账户转出的金额 这个函数和上面的函数是相对应的。 461 | 只有一个账户容许了其他账户能从我的账户转出的金额 上述的函数才能转账成功。 462 | */ 463 | function approve(address _spender, uint256 _value) public whenNotPaused returns (bool) { 464 | require(!frozen[_spender] && !frozen[msg.sender], "address frozen"); 465 | allowed[msg.sender][_spender] = _value; 466 | emit Approval(msg.sender, _spender, _value); 467 | return true; 468 | } 469 | 470 | /** 471 | ERC20接口 返回_owner账户容许_spender账户从自己名下转移出去的资产数量 472 | */ 473 | function allowance( 474 | address _owner, 475 | address _spender 476 | ) 477 | public 478 | view 479 | returns (uint256) 480 | { 481 | return allowed[_owner][_spender]; 482 | } 483 | 484 | // OWNER FUNCTIONALITY 485 | 486 | /** 487 | 这个函数被称为修饰函数 上面的whenNotPaused 也是一个修饰函数 实质是一种断言。 只有 488 | 断言通过 才会执行函数内部的内容 489 | */ 490 | modifier onlyOwner() { 491 | require(msg.sender == owner, "onlyOwner"); 492 | _; 493 | } 494 | 495 | /* 496 | 将只能合约的拥有者转给别人 497 | */ 498 | function transferOwnership(address _newOwner) public onlyOwner { 499 | require(_newOwner != address(0), "cannot transfer ownership to address zero"); 500 | emit OwnershipTransferred(owner, _newOwner); 501 | owner = _newOwner; 502 | } 503 | 504 | // PAUSABILITY FUNCTIONALITY 505 | 506 | /** 507 | * 修饰函数 要求处于非暂停交易状态 508 | */ 509 | modifier whenNotPaused() { 510 | require(!paused, "whenNotPaused"); 511 | _; 512 | } 513 | 514 | /** 515 | * 只有合约的拥有者才可以设置暂停交易 516 | */ 517 | function pause() public onlyOwner { 518 | require(!paused, "already paused"); 519 | paused = true; 520 | emit Pause(); 521 | } 522 | 523 | /** 524 | * 只有合约的拥有者才能取消暂停交易 525 | */ 526 | function unpause() public onlyOwner { 527 | require(paused, "already unpaused"); 528 | paused = false; 529 | emit Unpause(); 530 | } 531 | 532 | // LAW ENFORCEMENT FUNCTIONALITY 533 | 534 | /** 535 | 设置一个法定的的强制角色 这个角色可以冻结或者解冻别人账户的token 536 | 设置一个这样的角色要求首先调用方要么是合约的拥有者 要么自己已经是法定的强制者 537 | * @param _newLawEnforcementRole The new address allowed to freeze/unfreeze addresses and seize their tokens. 538 | */ 539 | function setLawEnforcementRole(address _newLawEnforcementRole) public { 540 | require(msg.sender == lawEnforcementRole || msg.sender == owner, "only lawEnforcementRole or Owner"); 541 | emit LawEnforcementRoleSet(lawEnforcementRole, _newLawEnforcementRole); 542 | lawEnforcementRole = _newLawEnforcementRole; 543 | } 544 | 545 | // 断言函数 要求调用方必须是强制者角色 546 | modifier onlyLawEnforcementRole() { 547 | require(msg.sender == lawEnforcementRole, "onlyLawEnforcementRole"); 548 | _; 549 | } 550 | 551 | /** 552 | 冻结某个账户的token 使用了断言 onlyLawEnforcementRole 也是只有调用方角色是 553 | 法定强制者才有权限冻结别人的token 554 | */ 555 | function freeze(address _addr) public onlyLawEnforcementRole { 556 | require(!frozen[_addr], "address already frozen"); 557 | frozen[_addr] = true; 558 | emit AddressFrozen(_addr); 559 | } 560 | 561 | /** 562 | 解冻某个账户的token 使用了断言 onlyLawEnforcementRole 也是只有调用方角色是 563 | 法定强制者才有权限解冻别人的token 564 | */ 565 | function unfreeze(address _addr) public onlyLawEnforcementRole { 566 | require(frozen[_addr], "address already unfrozen"); 567 | frozen[_addr] = false; 568 | emit AddressUnfrozen(_addr); 569 | } 570 | 571 | /** 572 | 摧毁冻结账户的token 也就是说如果这个地址是一个冻结地址调用这个函数会把这个地址的token销毁同时总供应数量也会被减少 573 | 当然这个函数也不是谁都可以调用的 只有法定的强制者才有权限 574 | */ 575 | function wipeFrozenAddress(address _addr) public onlyLawEnforcementRole { 576 | require(frozen[_addr], "address is not frozen"); 577 | uint256 _balance = balances[_addr]; 578 | balances[_addr] = 0; 579 | totalSupply_ = totalSupply_.sub(_balance); 580 | emit FrozenAddressWiped(_addr); 581 | emit SupplyDecreased(_addr, _balance); 582 | emit Transfer(_addr, address(0), _balance); 583 | } 584 | 585 | /** 586 | 用于检查某个地址是否被冻结了 587 | */ 588 | function isFrozen(address _addr) public view returns (bool) { 589 | return frozen[_addr]; 590 | } 591 | 592 | // SUPPLY CONTROL FUNCTIONALITY 593 | 594 | /** 595 | 设置token供应量的控制着 在合约初始化时 token供应量是合约发起则 调用这个函数可以更改 596 | 这个函数只有调用方已经是token供应量控制着或者整个合约的拥有者才能调用成功 597 | 也就是在整个合约中 合约的拥有者实质是可以控制一切权限的。 它能更改法定强制者 更改总token 598 | 供应量的控制者。 599 | */ 600 | function setSupplyController(address _newSupplyController) public { 601 | require(msg.sender == supplyController || msg.sender == owner, "only SupplyController or Owner"); 602 | require(_newSupplyController != address(0), "cannot set supply controller to address zero"); 603 | emit SupplyControllerSet(supplyController, _newSupplyController); 604 | supplyController = _newSupplyController; 605 | } 606 | 607 | modifier onlySupplyController() { 608 | require(msg.sender == supplyController, "onlySupplyController"); 609 | _; 610 | } 611 | 612 | /** 613 | 增加总的token供应量 并把新增供应量加到supplyController这个账户的名下。 614 | */ 615 | function increaseSupply(uint256 _value) public onlySupplyController returns (bool success) { 616 | totalSupply_ = totalSupply_.add(_value); 617 | balances[supplyController] = balances[supplyController].add(_value); 618 | emit SupplyIncreased(supplyController, _value); 619 | emit Transfer(address(0), supplyController, _value); 620 | return true; 621 | } 622 | 623 | /** 624 | 减少总的token供应量 待减少供应量从supplyController这个账户的名下减掉 。 625 | 这个函数要求supplyController 626 | */ 627 | function decreaseSupply(uint256 _value) public onlySupplyController returns (bool success) { 628 | require(_value <= balances[supplyController], "not enough supply"); 629 | balances[supplyController] = balances[supplyController].sub(_value); 630 | totalSupply_ = totalSupply_.sub(_value); 631 | emit SupplyDecreased(supplyController, _value); 632 | emit Transfer(supplyController, address(0), _value); 633 | return true; 634 | } 635 | } 636 | 637 | ``` 638 | 639 | ### PAX稳定币功能概述 640 | PAX除了具有这个ERC20的功能外,还具有一些其他功能: 641 | 642 | 1. 可以暂停整个代币转账 643 | 2. 可以增加或者减少整个代币的数量 644 | 3. 可以任意冻结或者解冻某个账户的代币 645 | 4. 可以销毁某个冻结账户的代币 646 | 5. 可以转移合约控制权。可以转移总供应量控制权。 647 | 648 | 总的来说PAX币做的限制特别多, 它的合约拥有机会可以做任何事情。 就算token转移给你了, 依然能分分钟钟消失。 649 | 650 | 651 | 652 | 653 | ## 最后 654 | 智能合约工作原理,整个开发,部署,调用流程也就这么多。 许多小细节可能没有详细说明, 但是最重要的部分已经进行了描述。 655 | 关于solidity语法, 如何使用truffle进行智能合约的开发,如何进行开发调试的等以后具体详细说明。 656 | 657 | -------------------------------------------------------------------------------- /evm移植/evm之源码分析.md: -------------------------------------------------------------------------------- 1 | 2 | ethereum的虚拟机源码所有部分在core/vm下。 去除测试总共有24个源码文件。 整个vm调用的入口在go-ethereum/core/state_transaction.go中。 我们主要是为了分析虚拟机源码,所以关于以太坊是如何进行交易转账忽略过去。 3 | 4 | ![WechatIMG4.jpeg](../img/source-code.jpg) 5 | 6 | 从上面的截图我们可以看出, 当以太坊的交易中to地址为nil时, 意味着部署合约, 那么就会调用evm.Create方法。 7 | 否则调用了evm.Call方法。 也就是说分析以太坊虚拟机源码时, 只要从这两个函数作为入口即可。 8 | 9 | 10 | 首先我们先看一下EVM数据结构: 11 | ```go 12 | type EVM struct { 13 | // Context provides auxiliary blockchain related information 14 | Context 15 | // StateDB是状态存储接口。 这个接口非常重要。 可以肯定的说一直evm中的大部分工作都是围绕这次接口进行的。 16 | StateDB StateDB 17 | // 记录当前调用的深度 18 | depth int 19 | 20 | // 记录链的配置 主要是以太坊经理过几次分叉和提案 为了兼容之前的区块信息 21 | // 所以做了一些兼容 移植的时候我们只考虑最新版本的内容 22 | chainConfig *params.ChainConfig 23 | // 这个参数 对我们移植过程中的意义不是很大 24 | chainRules params.Rules 25 | // 这个是虚拟机的一些配置参数 是创建解释器的初始化参数 比如所有操作码对应的函数也是在此处配置的 26 | vmConfig Config 27 | 28 | // 解释器对象 它是整个进行虚拟机代码执行的地方。 29 | interpreter *Interpreter 30 | 31 | // 用来终止代码执行 32 | abort int32 33 | // callGasTemp holds the gas available for the current call. This is needed because the 34 | // available gas is calculated in gasCall* according to the 63/64 rule and later 35 | // applied in opCall*. 36 | callGasTemp uint64 37 | } 38 | ``` 39 | 40 | 先看一看创建EVM的方法 41 | ```go 42 | func NewEVM(ctx Context, statedb StateDB, chainConfig *params.ChainConfig, vmConfig Config) *EVM { 43 | evm := &EVM{ 44 | Context: ctx, 45 | StateDB: statedb, 46 | vmConfig: vmConfig, 47 | chainConfig: chainConfig, 48 | chainRules: chainConfig.Rules(ctx.BlockNumber), 49 | } 50 | 51 | // 主要看这个地方 创建解释器 解释器是执行字节码的关键 52 | evm.interpreter = NewInterpreter(evm, vmConfig) 53 | return evm 54 | } 55 | 56 | func NewInterpreter(evm *EVM, cfg Config) *Interpreter { 57 | // 在这里设置 操作码对应的函数 58 | // 主要原因是以太坊经历版本迭代之后 操作码有了一些变化 59 | // 我们移植的时候 这个地方只会保留最新版本的操作码表 60 | if !cfg.JumpTable[STOP].valid { 61 | switch { 62 | case evm.ChainConfig().IsConstantinople(evm.BlockNumber): 63 | cfg.JumpTable = constantinopleInstructionSet 64 | case evm.ChainConfig().IsByzantium(evm.BlockNumber): 65 | cfg.JumpTable = byzantiumInstructionSet 66 | case evm.ChainConfig().IsHomestead(evm.BlockNumber): 67 | cfg.JumpTable = homesteadInstructionSet 68 | default: 69 | cfg.JumpTable = frontierInstructionSet 70 | } 71 | } 72 | 73 | return &Interpreter{ 74 | evm: evm, 75 | cfg: cfg, 76 | // gas表中记录了对应的执行操作需要花费的gas 移植的时候我们只保留一个 77 | gasTable: evm.ChainConfig().GasTable(evm.BlockNumber), 78 | } 79 | } 80 | ``` 81 | 接下来我们先分析部署合约的入口, 看一看整个合约部署的流程。 82 | ```go 83 | // Create creates a new contract using code as deployment code. 84 | func (evm *EVM) Create(caller ContractRef, code []byte, gas uint64, value *big.Int) (ret []byte, contractAddr common.Address, leftOverGas uint64, err error) { 85 | 86 | // 首先检测当前evm执行的深度 默认不应该超过1024 87 | if evm.depth > int(params.CallCreateDepth) { 88 | return nil, common.Address{}, gas, ErrDepth 89 | } 90 | // 这个函数我们不在追踪 其功能就是检测是否调用方的金额大约value 91 | if !evm.CanTransfer(evm.StateDB, caller.Address(), value) { 92 | return nil, common.Address{}, gas, ErrInsufficientBalance 93 | } 94 | // 首先获取调用者的nonce 然后再更新调用者的nonce 这个如果熟悉以太坊交易流程的话应该知道nonce的作用。 95 | nonce := evm.StateDB.GetNonce(caller.Address()) 96 | evm.StateDB.SetNonce(caller.Address(), nonce+1) 97 | 98 | // 下面这三句就是根据调用者地址 调用者nonce创建合约账户地址 并且判断是否这个合约地址确实没有部署过合约 99 | contractAddr = crypto.CreateAddress(caller.Address(), nonce) 100 | contractHash := evm.StateDB.GetCodeHash(contractAddr) 101 | if evm.StateDB.GetNonce(contractAddr) != 0 || (contractHash != (common.Hash{}) && contractHash != emptyCodeHash) { 102 | return nil, common.Address{}, 0, ErrContractAddressCollision 103 | } 104 | // 既然已经创建好了合约地址 那么就要为这个合约地址创建账户体系 105 | snapshot := evm.StateDB.Snapshot() 106 | // 所以下面这个函数在一直的时候我们的工作内容之一 107 | evm.StateDB.CreateAccount(contractAddr) 108 | if evm.ChainConfig().IsEIP158(evm.BlockNumber) { 109 | evm.StateDB.SetNonce(contractAddr, 1) 110 | } 111 | evm.Transfer(evm.StateDB, caller.Address(), contractAddr, value) 112 | 113 | // 创建一个合约对象 设置合约对象的参数 比如调用者 合约代码 合约hash的内容 114 | contract := NewContract(caller, AccountRef(contractAddr), value, gas) 115 | contract.SetCallCode(&contractAddr, crypto.Keccak256Hash(code), code) 116 | 117 | if evm.vmConfig.NoRecursion && evm.depth > 0 { 118 | return nil, contractAddr, gas, nil 119 | } 120 | 121 | if evm.vmConfig.Debug && evm.depth == 0 { 122 | evm.vmConfig.Tracer.CaptureStart(caller.Address(), contractAddr, true, code, gas, value) 123 | } 124 | start := time.Now() 125 | 126 | // 将evm对象 合约对象传入run函数开始执行 此函数是核心 等一会分析到Call入口的时候最终也会调用此函数 127 | ret, err = run(evm, contract, nil) 128 | 129 | // 上述函数执行完成后返回的就是我前一章所说的初始化后的合约代码 130 | // 也就是我们在remix上看到runtime的字节码 以后调用合约代码其实质就是 131 | // 执行返回后的代码 132 | 133 | // 下面的流程主要是一些检查 把返回的字节码保存到此合约账户名下 这样以后调用合约代码才能加载成功 134 | maxCodeSizeExceeded := evm.ChainConfig().IsEIP158(evm.BlockNumber) && len(ret) > params.MaxCodeSize 135 | // if the contract creation ran successfully and no errors were returned 136 | // calculate the gas required to store the code. If the code could not 137 | // be stored due to not enough gas set an error and let it be handled 138 | // by the error checking condition below. 139 | if err == nil && !maxCodeSizeExceeded { 140 | createDataGas := uint64(len(ret)) * params.CreateDataGas 141 | if contract.UseGas(createDataGas) { 142 | evm.StateDB.SetCode(contractAddr, ret) 143 | } else { 144 | err = ErrCodeStoreOutOfGas 145 | } 146 | } 147 | 148 | // When an error was returned by the EVM or when setting the creation code 149 | // above we revert to the snapshot and consume any gas remaining. Additionally 150 | // when we're in homestead this also counts for code storage gas errors. 151 | if maxCodeSizeExceeded || (err != nil && (evm.ChainConfig().IsHomestead(evm.BlockNumber) || err != ErrCodeStoreOutOfGas)) { 152 | evm.StateDB.RevertToSnapshot(snapshot) 153 | if err != errExecutionReverted { 154 | contract.UseGas(contract.Gas) 155 | } 156 | } 157 | // Assign err if contract code size exceeds the max while the err is still empty. 158 | if maxCodeSizeExceeded && err == nil { 159 | err = errMaxCodeSizeExceeded 160 | } 161 | if evm.vmConfig.Debug && evm.depth == 0 { 162 | evm.vmConfig.Tracer.CaptureEnd(ret, gas-contract.Gas, time.Since(start), err) 163 | } 164 | return ret, contractAddr, contract.Gas, err 165 | } 166 | ``` 167 | 168 | 所以我们下面就开始主要分析run函数 169 | ```go 170 | func run(evm *EVM, contract *Contract, input []byte) ([]byte, error) { 171 | if contract.CodeAddr != nil { 172 | // 首先会进入下面这个代码执行 它会根据给定的合约地址来判断是否是以太坊内部已经保存的合约 173 | // 如果是创建新合约 肯定不是内置合约 174 | precompiles := PrecompiledContractsHomestead 175 | if evm.ChainConfig().IsByzantium(evm.BlockNumber) { 176 | precompiles = PrecompiledContractsByzantium 177 | } 178 | if p := precompiles[*contract.CodeAddr]; p != nil { 179 | return RunPrecompiledContract(p, input, contract) 180 | } 181 | } 182 | // 所以最后我们最终到这里 此时input参数为nil 183 | return evm.interpreter.Run(contract, input) 184 | } 185 | 186 | // 解释器的Run函数才是真正执行合约代码的地方 187 | // 为了凸显主流程 我们隐藏部分内容 188 | func (in *Interpreter) Run(contract *Contract, input []byte) (ret []byte, err error) { 189 | if in.intPool == nil { 190 | in.intPool = poolOfIntPools.get() 191 | defer func() { 192 | poolOfIntPools.put(in.intPool) 193 | in.intPool = nil 194 | }() 195 | } 196 | 197 | // 下面这些变量应该说满足了一个字节码执行的所有条件 198 | // 有操作码 内存 栈 PC计数器 199 | // 强烈建议使用debug工具去跟踪一遍执行的流程 200 | // 其实它的执行流程就和上一章我们人肉执行的流程一样 201 | var ( 202 | op OpCode // current opcode 203 | mem = NewMemory() // bound memory 204 | stack = newstack() // local stack 205 | pc = uint64(0) // program counter 206 | cost uint64 207 | pcCopy uint64 // needed for the deferred Tracer 208 | gasCopy uint64 // for Tracer to log gas remaining before execution 209 | logged bool // deferred Tracer should ignore already logged steps 210 | ) 211 | contract.Input = input 212 | 213 | // Reclaim the stack as an int pool when the execution stops 214 | defer func() { in.intPool.put(stack.data...) }() 215 | 216 | // 开始循环PC计数执行 直到有中止执行或者跳出循环 217 | for atomic.LoadInt32(&in.evm.abort) == 0 { 218 | // 根据PC计数器获取操作码 219 | op = contract.GetOp(pc) 220 | // 根据操作码获取对应的操作函数 221 | operation := in.cfg.JumpTable[op] 222 | 223 | // 验证栈中的数据是否符合操作码需要的数据 224 | if err := operation.validateStack(stack); err != nil { 225 | return nil, err 226 | } 227 | // If the operation is valid, enforce and write restrictions 228 | if err := in.enforceRestrictions(op, operation, stack); err != nil { 229 | return nil, err 230 | } 231 | 232 | var memorySize uint64 233 | // 有些指令是需要额外的内存消耗 在jump_table.go文件中可以看到他们具体每个操作码的对应的额外内存消耗计算 234 | // 并不是所有的指令都需要计算消耗的内存 235 | // memorySize指向对应的计算消耗内存的函数 根据消耗的内存来计算消费的gas 236 | if operation.memorySize != nil { 237 | memSize, overflow := bigUint64(operation.memorySize(stack)) 238 | if overflow { 239 | return nil, errGasUintOverflow 240 | } 241 | if memorySize, overflow = math.SafeMul(toWordSize(memSize), 32); overflow { 242 | return nil, errGasUintOverflow 243 | } 244 | } 245 | // 计算此操作花费的gas数量 246 | cost, err = operation.gasCost(in.gasTable, in.evm, contract, stack, mem, memorySize) 247 | if err != nil || !contract.UseGas(cost) { 248 | return nil, ErrOutOfGas 249 | } 250 | if memorySize > 0 { 251 | mem.Resize(memorySize) 252 | } 253 | 254 | // 开始执行此操作码对应的操作函数 同时会返回执行结果同时也会更新PC计数器 255 | // 大部分的操作码对应的操作函数都是在instructions.go中可以找得到 256 | res, err := operation.execute(&pc, in.evm, contract, mem, stack) 257 | // 如果这个操作码是一个返回参数 那么就把需要的内容写入returnData 258 | // 按理说应该是只有return参数才会有范湖 259 | if operation.returns { 260 | in.returnData = res 261 | } 262 | 263 | // 到这里也就意味着一个操作码已经执行完成了 应该根据这次的执行结果来决定下一步的动作 264 | // 1. 如果执行出错了 直接返回错误 265 | // 2. 如果只能合约代码中止了(比如断言失败) 那么直接返回执行结果 266 | // 3. 如果是暂停指令 则直接返回结果 267 | // 4. 如果操作符不是一个跳转 则直接PC指向下一个指令 继续循环执行 268 | switch { 269 | case err != nil: 270 | return nil, err 271 | case operation.reverts: 272 | return res, errExecutionReverted 273 | case operation.halts: 274 | return res, nil 275 | case !operation.jumps: 276 | pc++ 277 | } 278 | } 279 | return nil, nil 280 | } 281 | 282 | ``` 283 | 284 | 到了这里整个部署合约流程就完成了, 部署合约时是从evm.Create->run->interper.run 然后在执行codeCopy指令后把runtime的内容返回出来。 285 | 在evm.Create函数中我们也看到了当run执行完成后会把runtime的合约代码最终设置到合约地址名下。 整个合约部署就算完成了。 286 | 287 | 分析完合约创建接着就该分析合约调用代码了。 调用智能合约和部署在以太坊交易上看来就是to的地址不在是nil而是一个具体的合约地址了。 288 | 同时input的内容不再是整个合约编译后的字节码了而是调用函数和对应的实参组合的内容。 这里就涉及到另一个东西那就是abi的概念。此处我不打算详细说明, abi描述了整个接口的详细信息, 根据abi可以解包和打包input调用的数据。 289 | 290 | ```go 291 | // 忽略一些隐藏了主线的内容 292 | func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftOverGas uint64, err error) { 293 | 294 | var ( 295 | to = AccountRef(addr) 296 | snapshot = evm.StateDB.Snapshot() 297 | ) 298 | // 首先要判断这个合约地址是否存在 如果不存在是否是内置的合约 299 | if !evm.StateDB.Exist(addr) { 300 | precompiles := PrecompiledContractsHomestead 301 | if evm.ChainConfig().IsByzantium(evm.BlockNumber) { 302 | precompiles = PrecompiledContractsByzantium 303 | } 304 | if precompiles[addr] == nil && evm.ChainConfig().IsEIP158(evm.BlockNumber) && value.Sign() == 0 { 305 | // Calling a non existing account, don't do antything, but ping the tracer 306 | if evm.vmConfig.Debug && evm.depth == 0 { 307 | evm.vmConfig.Tracer.CaptureStart(caller.Address(), addr, false, input, gas, value) 308 | evm.vmConfig.Tracer.CaptureEnd(ret, 0, 0, nil) 309 | } 310 | return nil, gas, nil 311 | } 312 | evm.StateDB.CreateAccount(addr) 313 | } 314 | // 执行进行以太币的转账过程 315 | evm.Transfer(evm.StateDB, caller.Address(), to.Address(), value) 316 | 317 | // 是不是很熟悉 不管是部署合约还是调用合约都要先创建合约对象 把合约加载出来挂到合约对象下 318 | contract := NewContract(caller, to, value, gas) 319 | contract.SetCallCode(&addr, evm.StateDB.GetCodeHash(addr), evm.StateDB.GetCode(addr)) 320 | 321 | start := time.Now() 322 | 323 | // 依然是调用run函数来执行代码 不同之处在于这次的input不在是nil了 而是交易的input内容 324 | // 在上一节中我们看到CALLDATALOAD这个指令会用到input的内容 325 | ret, err = run(evm, contract, input) 326 | 327 | if err != nil { 328 | evm.StateDB.RevertToSnapshot(snapshot) 329 | if err != errExecutionReverted { 330 | contract.UseGas(contract.Gas) 331 | } 332 | } 333 | // 最终返回执行结果 334 | return ret, contract.Gas, err 335 | } 336 | 337 | ``` 338 | 应该说到了这里只能合约流程就完事了, 可是也许你会好奇命名evm里面有那么多的内容没有分析到。 但是整个流程确实就是这些。 其他的比如栈对象是如何模拟的, 内存是如何模拟的。 操作码对应的操作函数,及其相关gas花费怎么计算的都没有说明。 可是我觉得首先知道整个流程和原理。阅读这些就比较容易了, 因为我们知道目的和原理, 就会明白它的那些代码的作用了。 如果我上去就说那些东西, 整个主线就会被淹没了。 339 | 340 | 最后还有一个比较重要的接口要说明一下, 它是我们接下来移植中要重点修改的内容。 341 | ```go 342 | type StateDB interface { 343 | // 创建账户函数 表明evm需要你执行创建一个新的账户体系 344 | CreateAccount(common.Address) 345 | 346 | // 减去一个账户的余额 347 | SubBalance(common.Address, *big.Int) 348 | // 添加一个账户的余额 349 | AddBalance(common.Address, *big.Int) 350 | // 获取一个账户的余额 351 | GetBalance(common.Address) *big.Int 352 | // 获取账户的nonce 因为以太坊要根据nonce在决定交易的执行顺序和合约地址的生成 353 | GetNonce(common.Address) uint64 354 | // 更新合约的nonce 355 | SetNonce(common.Address, uint64) 356 | 357 | // 获取合约地址的整个合约代码的hash值 358 | GetCodeHash(common.Address) common.Hash 359 | // 获取合约代码 360 | GetCode(common.Address) []byte 361 | // 设置合约代码 362 | SetCode(common.Address, []byte) 363 | // 获取合约代码的大小 364 | GetCodeSize(common.Address) int 365 | // 获取和添加偿还金额 366 | AddRefund(uint64) 367 | GetRefund() uint64 368 | // 注意这两个函数很重要 其实质就是相当于数据库的select和update 369 | // 一个智能合约的全局静态数据的读取和写入就是通过这两个函数 370 | GetState(common.Address, common.Hash) common.Hash 371 | SetState(common.Address, common.Hash, common.Hash) 372 | 373 | // 合约账户自杀 或者是否已经自杀 主要是以太坊的一个机制 自杀的合约会给与退费 374 | Suicide(common.Address) bool 375 | HasSuicided(common.Address) bool 376 | 377 | // 判断一个合约是否存在 378 | Exist(common.Address) bool 379 | // 判断合约是否为空 380 | // is defined according to EIP161 (balance = nonce = code = 0). 381 | Empty(common.Address) bool 382 | 383 | RevertToSnapshot(int) 384 | Snapshot() int 385 | 386 | // 此函数就是在我们在智能合约中执行emit命令时调用的 387 | AddLog(*types.Log) 388 | AddPreimage(common.Hash, []byte) 389 | 390 | // 这个接口在evm中没有使用到 我们可以写一个空函数 391 | ForEachStorage(common.Address, func(common.Hash, common.Hash) bool) 392 | } 393 | ``` 394 | 395 | 396 | 到此整个evm分析就结束了, 下一节进行移植evm时,涉及到应该更改的代码在详细说明, 需要实现的StateDB的每一个函数在具体描述。 397 | 398 | 399 | -------------------------------------------------------------------------------- /evm移植/index.md: -------------------------------------------------------------------------------- 1 | ## 移植evm虚拟机 2 | evm虚拟机设计的内容还是比较多的。 准备分下面几个章节进行分析和描述。 3 | 4 | 5 | - [x] [移植evm虚拟机之智能合约详解][1] 6 | - [x] [移植evm虚拟机之分析操作码][2] 7 | - [x] [移植evm虚拟机之源码分析][3] 8 | - [x] [移植evm虚拟机之实战][4] 9 | - [x] [移植evm虚拟机总结][5] 10 | 11 | 12 | [1]: ./evm之智能合约详解.md 13 | [2]: ./evm之操作码分析.md 14 | [3]: ./evm之源码分析.md 15 | [4]: ./evm之实战.md 16 | [5]: ./evm之总结.md 17 | -------------------------------------------------------------------------------- /google38470afd3c3e46d1.html: -------------------------------------------------------------------------------- 1 | google-site-verification: google38470afd3c3e46d1.html -------------------------------------------------------------------------------- /img/0FD1604CB898965C908B8D08C36D7EBC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wupeaking/tendermint_code_analysis/1f80951deee06b2281fe60b2af3cbcae4f62c501/img/0FD1604CB898965C908B8D08C36D7EBC.png -------------------------------------------------------------------------------- /img/149BC9339512B878285EF7D9AA4F7207.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wupeaking/tendermint_code_analysis/1f80951deee06b2281fe60b2af3cbcae4f62c501/img/149BC9339512B878285EF7D9AA4F7207.png -------------------------------------------------------------------------------- /img/1F50374E4165D3F086F83F28A4027EC4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wupeaking/tendermint_code_analysis/1f80951deee06b2281fe60b2af3cbcae4f62c501/img/1F50374E4165D3F086F83F28A4027EC4.jpg -------------------------------------------------------------------------------- /img/2046B1E1C9D205D48070AD7A62FD3624.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wupeaking/tendermint_code_analysis/1f80951deee06b2281fe60b2af3cbcae4f62c501/img/2046B1E1C9D205D48070AD7A62FD3624.jpg -------------------------------------------------------------------------------- /img/3D5F11F23D9D284B81181ABEB555081A.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wupeaking/tendermint_code_analysis/1f80951deee06b2281fe60b2af3cbcae4f62c501/img/3D5F11F23D9D284B81181ABEB555081A.png -------------------------------------------------------------------------------- /img/5E0910FA138824DDAD13A23C77E18193.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wupeaking/tendermint_code_analysis/1f80951deee06b2281fe60b2af3cbcae4f62c501/img/5E0910FA138824DDAD13A23C77E18193.png -------------------------------------------------------------------------------- /img/601D23F3E76830F3E0C55C7127B146A1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wupeaking/tendermint_code_analysis/1f80951deee06b2281fe60b2af3cbcae4f62c501/img/601D23F3E76830F3E0C55C7127B146A1.png -------------------------------------------------------------------------------- /img/6F3F6C7F4E2BE800C0A1C2E4D2CB64A8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wupeaking/tendermint_code_analysis/1f80951deee06b2281fe60b2af3cbcae4f62c501/img/6F3F6C7F4E2BE800C0A1C2E4D2CB64A8.png -------------------------------------------------------------------------------- /img/7C4BA46600C364D522BBB519B09F7689.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wupeaking/tendermint_code_analysis/1f80951deee06b2281fe60b2af3cbcae4f62c501/img/7C4BA46600C364D522BBB519B09F7689.jpg -------------------------------------------------------------------------------- /img/91F9F25EA7BC8AE39E9A3E1D7FC0783B.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wupeaking/tendermint_code_analysis/1f80951deee06b2281fe60b2af3cbcae4f62c501/img/91F9F25EA7BC8AE39E9A3E1D7FC0783B.jpg -------------------------------------------------------------------------------- /img/CA37F867C576AA2AE4B2BE82F3FEE28D.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wupeaking/tendermint_code_analysis/1f80951deee06b2281fe60b2af3cbcae4f62c501/img/CA37F867C576AA2AE4B2BE82F3FEE28D.png -------------------------------------------------------------------------------- /img/ED9351B67F09465C702F11BEBD85BC07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wupeaking/tendermint_code_analysis/1f80951deee06b2281fe60b2af3cbcae4f62c501/img/ED9351B67F09465C702F11BEBD85BC07.png -------------------------------------------------------------------------------- /img/evm-bytes.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wupeaking/tendermint_code_analysis/1f80951deee06b2281fe60b2af3cbcae4f62c501/img/evm-bytes.jpg -------------------------------------------------------------------------------- /img/evm-code.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wupeaking/tendermint_code_analysis/1f80951deee06b2281fe60b2af3cbcae4f62c501/img/evm-code.jpg -------------------------------------------------------------------------------- /img/source-code.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wupeaking/tendermint_code_analysis/1f80951deee06b2281fe60b2af3cbcae4f62c501/img/source-code.jpg -------------------------------------------------------------------------------- /node启动流程分析.md: -------------------------------------------------------------------------------- 1 | 再说启动流程之前我们看看一个github的项目。 [cobra](https://github.com/spf13/cobra) 它是一个是用于创建强大的现代CLI应用程序的库,也是用于生成应用程序和命令文件的程序。 简单点来说就是方便使用者更易创建命令行工具。 2 | 3 | 举个静态博客生成器hugo的例子: 4 | ```shell 5 | hugo help 6 | hugo is the main command, used to build your Hugo site. 7 | 8 | Hugo is a Fast and Flexible Static Site Generator 9 | built with love by spf13 and friends in Go. 10 | 11 | Complete documentation is available at http://gohugo.io/. 12 | 13 | Usage: 14 | hugo [flags] 15 | hugo [command] 16 | 17 | Available Commands: 18 | benchmark Benchmark Hugo by building a site a number of times. 19 | check Contains some verification checks 20 | config Print the site configuration 21 | convert Convert your content to different formats 22 | env Print Hugo version and environment info 23 | gen A collection of several useful generators. 24 | help Help about any command 25 | import Import your site from others. 26 | list Listing out various types of content 27 | new Create new content for your site 28 | server A high performance webserver 29 | version Print the version number of Hugo 30 | 31 | ``` 32 | 使用这个包可以方便管理这些子命令。 和这个包类似功能的还有一个叫做[cli](https://github.com/urfave/cli)。 之前我都是用的这个包。 大致流程都是一样的。 我们看一下官方的readme给的一个例子。 33 | 34 | ```shell 35 | package main 36 | 37 | import ( 38 | "fmt" 39 | "strings" 40 | 41 | "github.com/spf13/cobra" 42 | ) 43 | 44 | func main() { 45 | var echoTimes int 46 | 47 | var cmdPrint = &cobra.Command{ 48 | Use: "print [string to print]", 49 | Short: "Print anything to the screen", 50 | Long: `print is for printing anything back to the screen. 51 | For many years people have printed back to the screen.`, 52 | Args: cobra.MinimumNArgs(1), 53 | Run: func(cmd *cobra.Command, args []string) { 54 | fmt.Println("Print: " + strings.Join(args, " ")) 55 | }, 56 | } 57 | 58 | var cmdEcho = &cobra.Command{ 59 | Use: "echo [string to echo]", 60 | Short: "Echo anything to the screen", 61 | Long: `echo is for echoing anything back. 62 | Echo works a lot like print, except it has a child command.`, 63 | Args: cobra.MinimumNArgs(1), 64 | Run: func(cmd *cobra.Command, args []string) { 65 | fmt.Println("Print: " + strings.Join(args, " ")) 66 | }, 67 | } 68 | 69 | var cmdTimes = &cobra.Command{ 70 | Use: "times [# times] [string to echo]", 71 | Short: "Echo anything to the screen more times", 72 | Long: `echo things multiple times back to the user by providing 73 | a count and a string.`, 74 | Args: cobra.MinimumNArgs(1), 75 | Run: func(cmd *cobra.Command, args []string) { 76 | for i := 0; i < echoTimes; i++ { 77 | fmt.Println("Echo: " + strings.Join(args, " ")) 78 | } 79 | }, 80 | } 81 | 82 | cmdTimes.Flags().IntVarP(&echoTimes, "times", "t", 1, "times to echo the input") 83 | 84 | var rootCmd = &cobra.Command{Use: "app"} 85 | rootCmd.AddCommand(cmdPrint, cmdEcho) 86 | cmdEcho.AddCommand(cmdTimes) 87 | rootCmd.Execute() 88 | } 89 | 90 | /* 91 | 这样 我们就实现go run main.go print | echo | times 三个子命令了 92 | */ 93 | ``` 94 | 关于这个包我们就只说这么多,开始进行Tendermint的流程分析。 95 | 进入cmd/tendermint/main.go 文件中。 96 | ```go 97 | func main() { 98 | // 创建根命令 99 | rootCmd := cmd.RootCmd 100 | 101 | // 创建了一些子命令 这些命令在这里不再细说了 大家只要tendermint help一下就能命名其含义 如果想最终具体某个子命令的实现只需要到 102 | // cmd/tendermint/commands找到对应的实现就好了 在此处我们只关注node子命令的实现。 103 | rootCmd.AddCommand( 104 | cmd.GenValidatorCmd, 105 | cmd.InitFilesCmd, 106 | cmd.ProbeUpnpCmd, 107 | cmd.LiteCmd, 108 | cmd.ReplayCmd, 109 | cmd.ReplayConsoleCmd, 110 | cmd.ResetAllCmd, 111 | cmd.ResetPrivValidatorCmd, 112 | cmd.ShowValidatorCmd, 113 | cmd.TestnetFilesCmd, 114 | cmd.ShowNodeIDCmd, 115 | cmd.GenNodeKeyCmd, 116 | cmd.VersionCmd) 117 | 118 | // 这个是我们重点关注的子命令 用于创建node。 119 | nodeFunc := nm.DefaultNewNode 120 | 121 | // 添加node子命令 122 | rootCmd.AddCommand(cmd.NewRunNodeCmd(nodeFunc)) 123 | 124 | cmd := cli.PrepareBaseCmd(rootCmd, "TM", os.ExpandEnv(filepath.Join("$HOME", cfg.DefaultTendermintDir))) 125 | if err := cmd.Execute(); err != nil { 126 | panic(err) 127 | } 128 | } 129 | 130 | func NewRunNodeCmd(nodeProvider nm.NodeProvider) *cobra.Command { 131 | cmd := &cobra.Command{ 132 | Use: "node", 133 | Short: "Run the tendermint node", 134 | 135 | RunE: func(cmd *cobra.Command, args []string) error { 136 | // 当调用tendermint node .... 就会进入到这个函数 137 | 138 | // 这里就是先创建一个node对象 nodeProvider就是上面的nm.DefaultNewNode 139 | // 这个函数流程就是创建node对象 然后启动node 调用n.RunForever()进行守护 140 | // RunForever 其实就是监听系统的os.Interrupt, syscall.SIGTERM信号 当收到这两个信号时 141 | // 调用node.Stop 进行退出处理。否则一直在运行。 142 | n, err := nodeProvider(config, logger) 143 | if err != nil { 144 | return fmt.Errorf("Failed to create node: %v", err) 145 | } 146 | 147 | if err := n.Start(); err != nil { 148 | return fmt.Errorf("Failed to start node: %v", err) 149 | } 150 | logger.Info("Started node", "nodeInfo", n.Switch().NodeInfo()) 151 | 152 | // Trap signal, run forever. 153 | n.RunForever() 154 | 155 | return nil 156 | }, 157 | } 158 | 159 | AddNodeFlags(cmd) 160 | return cmd 161 | } 162 | ``` 163 | 分析完main函数 我们就要集中到node的创建和启动了, 那里就是整个Tendermint的启动核心。代码在node/node.go的文件中。 164 | 先看创建node的过程 165 | ```go 166 | func DefaultNewNode(config *cfg.Config, logger log.Logger) (*Node, error) { 167 | return NewNode(config, 168 | privval.LoadOrGenFilePV(config.PrivValidatorFile()), 169 | proxy.DefaultClientCreator(config.ProxyApp, config.ABCI, config.DBDir()), 170 | DefaultGenesisDocProviderFunc(config), 171 | DefaultDBProvider, 172 | DefaultMetricsProvider(config.Instrumentation), 173 | logger, 174 | ) 175 | } 176 | // 为了分析方便 会忽略一些细节 177 | func NewNode(config *cfg.Config, 178 | privValidator types.PrivValidator, 179 | clientCreator proxy.ClientCreator, 180 | genesisDocProvider GenesisDocProvider, 181 | dbProvider DBProvider, 182 | metricsProvider MetricsProvider, 183 | logger log.Logger) (*Node, error) { 184 | 185 | // 根据配置信息 创建数据库的使用 tendermint已经封装了leveldb(c/go 一个是原生go写的客户端 一个是cgo写的客户端) fsdb remotedb memdb的实现。 186 | // 如果不出意外我们使用的是leveldb。 当然也是可以扩展自己的后端存储 只要实现db定义的那几个接口就行。 187 | // 下面我们直接以leveldb作为数据存储来分析 188 | // 此处打开或者创建名称blockstore.db的数据库 189 | blockStoreDB, err := dbProvider(&DBContext{"blockstore", config}) 190 | if err != nil { 191 | return nil, err 192 | } 193 | // 创建区块存储对象 这个函数不知道大家还有没有印象 在Blockchain模块处有分析过。 回想一下Blockchain模块, store.go的文件中包含了对区块数据的读取和写入操作等功能 194 | blockStore := bc.NewBlockStore(blockStoreDB) 195 | 196 | // 同理 打开或者创建名称state.db的数据库 保存状态相关的内容 197 | stateDB, err := dbProvider(&DBContext{"state", config}) 198 | if err != nil { 199 | return nil, err 200 | } 201 | 202 | // 在加载状态数据之前, 我们先加载一下创世区块文件 也就是genesis.json文件 这个文件中保存了 203 | // 最初的状态信息 当然如果没有加载到 Tendermint会默认创建一个创世区块状态。 204 | genDoc, err := loadGenesisDoc(stateDB) 205 | if err != nil { 206 | genDoc, err = genesisDocProvider() 207 | if err != nil { 208 | return nil, err 209 | } 210 | saveGenesisDoc(stateDB, genDoc) 211 | } 212 | 213 | // 根据stateDB和之前加载的创世区块文件来加载出最新状态。 214 | // 如果数据库中没有读取到最新的状态 则从创世区块来生成状态。 215 | state, err := sm.LoadStateFromDBOrGenesisDoc(stateDB, genDoc) 216 | if err != nil { 217 | return nil, err 218 | } 219 | 220 | 221 | // 下面这几行就是创建和我们自己的应用层交互的地方 这里我们准备先不分析 222 | // 留到下一节分析abci接口的时候在追踪这个地方 但是有个地方我们我先说一下就是这个 223 | // NewHandshaker 这个函数创建handshaker之后 最后会在proxyApp.Start中调用了 224 | // handshaker.Handshake这个函数 这个函数会重放已经保存的区块内容将Tendermint保存的区块一一调用 225 | // 我们的APP层 然后进行APPHASH相关的校验 在共识算法中会有描述。 226 | // proxyApp.Start()执行之后 已存区块已经重放完成。 同时创建了内存池, 共识, 查询的应用客户端 227 | consensusLogger := logger.With("module", "consensus") 228 | handshaker := cs.NewHandshaker(stateDB, state, blockStore, genDoc) 229 | handshaker.SetLogger(consensusLogger) 230 | proxyApp := proxy.NewAppConns(clientCreator, handshaker) 231 | proxyApp.SetLogger(logger.With("module", "proxy")) 232 | if err := proxyApp.Start(); err != nil { 233 | return nil, fmt.Errorf("Error starting proxy app connections: %v", err) 234 | } 235 | 236 | //此处再次重新加载一下状态。 因为在重放的时候状态可能会改变 比如应用层在上次停掉时比Tendermint保存的区块高度少一个 重放的时候就会追上来 这就会导致状态发生了变化。 237 | state = sm.LoadState(stateDB) 238 | 239 | // Tendermint对验证角色进行了封装 默认是从配置文件中加载自己的验证器信息 240 | // 同时提供了通过socket来获取是有验证器信息的功能 241 | // 当然前提你要是一个验证者角色才行 242 | if config.PrivValidatorListenAddr != "" { 243 | var ( 244 | // TODO: persist this key so external signer 245 | // can actually authenticate us 246 | privKey = ed25519.GenPrivKey() 247 | pvsc = privval.NewSocketPV( 248 | logger.With("module", "privval"), 249 | config.PrivValidatorListenAddr, 250 | privKey, 251 | ) 252 | ) 253 | 254 | if err := pvsc.Start(); err != nil { 255 | return nil, fmt.Errorf("Error starting private validator client: %v", err) 256 | } 257 | 258 | privValidator = pvsc 259 | } 260 | 261 | // 开始创建内存池Reactor 关于内存池内容可以看看内存池的模块分析 262 | mempoolLogger := logger.With("module", "mempool") 263 | mempool := mempl.NewMempool( 264 | config.Mempool, 265 | proxyApp.Mempool(), 266 | state.LastBlockHeight, 267 | mempl.WithMetrics(memplMetrics), 268 | ) 269 | mempool.SetLogger(mempoolLogger) 270 | mempool.InitWAL() // no need to have the mempool wal during tests 271 | mempoolReactor := mempl.NewMempoolReactor(config.Mempool, mempool) 272 | mempoolReactor.SetLogger(mempoolLogger) 273 | if config.Consensus.WaitForTxs() { 274 | mempool.EnableTxsAvailable() 275 | } 276 | 277 | //打开evidence.db 创建EvidenceReactor 278 | evidenceDB, err := dbProvider(&DBContext{"evidence", config}) 279 | if err != nil { 280 | return nil, err 281 | } 282 | evidenceLogger := logger.With("module", "evidence") 283 | evidenceStore := evidence.NewEvidenceStore(evidenceDB) 284 | evidencePool := evidence.NewEvidencePool(stateDB, evidenceStore) 285 | evidencePool.SetLogger(evidenceLogger) 286 | evidenceReactor := evidence.NewEvidenceReactor(evidencePool) 287 | evidenceReactor.SetLogger(evidenceLogger) 288 | 289 | // 下面几句是 先创建一个blockExec 这个对象最重要的就是之前我们在state中说的ApplyBlock 290 | // 把Tendermint打包的区块提交到我们的应用层 然后更新Tendermint的状态,Mempool等。 具体查看state模块分析 291 | // 然后创建了Blockchain的Reactor 292 | blockExecLogger := logger.With("module", "state") 293 | // make block executor for consensus and blockchain reactors to execute blocks 294 | blockExec := sm.NewBlockExecutor(stateDB, blockExecLogger, proxyApp.Consensus(), mempool, evidencePool) 295 | // Make BlockchainReactor 296 | bcReactor := bc.NewBlockchainReactor(state.Copy(), blockExec, blockStore, fastSync) 297 | bcReactor.SetLogger(logger.With("module", "blockchain")) 298 | 299 | // 接下来是创建共识Reactor 300 | consensusState := cs.NewConsensusState( 301 | config.Consensus, 302 | state.Copy(), 303 | blockExec, 304 | blockStore, 305 | mempool, 306 | evidencePool, 307 | cs.WithMetrics(csMetrics), 308 | ) 309 | consensusState.SetLogger(consensusLogger) 310 | if privValidator != nil { 311 | consensusState.SetPrivValidator(privValidator) 312 | } 313 | consensusReactor := cs.NewConsensusReactor(consensusState, fastSync) 314 | consensusReactor.SetLogger(consensusLogger) 315 | 316 | p2pLogger := logger.With("module", "p2p") 317 | 318 | // 创建P2P的switch 将上面的Reactor加入进来 其实我们可以看到 之前分析的那么多的模块 很多实例的创建其实都是在这个函数中完成的。 319 | sw := p2p.NewSwitch(config.P2P, p2p.WithMetrics(p2pMetrics)) 320 | sw.SetLogger(p2pLogger) 321 | sw.AddReactor("MEMPOOL", mempoolReactor) 322 | sw.AddReactor("BLOCKCHAIN", bcReactor) 323 | sw.AddReactor("CONSENSUS", consensusReactor) 324 | sw.AddReactor("EVIDENCE", evidenceReactor) 325 | 326 | // 开始PEX的Reactor创建 327 | // 创建地址簿 这个地址簿是维护所有peer的地址信息 是对peer进行crud的关键。 328 | // 这个在p2p模块中, 好像我没有把这块的代码进行文档化(todo) 329 | addrBook := pex.NewAddrBook(config.P2P.AddrBookFile(), config.P2P.AddrBookStrict) 330 | addrBook.SetLogger(p2pLogger.With("book", config.P2P.AddrBookFile())) 331 | if config.P2P.PexReactor { 332 | // TODO persistent peers ? so we can have their DNS addrs saved 333 | pexReactor := pex.NewPEXReactor(addrBook, 334 | &pex.PEXReactorConfig{ 335 | Seeds: cmn.SplitAndTrim(config.P2P.Seeds, ",", " "), 336 | SeedMode: config.P2P.SeedMode, 337 | }) 338 | pexReactor.SetLogger(p2pLogger) 339 | sw.AddReactor("PEX", pexReactor) 340 | } 341 | 342 | sw.SetAddrBook(addrBook) 343 | 344 | // 下面是一个索引服务功能 这个是Tendermint提供的一个功能 就是可以对交易进行索引查找 这个功能我没有仔细分析 345 | // 个人认为这个功能意义不大 如果我实现了自己的APP 那么我可以在自己的APP层进行定制化的索引。 效果比这个好很多 346 | // 而且还不受这个限制。 347 | // 如果以后有时间 我会分析一下交易索引服务的原理 348 | var txIndexer txindex.TxIndexer 349 | switch config.TxIndex.Indexer { 350 | case "kv": 351 | store, err := dbProvider(&DBContext{"tx_index", config}) 352 | if err != nil { 353 | return nil, err 354 | } 355 | if config.TxIndex.IndexTags != "" { 356 | txIndexer = kv.NewTxIndex(store, kv.IndexTags(cmn.SplitAndTrim(config.TxIndex.IndexTags, ",", " "))) 357 | } else if config.TxIndex.IndexAllTags { 358 | txIndexer = kv.NewTxIndex(store, kv.IndexAllTags()) 359 | } else { 360 | txIndexer = kv.NewTxIndex(store) 361 | } 362 | default: 363 | txIndexer = &null.TxIndex{} 364 | } 365 | 366 | indexerService := txindex.NewIndexerService(txIndexer, eventBus) 367 | indexerService.SetLogger(logger.With("module", "txindex")) 368 | 369 | node := &Node{ 370 | config: config, 371 | genesisDoc: genDoc, 372 | privValidator: privValidator, 373 | 374 | sw: sw, 375 | addrBook: addrBook, 376 | 377 | stateDB: stateDB, 378 | blockStore: blockStore, 379 | bcReactor: bcReactor, 380 | mempoolReactor: mempoolReactor, 381 | consensusState: consensusState, 382 | consensusReactor: consensusReactor, 383 | evidencePool: evidencePool, 384 | proxyApp: proxyApp, 385 | txIndexer: txIndexer, 386 | indexerService: indexerService, 387 | eventBus: eventBus, 388 | } 389 | node.BaseService = *cmn.NewBaseService(logger, "Node", node) 390 | return node, nil 391 | } 392 | ``` 393 | 到了这里我们差不多就看完了node的创建。 总结一下床架node的过程: 394 | 395 | * 创建或者打开blockstore.db 最终创建bc实例和bcReactor 396 | * 创建或者打开state.db 最终更新state对象 397 | * 创建mempool实例 加载本地持久化的交易池 并创建mempoolReactor 398 | * 创建与APP之间的客户端 并重返所有保存的区块 399 | * 创建证据内存池和证据Reactor 400 | * 根据上面已经创建的对象开始创建consensus对象和Reactor 401 | * 开始创建p2p的Switch 将所有reactor加入。 402 | * 开启tx_index服务 403 | * 返回node实例 404 | 405 | 406 | 创建完实例就需要启动了 来看看启动流程 407 | ```go 408 | func (n *Node) OnStart() error { 409 | 410 | // 创建p2p的监听 有了监听才能接受别人的请求 这一步个人觉得在上面的函数里比较合适 这里应该启动SW 而不是还要进行一些初始化的工作 411 | l := p2p.NewDefaultListener( 412 | n.config.P2P.ListenAddress, 413 | n.config.P2P.ExternalAddress, 414 | n.config.P2P.UPNP, 415 | n.Logger.With("module", "p2p")) 416 | n.sw.AddListener(l) 417 | 418 | // 从我们的配置文件中加载一个node的配置 419 | // 结构类似这个 {"priv_key":{"type":"tendermint/PrivKeyEd25519","value":"I5Dn6uZXbNO+VgvXNgehFduA2HsdMs+XubFCWzOM0AYZ66I3Bjwakez1B+klii6Am6WAdP95AWIo8wMkkafUeg=="}} 420 | // 如果config 文件夹下没有node_key.json 则会自动创建一个 421 | nodeKey, err := p2p.LoadOrGenNodeKey(n.config.NodeKeyFile()) 422 | if err != nil { 423 | return err 424 | } 425 | n.Logger.Info("P2P Node ID", "ID", nodeKey.ID(), "file", n.config.NodeKeyFile()) 426 | 427 | nodeInfo := n.makeNodeInfo(nodeKey.ID()) 428 | n.sw.SetNodeInfo(nodeInfo) 429 | n.sw.SetNodeKey(nodeKey) 430 | 431 | //configure文件夹下的 addrbook.json 会加入我们自己的节点 432 | n.addrBook.AddOurAddress(nodeInfo.NetAddress()) 433 | 434 | // 把配置文件中配置的私有节点也加入addrBook 435 | n.addrBook.AddPrivateIDs(cmn.SplitAndTrim(n.config.P2P.PrivatePeerIDs, ",", " ")) 436 | 437 | // 开启rpc服务 找个时间专门分析一下rpc服务 Tendermint的rpc流程还是比较清晰的 438 | // 不像ethereum中使用了大量的反射包 查询一个具体的rpc方法实现非常麻烦 439 | if n.config.RPC.ListenAddress != "" { 440 | listeners, err := n.startRPC() 441 | if err != nil { 442 | return err 443 | } 444 | n.rpcListeners = listeners 445 | } 446 | 447 | // 性能监控相关的地方 我都忽略掉了 目前我都不会考虑这个地方的分析 448 | if n.config.Instrumentation.Prometheus && 449 | n.config.Instrumentation.PrometheusListenAddr != "" { 450 | n.prometheusSrv = n.startPrometheusServer(n.config.Instrumentation.PrometheusListenAddr) 451 | } 452 | 453 | // 终于可以启动SW了 启动Switch的过程也就意味着所有Reactor的启动 每个Reactor的启动就会启动相对应的各个模块的服务 具体的启动可以看各个模块的分析 454 | err = n.sw.Start() 455 | if err != nil { 456 | return err 457 | } 458 | 459 | // 先对配置中的持久地址进行一次拨号 后面在PEX中会每隔一段时间都会进行检查 460 | if n.config.P2P.PersistentPeers != "" { 461 | err = n.sw.DialPeersAsync(n.addrBook, cmn.SplitAndTrim(n.config.P2P.PersistentPeers, ",", " "), true) 462 | if err != nil { 463 | return err 464 | } 465 | } 466 | 467 | // 开启交易索引服务 468 | return n.indexerService.Start() 469 | } 470 | 471 | ``` 472 | 473 | 启动流程就是这么多 主要就是启动sw 然后启动rpc服务 最后再启动索引服务。 474 | 475 | 476 | 再看一下退出的流程: 477 | 478 | ```go 479 | func (n *Node) OnStop() { 480 | // 几乎所有的模块都实现了cmd.Server接口 n.BaseService.OnStop()是一个cmd的简单实现Server接口的实例 其实啥也没做 481 | n.BaseService.OnStop() 482 | // 索引服务关闭 483 | n.indexerService.Stop() 484 | 485 | // 这个比较重要 关闭所有的Reactor服务 区块链的各个模块服务在此处会被关闭掉 486 | n.sw.Stop() 487 | 488 | // 关闭rpc服务 489 | for _, l := range n.rpcListeners { 490 | n.Logger.Info("Closing rpc listener", "listener", l) 491 | if err := l.Close(); err != nil { 492 | n.Logger.Error("Error closing listener", "listener", l, "err", err) 493 | } 494 | } 495 | 496 | // privValidator 如果是通过socket进行验证器的获取 那么此处需要关闭socket 497 | if pvsc, ok := n.privValidator.(*privval.SocketPV); ok { 498 | if err := pvsc.Stop(); err != nil { 499 | n.Logger.Error("Error stopping priv validator socket client", "err", err) 500 | } 501 | } 502 | 503 | } 504 | ``` 505 | 506 | 整个Tendermint节点启动的流程大致就是这么多, 主要代码就在tendermint/node目录下。 在Tendermint中, 如果了解各个模块的工作内容, 差不多对整个流程的理解就没有什么太大的问题。 507 | Tendermint的各个模块启动最后均是由Switch的启动引发的, 每个Reactor启动时同时也会自己模块的各个功能启动。 同样在退出时也是由Switch关闭,调用各个Reactor的Stop功能, 最后使Reactor对应的各个模块的功能关闭掉。 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | -------------------------------------------------------------------------------- /p2p源码分析.md: -------------------------------------------------------------------------------- 1 | ![Class Diagram.png](img/6F3F6C7F4E2BE800C0A1C2E4D2CB64A8.png) 2 | 3 | ## 基本组件说明 4 | P2P模块涉及的最重要的组件如上图所示, 上述的UML图并没有列出某个类的所有属性和方法,只是列举了我认为比较重要的部分。 第一眼看到上面的类图我猜应该是什么也看不出来。 再仔细看我想依然是云山雾绕不知道整个P2P的流程。 所以类图只是给大家一个基本的组件印象。让大家能大致猜测一下各个组件的功能。 现在我们不妨按着上面的类图去大胆猜一猜上述的各个组件的功能。 5 | 6 | 我们先从Switch这个开始, switch给我的第一个印象是交换机的意思, 将网口的信息进行互相的交换。 在tendermint里我们发现它实现了BaseServer的接口。 其实Baseserver就是一个通用的服务管理接口, 在tendermint中到处可见这个接口的实现。 主要作用就是让服务能实现统一的启动和停止。 所以说Switch启动的入口就应该是Onstart()中。 switch里面拥有了AddReactor方法,那是不是应该认为所有的Reactor的实例与P2P的交互是通过这个组件完成的呢? 它还拥有DialPeerWithAddress方法,那是不是他还管理着创建新Peer的功能呢。 到这里可能你对Reactor是啥感到疑惑,简而言之它就是和整个P2P网络进行交互的组件, 在Tendermint中有6个Reactor实例分别是MEMPOOL,BLOCKCHAIN,CONSENSUS,EVIDENCE,PEX. 其中PEX我认为是一个比较特殊的Reactor,因为他还是和P2P有关联的一个组件。 关于Reactor不了解没有关系,后面我会介绍。 7 | 8 | 接着我们看看Peer这个组件,这个类中有Mconnect成员,Peer应该就是代表这个对等体。它实现了OnStart,Send方法,所以启动入口应该也是OnStart,Send方法应该就是想往对等体发送消息的方法。 可是作为一个Peer不能只发送消息不接收消息吧。 为啥没有看到Recv之类的函数呢。 别急,后面我也会在正确的时间来说明。 9 | 10 | Mconnnect应该就比较好理解了,它应该就是维护了网络连接,进行底层的网络数据传输功能了。 其实它不仅做了网络传输,还做了很多其他事情。这里先卖个关子,反正说多了也不一定记得住。 11 | 12 | AddrBook应该就是维护peer信息,记录连接的peer。查找可用的peer了。 13 | 14 | 15 | 16 | ## MConnecttion源码分析: 17 | 为啥会首先分析MConnecttion? 因为它是最底层的部分。 消息的写入和读取都是通过此组件完成的。 18 | 19 | 函数`NewMConnectionWithConfig`分析: 20 | ```go 21 | func NewMConnectionWithConfig(conn net.Conn, chDescs []*ChannelDescriptor, onReceive receiveCbFunc, onError errorCbFunc, config MConnConfig) *MConnection { 22 | if config.PongTimeout >= config.PingInterval { 23 | panic("pongTimeout must be less than pingInterval (otherwise, next ping will reset pong timer)") 24 | } 25 | 26 | mconn := &MConnection{ 27 | conn: conn, 28 | bufConnReader: bufio.NewReaderSize(conn, minReadBufferSize), 29 | bufConnWriter: bufio.NewWriterSize(conn, minWriteBufferSize), 30 | sendMonitor: flow.New(0, 0), 31 | recvMonitor: flow.New(0, 0), 32 | send: make(chan struct{}, 1), 33 | pong: make(chan struct{}, 1), 34 | onReceive: onReceive, 35 | onError: onError, 36 | config: config, 37 | } 38 | 39 | // Create channels 40 | var channelsIdx = map[byte]*Channel{} 41 | var channels = []*Channel{} 42 | 43 | for _, desc := range chDescs { 44 | channel := newChannel(mconn, *desc) 45 | channelsIdx[channel.desc.ID] = channel 46 | channels = append(channels, channel) 47 | } 48 | mconn.channels = channels 49 | mconn.channelsIdx = channelsIdx 50 | 51 | mconn.BaseService = *cmn.NewBaseService(nil, "MConnection", mconn) 52 | 53 | // maxPacketMsgSize() is a bit heavy, so call just once 54 | mconn._maxPacketMsgSize = mconn.maxPacketMsgSize() 55 | 56 | return mconn 57 | } 58 | ``` 59 | 上述函数创建了MConnecttion的对象实例, conn是TCP连接成功返回的对象, chDescs用于创建通道,通道在MConnecttion中用处巨大。 onReceive是当读取到数据之后进行回调, onError是错误发生时的回调。 60 | 特别要注意到` bufConnReader: bufio.NewReaderSize(conn, minReadBufferSize), 61 | bufConnWriter: bufio.NewWriterSize(conn, minWriteBufferSize),`是将net.Con封装成bufio的读写,这样可以方便了用类似文件IO的形式来对TCP流进行读写操作。 62 | 接下来就是根据chDescs创建通道。我们看一下通道的成员 63 | ```go 64 | type Channel struct { 65 | conn *MConnection 66 | desc ChannelDescriptor 67 | sendQueue chan []byte 68 | sendQueueSize int32 // atomic. 69 | recving []byte 70 | sending []byte 71 | recentlySent int64 // exponential moving average 72 | 73 | maxPacketMsgPayloadSize int 74 | 75 | Logger log.Logger 76 | } 77 | ``` 78 | sendQueue 是发送队列, recving是接收缓冲区, sending是发送缓冲区。 这里可以先说明的是Peer调用Send发送消息其实是调用MConnecttion的Send方法,那么MConnecttion的Send其实也只是把内容发送到Channel的sendQueue中, 然后会有专门的routine读取Channel进行实际的消息发送。 79 | 80 | 创建完MConnecttion实例之后,就像我们之前说的那样,应该会先启动它,我们来分析OnStart方法。 81 | ```go 82 | func (c *MConnection) OnStart() error { 83 | if err := c.BaseService.OnStart(); err != nil { 84 | return err 85 | } 86 | c.quit = make(chan struct{}) 87 | // 同步周期 88 | c.flushTimer = cmn.NewThrottleTimer("flush", c.config.FlushThrottle) 89 | // ping周期 90 | c.pingTimer = cmn.NewRepeatTimer("ping", c.config.PingInterval) 91 | c.pongTimeoutCh = make(chan bool, 1) 92 | c.chStatsTimer = cmn.NewRepeatTimer("chStats", updateStats) 93 | go c.sendRoutine() 94 | go c.recvRoutine() 95 | return nil 96 | } 97 | ``` 98 | 代码比较简单,创建两个goroutine,一个发送任务循环,一个进行接收任务循环。 这就和我们之前的Channel作用对上了。 先分析`sendRoutine` 99 | ```go 100 | func (c *MConnection) sendRoutine() { 101 | defer c._recover() 102 | 103 | FOR_LOOP: 104 | for { 105 | var _n int64 106 | var err error 107 | SELECTION: 108 | select { 109 | case <-c.flushTimer.Ch: 110 | // NOTE: flushTimer.Set() must be called every time 111 | // something is written to .bufConnWriter. 112 | c.flush() 113 | case <-c.chStatsTimer.Chan(): 114 | for _, channel := range c.channels { 115 | channel.updateStats() 116 | } 117 | case <-c.pingTimer.Chan(): 118 | c.Logger.Debug("Send Ping") 119 | _n, err = cdc.MarshalBinaryWriter(c.bufConnWriter, PacketPing{}) 120 | if err != nil { 121 | break SELECTION 122 | } 123 | c.sendMonitor.Update(int(_n)) 124 | c.Logger.Debug("Starting pong timer", "dur", c.config.PongTimeout) 125 | c.pongTimer = time.AfterFunc(c.config.PongTimeout, func() { 126 | select { 127 | case c.pongTimeoutCh <- true: 128 | default: 129 | } 130 | }) 131 | c.flush() 132 | case timeout := <-c.pongTimeoutCh: 133 | if timeout { 134 | c.Logger.Debug("Pong timeout") 135 | err = errors.New("pong timeout") 136 | } else { 137 | c.stopPongTimer() 138 | } 139 | case <-c.pong: 140 | c.Logger.Debug("Send Pong") 141 | _n, err = cdc.MarshalBinaryWriter(c.bufConnWriter, PacketPong{}) 142 | if err != nil { 143 | break SELECTION 144 | } 145 | c.sendMonitor.Update(int(_n)) 146 | c.flush() 147 | case <-c.quit: 148 | break FOR_LOOP 149 | case <-c.send: 150 | // Send some PacketMsgs 151 | eof := c.sendSomePacketMsgs() 152 | if !eof { 153 | // Keep sendRoutine awake. 154 | select { 155 | case c.send <- struct{}{}: 156 | default: 157 | } 158 | } 159 | } 160 | 161 | if !c.IsRunning() { 162 | break FOR_LOOP 163 | } 164 | if err != nil { 165 | c.Logger.Error("Connection failed @ sendRoutine", "conn", c, "err", err) 166 | c.stopForError(err) 167 | break FOR_LOOP 168 | } 169 | } 170 | 171 | // Cleanup 172 | c.stopPongTimer() 173 | } 174 | ``` 175 | 176 | c.flushTimer.Ch 进行周期性的flush,c.pingTimer.Chan()进行周期性的向tcp连接写入ping消息,c.pong表示需要进行pong回复, 这个不是周期性的自动写入,是因为收到了对方发来的ping消息,这个通道的写入是在recvRoutine函数中进行的。我们在recvRoutine再分析它。 忽略掉其他内容,我们重点看一下c.send, 也即是说当c.send有写入,我们就应该进行包发送了。 我们来看看`sendSomePacketMsgs` 做了哪些工作。 为了分析方便 删除了一些非主流程代码,并将分析内容写在注释中。 177 | 178 | ``` 179 | func (c *MConnection) sendPacketMsg() bool { 180 | 181 | for _, channel := range c.channels { 182 | // 检查channel.sendQueue 是否为0 channel.sending缓冲区是否为空 如果为空说明没有需要发送的内容了。 183 | // 如果缓冲区为空了 就要把channel.sendQueue内部排队的内容 移出一份到缓冲区中。 184 | if !channel.isSendPending() { 185 | continue 186 | } 187 | 188 | } 189 | if leastChannel == nil { 190 | return true 191 | } 192 | 193 | // 执行到这里说明有某个Channel内部有消息没发送 将消息发送出去 194 | _n, err := leastChannel.writePacketMsgTo(c.bufConnWriter) 195 | if err != nil { 196 | c.Logger.Error("Failed to write PacketMsg", "err", err) 197 | c.stopForError(err) 198 | return true 199 | } 200 | c.flushTimer.Set() 201 | return false 202 | } 203 | 204 | func (ch *Channel) writePacketMsgTo(w io.Writer) (n int64, err error) { 205 | var packet = ch.nextPacketMsg() 206 | // 将结构体进行二进制编码发送 这里不再进行深入探索。只需要明白返回的数据也必须使用对应的方法才能进行正确的解包。 因为TCP是流式的 207 | n, err = cdc.MarshalBinaryWriter(w, packet) 208 | ch.recentlySent += n 209 | return 210 | } 211 | 212 | func (ch *Channel) nextPacketMsg() PacketMsg { 213 | /* 214 | type PacketMsg struct { 215 | ChannelID byte 216 | EOF byte // 1 means message ends here. 217 | Bytes []byte 218 | } 219 | */ 220 | 221 | // 构造消息报文 如果缓冲区的内容过大,就要构造多个包进行封装。 以EOF为1来表示这个报文已经被完全封装了 222 | packet := PacketMsg{} 223 | packet.ChannelID = byte(ch.desc.ID) 224 | maxSize := ch.maxPacketMsgPayloadSize 225 | packet.Bytes = ch.sending[:cmn.MinInt(maxSize, len(ch.sending))] 226 | if len(ch.sending) <= maxSize { 227 | packet.EOF = byte(0x01) 228 | ch.sending = nil 229 | atomic.AddInt32(&ch.sendQueueSize, -1) // decrement sendQueueSize 230 | } else { 231 | packet.EOF = byte(0x00) 232 | ch.sending = ch.sending[cmn.MinInt(maxSize, len(ch.sending)):] 233 | } 234 | return packet 235 | } 236 | 237 | ``` 238 | 到了这个我们就把sendSomePacketMsgs功能分析完成了。 大致流程就是从Channel的缓存区去数据,构造PacketMsg,写入TCP连接中。如果一直有内容则一直去调用sendSomePacketMsgs。 239 | 240 | 241 | 接着分析`recvRoutine` 242 | 243 | ```go 244 | func (c *MConnection) recvRoutine() { 245 | defer c._recover() 246 | 247 | FOR_LOOP: 248 | for { 249 | // 正如我上文说到的TCP是流式的,需要正确的协议才能解析出来正确的报文,所以读取报文是和上述发送报文必须相同的协议。 250 | _n, err = cdc.UnmarshalBinaryReader(c.bufConnReader, &packet, int64(c._maxPacketMsgSize)) 251 | 252 | // 根据解析出来的报文类型做相关的操作。 253 | switch pkt := packet.(type) { 254 | case PacketPing: 255 | select { 256 | // 对方要求我们发送一个pong 所以置位pong标识, 在sendRoutine中做相应的动作。 257 | case c.pong <- struct{}{}: 258 | default: 259 | } 260 | case PacketPong: 261 | select { 262 | // 更新pong超时状态 263 | case c.pongTimeoutCh <- false: 264 | default: 265 | } 266 | case PacketMsg: 267 | channel, ok := c.channelsIdx[pkt.ChannelID] 268 | // 根据接收的报文,选择对应的Channel, 放入对应的接收缓存区中。 缓存区的作用是什么呢。 269 | // 在上文的发送报文中我们发现一个PacketMsg包中可能并没有包含完整的内容,只有EOF为1才标识发送完成了。 270 | // 所以下面这个函数其实就是先将接收到的内容放入缓存区,只有所有内容都收到之后才会组装成一个完整的内容。 271 | msgBytes, err := channel.recvPacketMsg(pkt) 272 | if err != nil { 273 | if c.IsRunning() { 274 | c.Logger.Error("Connection failed @ recvRoutine", "conn", c, "err", err) 275 | c.stopForError(err) 276 | } 277 | break FOR_LOOP 278 | } 279 | if msgBytes != nil { 280 | // 注意这个函数的调用非常重要,记得之前我说为啥只有Send没有Receive呢, 答案就在此处。 281 | // 也就是说MConnecttion会把接收到的完整消息通过回调的形式返回给上面。 这个onReceive回调和Reactor的OnReceive是啥关系呢 282 | // 以及这个ChannelID和Reactor又是啥关系呢 不着急, 后面我们慢慢分析。 反正可以确定的是MConnecttion通过这个回调函数把接收到的消息 283 | // 返回给你应用层。 284 | c.onReceive(pkt.ChannelID, msgBytes) 285 | } 286 | 287 | } 288 | } 289 | 290 | ``` 291 | 到了这里MConnecttion的主要功能也就分析差不多了 总结起来就是通过Send,TrySend暴露的函数 把待发送的内容转入对应的通道,置位c.send标志。 在sendRoutine中进行消息处理和发送。 292 | 同时在recevRoutine中接收消息,处理消息。同时根据回调函数返回给上一层。 分析完MConnecttion大家可能还会有一些疑惑, 这个Channel到底是怎么创建的, 还有这个Onreceive回调函数和Reactor是如何对应的呢。 接下来我们分析Peer的部分就会慢慢的了解了。 293 | 294 | 295 | ## Peer源码分析 296 | 297 | peer在p2p中表示一个对等体。 在tendermint中也是它和应用程序之间进行直接的消息交互。 peer实现了Peer这个接口的定义。 所以我们流程依然是先看创建的实例的部分,然后进入Onstart入口分析启动流程。 接着对一些重要的函数进行探究。 298 | 299 | 创建实例函数`newPeer` 300 | ```go 301 | func newPeer( 302 | pc peerConn, 303 | mConfig tmconn.MConnConfig, 304 | nodeInfo NodeInfo, 305 | reactorsByCh map[byte]Reactor, 306 | chDescs []*tmconn.ChannelDescriptor, 307 | onPeerError func(Peer, interface{}), 308 | ) *peer { 309 | 310 | // peerConn 是传递参数。 它拥有了net.Conn成员变量 311 | // 为什么会专门设置一个peerConn结构呢, 为啥不直接使用net.Conn 312 | // 因为peer既可能是一个客户端的连接也可能是一个服务端的连接。 313 | // 所有才有了这个结构。 peerConn的创建可以使用newOutboundPeerConn和 314 | // newInboundPeerConn两个函数来创建。 这两个函数是在switch组件中被调用的。 315 | // 当然 peer整个实例的创建都是在switch组件中调用的。 316 | /* 317 | type peerConn struct { 318 | outbound bool 319 | persistent bool 320 | config *config.P2PConfig 321 | conn net.Conn // source connection 322 | ip net.IP 323 | originalAddr *NetAddress // nil for inbound connections 324 | } 325 | */ 326 | p := &peer{ 327 | peerConn: pc, 328 | nodeInfo: nodeInfo, 329 | channels: nodeInfo.Channels, 330 | Data: cmn.NewCMap(), 331 | } 332 | // 创建MConnecttion实例这这个函数进行。 333 | p.mconn = createMConnection( 334 | pc.conn, 335 | p, 336 | reactorsByCh, 337 | chDescs, 338 | onPeerError, 339 | mConfig, 340 | ) 341 | p.BaseService = *cmn.NewBaseService(nil, "Peer", p) 342 | return p 343 | } 344 | ``` 345 | 这个函数接收参数, 构造对象, 创建MConnecttion实例。 具体分析`createMConnection` 346 | 347 | ```go 348 | func createMConnection( 349 | conn net.Conn, 350 | p *peer, 351 | reactorsByCh map[byte]Reactor, 352 | chDescs []*tmconn.ChannelDescriptor, 353 | onPeerError func(Peer, interface{}), 354 | config tmconn.MConnConfig, 355 | ) *tmconn.MConnection { 356 | 357 | // 看到没? 这里就是回调函数初始化的地方。 这里可以看到channel应该是和 358 | // Reactor是一一对应的。 每一个chID应该对应一个MConnecttion的channel。 当收到PackMsg时,根据id区分出应该投递给 359 | // 哪一个Reactor 然后调用对应的reactor.Receive将消息返还给上层应用。 360 | onReceive := func(chID byte, msgBytes []byte) { 361 | reactor := reactorsByCh[chID] 362 | if reactor == nil { 363 | panic(cmn.Fmt("Unknown channel %X", chID)) 364 | } 365 | reactor.Receive(chID, p, msgBytes) 366 | } 367 | 368 | // 这个错误回调看上去依然是根据参数传递过来的。 那么应该是在switch组件中才能看到对错误的处理方法。 369 | onError := func(r interface{}) { 370 | onPeerError(p, r) 371 | } 372 | 373 | return tmconn.NewMConnectionWithConfig( 374 | conn, 375 | chDescs, 376 | onReceive, 377 | onError, 378 | config, 379 | ) 380 | } 381 | ``` 382 | 383 | 所以说`newPeer`主要的工作就是创建MConnecttion实例。 384 | 385 | 来看看OnStart()做了什么 386 | ```go 387 | func (p *peer) OnStart() error { 388 | if err := p.BaseService.OnStart(); err != nil { 389 | return err 390 | } 391 | err := p.mconn.Start() 392 | return err 393 | } 394 | ``` 395 | so, 就是启动了MConnecttion而已。 MConnecttion只有启动了,上文中我们知道MConnecttion启动之后才开启了两个goroutine的循环。 396 | 397 | 398 | peer的两个重要函数Send和TrySend 399 | ```go 400 | // Send msg bytes to the channel identified by chID byte. Returns false if the 401 | // send queue is full after timeout, specified by MConnection. 402 | func (p *peer) Send(chID byte, msgBytes []byte) bool { 403 | if !p.IsRunning() { 404 | return false 405 | } else if !p.hasChannel(chID) { 406 | return false 407 | } 408 | return p.mconn.Send(chID, msgBytes) 409 | } 410 | 411 | // TrySend msg bytes to the channel identified by chID byte. Immediately returns 412 | // false if the send queue is full. 413 | func (p *peer) TrySend(chID byte, msgBytes []byte) bool { 414 | if !p.IsRunning() { 415 | return false 416 | } else if !p.hasChannel(chID) { 417 | return false 418 | } 419 | return p.mconn.TrySend(chID, msgBytes) 420 | } 421 | ``` 422 | 解释很明显,一个阻塞一个非阻塞。 就是调用MConnecttion进行数据发送。 423 | 分析到这里,有人可能会想,peer似乎没有做什么事情,大部分就是在调用MConnecttion,为啥非要封装一层呢。 来我们继续看。 424 | peer的几个比较重要的函数 425 | `func (p *peer) NodeInfo() NodeInfo` 返回peer的节点信息 426 | `func (pc *peerConn) HandshakeTimeout( 427 | ourNodeInfo NodeInfo, 428 | timeout time.Duration, 429 | ) (peerNodeInfo NodeInfo, err error) ` 进行节点信息交换。 430 | ``` 431 | // 此函数是阻塞的, 它是在创建peer之后首次被调用的。 因为只有进行了节点交换, 我们才能保存对等体的信息内容。 432 | func (pc *peerConn) HandshakeTimeout( 433 | ourNodeInfo NodeInfo, 434 | timeout time.Duration, 435 | ) (peerNodeInfo NodeInfo, err error) { 436 | // Set deadline for handshake so we don't block forever on conn.ReadFull 437 | if err := pc.conn.SetDeadline(time.Now().Add(timeout)); err != nil { 438 | return peerNodeInfo, cmn.ErrorWrap(err, "Error setting deadline") 439 | } 440 | 441 | var trs, _ = cmn.Parallel( 442 | func(_ int) (val interface{}, err error, abort bool) { 443 | _, err = cdc.MarshalBinaryWriter(pc.conn, ourNodeInfo) 444 | return 445 | }, 446 | func(_ int) (val interface{}, err error, abort bool) { 447 | _, err = cdc.UnmarshalBinaryReader( 448 | pc.conn, 449 | &peerNodeInfo, 450 | int64(MaxNodeInfoSize()), 451 | ) 452 | return 453 | }, 454 | ) 455 | if err := trs.FirstError(); err != nil { 456 | return peerNodeInfo, cmn.ErrorWrap(err, "Error during handshake") 457 | } 458 | 459 | // Remove deadline 460 | if err := pc.conn.SetDeadline(time.Time{}); err != nil { 461 | return peerNodeInfo, cmn.ErrorWrap(err, "Error removing deadline") 462 | } 463 | 464 | return peerNodeInfo, nil 465 | } 466 | ``` 467 | 468 | 到此我们就把Peer的内容给说完了, 是不是觉得有点不过瘾, peer听上去这么牛逼的名词怎么才只有这么点内容。 其实也就这点内容。因为在P2P中,核心就是发现节点,广播内容, 接收内容。 总之通过Peer的Send和TrySend我们就可以向对方的Peer发送消息了。 469 | 470 | 471 | ## Switch源码分析 472 | 473 | switch在之前我们猜测应该是有点交换机之意, 连接各个Reactor,进行信息的交换。 代码看下来, 个人认为这个组件最大的任务就是和Reactor进行交互。调用Reactor的接口函数。同时也暴露自己的函数供Reactor来调用。 我们先看一下它的结构. 474 | ```go 475 | type Switch struct { 476 | // 继承基本服务 方便统一启动和停止 477 | cmn.BaseService 478 | // 接收P2P的配置文件 所以说P2P的启动入口应该就是先启动Switch实例 479 | config *config.P2PConfig 480 | // 监听者列表 就是我们一般意义上在启动TCP服务端时候创建的那个listener 只是做了一次封装 对地址格式等内容做了一些处理 481 | // 封装成了对象 方便使用而已 482 | // Listener的代码在listener.go中 483 | listeners []Listener 484 | // 所有的创建的Reactor集合 485 | reactors map[string]Reactor 486 | // Reactor和通道之间的对应关系 也是通过这个传递给peer在往下传递到MConnecttion 487 | chDescs []*conn.ChannelDescriptor 488 | reactorsByCh map[byte]Reactor 489 | // peer集合 490 | peers *PeerSet 491 | dialing *cmn.CMap 492 | reconnecting *cmn.CMap 493 | nodeInfo NodeInfo // our node info 494 | nodeKey *NodeKey // our node privkey 495 | addrBook AddrBook 496 | 497 | filterConnByAddr func(net.Addr) error 498 | filterConnByID func(ID) error 499 | 500 | mConfig conn.MConnConfig 501 | 502 | rng *cmn.Rand // seed for randomizing dial times and orders 503 | 504 | metrics *Metrics 505 | } 506 | ``` 507 | 看看`NewSwitch`创建实例对象做了什么工作。 508 | ```go 509 | // NewSwitch creates a new Switch with the given config. 510 | func NewSwitch(cfg *config.P2PConfig, options ...SwitchOption) *Switch { 511 | sw := &Switch{ 512 | config: cfg, 513 | reactors: make(map[string]Reactor), 514 | chDescs: make([]*conn.ChannelDescriptor, 0), 515 | reactorsByCh: make(map[byte]Reactor), 516 | peers: NewPeerSet(), 517 | dialing: cmn.NewCMap(), 518 | reconnecting: cmn.NewCMap(), 519 | metrics: NopMetrics(), 520 | } 521 | 522 | // Ensure we have a completely undeterministic PRNG. 523 | sw.rng = cmn.NewRand() 524 | 525 | mConfig := conn.DefaultMConnConfig() 526 | mConfig.FlushThrottle = time.Duration(cfg.FlushThrottleTimeout) * time.Millisecond 527 | mConfig.SendRate = cfg.SendRate 528 | mConfig.RecvRate = cfg.RecvRate 529 | mConfig.MaxPacketMsgPayloadSize = cfg.MaxPacketMsgPayloadSize 530 | sw.mConfig = mConfig 531 | 532 | sw.BaseService = *cmn.NewBaseService(nil, "P2P Switch", sw) 533 | 534 | for _, option := range options { 535 | option(sw) 536 | } 537 | 538 | return sw 539 | } 540 | ``` 541 | 看上去就是初始化实例, 没啥太多动作。 542 | 我们依然来看看OnStart做了些什么动作。 543 | ```go 544 | func (sw *Switch) OnStart() error { 545 | 546 | // 首先调用Reactor 启动所有的Reactor 547 | // Reactor 是一个接口,反正你只要实现此接口函数 548 | // 那么函数中啥也不做也没关系。 不过如果啥也不做 549 | // 这个Reactor也就没有什么实际意义了。 550 | // 在创建Switch中 sw.reactors 里面是空的 551 | // 那么这些Reactor是怎么添加进来的呢 552 | // 所以Switch提供了一个方法叫做AddReactor 专门添加Reactor 553 | // 上面也说了在tendermint里面有6个Reactor 它们是在node/node.go 554 | // 文件中被添加的 类似于下面这样 555 | /* 556 | sw.AddReactor("MEMPOOL", mempoolReactor) 557 | sw.AddReactor("BLOCKCHAIN", bcReactor) 558 | sw.AddReactor("CONSENSUS", consensusReactor) 559 | sw.AddReactor("EVIDENCE", evidenceReactor) 560 | */ 561 | 562 | // 一会我们看看AddReactor方法做了哪些工作 563 | for _, reactor := range sw.reactors { 564 | err := reactor.Start() 565 | if err != nil { 566 | return cmn.ErrorWrap(err, "failed to start %v", reactor) 567 | } 568 | } 569 | // 这里就是启动本地socket监听了 同理也是有个AddListener方法添加listener对象 570 | // 实际在使用的过程中 一般只会在一个端口启动监听 571 | for _, listener := range sw.listeners { 572 | go sw.listenerRoutine(listener) 573 | } 574 | return nil 575 | } 576 | ``` 577 | 从上面的代码可以看出 在启动Switch之前应该先调用AddReactor和AddListener。 而且应该是将所有的 578 | Reactor都添加完成再启动。否则的话后面添加的Reactor就不会启动了。 579 | 接下来看看`AddReactor` 580 | ``` 581 | func (sw *Switch) AddReactor(name string, reactor Reactor) Reactor { 582 | 583 | // 调用reactor的GetChannels方法获取相关的通道描述 584 | // 也就是说一个Reactor可以启用好几个通道 但是这个通道ID是所有Reactor 585 | // 都不可以重复的。 586 | reactorChannels := reactor.GetChannels() 587 | for _, chDesc := range reactorChannels { 588 | chID := chDesc.ID 589 | if sw.reactorsByCh[chID] != nil { 590 | cmn.PanicSanity(fmt.Sprintf("Channel %X has multiple reactors %v & %v", chID, sw.reactorsByCh[chID], reactor)) 591 | } 592 | sw.chDescs = append(sw.chDescs, chDesc) 593 | sw.reactorsByCh[chID] = reactor 594 | } 595 | sw.reactors[name] = reactor 596 | 597 | // 这个接口很重要 它把Switch的对象又传递给你Reactor 这样Reactor也可以调用Switch的函数了 598 | // 这回真的是你中有我我中有你了 599 | reactor.SetSwitch(sw) 600 | return reactor 601 | } 602 | ``` 603 | 启动Switch还有除了启动Reactor还开始了监听socket的任务。 我们来看看`listenerRoutine` 604 | ```go 605 | func (sw *Switch) listenerRoutine(l Listener) { 606 | for { 607 | // 等待一个TCP连接 不过多解释 写过Linux的socket网络编程的应该自然明白这个流程 608 | inConn, ok := <-l.Connections() 609 | if !ok { 610 | break 611 | } 612 | // 这一步大致就是判断是否连接数过多 如果太多就不让连接了, 613 | maxPeers := sw.config.MaxNumPeers - DefaultMinNumOutboundPeers 614 | if maxPeers <= sw.peers.Size() { 615 | sw.Logger.Info("Ignoring inbound connection: already have enough peers", "address", inConn.RemoteAddr().String(), "numPeers", sw.peers.Size(), "max", maxPeers) 616 | inConn.Close() 617 | continue 618 | } 619 | 620 | // 执行到这里就添加一个入栈连接peer 之前我们在peer源码分析的时候说到会有客户端连接和服务端连接 621 | // 这里就是服务端连接 我们会一直跟踪addInboundPeerWithConfig这个函数 因为它很重要 622 | err := sw.addInboundPeerWithConfig(inConn, sw.config) 623 | if err != nil { 624 | sw.Logger.Info("Ignoring inbound connection: error while adding peer", "address", inConn.RemoteAddr().String(), "err", err) 625 | continue 626 | } 627 | } 628 | } 629 | func (sw *Switch) addInboundPeerWithConfig( 630 | conn net.Conn, 631 | config *config.P2PConfig, 632 | ) error { 633 | // 看见没 终于在这里调用了peer的newInboundPeerConn 本身作为服务端创建的连接 634 | // 有了连接还不够 我们要创建peer才行。 635 | peerConn, err := newInboundPeerConn(conn, config, sw.nodeKey.PrivKey) 636 | if err != nil { 637 | conn.Close() // peer is nil 638 | return err 639 | } 640 | // 那么addPeer 应该会创建新peer 并加入peer集合中吧 我们继续跟踪 641 | if err = sw.addPeer(peerConn); err != nil { 642 | peerConn.CloseConn() 643 | return err 644 | } 645 | return nil 646 | } 647 | 648 | // 这个代码非常长 我们做一下精简 649 | func (sw *Switch) addPeer(pc peerConn) error { 650 | 651 | // 看这里 看这里 创建新peer之前我们必须要进行节点之间的信息交换 获取到对方的节点信息 652 | peerNodeInfo, err := pc.HandshakeTimeout(sw.nodeInfo, time.Duration(sw.config.HandshakeTimeout)) 653 | if err != nil { 654 | return err 655 | } 656 | peerID := peerNodeInfo.ID 657 | // 验证节点有效性 658 | if err := peerNodeInfo.Validate(); err != nil { 659 | return err 660 | } 661 | // 这一步很重要 防止自己连接自己 662 | if sw.nodeKey.ID() == peerID { 663 | addr := peerNodeInfo.NetAddress() 664 | sw.addrBook.RemoveAddress(addr) 665 | sw.addrBook.AddOurAddress(addr) 666 | return ErrSwitchConnectToSelf{addr} 667 | } 668 | 669 | // 防止多次连接 670 | if sw.peers.Has(peerID) { 671 | return ErrSwitchDuplicatePeerID{peerID} 672 | } 673 | 674 | // 检查协议之间是否兼容 675 | if err := sw.nodeInfo.CompatibleWith(peerNodeInfo); err != nil { 676 | return err 677 | } 678 | 679 | // 当当当~~~~ 终于可以创建一个新的peer了 参数分别是TCP连接 连接配置 节点信息 Reactor 通道描述 错误回调 680 | peer := newPeer(pc, sw.mConfig, peerNodeInfo, sw.reactorsByCh, sw.chDescs, sw.StopPeerForError) 681 | 682 | // All good. Start peer 683 | if sw.IsRunning() { 684 | // startInitPeer 比较重要 我们继续跟踪这个函数 685 | if err = sw.startInitPeer(peer); err != nil { 686 | return err 687 | } 688 | } 689 | 690 | // 最后把peer加入switch自己维护的peer集合中 691 | if err := sw.peers.Add(peer); err != nil { 692 | return err 693 | } 694 | return nil 695 | } 696 | 697 | func (sw *Switch) startInitPeer(peer *peer) error { 698 | // 是的没毛病 既然创建了peer实例就要启动它 --> 启动Mconnection 699 | err := peer.Start() // spawn send/recv routines 700 | if err != nil { 701 | // Should never happen 702 | sw.Logger.Error("Error starting peer", "peer", peer, "err", err) 703 | return err 704 | } 705 | // 这一步非常必要 把新的peer传给你每一个Reactor 所以Reactor应该自己也要维护一个peer集合 706 | // 这里我们在引申想一下 既然创建了新peer 并把新的peer抛给了Reactor 那么应用层就可以和这个Peer之间进行消息交互了 707 | // 可是如果和这个peer进行交互出错了 肯定应该删除这个peer的 708 | // 后面我们会看这个peer错误处理的地方 709 | for _, reactor := range sw.reactors { 710 | reactor.AddPeer(peer) 711 | } 712 | return nil 713 | } 714 | 715 | ``` 716 | 到了这里 这个监听socket的任务分析就算完事了, 总结起来就是作为服务端接受一个新的socket连接, 先交换节点信息, 进行一系列检查, 然后创建新的peer, 启动新的peer。 将新peer传递给每一个Reactor。 可是这个只有入栈的peer创建, 总该有出栈的peer去创建吧。 总不能我只提供服务, 不获取吧。 717 | 718 | 这个时候我想换个方法分析, 添加入栈peer的时候是调用了函数`addInboundPeerWithConfig`那么添加出栈函数应该就是`addOutboundPeerWithConfig`。 719 | 我么来先分析这个函数看看它是不是添加出栈函数。 720 | ```go 721 | func (sw *Switch) addOutboundPeerWithConfig( 722 | addr *NetAddress, 723 | config *config.P2PConfig, 724 | persistent bool, 725 | ) error { 726 | // 这个很符合我们的预期 调用了peer的newOutboundPeerConn 创建新的出栈socket连接 727 | peerConn, err := newOutboundPeerConn( 728 | addr, 729 | config, 730 | persistent, 731 | sw.nodeKey.PrivKey, 732 | ) 733 | if err != nil { 734 | if persistent { 735 | go sw.reconnectToPeer(addr) 736 | } 737 | return err 738 | } 739 | // 看到没 所以还是调用了addPeer函数 继续添加peer的那一套流程 740 | if err := sw.addPeer(peerConn); err != nil { 741 | peerConn.CloseConn() 742 | return err 743 | } 744 | return nil 745 | } 746 | ``` 747 | 看来很符合我的猜想啊, 信心又增加了不少呢。 咳咳咳, 继续继续。 `addOutboundPeerWithConfig`只被`DialPeerWithAddress`来调用, 这个函数看名字就知道通过地址来连接一个Peer,所以很明显是创建一个出栈的peer。 那它是再在哪调用的呢。 现在先说结论, 它是在PEX这个Reactor中被调用的。 暂时我们先放一放。在分析PEX源码的时候再探究它。 先解决Switch中一些其他的疑问。 748 | 749 | 第一个疑问, 在peer中我们好像找到了当有消息回来时的OnReceive的回调但是没有找到OnErr的回调。 750 | 第二个疑问, 如果我们的Reactor发现某个peer出错了想移除它应该怎么做。 751 | 752 | 两个疑问合为一个, 关键就在`StopPeerForError`这个函数。 来, 继续追踪下去。 753 | ```go 754 | func (sw *Switch) StopPeerForError(peer Peer, reason interface{}) { 755 | // 名称暴露了一切 停止并且移除peer 756 | sw.stopAndRemovePeer(peer, reason) 757 | 758 | // 特殊peer处理 就是说如果我认为这个地址应该持续连接 就会进行重连 759 | if peer.IsPersistent() { 760 | addr := peer.OriginalAddr() 761 | if addr == nil { 762 | addr = peer.NodeInfo().NetAddress() 763 | } 764 | go sw.reconnectToPeer(addr) 765 | } 766 | } 767 | 768 | func (sw *Switch) stopAndRemovePeer(peer Peer, reason interface{}) { 769 | // 先从Switch自己维护的peer集合中删除这个peer 770 | sw.peers.Remove(peer) 771 | sw.metrics.Peers.Add(float64(-1)) 772 | // 调用peer的stop 这个我没有说明 就是关闭socket连接 做一些收尾工作 773 | peer.Stop() 774 | 775 | // 注意看这里 调用所有的Reactor 告诉所有的Reactor 你要移除这个peer了 776 | for _, reactor := range sw.reactors { 777 | reactor.RemovePeer(peer, reason) 778 | } 779 | } 780 | ``` 781 | 所以说当有Peer错误发生时应该调用Switch的StopPeerForError 把这个peer移除掉。 782 | 所以上面我说SetSwitch让Reactor你中有我我中有你, 在这里就显示出作用了。 783 | 上面的疑惑一其实也就解除了, OnErr的回调也是这个函数. 没毛病,设计的非常好。 厉害厉害。 784 | 785 | 到了这里, Switch的分析就差不多结束了。 但是Switch里面还是有很多公共方法我没有说明, 因为它们大致都很简单, 有的通过名字都能看出功能是什么。 所以也没必要花费时间去说明它。总结Switch的主要功能就是监听TCP连接,维护新peer的创建和删除,调用Reactor的各种回调处理。同时也将Switch传递给Reactor。供给Reactor进行调用。 786 | 787 | ## PEX源码分析 788 | 分析到这里, 我们大致明白了tendermint的P2P是如何向对等体发送消息, 如何将接收到的消息返回对等体的。可是P2P还有一个非常重要的功能就是进行节点发现。 如果不能发现节点,那这个P2P也就没有什么意义。 所以PEX的作用就是进行节点发现工作的。 当然PEX也是一个Reactor的具体实现。 分析完这个内容我们不仅会了解节点是如何发现的,而且也会了解一个Reactor是到底都实现了哪些内容。 789 | 790 | 先看创建实例`NewPEXReactor`的代码 791 | ```go 792 | type PEXReactor struct { 793 | p2p.BaseReactor 794 | 795 | book AddrBook 796 | config *PEXReactorConfig 797 | ensurePeersPeriod time.Duration // TODO: should go in the config 798 | 799 | // maps to prevent abuse 800 | requestsSent *cmn.CMap // ID->struct{}: unanswered send requests 801 | lastReceivedRequests *cmn.CMap // ID->time.Time: last time peer requested from us 802 | 803 | seedAddrs []*p2p.NetAddress 804 | 805 | attemptsToDial sync.Map // address (string) -> {number of attempts (int), last time dialed (time.Time)} 806 | } 807 | // 这个函数是在tendermint启动node节点的时候调用的, 创建一个AddrBook实例 然后将实例传递给此函数返回一个PEX的Reactor实例。 808 | 并注册到Switch中。 809 | func NewPEXReactor(b AddrBook, config *PEXReactorConfig) *PEXReactor { 810 | r := &PEXReactor{ 811 | book: b, 812 | config: config, 813 | ensurePeersPeriod: defaultEnsurePeersPeriod, 814 | requestsSent: cmn.NewCMap(), 815 | lastReceivedRequests: cmn.NewCMap(), 816 | } 817 | r.BaseReactor = *p2p.NewBaseReactor("PEXReactor", r) 818 | return r 819 | } 820 | ``` 821 | 先进入OnStart代码看看做了哪些动作。 822 | ```go 823 | func (r *PEXReactor) OnStart() error { 824 | // 启动地址簿 后面分析addrbook的时候再研究 825 | err := r.book.Start() 826 | if err != nil && err != cmn.ErrAlreadyStarted { 827 | return err 828 | } 829 | // 检查配置的种子节点格式是否正确 830 | numOnline, seedAddrs, err := r.checkSeeds() 831 | if err != nil { 832 | return err 833 | } else if numOnline == 0 && r.book.Empty() { 834 | return errors.New("Address book is empty, and could not connect to any seed nodes") 835 | } 836 | 837 | r.seedAddrs = seedAddrs 838 | 839 | // 根据配置文件自己是否是种子模式来启动不同的routine 840 | if r.config.SeedMode { 841 | go r.crawlPeersRoutine() 842 | } else { 843 | go r.ensurePeersRoutine() 844 | } 845 | return nil 846 | } 847 | // 先分析作为种子模式时 的情况 848 | // 每隔30S调用一次crawlPeers和attemptDisconnects 849 | func (r *PEXReactor) crawlPeersRoutine() { 850 | r.crawlPeers() 851 | 852 | // Fire periodically 853 | ticker := time.NewTicker(defaultCrawlPeersPeriod) 854 | 855 | for { 856 | select { 857 | case <-ticker.C: 858 | // 这个函数内容比较简单 查看每一个peer和自己连接是时长 如果超过3个小时 就断开 859 | r.attemptDisconnects() 860 | r.crawlPeers() 861 | case <-r.Quit(): 862 | return 863 | } 864 | } 865 | } 866 | 867 | func (r *PEXReactor) crawlPeers() { 868 | // 此函数从地址簿中获取所有存在的地址 然后根据最后一次尝试连接的时间进行排序 869 | peerInfos := r.getPeersToCrawl() 870 | 871 | now := time.Now() 872 | // Use addresses we know of to reach additional peers 873 | for _, pi := range peerInfos { 874 | // 如果上次尝试连接的时间和此处相差不到2分钟 则不进行连接 875 | if now.Sub(pi.LastAttempt) < defaultCrawlPeerInterval { 876 | continue 877 | } 878 | // 尝试和这个地址进行一次连接 879 | err := r.Switch.DialPeerWithAddress(pi.Addr, false) 880 | if err != nil { 881 | // 如果连接失败了 更新最后一次尝试连接的时间和次数 882 | r.book.MarkAttempt(pi.Addr) 883 | continue 884 | } 885 | 886 | peer := r.Switch.Peers().Get(pi.Addr.ID) 887 | if peer != nil { 888 | // 如果连接成功了 就想这个地址发送一个报文 这个报文的目的就是请求此peer知道的的peer 889 | r.RequestAddrs(peer) 890 | } 891 | } 892 | } 893 | ``` 894 | 总结一下作为种子模式的pex就是每隔一段时间就会检查当前peer集合中是否有连接时间超过3小时的peer, 如果有,除非设置了一直连接否则就断开。 然后去爬去新的节点。 如果从地址簿中爬去到新的节点就发送一份请求地址交换的报文过去。 895 | 896 | 接着看一下作为非种子节点时的流程。 897 | ```go 898 | // 每个一段时间调用ensurePeers 899 | func (r *PEXReactor) ensurePeersRoutine() { 900 | var ( 901 | seed = cmn.NewRand() 902 | jitter = seed.Int63n(r.ensurePeersPeriod.Nanoseconds()) 903 | ) 904 | 905 | // Randomize first round of communication to avoid thundering herd. 906 | // If no potential peers are present directly start connecting so we guarantee 907 | // swift setup with the help of configured seeds. 908 | if r.hasPotentialPeers() { 909 | time.Sleep(time.Duration(jitter)) 910 | } 911 | 912 | // fire once immediately. 913 | // ensures we dial the seeds right away if the book is empty 914 | r.ensurePeers() 915 | 916 | // fire periodically 917 | ticker := time.NewTicker(r.ensurePeersPeriod) 918 | for { 919 | select { 920 | case <-ticker.C: 921 | r.ensurePeers() 922 | case <-r.Quit(): 923 | ticker.Stop() 924 | return 925 | } 926 | } 927 | } 928 | 929 | 930 | func (r *PEXReactor) ensurePeers() { 931 | // 获取当前正在连接的peer的信息 932 | var ( 933 | out, in, dial = r.Switch.NumPeers() 934 | numToDial = defaultMinNumOutboundPeers - (out + dial) 935 | ) 936 | if numToDial <= 0 { 937 | return 938 | } 939 | 940 | 941 | // 下面目的好像是从地址簿中根据偏差值挑出需要数量的peer地址 然后进行连接。 942 | // 这个挑取的算法暂时没有仔细阅读。 宗旨是从地址簿中找到一些peer地址然后超时拨号连接。 943 | // bias to prefer more vetted peers when we have fewer connections. 944 | // not perfect, but somewhate ensures that we prioritize connecting to more-vetted 945 | // NOTE: range here is [10, 90]. Too high ? 946 | newBias := cmn.MinInt(out, 8)*10 + 10 947 | 948 | toDial := make(map[p2p.ID]*p2p.NetAddress) 949 | // Try maxAttempts times to pick numToDial addresses to dial 950 | maxAttempts := numToDial * 3 951 | 952 | for i := 0; i < maxAttempts && len(toDial) < numToDial; i++ { 953 | try := r.book.PickAddress(newBias) 954 | if try == nil { 955 | continue 956 | } 957 | if _, selected := toDial[try.ID]; selected { 958 | continue 959 | } 960 | if dialling := r.Switch.IsDialing(try.ID); dialling { 961 | continue 962 | } 963 | if connected := r.Switch.Peers().Has(try.ID); connected { 964 | continue 965 | } 966 | // TODO: consider moving some checks from toDial into here 967 | // so we don't even consider dialing peers that we want to wait 968 | // before dialling again, or have dialed too many times already 969 | r.Logger.Info("Will dial address", "addr", try) 970 | toDial[try.ID] = try 971 | } 972 | 973 | // Dial picked addresses 974 | for _, addr := range toDial { 975 | // 启动一个routine 进行单独拨号 一会单独最终它 976 | go r.dialPeer(addr) 977 | } 978 | 979 | // If we need more addresses, pick a random peer and ask for more. 980 | // 这个函数是为了补充地址簿的数量 如果地址簿中存储的peer地址过少 就会执行下面的代码 981 | if r.book.NeedMoreAddrs() { 982 | peers := r.Switch.Peers().List() 983 | peersCount := len(peers) 984 | if peersCount > 0 { 985 | peer := peers[cmn.RandInt()%peersCount] // nolint: gas 986 | r.Logger.Info("We need more addresses. Sending pexRequest to random peer", "peer", peer) 987 | // 同样向相邻的节点请求更多的地址 988 | r.RequestAddrs(peer) 989 | } 990 | } 991 | 992 | // If we are not connected to nor dialing anybody, fallback to dialing a seed. 993 | if out+in+dial+len(toDial) == 0 { 994 | r.Logger.Info("No addresses to dial nor connected peers. Falling back to seeds") 995 | // 这个函数比较简单 就是根据配置的种子地址 调用r.Switch.DialPeerWithAddress(seedAddr, false) 尝试连接peer 996 | r.dialSeeds() 997 | } 998 | } 999 | 1000 | // 来追踪 根据地址簿提供的地址进行拨号的流程 1001 | func (r *PEXReactor) dialPeer(addr *p2p.NetAddress) { 1002 | // 从地址簿中找出这个地址最后一次尝试连接的时间和尝试连接的次数 1003 | attempts, lastDialed := r.dialAttemptsInfo(addr) 1004 | 1005 | // 如果这个地址尝试的次数过多的话 就标记为坏的地址 其实就是从地址簿中移除掉 1006 | if attempts > maxAttemptsToDial { 1007 | r.Logger.Error("Reached max attempts to dial", "addr", addr, "attempts", attempts) 1008 | r.book.MarkBad(addr) 1009 | return 1010 | } 1011 | 1012 | //判断尝试次数计算下次尝试时间 如果时间未到不进行尝试连接 1013 | // 换句话说就是尝试的次数越多 下次尝试连接的间隔越长 1014 | if attempts > 0 { 1015 | jitterSeconds := time.Duration(cmn.RandFloat64() * float64(time.Second)) // 1s == (1e9 ns) 1016 | backoffDuration := jitterSeconds + ((1 << uint(attempts)) * time.Second) 1017 | sinceLastDialed := time.Since(lastDialed) 1018 | if sinceLastDialed < backoffDuration { 1019 | r.Logger.Debug("Too early to dial", "addr", addr, "backoff_duration", backoffDuration, "last_dialed", lastDialed, "time_since", sinceLastDialed) 1020 | return 1021 | } 1022 | } 1023 | // 这里就比较明白了 进行连接。 1024 | err := r.Switch.DialPeerWithAddress(addr, false) 1025 | if err != nil { 1026 | r.Logger.Error("Dialing failed", "addr", addr, "err", err, "attempts", attempts) 1027 | // 如果是校验失败了 就直接从地址簿移除 根本不给尝试的机会 主要目的应该是防止一些恶意的攻击 1028 | if _, ok := err.(p2p.ErrSwitchAuthenticationFailure); ok { 1029 | r.book.MarkBad(addr) 1030 | r.attemptsToDial.Delete(addr.DialString()) 1031 | } else { 1032 | r.book.MarkAttempt(addr) 1033 | // FIXME: if the addr is going to be removed from the addrbook (hard to 1034 | // tell at this point), we need to Delete it from attemptsToDial, not 1035 | // record another attempt. 1036 | // record attempt 1037 | r.attemptsToDial.Store(addr.DialString(), _attemptsToDial{attempts + 1, time.Now()}) 1038 | } 1039 | } else { 1040 | // cleanup any history 1041 | r.attemptsToDial.Delete(addr.DialString()) 1042 | } 1043 | } 1044 | ``` 1045 | 所以总结一下作为非种子节点其实就是一直进行连接, 尽量保存足够的连接数量。 如果数量不够就先从地址簿从取一些地址进行连接。 连接每一个地址的过程就如上面的表述的流程一样。 如果地址簿发现保存的地址数量太少就会尝试向已知的peer发送请求新peer的报文请求。如果这个时候还是没有足够的peer就只能向种子节点去请求了。 1046 | 1047 | 分析到这里我们应该有一些疑问, 如果每一个PEX只有发送请求新节点的报文好像不够吧,它也应该做出回应啊, 既然要做出回应,在之前的分析中我们知道如果想回应一个Read的数据,Reactor就要在Receive里做文章喽, 所以我们只需要看看PEX的Receive是怎么做的就好了。 1048 | ```go 1049 | func (r *PEXReactor) Receive(chID byte, src Peer, msgBytes []byte) { 1050 | msg, err := decodeMsg(msgBytes) 1051 | if err != nil { 1052 | r.Logger.Error("Error decoding message", "src", src, "chId", chID, "msg", msg, "err", err, "bytes", msgBytes) 1053 | r.Switch.StopPeerForError(src, err) 1054 | return 1055 | } 1056 | r.Logger.Debug("Received message", "src", src, "chId", chID, "msg", msg) 1057 | 1058 | switch msg := msg.(type) { 1059 | case *pexRequestMessage: 1060 | // 看到这个消息类型 就是我们刚才发生请求更多peer的报文 先进行错误处理 1061 | if err := r.receiveRequest(src); err != nil { 1062 | r.Switch.StopPeerForError(src, err) 1063 | return 1064 | } 1065 | if r.config.SeedMode { 1066 | // 如果我们是种子节点 我们就发生一些节点列表过去 1067 | r.SendAddrs(src, r.book.GetSelectionWithBias(biasToSelectNewPeers)) 1068 | // 发送完成后 断开和这个peer的连接 也就是说如果我们尝试连接种子节点 种子节点在回复更多peer之后就会关闭连接 因为维护每一个 1069 | //socket是要消耗资源的 1070 | r.Switch.StopPeerGracefully(src) 1071 | } else { 1072 | // 如果不是种子节点 发送完自身一直的节点列表之后 就保持连接 1073 | r.SendAddrs(src, r.book.GetSelection()) 1074 | } 1075 | 1076 | case *pexAddrsMessage: 1077 | // 这个消息表示收到了具体的peer列表 尝试加入地址簿中 这个函数还做了一个特别的动作就是 如果里面有种子节点 立刻尝试和种子节点进行一次连接 1078 | if err := r.ReceiveAddrs(msg.Addrs, src); err != nil { 1079 | r.Switch.StopPeerForError(src, err) 1080 | return 1081 | } 1082 | default: 1083 | r.Logger.Error(fmt.Sprintf("Unknown message type %v", reflect.TypeOf(msg))) 1084 | } 1085 | } 1086 | ``` 1087 | 1088 | 所以PEX的Receive主要功能就是处理了从其他peer接收到相应的报文做相应的处理。 然后我们顺便看看PEX的其接口。 1089 | `GetChannels`返回了PEX的通道描述 ID为0 优先级为1 1090 | `AddPeer` 当新peer加入之后, 如果地址簿保存的数量太少就尝试向这个peer请求更多peer 1091 | `RemovePeer`做一些辅助动作 1092 | 1093 | 到此, 整个PEX流程就算完成了。 1094 | 1095 | 1096 | 1097 | -------------------------------------------------------------------------------- /state源码分析.md: -------------------------------------------------------------------------------- 1 | 传统习惯 上高清无码自制大图: 2 | ![WX20180925-150939.png](img/5E0910FA138824DDAD13A23C77E18193.png) 3 | 不需要理解图中各个类的功能, 大致扫一眼留一下印象。 4 | State组件中有三个比较重要的地方,一个是State这个结构, 一个是BlockExector,还有一个是Store。 5 | 6 | 我们先看State结构。 它代表了区块的状态。 看一下它的详情数据结构: 7 | ```go 8 | type State struct { 9 | //链ID 整个链中都是不会变化的 10 | ChainID string 11 | // 表示上一个区块的高度 12 | LastBlockHeight int64 13 | // 距离上一个区块高度 已经打包的交易数量 14 | LastBlockTotalTx int64 15 | // 上一个区块ID (这个是一个复合类型 上一张我们列举过里面的字段) 16 | LastBlockID types.BlockID 17 | LastBlockTime time.Time 18 | 19 | // LastValidators is used to validate block.LastCommit. 20 | // Validators are persisted to the database separately every time they change, 21 | // so we can query for historical validator sets. 22 | // Note that if s.LastBlockHeight causes a valset change, 23 | // we set s.LastHeightValidatorsChanged = s.LastBlockHeight + 1 24 | // 代表当前验证者集合 25 | Validators *types.ValidatorSet 26 | // 上一个区块的验证者集合 27 | LastValidators *types.ValidatorSet 28 | LastHeightValidatorsChanged int64 29 | 30 | // Consensus parameters used for validating blocks. 31 | // 共识参数的配置 主要是 一个区块的大小 一个交易的大小 区块每个部分的大小 32 | ConsensusParams types.ConsensusParams 33 | LastHeightConsensusParamsChanged int64 34 | 35 | // Merkle root of the results from executing prev block 36 | LastResultsHash []byte 37 | 38 | // The latest AppHash we've received from calling abci.Commit() 39 | AppHash []byte 40 | } 41 | ``` 42 | State的成员变量应用的直接是一个值拷贝,意思很明确,成员函数都不会修改State成员属性。列一下主要函数功能: 43 | `Copy` 拷贝一个新的State数据返回 44 | `GetValidators` 获取当前验证者集合和上一个区块的验证者集合 45 | 看一下根据State内容创建区块`MakeBlock`这个函数。 46 | ```go 47 | func (state State) MakeBlock( 48 | height int64, 49 | txs []types.Tx, 50 | commit *types.Commit, 51 | evidence []types.Evidence, 52 | ) (*types.Block, *types.PartSet) { 53 | 54 | // 根据高度 交易列表 commit内容 创建一个区块结构 55 | block := types.MakeBlock(height, txs, commit, evidence) 56 | 57 | // 把状态内容赋值给区块信息 58 | block.ChainID = state.ChainID 59 | 60 | block.LastBlockID = state.LastBlockID 61 | block.TotalTxs = state.LastBlockTotalTx + block.NumTxs 62 | 63 | block.ValidatorsHash = state.Validators.Hash() 64 | block.ConsensusHash = state.ConsensusParams.Hash() 65 | block.AppHash = state.AppHash 66 | block.LastResultsHash = state.LastResultsHash 67 | return block, block.MakePartSet(state.ConsensusParams.BlockGossip.BlockPartSizeBytes) 68 | } 69 | ``` 70 | 71 | State就是这些内容, 我们主要分析的是BlockExector的ApplyBlock这个函数。 先看一下state/execution.go前几行的注释。 72 | ```go 73 | // BlockExecutor handles block execution and state updates. 74 | // It exposes ApplyBlock(), which validates & executes the block, updates state w/ ABCI responses, 75 | // then commits and updates the mempool atomically, then saves state. 76 | BlockExector暴露ApplyBlock这个函数 验证和执行区块信息并且更新状态信息, 同时根据APP的回应更新APPHASH。 77 | ``` 78 | 之前我们在BlockChain中看到了这个函数的调用, 我们是一笔带过, 没有具体分析。 现在我们想一想上次分析BlockChain调用此函数的情况, 也即是当我们下载了一个新的区块时,先进行区块校验`state.Validators.VerifyCommit`(这个函数我们也会分析到) 然后校验通过之后调用`ApplyBlock`更新状态。返回下一个区块的状态。 现在我们就具体看一下ApplyBlock都做了哪些工作。 79 | 80 | ```go 81 | func (blockExec *BlockExecutor) ApplyBlock(state State, blockID types.BlockID, block *types.Block) (State, error) { 82 | 83 | // 对区块进行详细的验证 下面专门追踪这个函数 84 | if err := blockExec.ValidateBlock(state, block); err != nil { 85 | return state, ErrInvalidBlock(err) 86 | } 87 | 88 | // 将区块内容提交给ABCI应用层 这里涉及了ABCI的好几个接口函数 BeginBlock, DeliverTx, EndBlock 89 | // 返回ABCI返回的结果 90 | abciResponses, err := execBlockOnProxyApp(blockExec.logger, blockExec.proxyApp, block, state.LastValidators, blockExec.db) 91 | if err != nil { 92 | return state, ErrProxyAppConn(err) 93 | } 94 | 95 | fail.Fail() // XXX 96 | 97 | // 把返回的结果保存到数据中 98 | saveABCIResponses(blockExec.db, block.Height, abciResponses) 99 | 100 | fail.Fail() // XXX 101 | 102 | // 根据当前状态, 以及ABCI的返回结果和当前区块的内容 返回下一个状态信息 103 | state, err = updateState(state, blockID, &block.Header, abciResponses) 104 | if err != nil { 105 | return state, fmt.Errorf("Commit failed for application: %v", err) 106 | } 107 | 108 | // 调用ABCI的Commi函数返回APPhash 同时更新内存池中的交易 109 | appHash, err := blockExec.Commit(block) 110 | if err != nil { 111 | return state, fmt.Errorf("Commit failed for application: %v", err) 112 | } 113 | 114 | // 更新证据池的内容 115 | blockExec.evpool.Update(block, state) 116 | 117 | fail.Fail() // XXX 118 | 119 | // update the app hash and save the state 120 | state.AppHash = appHash 121 | // 将此次状态的内容持久化保存 122 | SaveState(blockExec.db, state) 123 | 124 | fail.Fail() // XXX 125 | 126 | // events are fired after everything else 127 | // NOTE: if we crash between Commit and Save, events wont be fired during replay 128 | fireEvents(blockExec.logger, blockExec.eventBus, block, abciResponses) 129 | 130 | return state, nil 131 | } 132 | ``` 133 | 134 | 总结一下ApplyBlock的功能: 135 | 1. 根据当前状态和区块内容来验证当前区块是否符合要求 136 | 2. 提交区块内容到ABCI的应用层, 返回应用层的回应 137 | 3. 根据当前区块的信息,ABCI回应的内容,生成下一个State 138 | 4. 再次调用ABCI的Commit返回当前APPHASH 139 | 5. 持久化此处状态同时返回下一次状态的内容。 140 | 141 | 具体分析上述的每一个函数的流程。 142 | `validateBlock` 验证区块是否合法 143 | ```go 144 | func validateBlock(stateDB dbm.DB, state State, block *types.Block) error { 145 | /* 146 | func (b *Block) ValidateBasic() error { 147 | if b == nil { 148 | return errors.New("Nil blocks are invalid") 149 | } 150 | b.mtx.Lock() 151 | defer b.mtx.Unlock() 152 | 153 | newTxs := int64(len(b.Data.Txs)) 154 | if b.NumTxs != newTxs { 155 | return fmt.Errorf("Wrong Block.Header.NumTxs. Expected %v, got %v", newTxs, b.NumTxs) 156 | } 157 | if !bytes.Equal(b.LastCommitHash, b.LastCommit.Hash()) { 158 | return fmt.Errorf("Wrong Block.Header.LastCommitHash. Expected %v, got %v", b.LastCommitHash, b.LastCommit.Hash()) 159 | } 160 | if b.Header.Height != 1 { 161 | if err := b.LastCommit.ValidateBasic(); err != nil { 162 | return err 163 | } 164 | } 165 | if !bytes.Equal(b.DataHash, b.Data.Hash()) { 166 | return fmt.Errorf("Wrong Block.Header.DataHash. Expected %v, got %v", b.DataHash, b.Data.Hash()) 167 | } 168 | if !bytes.Equal(b.EvidenceHash, b.Evidence.Hash()) { 169 | return errors.New(cmn.Fmt("Wrong Block.Header.EvidenceHash. Expected %v, got %v", b.EvidenceHash, b.Evidence.Hash())) 170 | } 171 | return nil 172 | } 173 | */ 174 | 175 | // 先对区块数据结构进行验证 看看是否参数都已经正确 为了方便 我把函数列举在上面 176 | if err := block.ValidateBasic(); err != nil { 177 | return err 178 | } 179 | 180 | // 验证链ID 高度 上一个区块ID 区块交易的数量 181 | if block.ChainID != state.ChainID { 182 | return fmt.Errorf("Wrong Block.Header.ChainID. Expected %v, got %v", state.ChainID, block.ChainID) 183 | } 184 | if block.Height != state.LastBlockHeight+1 { 185 | return fmt.Errorf("Wrong Block.Header.Height. Expected %v, got %v", state.LastBlockHeight+1, block.Height) 186 | } 187 | if !block.LastBlockID.Equals(state.LastBlockID) { 188 | return fmt.Errorf("Wrong Block.Header.LastBlockID. Expected %v, got %v", state.LastBlockID, block.LastBlockID) 189 | } 190 | newTxs := int64(len(block.Data.Txs)) 191 | if block.TotalTxs != state.LastBlockTotalTx+newTxs { 192 | return fmt.Errorf("Wrong Block.Header.TotalTxs. Expected %v, got %v", state.LastBlockTotalTx+newTxs, block.TotalTxs) 193 | } 194 | if !bytes.Equal(block.AppHash, state.AppHash) { 195 | return fmt.Errorf("Wrong Block.Header.AppHash. Expected %X, got %v", state.AppHash, block.AppHash) 196 | } 197 | if !bytes.Equal(block.ConsensusHash, state.ConsensusParams.Hash()) { 198 | return fmt.Errorf("Wrong Block.Header.ConsensusHash. Expected %X, got %v", state.ConsensusParams.Hash(), block.ConsensusHash) 199 | } 200 | if !bytes.Equal(block.LastResultsHash, state.LastResultsHash) { 201 | return fmt.Errorf("Wrong Block.Header.LastResultsHash. Expected %X, got %v", state.LastResultsHash, block.LastResultsHash) 202 | } 203 | if !bytes.Equal(block.ValidatorsHash, state.Validators.Hash()) { 204 | return fmt.Errorf("Wrong Block.Header.ValidatorsHash. Expected %X, got %v", state.Validators.Hash(), block.ValidatorsHash) 205 | } 206 | 207 | // Validate block LastCommit. 208 | if block.Height == 1 { 209 | if len(block.LastCommit.Precommits) != 0 { 210 | return errors.New("Block at height 1 (first block) should have no LastCommit precommits") 211 | } 212 | } else { 213 | // LastValidators 表示上次的所有验证者合集 214 | if len(block.LastCommit.Precommits) != state.LastValidators.Size() { 215 | return fmt.Errorf("Invalid block commit size. Expected %v, got %v", 216 | state.LastValidators.Size(), len(block.LastCommit.Precommits)) 217 | } 218 | // 注意这个地方 我们是根据此次提交的区块信息 来验证上一个块的内容 219 | // 迭代上一个区块保存的所有验证者 确保每一个验证者签名正确 220 | // 最后确认所有的有效的区块验证者的投票数要大于整个票数的2/3 221 | err := state.LastValidators.VerifyCommit( 222 | state.ChainID, state.LastBlockID, block.Height-1, block.LastCommit) 223 | if err != nil { 224 | return err 225 | } 226 | } 227 | for _, ev := range block.Evidence.Evidence { 228 | if err := VerifyEvidence(stateDB, state, ev); err != nil { 229 | return types.NewEvidenceInvalidErr(ev, err) 230 | } 231 | } 232 | 233 | return nil 234 | } 235 | ``` 236 | 237 | 接下来看看`execBlockOnProxyApp` 是向ABCI提交了哪些内容。 238 | ```go 239 | func execBlockOnProxyApp(logger log.Logger, proxyAppConn proxy.AppConnConsensus, 240 | block *types.Block, lastValSet *types.ValidatorSet, stateDB dbm.DB) (*ABCIResponses, error) { 241 | var validTxs, invalidTxs = 0, 0 242 | 243 | txIndex := 0 244 | // 首先构建Response 245 | abciResponses := NewABCIResponses(block) 246 | 247 | // 注意这个是执行ABCI后的回调 之前在Mempool也分析过回调 248 | // 这里不在进行追踪 直接说结论 也就是在提交每一个交易给ABCI之后 然后在调用此函数 249 | // 这个回调只是统计了哪些交易在应用层被任务是无效的交易 250 | // 从这里我们也可以看出来 应用层无论决定提交的交易是否有效 tendermint都会将其打包到区块链中 251 | proxyCb := func(req *abci.Request, res *abci.Response) { 252 | switch r := res.Value.(type) { 253 | case *abci.Response_DeliverTx: 254 | txRes := r.DeliverTx 255 | if txRes.Code == abci.CodeTypeOK { 256 | validTxs++ 257 | } else { 258 | logger.Debug("Invalid tx", "code", txRes.Code, "log", txRes.Log) 259 | invalidTxs++ 260 | } 261 | abciResponses.DeliverTx[txIndex] = txRes 262 | txIndex++ 263 | } 264 | } 265 | proxyAppConn.SetResponseCallback(proxyCb) 266 | 267 | // 从区块中加载出整个验证者和错误验证者 268 | signVals, byzVals := getBeginBlockValidatorInfo(block, lastValSet, stateDB) 269 | 270 | // 开始调用ABCI的BeginBlock 同时向其提交区块hash 区块头信息 上一个区块的验证者 出错的验证者 271 | _, err := proxyAppConn.BeginBlockSync(abci.RequestBeginBlock{ 272 | Hash: block.Hash(), 273 | Header: types.TM2PB.Header(&block.Header), 274 | LastCommitInfo: abci.LastCommitInfo{ 275 | CommitRound: int32(block.LastCommit.Round()), 276 | Validators: signVals, 277 | }, 278 | ByzantineValidators: byzVals, 279 | }) 280 | if err != nil { 281 | logger.Error("Error in proxyAppConn.BeginBlock", "err", err) 282 | return nil, err 283 | } 284 | 285 | // 迭代提交每一个交易给应用层 286 | for _, tx := range block.Txs { 287 | proxyAppConn.DeliverTxAsync(tx) 288 | if err := proxyAppConn.Error(); err != nil { 289 | return nil, err 290 | } 291 | } 292 | 293 | // 通知ABCI应用层此次区块已经提交完毕了 注意这个步骤是可以更新验证者的 更新的验证者也就是下一个区块的所有验证者 294 | abciResponses.EndBlock, err = proxyAppConn.EndBlockSync(abci.RequestEndBlock{Height: block.Height}) 295 | if err != nil { 296 | logger.Error("Error in proxyAppConn.EndBlock", "err", err) 297 | return nil, err 298 | } 299 | 300 | logger.Info("Executed block", "height", block.Height, "validTxs", validTxs, "invalidTxs", invalidTxs) 301 | 302 | valUpdates := abciResponses.EndBlock.ValidatorUpdates 303 | if len(valUpdates) > 0 { 304 | logger.Info("Updates to validators", "updates", abci.ValidatorsString(valUpdates)) 305 | } 306 | 307 | return abciResponses, nil 308 | } 309 | ``` 310 | 311 | 到了这里也就是说应用层也已经执行了这个区块的内容, 接下来我们根据当前State, Block, APPResponse来产生下次的状态。 312 | `updateState` 313 | ```go 314 | func updateState(state State, blockID types.BlockID, header *types.Header, 315 | abciResponses *ABCIResponses) (State, error) { 316 | // 先记一下当前所有的验证者 317 | prevValSet := state.Validators.Copy() 318 | nextValSet := prevValSet.Copy() 319 | 320 | // 根据abciResponses返回的验证者来更新当前的验证者集合 321 | // 更新原则是这样: 322 | // 如果当前不存在则直接加入一个验证者 323 | // 如果当前存在并且投票权为0则删除 324 | // 如果当前存在其投票权不为0则更新 325 | lastHeightValsChanged := state.LastHeightValidatorsChanged 326 | if len(abciResponses.EndBlock.ValidatorUpdates) > 0 { 327 | err := updateValidators(nextValSet, abciResponses.EndBlock.ValidatorUpdates) 328 | if err != nil { 329 | return state, fmt.Errorf("Error changing validator set: %v", err) 330 | } 331 | // change results from this height but only applies to the next height 332 | lastHeightValsChanged = header.Height + 1 333 | } 334 | 335 | // Update validator accums and set state variables 336 | nextValSet.IncrementAccum(1) 337 | 338 | // 根据返回结果更新一下共识参数 339 | nextParams := state.ConsensusParams 340 | lastHeightParamsChanged := state.LastHeightConsensusParamsChanged 341 | if abciResponses.EndBlock.ConsensusParamUpdates != nil { 342 | // NOTE: must not mutate s.ConsensusParams 343 | nextParams = state.ConsensusParams.Update(abciResponses.EndBlock.ConsensusParamUpdates) 344 | err := nextParams.Validate() 345 | if err != nil { 346 | return state, fmt.Errorf("Error updating consensus params: %v", err) 347 | } 348 | // change results from this height but only applies to the next height 349 | lastHeightParamsChanged = header.Height + 1 350 | } 351 | 352 | //返回此次区块被验证成功之后的State 此State也就是为了验证下一个区块 353 | //注意APPHASH还没有更新 因为还有一步没有做 354 | return State{ 355 | ChainID: state.ChainID, 356 | LastBlockHeight: header.Height, 357 | LastBlockTotalTx: state.LastBlockTotalTx + header.NumTxs, 358 | LastBlockID: blockID, 359 | LastBlockTime: header.Time, 360 | Validators: nextValSet, 361 | LastValidators: state.Validators.Copy(), 362 | LastHeightValidatorsChanged: lastHeightValsChanged, 363 | ConsensusParams: nextParams, 364 | LastHeightConsensusParamsChanged: lastHeightParamsChanged, 365 | LastResultsHash: abciResponses.ResultsHash(), 366 | AppHash: nil, 367 | }, nil 368 | } 369 | ``` 370 | 接着就是向ABCI提交commit 告诉ABCI此次区块已经完全确认了。 具体内容比较简单,不在分析,就是调用ABCI的Commit 返回APPHASH, 371 | 调用mempool的Update 移除掉此次被打包的交易. 372 | 接下来保存当前状态`SaveState` 373 | ```go 374 | func saveState(db dbm.DB, state State, key []byte) { 375 | nextHeight := state.LastBlockHeight + 1 376 | // 保存下一个区块的所有验证者信息 key为validatorsKey:height 377 | // value为所有验证者信息 378 | saveValidatorsInfo(db, nextHeight, state.LastHeightValidatorsChanged, state.Validators) 379 | // 保存下一个区块的共识参数 key为consensusParamsKey:height 380 | saveConsensusParamsInfo(db, nextHeight, state.LastHeightConsensusParamsChanged, state.ConsensusParams) 381 | // State的key为`stateKey` value为State的二进制序列化 382 | db.SetSync(stateKey, state.Bytes()) 383 | } 384 | ``` 385 | 到了这个地方, State的主要功能就算分析完了,State其实就是代表本节点执行的区块链的最新状态,它同时也是下一个区块执行的验证基础。 386 | 这个组件里非常重要的函数就是ApplyBlock了, 基于tendermint来创建自己的区块链, 我们要实现的几个重要接口函数中, 如果从数量上来说这里调用的最多, 从开始一个新区块,到提交交易,再到接收这个区块,最终确认区块。可以说重要的步骤都是在这里完成的。也就是说当tendermint的一个区块被生成之后,此函数是必须被调用的。 因为之后这个函数被调用之后,APP层才算完成了打包区块的。 想象一下一个新区块被生成也就两个地方, 如果他不是验证者,那么它只能是一个同步者,也即是只能下载区块。 这个我们已经见到过就是在Blockchain中下载新区块的地方, 如果他是一个验证者, 那么应该在共识模块被调用。 后面我们去分析共识算法的地方在执行去看看。 387 | 388 | 389 | 整体来说, State的代码量不是很多,注释也非常明确。 如果看到这里你还有一些疑问, 或者感觉总是有些说不上来的不明白的地方,其实也没关系 因为我们还没有看完所有的代码, 整个流程还没打通。 但是不管怎么变化, 一个区块链项目总是少不了这些东西, P2P, 区块数据结构, 交易, 内存池, 共识引擎等等。 如果明白他们所有组件的结合方式,数据流转方向应该整个流程就会通了。 让我们继续前行! 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | --------------------------------------------------------------------------------