1、共识机制

区块链采用去中心化的设计,节点是各处分散且平行的,所以必须设计一套制度,来维护系统的运作顺序与公平性,统一区块链的版本,并奖励提供资源维护区块链的使用者,以及惩罚恶意的危害者。这样的制度,必须依赖某种方式来证明,是由谁取得了一个区块链的打包权(或称记账权),并且可以获取打包这一个区块的奖励;又或者是谁意图进行危害,就会获得一定的惩罚,这就是共识机制

共识算法是区块链项目的核心之一,每一个运行着的区块链都需要一个共识算法来保证出块的有效性和有序性。

在以太坊的官方源码中,有两个共识算法:clique和ethash,它们都位于以太坊项目的consensus目录下。clique目录下的代码实现的是PoA(权威证明,Proof of Authority)共识;在ethash目录下实现的是PoW(工作量证明,Proof of Work)共识。

在以太坊中clique仅在测试网络里使用,真实的以太坊主网还是使用PoW算法(ethash模块实现)。但在自己组成私有网络时,你可以自由选择使用clique还是ethash。

2、POA需要解决的主要问题

PoA的基本思想来源于现实世界:授权一定数量的“专家”,由这些人相互合作打包区块并对区块链进行维护,其它人则无权打包区块,并且普通人相信成为“专家”的人会努力维护区块链的正常秩序。

“专家”需要公开自己的身份。这也是PoA设计初衷的一部分:设计者认为每个人都是爱惜自己的声誉的,通过公开自己身份,专家会为了自己的声誉努力做正确的事,而不是作恶。

PoA共识中出块权掌握在部分“专家”手里,而普通人是无法参与的(无论你有多少算力、多少权益)。可见PoA共识牺牲了一部去中心化的特性,换来了一种可控性。

为了描述的一致性,我们将“专家”称为“签名者”,即有权生成新区块并签名的账号地址。

PoA的实现,必须要解决好这两个问题。

问题1:如何实现签名者的引进和踢出

PoA的第一个问题是需要解决签名者更换的问题。在PoA中,签名者必须保证多数情况下在线出块。然而随着项目的不断运行,不可能所有签名者都一直有资源、有意愿继续工作;另外偶尔签名者也会作恶,必须及时将作恶的人踢出。(作为对比,在PoW中,任何一个人都可以随时接入区块链网络并尝试出块,也可以随时退出网络)

问题2:如何控制出块时机

首先要明确的是,出块时机由两方面决定:一是出块时间;二是由谁出块。在PoA中,签名者之间是合作关系,大家“和和气气”,什么时间出块、由谁出都要按规则来,不能争不能抢。因此需要有良好的规则控制出块时机。(作为对比,在PoW中,出块时间根据历史出块记录动态调整;由谁出块是由算力决定的:算力越强,越能获得出块权。可见在PoW中签名者之间是竞争的关系,出块时机由能力确定)

下面我们看看clique是如何解决这些问题的。

3、clique的设计概要

clique模块的原作者在这篇文章里详细说明了clique的设计和背景。

为了表达清晰,我们需要提先说明几个原文中的数据和名词的定义:

(1)checkpoint: 一个特殊的block,它的高度是EPOCH_LENGTH的整数倍,block中不包含投票信息但包含当时所有的签名者列表

(2)SIGNER_COUNT: 某一时刻签名者的数量

(3)SIGNER_LIMIT: 连续的块的数量,在这些连续的块中,某一签名者最多只能签一个块;同时也是投票生效的票数的最小值

(4)BLOCK_PERIOD: 两个相邻的块的Time字段的最小差值,也是出块周期

(5)EPOCH_LENGTH: 两个checkpoint之间的block的数量。达到这个数量后会生成checkpoint以及清除当前所有未生效的投票信息

(6)DIFF_INTURN: 出块状态(difficulty)之一,此状态代表“按道理已经轮到我出块”

