智能合约就像是一份合同一样,你书写的每个代码都是合同的内容。所以合约一经部署,合约内容就无法修改,好比你和别人签苹写了合同之后,是没法再修改的。这也是正是基于区块链不可篡改的特性。
在通常的编程语言中,一般会使用标准输出来打印“Hello World”,Solidity作为一门智能合约编程语言,是有别与一般的编程语言,他是基于以太坊,并且是用来编写“合约”的。所以这里不再使用“HelloWorld”作为示例,而是使用Solidity官网中的一个入门示例。
Solidity开发环境(3)--Mist
Remix是一个solidity的开发环境,他本身包含了EVM以及一些预置的帐户信息,所以可以进行测试开发。如果我们要把自己的合约部署到本地搭建的以太坊私链上要怎么做呢?一种是通过节点控制台命令将Remix生成的ABI和bytecode部署到私链上,一种是使用Mist钱包工具进行合约的部署(调用RPC接口进行部署)。
GO-HPB源码解读--work.go
在了解HPB持久化区块的时候,在levelDB的put方法中打印了一下调用栈,发现一共调用了5次,日志如下:
1 | INFO [01-27|22:54:54] HPB : Successfully sealed new block number -> =2 hash -> =8060c5…80af4c difficulty -> =1 |
GO-HPB源码解读--PRC服务启动
RPC服务是在节点启动的时候通过参数–rpcaddr 0.0.0.0 –rpcport 8541来指定的,启动代码是:
1 | hpbnode.SetNodeAPI() |
其中SetNodeAPI是把所有的API定义以[]rpc.API的数据类型保存hpbnode.RpcAPIs,然后再传给Start方法。API的结构如下,其中public为false的话是不会发布出去了。
GO-HPB源码解读--P2P启动
P2P启动是在节点启动的时候进行的,启动代码是hpbnode.Hpbpeermanager.Start(hpbnode.hpberbase)
1 | func (hpbnode *Node) Start(conf *config.HpbConfig) error { |
GO-HPB源码解读--交易入池(二)
上一节看到交易通过pool.promoteTx(addr, hash, tx)方法进行了传递,在该方法的最后一步通过pool.txFeed.Send(bc.TxPreEvent{tx}),将交易封装成bc.TxPreEvent发送了出去。而这个Feed实现了订阅发布模式,Send方法是发布消息,SubscribeTxPreEvent是订阅消息,订单的时候只需要把订单者的chan发进来就可以了。 (Feed通过反射包reflect中的selectcase、trySend和tryReceive方法进行的,操作包装在Feed文件中,golang的源码包中有all_test.go测试文件可以详细看下)
订阅这个消费的一共有三个地方,分中在api_backend.go、synctrl.go、worker.go中。这里主要看一下广播和evn执行。
- api_backend.go中只是订单并没处理
- synctrl.go中进行消费的广播
- worker.go中进行了evn执行
GO-HPB源码解读--交易入池(一)
在本地启动节点后,通过控制台输入命令hpb.sendTransaction({from:"db3cf5c98af974d3cccdc8662f1ab3c27c6e47ee",to:"742c8a59152a397ad09f7fb280c4fc378598a4ba",value:web3.toWei(10000,"hpb")})来完成转账交易。交易内容最初始会进入到SendTransaction。
- 打印了一下传进来的参数args
{From:[219 60 245 201 138 249 116 211 204 205 200 102 47 26 179 194 124 110 71 238] To:[116 44 138 89 21 42 57 122 208 159 127 178 128 196 252 55 133 152 164 186] Gas:GasPrice: Value:0x21e19e0c9bab2400000 Data:0x Nonce: } - 首先检查一下from地址是不是存在钱包中,如果不存在就返回错误
- 锁定交易的nonce,因为nonce是唯一的,防止交易的重播攻击
- 对交易需要的其他参数进行设置,Gas默认值为90000,GasPrice和Nonce经由计算获得,这个后续再分析
- 创建一个交易对象,将以上参数进行赋值
- 使用from的帐户钱包对交易进行签名,来证明这签交易确实是from发出的。
签名过程:
(1). 首先使用from的私钥对nonce、price、gaslimit、recipient、amount、payload进行secp256k1签名
(2). 然后签名结果分解成R、S、V,并赋给交易对象,注意这个时候交易对象中并没有from地址 - 最后提交交易
GO-HPB源码解读--投票机制
在挖矿流程的最近一节中,提到了voting.GetHpbNodeSnap的投票机制。投票的目的就是来计算出本节点是否能够签名区块,所以每次在组装区块的时候都会获取上一次的投票结果,如果出现异常或者投票周期到了,则重新计算投票结果。
投票结果是通过结构体进行保存的。
CandAddress 节点地址(当前的授权用户)
VoteNumbers 获得票数
VoteIndexs 获胜者的指标总和
VotePercent 占比
1 | type Tally struct { |
接下来看一下代码实现,其中HpbNodeCheckpointInterval==200
- 如果number==0,就先生成一个初始快照并返回
- 如果number<200,就从缓存/数据库中取出初始快照,如果取出失败,则生成一个初始快照并返回
- 其他情况就是number>=200,如果number正好是200的整数倍数的话,则生成快照,否则从缓存/数据库中获取上一次的快照,如果获取失败则重新生成上一次的快照。举个栗子,number=666,则获取number=600的快照,如果number=800,则生成新的快照。很明显间隔相当于从0开始算的,并不是最近的200个块。但是。。。往下看number是怎么使用的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64func GetHpbNodeSnap(db hpbdb.Database, recents *lru.ARCCache, signatures *lru.ARCCache, config *config.PrometheusConfig, chain consensus.ChainReader, number uint64, hash common.Hash, parents []*types.Header) (*snapshots.HpbNodeSnap, error) {
// 首次要创建
if number == 0 {
if snapg, err := GenGenesisSnap(db, recents, signatures, config, chain); err == nil {
return snapg, err
}
}
//前十轮不会进行投票,前10轮采用区块0时候的数据
//先获取缓存,如果缓存中没有则获取数据库,为了提升速度
//if(true){
if number < consensus.HpbNodeCheckpointInterval {
genesis := chain.GetHeaderByNumber(0)
hash := genesis.Hash()
// 从缓存中获取
if snapcd, err := GetDataFromCacheAndDb(db, recents, signatures, config, hash); err == nil {
log.Debug("HPB_VOTING: Loaded voting Hpb Node Snap form cache and db", "number", number, "hash", hash)
return snapcd, err
} else {
if snapg, err := GenGenesisSnap(db, recents, signatures, config, chain); err == nil {
log.Debug("HPB_VOTING: Loaded voting Hpb Node Snap form genesis snap", "number", number, "hash", hash)
return snapg, err
}
}
}
// 开始考虑10轮之后的情况,往前回溯3轮,以保证一致性。
// 开始计算最后一次的确认区块
latestCheckPointNumber := uint64(math.Floor(float64(number/consensus.HpbNodeCheckpointInterval))) * consensus.HpbNodeCheckpointInterval
//log.Error("Current latestCheckPointNumber in hpb voting:",strconv.FormatUint(latestCheckPointNumber, 10))
header := chain.GetHeaderByNumber(uint64(latestCheckPointNumber))
latestCheckPointHash := header.Hash()
if number%consensus.HpbNodeCheckpointInterval != 0 {
if snapcd, err := GetDataFromCacheAndDb(db, recents, signatures, config, latestCheckPointHash); err == nil {
//log.Info("##########################HPB_VOTING: Loaded voting Hpb Node Snap form cache and db", "number", number, "latestCheckPointNumber", latestCheckPointNumber)
return snapcd, err
} else {
if snapa, err := snapshots.CalculateHpbSnap(uint64(1), signatures, config, number, latestCheckPointNumber, latestCheckPointHash, chain); err == nil {
//log.Info("@@@@@@@@@@@@@@@@@@@@@@@@HPB_VOTING: CalculateHpbSnap", "number", number, "latestCheckPointNumber", latestCheckPointNumber)
if err := StoreDataToCacheAndDb(recents, db, snapa, latestCheckPointHash); err != nil {
return nil, err
}
return snapa, err
}
}
} else {
if snapa, err := snapshots.CalculateHpbSnap(uint64(1), signatures, config, number, latestCheckPointNumber, latestCheckPointHash, chain); err == nil {
//log.Info("@@@@@@@@@@@@@@@@@@@@@@@@HPB_VOTING: CalculateHpbSnap", "number", number, "latestCheckPointNumber", latestCheckPointNumber)
//新轮次计算完高性能节点立即更新节点类型
//prometheus.SetNetNodeType(snapa)
if err := StoreDataToCacheAndDb(recents, db, snapa, latestCheckPointHash); err != nil {
return nil, err
}
return snapa, err
} else {
return nil, err
}
}
return nil, nil
}
具体生成快照的算法是在CalculateHpbSnap中实现的,也就是区块链核的共识算法。先看一下参数含意:
字段|类型|含义|备注
-|-|-|-
index | |投票轮次,CalculateHpbSnap调用时是第一次,所以传1 |
*lru | |最近的签名,使用ARC算法实现 |
config | |共识配置,Prometheus的属性 |
number | |区块序号 |
latestCheckPointNum |最近应该生成快照对应的区块序号,也就是200的整数倍 | |
latestCheckPointHash |最近应该生成快照对应的区块哈希 | |
chain |区块链指针 | |
快照生成主要通过number参数选举出一定的区块生成节点,这块要主算法实现,为了方便理解,假设在生成区块时的number为210、650、1875,也就是调用GetHpbNodeSnap方法传入的number,表示为{210|650|1875},最后计算得到latestCheckPointNum是{200|600|1800},将这两个参数传入CalculateHpbSnap,注意此时index传入的是1
- 选出一定范围内的区块头集合,区间界线为from到latestCheckPointNum-100,from计算方法是latestCheckPointNum - index*consensus.HpbNodeCheckpointInterval,所以可以得到的范围是{0-100|400-500|1600-1700}
- 通过chain取出{0-100|400-500|1600-1700}范围内所有区块头,赋给headers变量,一共100个区块头(理想情况下)。如果在获取头的时候累计出了20次没取到,直接返回异常errors.New(“get hpb snap but missing header”)
- 对区块头集合headers进行连续性检查,也就是所有区块头的number必须是连续的,否则返回异常
- 初始化快照对象snap,并填装snap的Tally属性,注意snap的Tally是map[common.Address]Tally类型的,key是add,value是Tally,而value的类型Tally本身又是一个结构体。填装过程就是把header对应的所有address作为键put到map中,value是Tally结构体。
*Tally初始值是
*如果key重复出现,则需要把VoteNumbers和VoteIndexs对应的新旧值进行累加,重新计算VotePercent=VoteIndexs/VoteNumbers并赋值VoteNumbers: 1, VoteIndexs: header.VoteIndex, VotePercent: header.VoteIndex,
这样就得到了100一个header对应的address的Tally,当然结果可能会少于100个,因为有重复的。 - 将map再复制到临时变量tallytemp中,分别按照CandAddress和百分比VotePercent从小到大进行排序,排序结果保存到变更finaltally中
- 接下来通过当前高性能节点数量来解决是否需要再次进行获取一些headers来进行投票计算,也就是上边1-5步骤
- 系统默认的高性能节点个数是consensus.HpbNodenumber=31个,如果finaltally长度大等于31,则把排序后的后边的31个节点地址添加到snap.Signers中,即授权用户,表示可以进行挖矿。
- 如果finaltally小于31,而且已经进行了4次投票计算(1-5步骤),就把finaltally中所有的地址进行授权
- 如果finaltally小于31,而且进行了不到4次投票计算,则往前回溯再取出一些区块头(地址)进行投票。但是往前回溯有个条件,就是前边有足够的区块头可以获取,如果前边的区块头不够充足了,则只能把当前finaltally的地址全部授权了。是否可以往前回溯的判断条件是index<number/200,比如第一轮投票的时候number={210|650|1875},投了{0-100|400-500|1600-1700}区间的节点地址,最终结果不够31个高性能节点,那么进行第二轮投票。2<210/200为false,这种情况就不能往前回溯了;2<650/200为true,则可以回溯,然后递归调用CalculateHpbSnap方法,这时传入的index=2,number和latestCheckPointNum分别向前移200个单位,即number={-|450|1675},latestCheckPointNum={-|400|1600},递归后可得范围为 {-|200-300|1400-1500}。可以发现每次中间隔100个单位。
- 在进行了下一投票计算(递归调用)之后,得到新的节点地址集合,把其中已经存在于上一次finaltally中的地址删除掉,此时如果新的节点地址一个也没有,则结束投票,如果还有的话就对新的节点地址集合进行排序
- 排序完了之后,如果新旧集合的长度和小等于31,就把新旧集合合并一下,否则的话把新集合的数据补充到旧集合中,直到旧集合够31个节点了。
- 最后把zeroaddr地址删除掉。(不是很清楚,什么情况下会有存在zeroaddr地址)
1 | func CalculateHpbSnap(index uint64, signatures *lru.ARCCache, config *config.PrometheusConfig, number uint64, latestCheckPointNum uint64, latestCheckPointHash common.Hash, chain consensus.ChainReader) (*HpbNodeSnap, error) { |
在确定节点地址区间的逻辑比较简单,就是从前往后,隔100,标记100个为可取的,然后从后往前取倒数第一个100,去重后够31个就可以了,不够了再取倒数第2个100。代码算法写的复杂了,还用了递归。。。。。
VoteIndexs这个是从区块头中取出来的,没找到最初是在哪儿set进去的。
真累人………