【源码阅读】Tendermint中state部分代码详解

state部分学习笔记

Tendermint采用的分布式计算模型为复制状态机(State Machine Replication),其基本思路就是通过在多个节点间通过同步输入序列来保证各节点状态机的同步。该文章很好地讲解了状态机和复制状态机的概念和目的。而state部分就是负责同步节点间的状态信息,里面的state.go文件定义了一些与状态相关的函数以及一个区块创建的函数,而execution.go文件则定义了一个区块的完整生命周期所涉及的函数。

1. /tendermint/state/state.go

  1. var (
    	stateKey = []byte("stateKey")
    )
    
    var InitStateVersion = tmstate.Version{
    	Consensus: tmversion.Consensus{
    		Block: version.BlockProtocol,
    		App:   0,
    	},
    	Software: version.TMCoreSemVer,
    }
    
    • 首先指定了数据库的访问密码,用于将状态信息存储到数据库时访问用。

    • 并且初始化了状态的版本。共有三种版本:

      • 区块协议的版本:涉及所有区块数据结构和处理;
      • Tendermint软件版本
      • Consensus.App的版本
    • 这里只对前两个版本进行了初始化,而最后一个是在握手的过程中、监听到应用层正在运行的协议时进行设置的;

    • InitStateVersion用于本文件后续函数中创建创世区块时生成对应状态。

  2. type State struct {
    	Version tmstate.Version
    
    	ChainID       string
    	InitialHeight int64
    
    	LastBlockHeight int64
    	LastBlockID     types.BlockID
    	LastBlockTime   time.Time
        
    	NextValidators              *types.ValidatorSet
    	Validators                  *types.ValidatorSet
    	LastValidators              *types.ValidatorSet
    	LastHeightValidatorsChanged int64
    
    	ConsensusParams                  tmproto.ConsensusParams
    	LastHeightConsensusParamsChanged int64
    
    	LastResultsHash []byte
    
    	AppHash []byte
    }
    
    • State结构体:对最新提交的共识区块进行简单的描述,包括验证新快所需的所有信息,所有字段都是公开的,但是不能对其直接进行更改,而是调用两个函数:state.Copy()或state.NextState();

    • 下面对每个字段进行解释,部分内容借鉴自该文章,强烈建议看看原文:

      • Version:tmstate.Version类型,该类型同样包括了上文出现的三种版本信息;

      • ChainID:string类型,区块链标识ID号,用来唯一标志一条链,不可更改;

      • InitialHeight:int64类型,初始化高度,需要设置为1,因为0被创世区块占用了;

      • LastBlockHeight:int64类型,上一区块的高度,初始化为0,表示上一块是创世区块;

      • LastBlockID:types.BlockID类型,上一区块的区块ID:

        type BlockID struct {
        	Hash          tmbytes.HexBytes `json:"hash"`
        	PartSetHeader PartSetHeader    `json:"parts"`
        }
        
        //-------------------------------------
        
        type PartSetHeader struct {
        	Total uint32           `json:"total"`
        	Hash  tmbytes.HexBytes `json:"hash"`
        }
        

        BlockID包含了区块的两种不同Merkle根散列值:

        • 第一种是做为区块的主散列(hash),它是区块header中所有字段的Merkle根散列值;

        • 第二种是服务于区块共识阶段的安全协议(包含在PartsHeader字段中),是将整个区块序列化后切分成片段的Merkle根散列值。

        而在PartSetHeader结构体中,又分为两个数据:

        • Total字段用于记录PartSet的总数;
        • Hash字段记录了这些PartSet的Merkle根散列值。

        之所以要这样设计 BlockID,是因为 Tendermint 借鉴了 LibSwift,把区块拆分成很多个 Part,每个 Part 各自在 P2P 网络上进行广播,其它全节点需要逐个接收和验证这些 Part,然后再把它们拼合起来。这样可以达到更快的区块传播速度。

      • LastBlockTime:time.Time类型(go内置时间类型),记录上一区块的时间戳;

      • NextValidators:types.ValidatorSet指针类型,用于验证下下一个区块的commit信息,这个commit信息实际上是下下一个区块所获得的若干 Validator (验证者)的签名,以下同理;

      • Validators:types.ValidatorSet指针类型,用于验证下一区块的commit信息;

      • LastValidators:types.ValidatorSet指针类型,用于验证本区块的commit信息;

      • LastHeightValidatorsChanged:int64类型,表示下一次修改这些参数的区块高度;

        这四个变量是为了方便在每个区块都能进行Validator集合变动来设计的,思路如下:

        Tendermint 依赖上层应用来决定 Validator 集合应该如何变动,而且每个区块都可以更改 Validator 集合。当前区块对 Validator 集合所作出的修改,要隔一个区块,到下下个区块才能生效。因此,Tendermint 使用了三个变量来跟踪 Validator 集合的变动:NextValidators、Validators、LastValidators,它们有效的时间分别是下一个区块,当前正在投票决策的区块和上一个区块。比如执行高度为 100 的区块时,修改了 Validator 集合,那么要在决策高度为 102 的区块时才会按照更新后的 Validator 集合进行投票。当高度为 100 的区块被执行完毕后,新的 Validator 集合被保存在 NextValidators 变量中;当高度为 101 的区块被执行时,这一集合被保存在 Validator 中;当高度为 101 的区块被执行完毕后,这一集合被保存在 LastValidator 中。因此,对高度为 102 的区块投票,将来自于这个LastValidator集合中的 Validator。LastHeightValidatorsChanged 表示下一个发生了 Validator 集合变动的区块的高度,在上述的例子中,此变量取值为 102。

      • ConsensusParams:tmproto.ConsensusParams类型,包含一些和共识相关的参数。这些参数同样可以被上层逻辑所修改;

      • LastHeightConsensusParamsChanged:同理,表示下次发生ConsensusParams更新的区块的高度,在上述例子中,此变量取值为101;

      • LastResultsHash:[]byte类型,上层应用在执行完一个交易后,向 Tendermint 返回一些 Results,该值保存了上个区块执行过程中所返回的所有 Results 所形成的 Merkle 树的根Hash;

      • AppHash:[]byte类型,保存了上层应用在执行完上一区块之后的内部状态所构成 Merkle 树的根 Hash。

  3. func (state State) Copy() State {
    	return State{
    		Version:       state.Version,
    		ChainID:       state.ChainID,
    		InitialHeight: state.InitialHeight,
    
    		LastBlockHeight: state.LastBlockHeight,
    		LastBlockID:     state.LastBlockID,
    		LastBlockTime:   state.LastBlockTime,
    
    		NextValidators:              state.NextValidators.Copy(),
    		Validators:                  state.Validators.Copy(),
    		LastValidators:              state.LastValidators.Copy(),
    		LastHeightValidatorsChanged: state.LastHeightValidatorsChanged,
    
    		ConsensusParams:                  state.ConsensusParams,
    		LastHeightConsensusParamsChanged: state.LastHeightConsensusParamsChanged,
    
    		AppHash: state.AppHash,
    
    		LastResultsHash: state.LastResultsHash,
    	}
    }
    

    该函数为节点可能出现的状态改变保存了一份状态副本。之所以需要采用函数的形式而不直接令两个State结构体相等,原因是结构体中的三个Validators内部是指针类型的数据,因此需要单独定义函数来进行内容的拷贝和新指针的生成。该函数作为唯一能更改state的函数,被各个文件广泛调用用于同步(复制)状态。

  4. func (state State) Equals(state2 State) bool {
    	sbz, s2bz := state.Bytes(), state2.Bytes()
    	return bytes.Equal(sbz, s2bz)
    }
    
    func (state State) Bytes() []byte {
    	sm, err := state.ToProto()
    	if err != nil {
    		panic(err)
    	}
    	bz, err := proto.Marshal(sm)
    	if err != nil {
    		panic(err)
    	}
    	return bz
    }
    
    func (state State) IsEmpty() bool {
    	return state.Validators == nil // XXX can't compare to Empty
    }
    

    Equals函数用于判断两种状态是否一致,一致则返回true,只用于测试文件。不直接使用==来判断的原因是状态是一个复杂的结构体形式,想要判断两个状态相等就需要调用Bytes函数,该函数又使用类型转换函数proto和序列化函数marshal来将两个状态转化为可比较的有序字节流形式。Bytes函数主要在数据库存储状态(同文件夹下的store.go文件)时被调用,字节流化和序列化两个函数只要无法完成转化就会报错。IsEmpty函数用于判断一个状态是否等于空状态。下面我们来看类型转换函数ToProto怎么实现。

  5. func (state *State) ToProto() (*tmstate.State, error) {
    	if state == nil {
    		return nil, errors.New("state is nil")
    	}
    
    	sm := new(tmstate.State)
    
    	sm.Version = state.Version
    	sm.ChainID = state.ChainID
    	sm.InitialHeight = state.InitialHeight
    	sm.LastBlockHeight = state.LastBlockHeight
    
    	sm.LastBlockID = state.LastBlockID.ToProto()
    	sm.LastBlockTime = state.LastBlockTime
    	vals, err := state.Validators.ToProto()
    	if err != nil {
    		return nil, err
    	}
    	sm.Validators = vals
    
    	nVals, err := state.NextValidators.ToProto()
    	if err != nil {
    		return nil, err
    	}
    	sm.NextValidators = nVals
    
    	if state.LastBlockHeight >= 1 { // At Block 1 LastValidators is nil
    		lVals, err := state.LastValidators.ToProto()
    		if err != nil {
    			return nil, err
    		}
    		sm.LastValidators = lVals
    	}
    
    	sm.LastHeightValidatorsChanged = state.LastHeightValidatorsChanged
    	sm.ConsensusParams = state.ConsensusParams
    	sm.LastHeightConsensusParamsChanged = state.LastHeightConsensusParamsChanged
    	sm.LastResultsHash = state.LastResultsHash
    	sm.AppHash = state.AppHash
    
    	return sm, nil
    }
    
    • 可以看到大部分数据无需变动,只有LastBlockID和三个Validators需要使用ToProto函数进行类型转换;
    • ToProto函数内部其实就是一个简单的值传递,将PartSetHeader类型的数据传给了tmproto.PartSetHeader类型,这样就可以使用marshal进行序列化;
    • 进行proto类型转化操作的目的是:上述几个数据是自定义的数据类型,无法直接通过MarShal函数序列化从而进行消息传递,需要将其类型转换为proto类型之后才能进行Marshal序列化;
    • 需要注意的是如果这个状态是第一个块的状态,那么它的上一个块高度为0,则无需对LastValidators进行转化。
  6. func FromProto(pb *tmstate.State) (*State, error) { //nolint:golint
    	if pb == nil {
    		return nil, errors.New("nil State")
    	}
    
    	state := new(State)
    
    	state.Version = pb.Version
    	state.ChainID = pb.ChainID
    	state.InitialHeight = pb.InitialHeight
    
    	bi, err := types.BlockIDFromProto(&pb.LastBlockID)
    	if err != nil {
    		return nil, err
    	}
    	state.LastBlockID = *bi
    	state.LastBlockHeight = pb.LastBlockHeight
    	state.LastBlockTime = pb.LastBlockTime
    
    	vals, err := types.ValidatorSetFromProto(pb.Validators)
    	if err != nil {
    		return nil, err
    	}
    	state.Validators = vals
    
    	nVals, err := types.ValidatorSetFromProto(pb.NextValidators)
    	if err != nil {
    		return nil, err
    	}
    	state.NextValidators = nVals
    
    	if state.LastBlockHeight >= 1 { // At Block 1 LastValidators is nil
    		lVals, err := types.ValidatorSetFromProto(pb.LastValidators)
    		if err != nil {
    			return nil, err
    		}
    		state.LastValidators = lVals
    	} else {
    		state.LastValidators = types.NewValidatorSet(nil)
    	}
    
    	state.LastHeightValidatorsChanged = pb.LastHeightValidatorsChanged
    	state.ConsensusParams = pb.ConsensusParams
    	state.LastHeightConsensusParamsChanged = pb.LastHeightConsensusParamsChanged
    	state.LastResultsHash = pb.LastResultsHash
    	state.AppHash = pb.AppHash
    
    	return state, nil
    }
    

    上一个函数的相反操作,把转化后的proto类型的数据重新转换回Tendermint里的数据类型。里面用的函数不需要在项目中修改,就不细看了。

  7. func (state State) MakeBlock(
    	height int64,
    	txs []types.Tx,
    	commit *types.Commit,
    	evidence []types.Evidence,
    	proposerAddress []byte,
    ) (*types.Block, *types.PartSet) {
    	// Build base block with block data.
    	block := types.MakeBlock(height, txs, commit, evidence)
    
    	// Set time.
    	var timestamp time.Time
    	if height == state.InitialHeight {
    		timestamp = state.LastBlockTime // genesis time
    	} else {
    		timestamp = MedianTime(commit, state.LastValidators)
    	}
    
    	// Fill rest of header with state data.
    	block.Header.Populate(
    		state.Version.Consensus, state.ChainID,
    		timestamp, state.LastBlockID,
    		state.Validators.Hash(), state.NextValidators.Hash(),
    		types.HashConsensusParams(state.ConsensusParams), state.AppHash, state.LastResultsHash,
    		proposerAddress,
    	)
    
    	return block, block.MakePartSet(types.BlockPartSizeBytes)
    }
    
    • 首先根据传入的height、txs、commit、evidence来构建新的区块;

    • 由于生成了新的区块,因此需要根据最新state来更新时间戳:

      • 如果给定块是第一块,那么时间戳就是上一区块(创世区块)的时间戳,因为中间没有其他块;

      • 如果给定块不是第一块,那么时间戳需要经过MedianTime函数的计算:

        // 该函数就位于上述MakeBlock函数下面
        func MedianTime(commit *types.Commit, validators *types.ValidatorSet) time.Time {
        	weightedTimes := make([]*tmtime.WeightedTime, len(commit.Signatures))
        	totalVotingPower := int64(0)
        
        	for i, commitSig := range commit.Signatures {
        		if commitSig.Absent() {
        			continue
        		}
        		_, validator := validators.GetByAddress(commitSig.ValidatorAddress)
        		// If there's no condition, TestValidateBlockCommit panics; not needed normally.
        		if validator != nil {
        			totalVotingPower += validator.VotingPower
        			weightedTimes[i] = tmtime.NewWeightedTime(commitSig.Timestamp, validator.VotingPower)
        		}
        	}
        
        	return tmtime.WeightedMedian(weightedTimes, totalVotingPower)
        }
        

        该函数计算了给定块的commit的时间戳和当前状态中存储的LastValidators集合的时间戳的中位数,这样就保证了新块的时间戳始终落在诚实节点发送的时间戳之内,保证了拜占庭节点无法随意增减计算出的值。

        • 可以这样理解上面的总结:假设有f个拜占庭节点,和2f+1个诚实节点,首先我们要丢弃f个选票,最坏的情况是我们丢弃的全部都是诚实节点,那么就剩下了f+1个诚实节点;现在这f个合谋的拜占庭节点想要对区块的时间戳进行攻击。正常情况下区块的时间戳应该是依次递增的,如果区块的时间戳不是依次递增的就会出现问题,因此拜占庭节点想要攻击时间戳就有两种方式:1)将该区块的时间戳设置为一个极小值,这样就直接出现问题;2)将该区块的时间戳设置为一个极大值,这样当下一个区块上链时,无需攻击就会因为时间戳小于上一块的时间戳而出问题。但是我们又需要从剩余的2f+1个节点提出的时间戳中找到正确的时间戳,因此我们就选取最中间的时间戳,这样f个拜占庭节点所提出的错误时间戳要么分布在前f个里,要么分布在后f个里,最中间的时间戳一定是诚实节点所提出的正确的时间戳。

        • 具体实现过程是在MedianTime函数中计算出总权重,并且构造一个权重和时间戳对应的数组,将其传入WeightedMedia函数,函数如下:

          // /tendermint/types/time/time.go
          func WeightedMedian(weightedTimes []*WeightedTime, totalVotingPower int64) (res time.Time) {
          	median := totalVotingPower / 2
          
          	sort.Slice(weightedTimes, func(i, j int) bool {
          		if weightedTimes[i] == nil {
          			return false
          		}
          		if weightedTimes[j] == nil {
          			return true
          		}
          		return weightedTimes[i].Time.UnixNano() < weightedTimes[j].Time.UnixNano()
          	})
          
          	for _, weightedTime := range weightedTimes {
          		if weightedTime != nil {
          			if median <= weightedTime.Weight {
          				res = weightedTime.Time
          				break
          			}
          			median -= weightedTime.Weight
          		}
          	}
          	return
          }
          

          该函数首先按照时间戳的先后顺序对数组进行排序,排序完成后从头开始依次检查权重,一旦检查到某个权重之后超过了总权重的1/2,就将该权重对应的时间戳返回作为中位数时间戳;那么直接用时间戳的数量来确定中位数不就好了,为什么还需要引入权重来判断是否到一半呢?

          • 是因为Tendermint的拜占庭假设并不是拥有超过2/3数量的诚实节点,而是拥有超过2/3票权的诚实节点,因此上文中提到的节点数量都可以抽象理解为节点所含的权重。
    • 回到生成新区块上来,在完成了时间戳的赋值后,还需要将state中的数据赋给新区块的header,其中:

      • Validators和NextValidators需要计算出由集合中的Validators作为叶节点组成的Merkle树的根hash之后再传给header,这样减小了header的存储量,同时Merkle树的使用也使得每个Validators都是可验证的;

      • ConsensusParams需要经过函数HashConsensusParams,将共识参数表中的Block.MaxBytes和Block.MaxGas两个参数取出来做一次hash即可,使得区块在不破坏协议的前提下包含尽可能多的交易数据;

      • LastValidators不需要传给header,这就不得不说到Tendermint中区块的结构:

        • Header:区块头(下文详述)
        • Data:交易列表
        • Evidence:Validator 作恶的证据列表
        • LastCommit:上一个区块所获得的若干 Validator 的签名

        其中LastCommit就负责存储LastValidators中的集合,因此不需要再存入Header中。

    • 至此新区块生成完毕,函数最终的返回值除了block本身以外,还返回了一个block集合。该集合通过方法block.MakePartSet生成,参数为提前指定好的64KB,该集合标定了哪些区块需要通过Gossip协议同步给其他peers。

    • MakeBlock函数主要在同文件夹下的execution.go文件中被调用,因此推断该文件是用于管理区块的整个生命周期的文件,后续会继续分析该文件。

  8. func MakeGenesisStateFromFile(genDocFile string) (State, error) {
    	genDoc, err := MakeGenesisDocFromFile(genDocFile)
    	if err != nil {
    		return State{}, err
    	}
    	return MakeGenesisState(genDoc)
    }
    
    func MakeGenesisDocFromFile(genDocFile string) (*types.GenesisDoc, error) {
    	genDocJSON, err := os.ReadFile(genDocFile)
    	if err != nil {
    		return nil, fmt.Errorf("couldn't read GenesisDoc file: %v", err)
    	}
    	genDoc, err := types.GenesisDocFromJSON(genDocJSON)
    	if err != nil {
    		return nil, fmt.Errorf("error reading GenesisDoc: %v", err)
    	}
    	return genDoc, nil
    }
    
    func MakeGenesisState(genDoc *types.GenesisDoc) (State, error) {
    	err := genDoc.ValidateAndComplete()
    	if err != nil {
    		return State{}, fmt.Errorf("error in genesis file: %v", err)
    	}
    
    	var validatorSet, nextValidatorSet *types.ValidatorSet
    	if genDoc.Validators == nil {
    		validatorSet = types.NewValidatorSet(nil)
    		nextValidatorSet = types.NewValidatorSet(nil)
    	} else {
    		validators := make([]*types.Validator, len(genDoc.Validators))
    		for i, val := range genDoc.Validators {
    			validators[i] = types.NewValidator(val.PubKey, val.Power)
    		}
    		validatorSet = types.NewValidatorSet(validators)
    		nextValidatorSet = types.NewValidatorSet(validators).CopyIncrementProposerPriority(1)
    	}
    
    	return State{
    		Version:       InitStateVersion,
    		ChainID:       genDoc.ChainID,
    		InitialHeight: genDoc.InitialHeight,
    
    		LastBlockHeight: 0,
    		LastBlockID:     types.BlockID{},
    		LastBlockTime:   genDoc.GenesisTime,
    
    		NextValidators:              nextValidatorSet,
    		Validators:                  validatorSet,
    		LastValidators:              types.NewValidatorSet(nil),
    		LastHeightValidatorsChanged: genDoc.InitialHeight,
    
    		ConsensusParams:                  *genDoc.ConsensusParams,
    		LastHeightConsensusParamsChanged: genDoc.InitialHeight,
    
    		AppHash: genDoc.AppHash,
    	}, nil
    }
    
    • 这三个函数功能是通过读取本地文件反序列化出创世区块JSON文件从而生成创世区块配置文件,最终生成创世区块并返回生成创世区块后的state。这里不返回创世区块的原因是创世区块并没有实际内容,起作用主要是给系统中所有的用户标定一个相同起点,因此只需要得到生成创世区块后的状态信息即可,其他节点在同步(复制)该状态后继续进行后续工作即可。