(7)DIFF_NOTURN: 出块状态(difficulty)之一,此状态代表“按道理还没轮到我出块”

以上信息的进一步解释:

epoch and checkpoint:

在clique中,有一个值叫做"epoch"。当一个block的高度恰好是"epoch"值的整数倍时,这个block便不会包含任何投票信息,而是包含了当前所有的签名者列表。这个block被叫做checkpoint。可以看出,checkpoint类似于一个“里程碑”,可以用来表示“到目前为止,有效的签名者都记录在我这里了”;而epoch就是设立里程碑的距离。

"epoch"的存在,是为了避免没有尽头的投票窗口,也是为了周期性的清除除旧的投票提案。更进一步地,在checkpoint中存在的签名者列表,可以让节点间基于中间某个checkpoint就可以同步到签名者列表,而不需要整个链上的数据。

Snapshot:

Snapshot对象是clique中比较重要的一个对象,它的作用是统计并保存链的某段高度区间的投票信息和签名者列表。这个统计区间是从某个checkpoint开始(包括genesis block),到某个更高高度的block。在Snapshot对象中用到了两个重要的结构体:Vote和Tally,我们先对它们进行一下说明,再来详细说一下Snapshot结构体。

Vote struct:

Vote代表的是一次投票的详细信息,包括谁给谁投的票、投的加入票还是踢出票等等。它的结构体定义如下:

type Vote struct {Signer    common.Address // 此次投票是由谁投的Block     uint64         // 此次投票是在哪个高度的block上投的Address   common.Address // 此次投票是投给谁的Authorize bool           // 这是一个加入票(申请被投人成为签名者)还是踢出票(申请将被投人踢出签名者列表)
}

Tally struct:

Tally结构体是对所有被投人的投票结果统计。注意它与Vote结构体的区别:Vote是投票过程的记录(如A给B投了一个授权票),而Tally是对结果的统计(类似于选班长唱票时计票员在黑板上画的“正”字)。Tally的定义如下:

type Tally struct {Authorize bool // 这是加入票的统计还是踢出票的统计Votes     int  // 目前为止累计的票数
}

如果只看这里你可能会意外这里并没有“针对谁进行的统计”的信息,这是因为Tally在Snapshot结构体是是作为map的一部分的,参看下面对Snapshot结构体字段的说明。

inturn and noturn:

前面说过,clique作为PoA的实现,挖矿的人之间是合作关系,因此需要有规则规定某一时刻应该由谁出块。在clique中,inturn状态代表的是“按道理轮到我出块了”,而noturn正好相反。

在代码中,inturn的值为diffInTurn,noturn的值为diffNoTurn。Header.Difficulty字段用来保存相应的值,它的计算方式非常简单,具体可以查看Snapshot.inturn方法,这里不再多说。

Clique.Seal方法中,签名时会进行一定时间的等待。如果Header.Difficulty的值为diffNoTurn,则会比diffInTurn的块随机多等待一些时间,通过这种方式可以保证轮到出块的人可以优先出块。代码如下:

func (c *Clique) Seal(chain consensus.ChainReader, block *types.Block, results chan<- *types.Block, stop <-chan struct{}) error {......//计算正常的等待出块时间delay := time.Unix(header.Time.Int64(), 0).Sub(time.Now()) // nolint: gosimpleif header.Difficulty.Cmp(diffNoTurn) == 0 {//没有轮到我们出块,多等一会// It's not our turn explicitly to sign, delay it a bitwiggle := time.Duration(len(snap.Signers)/2+1) * wiggleTimedelay += time.Duration(rand.Int63n(int64(wiggle)))log.Trace("Out-of-turn signing requested", "wiggle", common.PrettyDuration(wiggle))}......
}

clique中最重要的两个数据结构:

一是共识引擎的结构:

    type Clique struct {config *params.CliqueConfig // 系统配置参数db ethdb.Database // 数据库: 用于存取检查点快照recents *lru.ARCCache //保存最近block的快照, 加速reorgssignatures *lru.ARCCache //保存最近block的签名, 加速挖矿proposals map[common.Address]bool //当前signer提出的proposals列表signer common.Address // signer地址signFn SignerFn // 签名函数lock sync.RWMutex // 读写锁}

二是snapshot的结构:

    type Snapshot struct {config *params.CliqueConfig // 系统配置参数sigcache *lru.ARCCache // 保存最近block的签名缓存,加速ecrecoverNumber uint64 // 创建快照时的block号Hash common.Hash // 创建快照时的block hashSigners map[common.Address]struct{} // 此刻的授权的signersRecents map[uint64]common.Address // 最近的一组signers, key=blockNumberVotes []*Vote // 按时间顺序排列的投票列表Tally map[common.Address]Tally // 当前的投票计数,以避免重新计算}

除了这两个结构, 对block头的部分字段进行了复用定义, ethereum的block头定义:

    type Header struct {ParentHash common.Hash UncleHash common.Hash Coinbase common.Address Root common.Hash TxHash common.Hash ReceiptHash common.Hash Bloom Bloom Difficulty *big.Int Number *big.Int GasLimit *big.Int GasUsed *big.Int Time *big.Int Extra []byte MixDigest common.Hash Nonce BlockNonce }

(1)创世块中的Extra字段包括:

  • 32字节的前缀(extraVanity)
  • 所有signer的地址
  • 65字节的后缀(extraSeal): 保存signer的签名

(2)其他block的Extra字段只包括extraVanity和extraSeal

(3)Time字段表示产生block的时间间隔是:blockPeriod(15s)

(4)Nonce字段表示进行一个投票: 添加( nonceAuthVote: 0xffffffffffffffff )或者移除( nonceDropVote: 0x0000000000000000 )一个signer

(5)Coinbase字段存放 被投票 的地址

  • 举个栗子: signerA的一个投票:加入signerB, 那么Coinbase存放B的地址

(6)Difficulty字段的值: 1-是 本block的签名者 (in turn), 2- 非本block的签名者 (out of turn)

4、clique如何解决问题

接下来我们看一下clique是如何解决上一节提到的两个问题的。

解决问题1:如何实现签名者的引进和踢出

clique中签名者的引进和踢出是通过已有签名者进行投票实现的,并且加入了更加详细的控制。

下面我们看一下clique中的投票规则

(1)投票信息保存在block中。一个block只有一个投票信息,且只能在自己生成的block上保存。

(2)针对某个被投人的票数超过SIGNER_LIMIT时,投票结果立即生效。

(3)投票生效后,立即清除所有被投人是当前生效人的投票信息。如果投的是踢出票,则被投人之前投出的、但还未生效的投票全部失效。

(4)踢出一个签名者以后,可能会导致原来不通过的投票理论上可以通过。clique不特意处理这种情况,等待下次统计时再判断。

(5)发起一个投票后,客户端不会被立即清除投票信息,而是在之后每次出块时都会选一个继续投票。因为区块链中的有效区块有重新调整的可能性,所以不能认为投票生效了之后就会一直生效。

(6)无效的投票:被投票人不在当前签名者列表中但投的是踢出票,或被投票人在当前签名列表中但投的是引进票。

(7)为了使编码简单,无效的投票不会受到惩罚(其实我认为有些功能实现也依赖于无效的投票)。

(8)在每个EPOCH_LENGTH内,一个签名者给同一个账号地址重复投票时,会先将上次的投票信息清除,然后再统计本次的投票信息(如果本次为无效的投票不会恢复已经清除的上次投票信息)

(9)每个checkpoint不进行投票,而只是包含当前签名者列表信息。对于其它区块,可以用来携带投票信息。

上面重复投票的处理处理方式会产生两个结果(假设投票人是A,被投票人是B):

(1)在当前EPOCH_LENGTH内,A给B只能投一票;

(2)在当前EPOCH_LENGTH内,如果给B的投票未生效(总票数未超过SIGNER_LIMIT)时A想把投给B的票撤消,那么A可以投一次跟之前相反的票。因为新的投票会导致旧的投票信息清除,而如果旧的投票是有效的则新的投票必定是无效的,因而也不会进入投票统计。

解决问题2:如何控制出块时机
前面我们说过,出块时机由两方面决定:一是出块时间;二是由谁出块。下面我们看看clique是如何解决这些问题的。

(1)出块时间:在clique中,出块时间是固定的,由BLOCK_PERIOD决定。

(2)由谁出块:clique中出块权的确定稍微复杂,具体规则为:

  • 签名者在签名者列表中且在SIGNER_LIMIT内没出过块
  • 如果签名者是DIFF_INTURN状态,则拥有较高出块权(等待出块时间到来,签名区块并立即广播出去)
  • 如果签名者是DIFF_NOTURN状态,则拥有较低出块权(等待出块时间到来,再延迟一下(延迟时间为rand(SIGNER_COUNT * 500ms))

可见出块权由两方面确定:一是最近是否出过块,如果出过则没有出块权;二是DIFF_INTURN / DIFF_NOTURN状态,IFF_INTURN拥有较高出块权。

理解这些规则以后,我们就可以自己实现一个PoA共识算法了。

5、工作流程

PoA的工作流程如下:

(1)在创世块中指定一组初始授权的signers, 所有地址 保存在创世块Extra字段中

(2)启动挖矿后, 该组signers开始对生成的block进行 签名并广播.

(3)签名结果 保存在区块头的Extra字段中

(4)Extra中更新当前高度已授权的 所有signers的地址 ,因为有新加入或踢出的signer

(5)每一高度都有一个signer处于IN-TURN状态, 其他signer处于OUT-OF-TURN状态, IN-TURN的signer签名的block会 立即广播 , OUT-OF-TURN的signer签名的block会 延时 一点随机时间后再广播, 保证IN-TURN的签名block有更高的优先级上链

(6)如果需要加入一个新的signer, signer通过API接口发起一个proposal, 该proposal通过复用区块头 Coinbase(新signer地址)和Nonce("0xffffffffffffffff") 字段广播给其他节点. 所有已授权的signers对该新的signer进行"加入"投票, 如果赞成票超过signers总数的50%, 表示同意加入

(7)如果需要踢出一个旧的signer, 所有已授权的signers对该旧的signer进行"踢出"投票, 如果赞成票超过signers总数的50%, 表示同意踢出

这张图里隐藏了Snapshot的功能。整个出块的功能主要由Prepare和Seal完成。在Prepare中准备一些与PoA相关的信息,在Seal中进行签名出块。需要特别注意的是,出块的时间是在Seal中控制的,而非miner中。

6、投票策略

因为blockchain可能会小范围重组(small reorgs), 常规的投票机制(cast-and-forget, 投票和忘记)可能不是最佳的,因为包含单个投票的block可能不会在最终的链上,会因为已有最新的block而被抛弃。

一个简单但有效的办法是对signers配置"提议(proposal)".例如 "add 0x...", "drop 0x...", 有多个并发的提议时, 签名代码"随机"选择一个提议注入到该签名者签名的block中,这样多个并发的提议和重组(reorgs)都可以保存在链上.

该列表可能在一定数量的block/epoch 之后过期,提案通过并不意味着它不会被重新调用,因此在提议通过时不应立即丢弃。

(1)加入和踢除新的signer的投票都是立即生效的,参与下一次投票计数

(2)加入和踢除都需要 超过当前signer总数的50% 的signer进行投票

(3)可以踢除自己(也需要超过50%投票)

(4)可以并行投票(A,B交叉对C,D进行投票), 只要最终投票数操作50%

(5)进入一个新的epoch, 所有之前的pending投票都作废, 重新开始统计投票

投票场景举例:

(1)ABCD, AB先分别踢除CD, C踢除D, 结果是剩下ABC

(2)ABCD, AB先分别踢除CD, C踢除D, B又投给C留下的票, 结果是剩下ABC

(3)ABCD, AB先分别踢除CD, C踢除D, 即使C投给自己留下的票, 结果是剩下AB

(4)ABCDE, ABC先分别加入F(成功,ABCDEF), BCDE踢除F(成功,ABCDE), DE加入F(失败,ABCDE), BCD踢除A(成功, BCDE), B加入F(此时BDE加入F,满足超过50%投票), 结果是剩下BCDEF。

7.代码分析

7.1 consenesus/clique/clique.go

1.常量和变量意义:

checkpointInterval = 1024 ,每隔1024块保存投票快照到数据库
inmemorySnapshots  = 128 ,保存在内存中的快照数量
inmemorySignatures = 4096 ,保存在内存中的最近区块的签名者数量
wiggleTime = 500 * time.Millisecond ,用于非顺序出块人出块延迟时间计算,在0 ~ (signerCount/2+1)*wiggleTime范围内随机取一个值作为延迟时间
epochLength = uint64(30000) ,每隔30000块清空所有投票
extraVanity = 32 ,extra-data保留32个字节的前缀
extraSeal   = 65 ,extra-data为区块signer保留65个字节的后缀
nonceAuthVote = hexutil.MustDecode("0xffffffffffffffff") ,投票加入一个签名者使用的nonce
nonceDropVote = hexutil.MustDecode("0x0000000000000000") ,投票踢出一个签名者使用的nonce
diffInTurn = big.NewInt(2) ,出块人是顺序出块人时的区块难度值
diffNoTurn = big.NewInt(1) ,出块人不是顺序出块人时的区块难度值

2.重要方法:

注意:有些参数,比如 coinbase,difficulty 等在POA 与 POW中的含义不同;

Prepare 给header.Coinbase赋值(投票目标地址)给header.Nonce赋值(投票类型为加入或提出签名者)给header.Difficulty赋值(是顺序出块人为2,否则为1)给header.Extra赋值(32字节前缀+所有签名者地址+65字节后缀用于区块签名)给header.MixDigest赋值为空,摘要,在pow中用于防篡改校验(VerifySeal)给header.Time赋值(等于parent.Time+Period或等于now>parent.Time+Period)
Finalize 给header.Root赋值给header.UncleHash赋值为空(poa没有叔区块)构建block返回
Seal 判断在最近出块记录中,则不允许出块(维持一个大小为signercount/2+1的signer队列Recents,用于判断最近是否出过块)计算应该delay的时间拷贝签名到header.Extra后65字节delay到出块时间后将sealed block放入worker.resultCh
VerifySeal验证header.Number不为0验证区块签名者在签名者列表中验证区块签名者最近没出过块验证header.Difficulty和轮次是否匹配
snapshot获取基于某个块的Snapshot优先从内存中获取如果内存中没有,且恰好number是checkpointInterval的整数倍,从数据库取如果恰好number是epochLength的整数倍,创建一个Snapshot并保存还是没有从上一块(number-1)取取到了Snapshot,如果是从number块前面的块取的,要snap.apply(headers)Snapshot保存到缓存中如果恰好number是checkpointInterval的整数倍且有执行apply,保存Snapshot到数据库
VerifyHeader验证header有效性
APIs获取共识引擎提供的RPC接口

7.2 consenesus/clique/snapshot.go

1.结构体:

type Snapshot struct {            // Snapshot是基于某个区块高度的投票认证状态config   *params.CliqueConfig // 配置参数sigcache *lru.ARCCache        // 缓存最近区块签名地址,用于快速得到签名地址Number  uint64                      // Snapshot创建时的区块高度Hash    common.Hash                 // Snapshot创建时的区块哈希Signers map[common.Address]struct{} // 已认证签名者集合Recents map[uint64]common.Address   // 最近已签名区块的签名者集合 //数量为 SIGNER_COUNT/2+1 ,可保证即使存在恶意signer,他最多只能攻击连续块 SIGNER_COUNT/2+1 中的1个Votes   []*Vote                     // 按时间排序的投票集合Tally   map[common.Address]Tally    // 当前投票记录避免重复计算
}type Tally struct {                   // 投票记录Authorize bool `json:"authorize"` // 投票是加入或者提出某个账户Votes     int  `json:"votes"`     // 想要通过的提议当前的投票数
}type Vote struct {Signer    common.Address        // 投这个票的已认证签名者Block     uint64                // 投这个票的区块高度(太旧的投票是过期投票)Address   common.Address        // 被投的账户Authorize bool                  // 是认证还是解除认证这个被投票的账户
}

2.重要方法:

apply验证headers有效性和连续性snap := s.copy()遍历headers:每隔epochLength块清空Votes和Tally删除Recents中最旧的一个singer使其能够再次签名获取header的签名者,判断如果不在签名者列表中或者在Recents列表中,return,否则将签名者放入Recents中遍历Votes,丢弃掉之前这个header的签名者投给同一个账户(header.Coinbase)的投票投票,即在Snapshot.Tally和Snapshot.Votes中添加记录根据投票记录Tally判断如果当前被投票地址header.Coinbase票数大于当前签名者列表长度/2:如果提议为加入新的签名者,签名者列表加入新签名者header.Coinbase;如果提议为踢出已认证签名者,将其从签名者列表删除,然后删除Recents中最旧的一个,丢弃所有该签名者投的票丢弃之前所有投给该签名者的票和记录更新snap.Number和snap.Hash到最新

7.3 consenesus/clique/api.go

1.结构体:

type API struct {  //API是一个面向用户的RPC接口对象,用于控制signer和poa投票机制chain  consensus.ChainReaderclique *Clique
}

2.clique共识引擎提供的RPC接口有:

func (api *API) GetSnapshot(number *rpc.BlockNumber) (*Snapshot, error)  ,基于区块高度为number的块得到状态快照(state snapshot)
func (api *API) GetSnapshotAtHash(hash common.Hash) (*Snapshot, error)  ,基于区块哈希为hash的块得到状态快照
func (api *API) GetSigners(number *rpc.BlockNumber) ([]common.Address, error)  ,基于区块高度为number的块得到签名者列表
func (api *API) GetSignersAtHash(hash common.Hash) ([]common.Address, error)  ,基于区块哈希为hash的块得到签名者列表
func (api *API) Proposals() map[common.Address]bool  ,获取当前所有提议
func (api *API) Propose(address common.Address, auth bool)  ,提议加入新的认证签名者或踢出现有认证签名者
func (api *API) Discard(address common.Address)  ,丢弃已有的一个提议

参考文档:

(1)https://www.jianshu.com/p/2be997c4705a

(2)https://www.jianshu.com/p/7a979813d368

第03篇 以太坊POA联盟链介绍相关推荐

  1. 以太坊搭建联盟链_转载:以太坊联盟链quorum搭建(一)

    原文链接:https://blog.csdn.net/zhj_fly/article/details/80560744 关于quorum的介绍可以看一些这几篇文章: 官网教程中创建了7个节点,由这7个 ...

  2. 以太坊搭建联盟链详细教程

    虫洞社区签约作者介绍 风逝 区块链 云旗天下 安装以太坊geth客户端 需要先安装go环境 安装教程 下载以太坊源码 github.com/ethereum/go-ethereum 进入 go-eth ...

  3. 以太坊搭建联盟链_区块链知识普及:什么是以太坊

    以太坊(Ethereum)是下一代密码学账本,可以支持众多的高级功能,包括用户发行货币,智能协议,去中心化的交易和设立去中心化自治组织(DAOs)或去中心化自治公司(DACs).以太坊并不是把每一单个 ...

  4. 以太坊搭建联盟链教程

    Quorum是一种基于以太坊的分布式分类账协议,具有交易/合同隐私和新的共识机制. Quorum是go-ethereum的一个分支,并根据go-ethereum发布进行更新. go-ethereum的 ...

  5. 以太坊《私有链和联盟链的机会与挑战》报告

    以太坊平台评估 私有链和联盟链的机会与挑战 作者:Vitalik Buterin  翻译:万向区块链实验室/ChinaLedger 联盟  排版/校对:区块链铅笔(ChainB.com) (注:本文属 ...

  6. 以太坊开启区块链2.0时代

    链客,专为开发者而生,有问必答! 此文章来自区块链技术社区,未经允许拒绝转载. 今天我们唠唠以太坊,这个开启区块链2.0时代的伟大项目. 前面我们说比特币实际上是一个大账本,从比特币白皮书里走出来的区 ...

  7. 以太坊POA共识机制Clique源码分析

    以太坊中除了基于运算能力的POW(Ethash)外,还有基于权利证明的POA共识机制,Clique是以太坊的POA共识算法的实现,这里主要对POA的Clique相关源码做一个解读分析. Clique的 ...

  8. Windows搭建以太坊的私有链环境

    Windows搭建以太坊的私有链环境 1.下载Geth.exe 运行文件,并安装 https://github.com/ethereum/go-ethereum/releases/ 下载后,只有一个G ...

  9. 比特币vs分布式账本vs以太坊vs区块链

    链客,专为开发者而生,有问必答! 此文章来自区块链技术社区,未经允许拒绝转载. 我们经常被告知,区块链 - 比特币背后的技术 - 不仅将重新布线银行业,还包括社会保障支付,医疗保健甚至数字投票. 到目 ...

最新文章

  1. 【多标签文本分类】Initializing neural networks for hierarchical multi-label text classification
  2. nginx reload内存碎片问题-(一)
  3. python多线程下载文件
  4. java List 排序 Collections.sort() 对 List 排序
  5. java comparator_【面试题】Java必考面试题全集(15)
  6. 单片机外围模块漫谈之二,如何提高ADC转换精度
  7. mysql 二进制日志变化_MySQL运维之二进制日志
  8. 教你如何在Android Studio中使用DDMS工具查看logcat——移动测试Android app(app的性能监控与测试)
  9. 架构设计的立方体扩展
  10. SpringBoot(13)--- 数据库操作(集成MyBatis)
  11. javascript java html_JS入门篇(二):在html中如何使用Javascript
  12. 【炼丹技巧】惊了,掌握了这个炼丹技巧的我开始突飞猛进
  13. 算法精解----快速排序2
  14. LSD slam with stereo cameras (使用双目相机的LSD算法)
  15. 压力测试工具Apache Bench:2:基于Alpine的Apache Bench镜像
  16. 能链发布数字藏品为用户定制专属数字礼物
  17. 【转】基于gamebryo引擎开发过程中组件的应用和取舍 By 宋晓宇
  18. 环签名原理与隐私保护
  19. 天马杯-NLP赛道(科技新闻分类与摘要)赛后总结
  20. ad19怎么手动布线_AD18/19自动布线之坑

热门文章

  1. dialer(dialer接口是什么意思)
  2. 华中之旅-- 初到武汉
  3. 苹果微信验证失败,安卓成功_苹果将​​在微软失败的地方成功吗?
  4. 卓海科技冲刺创业板:拟募资5.47亿 相宇阳控制52.9%股权
  5. csgo国内国服饰品皮肤开箱网站大全
  6. 从M1、Grace再到华为,缝合风为何会在芯片大厂中流行
  7. 【UML】用例图、活动图、类图、顺序图练习题
  8. 520送男友的纪念礼物,最适合跑步/运动的蓝牙耳机推荐
  9. 【用Python模拟实现人生重开模拟器】
  10. mysql path妙用