2. /tendermint/state/execution.go

  1. type BlockExecutor struct {
    	// save state, validators, consensus params, abci responses here
    	store Store
    
    	// execute the app against this
    	proxyApp proxy.AppConnConsensus
    
    	// events
    	eventBus types.BlockEventPublisher
    
    	// manage the mempool lock during commit
    	// and update both with block results after commit.
    	mempool mempl.Mempool
    	evpool  EvidencePool
    
    	logger log.Logger
    
    	metrics *Metrics
    }
    
    type BlockExecutorOption func(executor *BlockExecutor)
    
    func BlockExecutorWithMetrics(metrics *Metrics) BlockExecutorOption {
    	return func(blockExec *BlockExecutor) {
    		blockExec.metrics = metrics
    	}
    }
    
    • 首先定义了BlockExecutor结构体,该结构体用于处理区块操作和状态更新,其他文件可以通过函数ApplyBlock来使用BlockExecutor来对区块及其状态进行操作。下面来具体看结构体内容:
      • store:Store类型,store.go文件中定义的接口类型,用于保存状态、验证者、共识信息、ABCI响应;
      • proxyApp:proxy.AppConnConsensus类型,与区块执行相关的ABCI接口,目的是在接收到一个合法区块之后,需要将区块提交给App来执行;
      • eventBus:types.BlockEventPublisher类型,负责打印一切与区块相关的事件events;
      • mempool:mempl.Mempool类型,引入该类型主要是用于管理commit阶段mempool中的锁;
      • evpool:EvidencePool类型,管理生成区块时所需的Evidence信息;
      • logger:log.Logger类型,commit阶段后进行日志信息的同步;
      • metrics:*Metrics,commit阶段后进行配置信息的同步。
    • 对func(executor *BlockExecutor)函数进行重命名,方便后续对Executor进行灵活的信息配置;
    • 后续立刻给出了一个示例,BlockExecutorWithMetrics函数可以返回上述类型的函数,通过传入的metrics参数对BlockExecutor进行配置。
  2. func NewBlockExecutor(
    	stateStore Store,
    	logger log.Logger,
    	proxyApp proxy.AppConnConsensus,
    	mempool mempl.Mempool,
    	evpool EvidencePool,
    	options ...BlockExecutorOption,
    ) *BlockExecutor {
    	res := &BlockExecutor{
    		store:    stateStore,
    		proxyApp: proxyApp,
    		eventBus: types.NopEventBus{},
    		mempool:  mempool,
    		evpool:   evpool,
    		logger:   logger,
    		metrics:  NopMetrics(),
    	}
    
    	for _, option := range options {
    		option(res)
    	}
    
    	return res
    }
    
    • NewBlockExecutor函数根据给定参数构建新的BlockExecutor,其中:
      • 没有配置eventBus,后续需要使用SetEventBus函数来进行配置;
      • 通过未知数量的options函数传入来对BlockExecutor进行灵活的参数配置。
    • 最终该函数返回生成的BlockExecutor实例;
    • 该函数广泛用于Tendermint的各个文件中,用于管理区块和状态信息。
  3. func (blockExec *BlockExecutor) Store() Store {
    	return blockExec.store
    }
    
    • 该函数返回BlockExecutor中的store实例,主要被共识阶段的文件调用,用于访问状态数据库。
  4. func (blockExec *BlockExecutor) SetEventBus(eventBus types.BlockEventPublisher) {
    	blockExec.eventBus = eventBus
    }
    
    • 上文提到的eventBus设置函数,如果没有调用该函数,那么生成的BlockExecutor中eventBus字段是空结构体。
  5. func (blockExec *BlockExecutor) CreateProposalBlock(
    	height int64,
    	state State, commit *types.Commit,
    	proposerAddr []byte,
    ) (*types.Block, *types.PartSet) {
    
    	maxBytes := state.ConsensusParams.Block.MaxBytes
    	maxGas := state.ConsensusParams.Block.MaxGas
    
    	evidence, evSize := blockExec.evpool.PendingEvidence(state.ConsensusParams.Evidence.MaxBytes)
    
    	// Fetch a limited amount of valid txs
    	maxDataBytes := types.MaxDataBytes(maxBytes, evSize, state.Validators.Size())
    
    	txs := blockExec.mempool.ReapMaxBytesMaxGas(maxDataBytes, maxGas)
    
    	return state.MakeBlock(height, txs, commit, evidence, proposerAddr)
    }
    
    • 该函数作用是调用state中的MakeBlock函数来创建新的区块和Gossip集合;
    • 该函数接受传入的高度信息height、状态state、commit信息、提议者地址,从state中获取最大字节数和最大gas用于限定区块大小,再从操作对象BlockExecutor中的evpool中读取evidence、mempool中读取交易信息,最终根据这些信息构建新的block;
    • 该函数在共识阶段被调用,用于提议新的区块时进行区块生成。
  6. func (blockExec *BlockExecutor) ValidateBlock(state State, block *types.Block) error {
    	err := validateBlock(state, block)
    	if err != nil {
    		return err
    	}
    	return blockExec.evpool.CheckEvidence(block.Evidence.Evidence)
    }
    
    • 该函数用于验证给定状态下的区块是否合法,最后还调用了CheckEvidence函数查询给定Evidence来判断区块是否作恶,总结来说就是对区块的合法性进行了检查。

    • 其中validateBlock函数如下:

      func validateBlock(state State, block *types.Block) error {
      	// Validate internal consistency.
      	if err := block.ValidateBasic(); err != nil {
      		return err
      	}
      
      	// Validate basic info.
      	if block.Version.App != state.Version.Consensus.App ||
      		block.Version.Block != state.Version.Consensus.Block {
      		return fmt.Errorf("wrong Block.Header.Version. Expected %v, got %v",
      			state.Version.Consensus,
      			block.Version,
      		)
      	}
      	if block.ChainID != state.ChainID {
      		return fmt.Errorf("wrong Block.Header.ChainID. Expected %v, got %v",
      			state.ChainID,
      			block.ChainID,
      		)
      	}
      	if state.LastBlockHeight == 0 && block.Height != state.InitialHeight {
      		return fmt.Errorf("wrong Block.Header.Height. Expected %v for initial block, got %v",
      			block.Height, state.InitialHeight)
      	}
      	if state.LastBlockHeight > 0 && block.Height != state.LastBlockHeight+1 {
      		return fmt.Errorf("wrong Block.Header.Height. Expected %v, got %v",
      			state.LastBlockHeight+1,
      			block.Height,
      		)
      	}
      	// Validate prev block info.
      	if !block.LastBlockID.Equals(state.LastBlockID) {
      		return fmt.Errorf("wrong Block.Header.LastBlockID.  Expected %v, got %v",
      			state.LastBlockID,
      			block.LastBlockID,
      		)
      	}
      
      	// Validate app info
      	if !bytes.Equal(block.AppHash, state.AppHash) {
      		return fmt.Errorf("wrong Block.Header.AppHash.  Expected %X, got %v",
      			state.AppHash,
      			block.AppHash,
      		)
      	}
      	hashCP := types.HashConsensusParams(state.ConsensusParams)
      	if !bytes.Equal(block.ConsensusHash, hashCP) {
      		return fmt.Errorf("wrong Block.Header.ConsensusHash.  Expected %X, got %v",
      			hashCP,
      			block.ConsensusHash,
      		)
      	}
      	if !bytes.Equal(block.LastResultsHash, state.LastResultsHash) {
      		return fmt.Errorf("wrong Block.Header.LastResultsHash.  Expected %X, got %v",
      			state.LastResultsHash,
      			block.LastResultsHash,
      		)
      	}
      	if !bytes.Equal(block.ValidatorsHash, state.Validators.Hash()) {
      		return fmt.Errorf("wrong Block.Header.ValidatorsHash.  Expected %X, got %v",
      			state.Validators.Hash(),
      			block.ValidatorsHash,
      		)
      	}
      	if !bytes.Equal(block.NextValidatorsHash, state.NextValidators.Hash()) {
      		return fmt.Errorf("wrong Block.Header.NextValidatorsHash.  Expected %X, got %v",
      			state.NextValidators.Hash(),
      			block.NextValidatorsHash,
      		)
      	}
      
      	// Validate block LastCommit.
      	if block.Height == state.InitialHeight {
      		if len(block.LastCommit.Signatures) != 0 {
      			return errors.New("initial block can't have LastCommit signatures")
      		}
      	} else {
      		// LastCommit.Signatures length is checked in VerifyCommit.
      		if err := state.LastValidators.VerifyCommit(
      			state.ChainID, state.LastBlockID, block.Height-1, block.LastCommit); err != nil {
      			return err
      		}
      	}
      
      	// NOTE: We can't actually verify it's the right proposer because we don't
      	// know what round the block was first proposed. So just check that it's
      	// a legit address and a known validator.
      	if len(block.ProposerAddress) != crypto.AddressSize {
      		return fmt.Errorf("expected ProposerAddress size %d, got %d",
      			crypto.AddressSize,
      			len(block.ProposerAddress),
      		)
      	}
      	if !state.Validators.HasAddress(block.ProposerAddress) {
      		return fmt.Errorf("block.Header.ProposerAddress %X is not a validator",
      			block.ProposerAddress,
      		)
      	}
      
      	// Validate block Time
      	switch {
      	case block.Height > state.InitialHeight:
      		if !block.Time.After(state.LastBlockTime) {
      			return fmt.Errorf("block time %v not greater than last block time %v",
      				block.Time,
      				state.LastBlockTime,
      			)
      		}
      		medianTime := MedianTime(block.LastCommit, state.LastValidators)
      		if !block.Time.Equal(medianTime) {
      			return fmt.Errorf("invalid block time. Expected %v, got %v",
      				medianTime,
      				block.Time,
      			)
      		}
      
      	case block.Height == state.InitialHeight:
      		genesisTime := state.LastBlockTime
      		if !block.Time.Equal(genesisTime) {
      			return fmt.Errorf("block time %v is not equal to genesis time %v",
      				block.Time,
      				genesisTime,
      			)
      		}
      
      	default:
      		return fmt.Errorf("block height %v lower than initial height %v",
      			block.Height, state.InitialHeight)
      	}
      
      	// Check evidence doesn't exceed the limit amount of bytes.
      	if max, got := state.ConsensusParams.Evidence.MaxBytes, block.Evidence.ByteSize(); got > max {
      		return types.NewErrEvidenceOverflow(max, got)
      	}
      
      	return nil
      }
      
      • 该函数首先调用了ValidateBasic函数,该函数检查了区块的内部一致性。该检查是基本检查,不涉及状态信息,仅仅判断区块内部数据是否不合理,例如哈希值是否正确、Header是否合法、Evidence是否合法;
      • 之后该函数将所给区块和当前state的信息进行了全面对比,来检查该区块是否是该状态下的合法区块,涉及版本、链ID、高度Height等上节中介绍过的state中的信息。
    • 最后的CheckEvidence在/tendermint/evidence/pool.go中实现,具体功能是检查evidencePool中的每个证据,以确保没有重复的evidence并且原本不存在的evidence被添加到evidencePool中。

  7. func (blockExec *BlockExecutor) ApplyBlock(
    	state State, blockID types.BlockID, block *types.Block,
    ) (State, int64, error) {
    
    	if err := validateBlock(state, block); err != nil {
    		return state, 0, ErrInvalidBlock(err)
    	}
    
    	startTime := time.Now().UnixNano()
    	abciResponses, err := execBlockOnProxyApp(
    		blockExec.logger, blockExec.proxyApp, block, blockExec.store, state.InitialHeight,
    	)
    	endTime := time.Now().UnixNano()
    	blockExec.metrics.BlockProcessingTime.Observe(float64(endTime-startTime) / 1000000)
    	if err != nil {
    		return state, 0, ErrProxyAppConn(err)
    	}
    
    	fail.Fail() // XXX
    
    	// Save the results before we commit.
    	if err := blockExec.store.SaveABCIResponses(block.Height, abciResponses); err != nil {
    		return state, 0, err
    	}
    
    	fail.Fail() // XXX
    
    	// validate the validator updates and convert to tendermint types
    	abciValUpdates := abciResponses.EndBlock.ValidatorUpdates
    	err = validateValidatorUpdates(abciValUpdates, state.ConsensusParams.Validator)
    	if err != nil {
    		return state, 0, fmt.Errorf("error in validator updates: %v", err)
    	}
    
    	validatorUpdates, err := types.PB2TM.ValidatorUpdates(abciValUpdates)
    	if err != nil {
    		return state, 0, err
    	}
    	if len(validatorUpdates) > 0 {
    		blockExec.logger.Debug("updates to validators", "updates", types.ValidatorListString(validatorUpdates))
    	}
    
    	// Update the state with the block and responses.
    	state, err = updateState(state, blockID, &block.Header, abciResponses, validatorUpdates)
    	if err != nil {
    		return state, 0, fmt.Errorf("commit failed for application: %v", err)
    	}
    
    	// Lock mempool, commit app state, update mempoool.
    	appHash, retainHeight, err := blockExec.Commit(state, block, abciResponses.DeliverTxs)
    	if err != nil {
    		return state, 0, fmt.Errorf("commit failed for application: %v", err)
    	}
    
    	// Update evpool with the latest state.
    	blockExec.evpool.Update(state, block.Evidence.Evidence)
    
    	fail.Fail() // XXX
    
    	// Update the app hash and save the state.
    	state.AppHash = appHash
    	if err := blockExec.store.Save(state); err != nil {
    		return state, 0, err
    	}
    
    	fail.Fail() // XXX
    
    	// Events are fired after everything else.
    	// NOTE: if we crash between Commit and Save, events wont be fired during replay
    	fireEvents(blockExec.logger, blockExec.eventBus, block, abciResponses, validatorUpdates)
    
    	return state, retainHeight, nil
    }
    
    • ApplyBlock根据状态验证块,针对应用程序执行它,触发相关事件,提交应用程序,并保存新的状态和响应。它返回新的状态和要保留的块高度(修剪旧的块),如果过程中出错则在高度位置返回0,并返回出错信息;否则返回正常高度信息并在error处返回nil。它是唯一需要从这个包外部调用的函数,以处理和提交整个块。它需要一个blockID来避免重新计算parts的哈希值。

    • 调用上文介绍过的validateBlock来验证区块的基本信息是否合法;

    • 调用execBlockOnProxyApp函数来在ABCI接口上验证并执行区块中封装的交易,同时记录该操作所需的时间,存入metrics中。函数如下:

      func execBlockOnProxyApp(
      	logger log.Logger,
      	proxyAppConn proxy.AppConnConsensus,
      	block *types.Block,
      	store Store,
      	initialHeight int64,
      ) (*tmstate.ABCIResponses, error) {
      	var validTxs, invalidTxs = 0, 0
      
      	txIndex := 0
      	abciResponses := new(tmstate.ABCIResponses)
      	dtxs := make([]*abci.ResponseDeliverTx, len(block.Txs))
      	abciResponses.DeliverTxs = dtxs
      
      	// Execute transactions and get hash.
      	proxyCb := func(req *abci.Request, res *abci.Response) {
      		if r, ok := res.Value.(*abci.Response_DeliverTx); ok {
      			// TODO: make use of res.Log
      			// TODO: make use of this info
      			// Blocks may include invalid txs.
      			txRes := r.DeliverTx
      			if txRes.Code == abci.CodeTypeOK {
      				validTxs++
      			} else {
      				logger.Debug("invalid tx", "code", txRes.Code, "log", txRes.Log)
      				invalidTxs++
      			}
      
      			abciResponses.DeliverTxs[txIndex] = txRes
      			txIndex++
      		}
      	}
      	proxyAppConn.SetResponseCallback(proxyCb)
      
      	commitInfo := getBeginBlockValidatorInfo(block, store, initialHeight)
      
      	byzVals := make([]abci.Evidence, 0)
      	for _, evidence := range block.Evidence.Evidence {
      		byzVals = append(byzVals, evidence.ABCI()...)
      	}
      
      	// Begin block
      	var err error
      	pbh := block.Header.ToProto()
      	if pbh == nil {
      		return nil, errors.New("nil header")
      	}
      
      	abciResponses.BeginBlock, err = proxyAppConn.BeginBlockSync(abci.RequestBeginBlock{
      		Hash:                block.Hash(),
      		Header:              *pbh,
      		LastCommitInfo:      commitInfo,
      		ByzantineValidators: byzVals,
      	})
      	if err != nil {
      		logger.Error("error in proxyAppConn.BeginBlock", "err", err)
      		return nil, err
      	}
      
      	// run txs of block
      	for _, tx := range block.Txs {
      		proxyAppConn.DeliverTxAsync(abci.RequestDeliverTx{Tx: tx})
      		if err := proxyAppConn.Error(); err != nil {
      			return nil, err
      		}
      	}
      
      	// End block.
      	abciResponses.EndBlock, err = proxyAppConn.EndBlockSync(abci.RequestEndBlock{Height: block.Height})
      	if err != nil {
      		logger.Error("error in proxyAppConn.EndBlock", "err", err)
      		return nil, err
      	}
      
      	logger.Info("executed block", "height", block.Height, "num_valid_txs", validTxs, "num_invalid_txs", invalidTxs)
      	return abciResponses, nil
      }
      
      • 设置validTxs和invalidTxs来统计合法和不合法的交易数量,txIndex来记录交易的索引值;
      • 定义序列化回调函数proxyCb,来统计合法/不合法交易数量并生成[索引值-交易内容]的字典,并将该函数传入ABCI接口定义中;
      • 调用getBeginBlockValidatorInfo函数,获取初始高度的区块的验证者信息;
      • 使用byzVals来存储evidence数据;
      • 分别完成区块起始状态同步、遍历所有交易并执行、区块终止状态同步三步骤,完成全过程。
    • 调用SaveABCIResponces函数来在commit之前保存当前高度下ABCI的响应,设想一下场景:当客户端应用层commit之后但是在状态更改并同步之前程序崩溃了,那么这次的客户端的响应将直接消失,但是客户端并不知道响应丢失,这就造成了应用层数据和数据库数据不一致的情况。因此通过这一层保存措施可以防止程序崩溃导致的ABCI响应丢失,即使程序崩溃也可以通过该信息恢复ABCI响应从而同步数据,函数如下:

      func (store dbStore) SaveABCIResponses(height int64, abciResponses *tmstate.ABCIResponses) error {
      	var dtxs []*abci.ResponseDeliverTx
      	// strip nil values,
      	for _, tx := range abciResponses.DeliverTxs {
      		if tx != nil {
      			dtxs = append(dtxs, tx)
      		}
      	}
      	abciResponses.DeliverTxs = dtxs
      
      	// If the flag is false then we save the ABCIResponse. This can be used for the /BlockResults
      	// query or to reindex an event using the command line.
      	if !store.DiscardABCIResponses {
      		bz, err := abciResponses.Marshal()
      		if err != nil {
      			return err
      		}
      		if err := store.db.Set(calcABCIResponsesKey(height), bz); err != nil {
      			return err
      		}
      	}
      
      	// We always save the last ABCI response for crash recovery.
      	// This overwrites the previous saved ABCI Response.
      	response := &tmstate.ABCIResponsesInfo{
      		AbciResponses: abciResponses,
      		Height:        height,
      	}
      	bz, err := response.Marshal()
      	if err != nil {
      		return err
      	}
      
      	return store.db.SetSync(lastABCIResponseKey, bz)
      }
      
      • 除了固定保存最新的ABCI响应用于恢复系统崩溃以外,该函数还对store中的DiscardABCIResponses参数进行了判断,如果为false则保存所有ABCI响应,这样可以用于后续区块结果的查询或命令行的重新索引。
    • 调用validateValidatorUpdates函数来检查验证者更新是否正常进行,函数如下:

      func validateValidatorUpdates(abciUpdates []abci.ValidatorUpdate,
      	params tmproto.ValidatorParams) error {
      	for _, valUpdate := range abciUpdates {
      		if valUpdate.GetPower() < 0 {
      			return fmt.Errorf("voting power can't be negative %v", valUpdate)
      		} else if valUpdate.GetPower() == 0 {
      			// continue, since this is deleting the validator, and thus there is no
      			// pubkey to check
      			continue
      		}
      
      		// Check if validator's pubkey matches an ABCI type in the consensus params
      		pk, err := cryptoenc.PubKeyFromProto(valUpdate.PubKey)
      		if err != nil {
      			return err
      		}
      
      		if !types.IsValidPubkeyType(params, pk.Type()) {
      			return fmt.Errorf("validator %v is using pubkey %s, which is unsupported for consensus",
      				valUpdate, pk.Type())
      		}
      	}
      	return nil
      }
      
      • 其实就是检查新的验证者:
        • 是否有正常的(大于0)票权;
        • 是否能够反序列化出符合共识协议的公钥;
        • 的公钥是否属于共识后验证者的公钥之一;
    • 在验证新的验证者的合法性后,就调用ValidatorUpdates函数进行验证者的更新,函数如下:

      func (pb2tm) ValidatorUpdates(vals []abci.ValidatorUpdate) ([]*Validator, error) {
      	tmVals := make([]*Validator, len(vals))
      	for i, v := range vals {
      		pub, err := cryptoenc.PubKeyFromProto(v.PubKey)
      		if err != nil {
      			return nil, err
      		}
      		tmVals[i] = NewValidator(pub, v.Power)
      	}
      	return tmVals, nil
      }
      
      • 其实就是根据新的验证者的公钥和票权两个信息,调用NewValidator函数新建验证者实例,并组成切片以便后续调用;

      在完成验证者的更新后,还需要将更新后的验证者集合信息写入日志文件;

    • 调用updateState函数,根据新区块Header和ABCI响应进行状态state的更新,函数如下:

      func updateState(
      	state State,
      	blockID types.BlockID,
      	header *types.Header,
      	abciResponses *tmstate.ABCIResponses,
      	validatorUpdates []*types.Validator,
      ) (State, error) {
      
      	// Copy the valset so we can apply changes from EndBlock
      	// and update s.LastValidators and s.Validators.
      	nValSet := state.NextValidators.Copy()
      
      	// Update the validator set with the latest abciResponses.
      	lastHeightValsChanged := state.LastHeightValidatorsChanged
      	if len(validatorUpdates) > 0 {
      		err := nValSet.UpdateWithChangeSet(validatorUpdates)
      		if err != nil {
      			return state, fmt.Errorf("error changing validator set: %v", err)
      		}
      		// Change results from this height but only applies to the next next height.
      		lastHeightValsChanged = header.Height + 1 + 1
      	}
      
      	// Update validator proposer priority and set state variables.
      	nValSet.IncrementProposerPriority(1)
      
      	// Update the params with the latest abciResponses.
      	nextParams := state.ConsensusParams
      	lastHeightParamsChanged := state.LastHeightConsensusParamsChanged
      	if abciResponses.EndBlock.ConsensusParamUpdates != nil {
      		// NOTE: must not mutate s.ConsensusParams
      		nextParams = types.UpdateConsensusParams(state.ConsensusParams, abciResponses.EndBlock.ConsensusParamUpdates)
      		err := types.ValidateConsensusParams(nextParams)
      		if err != nil {
      			return state, fmt.Errorf("error updating consensus params: %v", err)
      		}
      
      		state.Version.Consensus.App = nextParams.Version.AppVersion
      
      		// Change results from this height but only applies to the next height.
      		lastHeightParamsChanged = header.Height + 1
      	}
      
      	nextVersion := state.Version
      
      	// NOTE: the AppHash has not been populated.
      	// It will be filled on state.Save.
      	return State{
      		Version:                          nextVersion,
      		ChainID:                          state.ChainID,
      		InitialHeight:                    state.InitialHeight,
      		LastBlockHeight:                  header.Height,
      		LastBlockID:                      blockID,
      		LastBlockTime:                    header.Time,
      		NextValidators:                   nValSet,
      		Validators:                       state.NextValidators.Copy(),
      		LastValidators:                   state.Validators.Copy(),
      		LastHeightValidatorsChanged:      lastHeightValsChanged,
      		ConsensusParams:                  nextParams,
      		LastHeightConsensusParamsChanged: lastHeightParamsChanged,
      		LastResultsHash:                  ABCIResponsesResultsHash(abciResponses),
      		AppHash:                          nil,
      	}, nil
      }
      
      • 首先复制state中存储的NextValidators信息,用于后续如果正常更新验证者,则将其传入Validators字段,用于在三个集合之间传递Validators集合;

      • 调用Validators的UpdateWithChangeSet方法进行Validators的更新,同时将LastHeightValidatorsChanged字段改为当前高度+1+1,在两层延迟之后才真正应用;

      • 调用UpdateConsensusParams函数进行共识参数的更新,并将LastHeightConsensusParamsChanged字段改为当前高度+1,在一层延迟之后才真正应用;

      • 最后返回新的状态,其中:

        • 更新后的Validators集合传入新状态的NextValidators字段;

        • 旧状态的NextValidators集合传入新状态的Validators字段;

        • 旧状态的Validators集合传入新状态的LastValidators字段,在下一层开始起作用;

        • 使用ABCIResponsesResultsHash计算ABCI响应的哈希值传入新状态的LastResultsHash字段;

        • 而新状态的AppHash字段则为nil,因为此时状态还没有真正进行同步,需要在状态同步于数据库之后再填入该字段;

    • 调用Commit函数对区块提议进行确认,并获取AppHash,函数如下:

      func (blockExec *BlockExecutor) Commit(
      	state State,
      	block *types.Block,
      	deliverTxResponses []*abci.ResponseDeliverTx,
      ) ([]byte, int64, error) {
      	blockExec.mempool.Lock()
      	defer blockExec.mempool.Unlock()
      
      	// while mempool is Locked, flush to ensure all async requests have completed
      	// in the ABCI app before Commit.
      	err := blockExec.mempool.FlushAppConn()
      	if err != nil {
      		blockExec.logger.Error("client error during mempool.FlushAppConn", "err", err)
      		return nil, 0, err
      	}
      
      	// Commit block, get hash back
      	res, err := blockExec.proxyApp.CommitSync()
      	if err != nil {
      		blockExec.logger.Error("client error during proxyAppConn.CommitSync", "err", err)
      		return nil, 0, err
      	}
      
      	// ResponseCommit has no error code - just data
      	blockExec.logger.Info(
      		"committed state",
      		"height", block.Height,
      		"num_txs", len(block.Txs),
      		"app_hash", fmt.Sprintf("%X", res.Data),
      	)
      
      	// Update mempool.
      	err = blockExec.mempool.Update(
      		block.Height,
      		block.Txs,
      		deliverTxResponses,
      		TxPreCheck(state),
      		TxPostCheck(state),
      	)
      
      	return res.Data, res.RetainHeight, err
      }
      
      • commit函数运行过程中对mempool资源进行了加锁(读写锁),在退出函数前解锁;
      • FlushAppConn函数刷新mempool连接,以确保在commit前所有ABCI的异步响应都已经完成了;
      • CommitSync函数对区块进行了确认,并从ABCI接口得到了同步状态后的AppHash值;
      • Update函数对mempool进行了更新,去掉了被选中的交易,并更新了mempool的size;
      • 最终函数返回确认后的AppHash和维持的高度值。
    • 使用Update函数来更新证据池EvidencePool,传入当前状态与该高度下提交的证据,并完成以下操作:

      • 从共识过程中获取任何冲突的投票,并将其形成DuplicateVoteEvidence添加到证据池中;
      • 更新证据池的状态;
      • 将已经确认的提交证据转移到确认池中;
      • 删除高度与时间过期的证据;
    • 将commit阶段得到的AppHash传入当前state;

    • fireEvents函数用于给节点内部组件发送新区块生成的相关信息,订阅该信息的组件会收到该消息并在内部进行结构或数据调整;

    • 至此,ApplyBlock函数正式结束,最终返回生成区块后的状态,维持的高度,和nil(无错误)。

  8. func ExecCommitBlock(
    	appConnConsensus proxy.AppConnConsensus,
    	block *types.Block,
    	logger log.Logger,
    	store Store,
    	initialHeight int64,
    ) ([]byte, error) {
    	_, err := execBlockOnProxyApp(logger, appConnConsensus, block, store, initialHeight)
    	if err != nil {
    		logger.Error("failed executing block on proxy app", "height", block.Height, "err", err)
    		return nil, err
    	}
    
    	// Commit block, get hash back
    	res, err := appConnConsensus.CommitSync()
    	if err != nil {
    		logger.Error("client error during proxyAppConn.CommitSync", "err", res)
    		return nil, err
    	}
    
    	// ResponseCommit has no error or log, just data
    	return res.Data, nil
    }
    
    • 当应用层执行的交易远远落后于区块链上的交易时,使用该函数,在缺少状态state的情况下执行区块中的交易,在执行过程中不对状态进行验证或改变,最终返回执行后的AppHash值。

参考文章

  1. https://zhuanlan.zhihu.com/p/339156677
  2. https://plainchant.gitbook.io/plainchant/qu-kuai-lian/cosmos/tendermint-de-qu-kuai-gou-cheng
  3. https://txiner.top/post/Tendermint阅读VIII/
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值