汪晓明对区块链、以太坊的思考

记录创业、生活的所思所感,探讨去中心化思想,推动区块链的发展。

HPB49:P2P网络数据交互

P2P网络数据交互

1. 发送交易数据SendTransactions

事件触发交易广播txBroadcastLoop

本地发送了一个交易,或者是接收到别人发来的交易信息。 txpool会产生一条消息,消息被传递到txCh通道。然后被goroutine txBroadcastLoop()处理, 发送给其他不知道这个交易的peer。

ProtocolManager在Start的时候,订阅TxPreEvent并启动txBroadcastLoop协程监听事件。

当监听到事件后,调用BroadcastTx进行广播,广播按照委员及候选委员,接入节点,轻节点逐层广播。

发送交易之前,会把tx.Hash放到peer的knownTxs中:

新连接建立txsyncLoop

txsyncLoop负责每个新连接的初始事务同步。 当新的peer出现时,我们转发所有当前待处理的事务。

在txsyncLoop函数中定义了一个send函数来广播交易信息:

2. 发送区块哈希值SendNewBlockHashes

广播挖矿区块 NewMinedBlockEvent

ProtocolManager在Start的时候,订阅NewMinedBlockEvent并启动 minedBroadcastLoop()协程监听事件。

监听到事件后,开始广播区块信息。

先根据BroadcastBlock输入的参数propagate决定是否广播区块,当propagate为true时,广播区块信息。之后开始广播区块哈希。

广播时,先把hash放到knownBlocks里面,在广播区块和区块哈希

基于块通知的同步Fetcher

Fetcher Start函数中启动协程:

Fetcher模块的queue里面缓存了已经完成fetch的block,等待按照顺序插入到本地的区块链中。优先级别就是他们的区块号,这样区块数小的排在最前面。最后调用insert方法把给定的区块插入本地的区块链。

在insert函数中,有两处广播:一是如果区块头通过验证,那么马上对区块进行广播;二是如果插入成功, 那么广播区块,第二个参数为false,那么只会对区块的hash进行广播。

定时同步syncer

syncer中会定时的同BestPeer()来同步信息: 当有新的Peer增加的时候 会同步, 这个时候可能触发区块广播; 定时触发 10秒一次。

3. 发送区块内容SendNewBlock

参照SendNewBlockHashes的处理流程。

4. 发送区块头信息SendBlockHeaders

在通过握手后runPeer时,会运行protocol的run函数,接着调用startProtocols函数,进而进入NewProtocolManager的时候定义的Run,每一个SubProtocols都有一个Run。

这个run方法首先创建了一个peer对象,然后调用了handle方法来处理这个peer。注意,这里的peer区别于p2p中的peer,但是它包含p2p的peer。

在handle最后,循环调用handleMsg, 这个方法很长,主要是处理接收到各种消息之后的应对措施。

对于GetBlockHeadersMsg的消息处理,结果调用SendBlockHeaders返回给对端:

首先解码msg,解析出getBlockHeadersData结构体。

查找方式:

从Hash指定的开始朝创世区块移动,也就是反向移动。

从Hash指定的开始正向移动。

通过Number反向查找。

通过Number正向查找。

查找结果发给对端:

5. 发送区块体信息SendBlockBodies

没有用到。

6. RLP编码发送区块体信息SendBlockBodiesRLP

调用流程参考“4 发送区块头信息SendBlockHeaders”。

收到GetBlockBodiesMsg,解析msg信息,组织bodies并发送给对端。

7. 发送节点信息SendNodeData

调用流程参考“4 发送区块头信息SendBlockHeaders”。

GetNodeDataMsg对应的协议版本要大于等于eth63。

8. RLP编码发送节点信息SendReceiptsRLP

调用流程参考“4 发送区块头信息SendBlockHeaders”。

9. 请求一个区块头RequestOneHeader

调用流程参考“4 发送区块头信息SendBlockHeaders”。

10. 通过Hash请求区块头RequestHeadersByHash

首先,在协议初始化的时候,调用protocolManager.Start

之后启动syncer(), syncer中会定时的同BestPeer()来同步信息: 当有新的Peer增加的时候 会同步; 定时触发 10秒一次同步。

)

pm.synchronise会调用中 Downloader中的同步函数。 Synchronise试图和一个peer来同步,如果同步过程中遇到一些错误,那么会删除掉Peer。然后会被重试。

最后,在syncWithPeer中会启动几个fetcher 分别负责header,bodies,receipts处理。spawnSync给每个fetcher启动一个goroutine, 然后阻塞的等待fetcher出错。

在fetchHeight中,会发出RequestHeadersByHash请求。

fetchHeaders方法用来获取header。 然后根据获取的header去获取body和receipt等信息。fetchHeaders不断的重复这样的操作,发送header请求,等待所有的返回,直到完成所有的header请求。

11. 通过Number请求区块头RequestHeadersByNumber

调用流程参考“10 通过Hash请求区块头RequestHeadersByHash”。

12. 请求区块体RequestBodies

调用流程参考“10 通过Hash请求区块头RequestHeadersByHash”。

13. 请求收据RequestReceipts

调用流程参考“10 通过Hash请求区块头RequestHeadersByHash”。

14. 请求节点信息RequestNodeData

在创建Downloader的时候,会同时启动协程 startFetcher,进而启动runStateSync。

15. 握手Handshake

head是当前的区块头,genesis是创世区块的信息,只有创世区块相同才能握手成功。如果接收到任何一个错误(发送,接收),或者是超时,那么就断开连接,握手失败。

readStatus,检查对端返回的各种情况。

感谢HPB团队整理。

HPB48:P2P网络数据处理流程

P2P网络数据处理流程

监听(ListenLoop)+拨号(Dial) –> 建立连接(SetupConn) –> Enc 握手(doEncHandshake) –> 协议握手(doProtoHandshake) –> 添加Peer Addpeer –> Run Peer

1. Enc握手 doEncHandshake

1
2
3
4
5
监听时接收到Enc握手:receiverEncHandshake

拨号时发起初始End握手:initiatorEncHandshake

链接的发起者被称为initiator(主动拨号),链接的被动接受者被成为receiver(被动监听)。 这两种模式下处理的流程是不同的,完成握手后, 生成了一个sec可以理解为拿到了对称加密的密钥。 然后创建了一个newRLPXFrameRW帧读写器,完成加密信道的创建过程。

initiatorEncHandshake 和receiverEncHandshake有些像,但逻辑处理是相反的过程。

makeAuthMsg

makeAuthMsg这个方法创建了handshake message。 首先对端的公钥可以通过对端的ID来获取。对端的公钥对于发起者来说是知道的;对于接收者来说是不知道的。

  • 根据对端的ID计算出对端公钥remotePub
  • 生成一个随机的初始值initNonce
  • 生成一个随机的私钥
  • 使用自己的私钥和对方的公钥生成的一个共享秘密
  • 用共享秘密来加密这个initNonce
  • 这里把发起者的公钥告知对方

这一步,主要是构建authMsgV4结构体。

sealEIP8

sealEIP8对msg进行rlp的编码,填充一下数据,然后使用对方的公钥把数据进行加密。

readHandshakeMsg

readHandshakeMsg有两个地方调用: 一个是在initiatorEncHandshake,另外一个就是在receiverEncHandshake。 这个方法比较简单, 首先用一种格式尝试解码,如果不行就换另外一种。基本上就是使用自己的私钥进行解码然后调用rlp解码成结构体。 结构体的描述就是authRespV4,里面最重要的就是对端的随机公钥。 双方通过自己的私钥和对端的随机公钥可以得到一样的共享秘密。 而这个共享秘密是第三方拿不到的。

secrets

secrets函数是在handshake完成之后调用。它通过自己的随机私钥和对端的公钥来生成一个共享秘密,这个共享秘密是瞬时的(只在当前这个链接中存在)。

这个函数计算出IngressMAC和EgressMAC用于rlpxFrameRW中ReadMsg,WriteMsg数据的接收发送。

数据帧结构

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
64
65
66
67
68
69
70
71
72
73
74
75
normal = not chunked

  chunked-0 = First frame of a multi-frame packet

  chunked-n = Subsequent frames for multi-frame packet

  || is concatenate

  ^ is xor

 

Single-frame packet:

header || header-mac || frame || frame-mac

 

Multi-frame packet:

header || header-mac || frame-0 ||

[ header || header-mac || frame-n || ... || ]

header || header-mac || frame-last || frame-mac

 

header: frame-size || header-data || padding

frame-size: 3-byte integer size of frame, big endian encoded (excludes padding)

header-data:

    normal: rlp.list(protocol-type[, context-id])

    chunked-0: rlp.list(protocol-type, context-id, total-packet-size)

    chunked-n: rlp.list(protocol-type, context-id)

    values:

        protocol-type: < 2**16

        context-id: < 2**16 (optional for normal frames)

        total-packet-size: < 2**32

padding: zero-fill to 16-byte boundary

 

header-mac: right128 of egress-mac.update(aes(mac-secret,egress-mac) ^ header-ciphertext).digest

 

frame:

    normal: rlp(packet-type) [|| rlp(packet-data)] || padding

    chunked-0: rlp(packet-type) || rlp(packet-data...)

    chunked-n: rlp(...packet-data) || padding

padding: zero-fill to 16-byte boundary (only necessary for last frame)

 

frame-mac: right128 of egress-mac.update(aes(mac-secret,egress-mac) ^ right128(egress-mac.update(frame-ciphertext).digest))

 

egress-mac: h256, continuously updated with egress-bytes*

ingress-mac: h256, continuously updated with ingress-bytes*

2. 协议握手doProtoHandshake

这个方法比较简单,加密信道已经创建完毕。 我们看到这里只是约定了是否使用Snappy加密然后就退出了。

在这个函数,发送给对方 handshakeMsg = 0x00,在readProtocolHandshake中读取接收对方发过来的handshakeMsg。

3. RLPX 数据分帧

在完成Encode握手之后,调用newRLPXFrameRW方法创建rlpxFrameRW对象,这的对象提供ReadMsg和WriteMsg方法

ReadMsg

)

1读取帧头header

2 验证帧头MAC

3 获取帧体Frame大小

4 读取帧体数据

5 验证帧体MAC信息

6 解密帧体内容(NewCTR à XORKeyStream)

7 解码帧体(RLP Decode)

8 解析帧体结构(msg.Size & msg.Payload)

9 snappy解码

WriteMsg

1 RLP编码msg.Code

2 如果snappy,就对读取payload并进行snappy编码

3 写帧头header (32字节)

4 写帧头MAC

5 写帧体信息(ptype+payload+padding)

6 写帧体MAC

4. runPeer

newPeerHook,建立peer的钩子函数

广播PeerEventTypeAdd事件

运行protocol

广播PeerEventTypeDrop事件

删除peer

run protocol

1 启动协程readLoop,读取消息并根据msg.Code处理消息:

pingMsg->pongMsg

discMsg->RLP解码msg.Payload返回reason

其他协议消息处理,根据msg.Code的取值范围,把msg分给注册的协议进行处理。

2 启动协程pingLoop

根据pingInterval(15秒)定时发送pingMsg消息

3 启动协议

startProtocols主要功能是启动协程运行注册协议的run函数proto.Run(p, rw),这个rw参数类型是protoRW,它实现的ReadMsg和WriteMsg增加msg.Code取值范围的处理。不同的protocol有不同的code取值范围,根据offset和Length确定。

感谢HPB团队整理。

HPB47:ETH-Pow算法分析

1. Ethash 算法

1.1 Ethash

Ethash是以太坊1.0中使用的PoW(工作量证明)算法,它是Hashimoto算法结合Dagger之后产生的一个变种。它的特点是计算的效率基本与CPU无关,却和内存大小和内存带宽正相关。因此通过共享内存的方式大规模部署的矿机芯片并不能在挖矿效率上有线性或者超线性的增长。

该算法的一般流程如下:

  • 首先根据块信息计算一个种子(seed, c++代码中为seedhash)
  • 使用这个种子,计算出一个16MB的cache数据。轻客户端需要存储这份cache.
  • 通过cache,计算出一个1GB(初始大小)的数据集(DAG),DAG可以理解为是一个完整的搜索空间,全客户端和矿工需要存储完整的DAG,挖矿过程中需要从DAG中重复的随机抽取数据拿去和其他数据计算mixhash,DAG中每个元素的生成只依赖于cache中的少量数据。每到一个新的纪元DAG会完全不一样,并且它的大小也随时间线性增长。
  • 由于仅根据cache就可以使用少量内存快速的计算出DAG中指定位置的数据,所以轻客户端只需要存储cache就可以高效的进行校验。

1.2 内存难解

由于比特币将hash算法作为pow工作量证明的重要手段,后续的各种采用pow的数字货币也延续了这个设计,以SHA256、MD5(MD5后来被证明不具备强碰撞性数字货币一般不用)为代表算法。在设计之初都是算力敏感型,意味着计算资源是瓶颈,主频越高的 CPU 进行 Hash 的速度也越快。这个设计直接导致后来的矿机出现,采用ASIC芯片的矿机更是将这种运算能力成倍提升,更多矿场的出现使得当时的比特币面临算力中心化的威胁。为了限制计算能力的依赖,人们开始寻求新的算法,既然要限制CPU的能力,目光自然投向存储依赖,也就是内存依赖。

​ Hashimoto算法采用IO饱和的策略来对抗ASIC,使内存读取成为采矿过程中的限制因素。

​ Dagger算法使用DAG(directed acyclic graphs 有向无环图)来同时实现内存难解和内存易验证两个特点。 主要原理是,计算每个nonce需要DAG中的一小部分,采矿过程需要存储完整的DAG,禁止每次计算DAG的相应子集,而验证过程是允许的。

1.3 参数定义

WORD_BYTES 4 Word的字节数
DATASET_BYTES_INIT 2**30 1GB Dataset的初始大小 |
DATASET_BYTES_GROWTH 2**23 8MB 每个纪元dataset的增长量 |
CACHE_BYTES_INIT 2**24 16MB Cache的初始大小 |
CHCHE_BYTES_GROWTH 2**17 128KB 每个纪元cache的增长量 |
CACHE_MULTIPLIER 1024 Size of the DAG relative to the cache |
EPOCH_LENGTH 30000 每个epoch的块数 |
MIX_BYTES 128 Mix的宽度 |
HASH_BYTES 64 Hash的长度 |
DATASET_PARENTS 256 每个数据集元素的parents数量 |
CACHE_ROUNDS 3 计算cache时的轮数 |
ACCESSES 64 Hashimoto循环的次数 |

2 DAG

DAG是ethash算法中需要频繁访问的数据集,这个为每个epoch生成的。DAG要花很长时间生成,如果客户端至少按照需要生成它,那么在找到新epoch第一个区块之前,每个epoch过渡都要等待很长时间。然而,DAG的生成只取决于区块数量,所以可以预先计算出DAG来避免在每个epoch过渡过长的等待时间。

DAG的生成流程如下:

2.1 Dag_size 和Cache_size

每个epoch的dagsize和cachesize都不同,上面已经定义了创世时的初始值,以太坊还提供了一个表来存储接下来2048个纪元(大约20年)的各个值。详见官网或源码cpp-ethereum/libethash/data_sizes.h.

获取datasize 和cachesize的方法如下:

2.2 Seedhash

算法中需要一个seedhash,由下面程序生成,从程序可见每个epoch的seed是不变的。

2.3 Cache

使用seedhash计算cache。

2.4 DAG

最后使用cache计算DAG,light参数中保存的是cache数据.

2.5 DAG文件

DAG每次生成都需要很长时间,因此生成时候需要存在文件中,再使用mmap映射到内存中。DAG文件路径一般如下

Mac/Linux : $HOME/.ethash/full-R

Windows: $HOME/Appdata/Local/Ethash/full-R

是ethash算法的版本号,在libethash/ethash.h 中REVISION定义。

是上面计算出来的seedhash

路径下可能会有多个DAG文件,这取决于用户或者客户端是否删除过时的DAG文件。

格式:

DAG文件以8字节的幻数开头,值为0xfee1deadbaddcafe, 以小端格式写入。接下来是小端格式写入的dataset数据。

3 Ethash实现

3.1 Ethash

图1 算法流程图

参数说明:

Header_hash: 是当前块头部数据的hash值,在矿机调用get_ethwork时从任务参数中获取。

Nonce: 是每次计算ethash使用不同的数,不能重复。可以取时间戳或随机数作为起始值,然后递增。

对于矿工来说,如果result的值小于或等于target,那么就完成了挖矿过程,将当前的nonce和mix_hash作为工作量证明提交工作;如果result的值大于target,那么就需要改变nonce的值,再次调用ethash算法.

Ethash算法程序如下:

从图中看,每次ethash从DAG随机取64128=8192Bytes, 以GTX1070显卡为例,带宽为256GB/s, 那么每秒能承受256102410241024/8192=33554432次ethash运算,即33MH/s的算力。可见,该算法对内存带宽的要求很高。

3.2 快速验证

当验证一个工作提交是否有效时,速度很快。

下面是快速验证程序:

感谢HPB团队整理。

HPB46:Solidity编译器和简单调试

Solidity编译器和简单调试

作者:HPB团队整理

1 安装Solidity编译器

1.1 通过docker安装Solc

搜索docker的Solc镜像

docker search —no-trunc ethereum/solc

通过docker安装Solc

docker pull docker.io/ethereum/solc:stable

1.2 运行Solc容器

运行如下命令

docker run —rm -it —privileged=true —net=host -v /home/hpbroot/ethereum_go/contract:/contract —name solc ethereum/solc:stable –version

查看是否成功

2 新建spring boot工程

2.1 通过Eclipse新建工程

首先,新建ContractCompile工程

在springboot配置文件application.properties中添加如下

1
2
3
4
5
6
7
web3j.contract.solcCmd=docker run --rm -it --privileged=true --net=host -v /home/hpbroot/ethereum_go/contract:/contract --name solc ethereum/solc:stable
在pom文件添加依赖
<dependency>
  <groupId>org.ethereum</groupId>
  <artifactId>solcJ-all</artifactId>
  <version>0.4.10</version>
</dependency>

新建ContractConfig类

1
2
3
4
5
6
7
8
9
10
11
@Component
@ConfigurationProperties(prefix = "web3j.contract")
public class ContractConfig {
  private String solcCmd;
  public String getSolcCmd() {
      return solcCmd;
  }
  public void setSolcCmd(String solcCmd) {
      this.solcCmd = solcCmd;
  }
}

2.2 调用智能合约编译器的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
String soliditySrcCode =MapUtils.getString(preParam, "soliditySrcCode");
if(StringUtils.isBlank(soliditySrcCode)) {
  param.put(ContractConstant.RETURN_CODE, ContractConstant.ERROR_CODE);
  param.put(ContractConstant.RETURN_MSG, ContractConstant.NOSRCCODE);
  return param;
}
SolidityCompiler solidityCompiler = SolidityCompiler.getInstance(getLog(),contractConfig.getSolcCmd());
byte[] source = soliditySrcCode.getBytes(StandardCharsets.UTF_8);
CompilerResult compilerResult = solidityCompiler.compileSrc(source,
SolidityCompiler.Options.ABI, SolidityCompiler.Options.BIN);
param.put(ContractConstant.RETURN_CODE, ContractConstant.SUCCESS_CODE);
param.put(ContractConstant.RETURN_MSG, ContractConstant.SUCCESS_MSG);
if(compilerResult.isFailed()) {
  param.put(ContractConstant.RETURN_CODE, ContractConstant.ERROR_CODE);
  param.put(ContractConstant.RETURN_MSG, compilerResult.getErrors());
}

3 调用编译智能合约源文件的代码

3.1 编写智能合约源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
contract SampleRecipientSuccess {
  address public from;
  uint256 public value;
  address public tokenContract;
  bytes public extraData;
  event ReceivedApproval(uint256 _value);
  function receiveApproval(address _from, uint256 _value, address _tokenContract, bytes _extraData) {
    from = _from;
    value = _value;
    tokenContract = _tokenContract;
    extraData = _extraData;
    ReceivedApproval(_value);
  }
}

3.2 通过HTTP调用智能合约的J2EE组件

1
2
3
4
5
6
7
8
9
10
11
12
13
String contractPath="/SampleRecipientSuccess.sol";
      String contractString = FileUtils.readFileToString(new File(contractPath),StandardCharsets.UTF_8);
      HashMap<String, Object> hashMap = new HashMap<String,Object>();
      hashMap.put("soliditySrcCode", contractString);
      String url = "http://192.168.3.43:18080/ContractCompile/compileContractCmd";
      ResponseEntity<Map> postForEntity = getRestTemplate().postForEntity(url, hashMap, Map.class);
      Map body = postForEntity.getBody();
      String returnCode = MapUtils.getString(body, ContractConstant.RETURN_CODE);
      if(ContractConstant.SUCCESS_CODE.equals(returnCode)) {
          String result = MapUtils.getString(body, "result");
          Map<String, Object> parseResult = parseResult(result);
          System.out.println(AppObjectUtil.toJson(parseResult));
      }

3.3 智能合约编译器组件返回的编译数据

1
{"abis":[{"constant":true,"inputs":[],"name":"value","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"tokenContract","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"extraData","outputs":[{"name":"","type":"bytes"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_value","type":"uint256"},{"name":"_tokenContract","type":"address"},{"name":"_extraData","type":"bytes"}],"name":"receiveApproval","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"from","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"anonymous":false,"inputs":[{"indexed":false,"name":"_value","type":"uint256"}],"name":"ReceivedApproval","type":"event"}],"bin":"6060604052341561000c57fe5b5b6103d38061001c6000396000f300606060405263ffffffff60e060020a6000350416633fa4f245811461004d57806355a373d61461006f578063609d33341461009b5780638f4ffcb11461012b578063d5ce338914610199575bfe5b341561005557fe5b61005d6101c5565b60408051918252519081900360200190f35b341561007757fe5b61007f6101cb565b60408051600160a060020a039092168252519081900360200190f35b34156100a357fe5b6100ab6101da565b6040805160208082528351818301528351919283929083019185019080838382156100f1575b8051825260208311156100f157601f1990920191602091820191016100d1565b505050905090810190601f16801561011d5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b341561013357fe5b604080516020600460643581810135601f8101849004840285018401909552848452610197948235600160a060020a039081169560248035966044359093169594608494929391019190819084018382808284375094965061026895505050505050565b005b34156101a157fe5b61007f6102f8565b60408051600160a060020a039092168252519081900360200190f35b60015481565b600254600160a060020a031681565b6003805460408051602060026001851615610100026000190190941693909304601f810184900484028201840190925281815292918301828280156102605780601f1061023557610100808354040283529160200191610260565b820191906000526020600020905b81548152906001019060200180831161024357829003601f168201915b505050505081565b60008054600160a060020a0380871673ffffffffffffffffffffffffffffffffffffffff19928316179092556001859055600280549285169290911691909117905580516102bd906003906020840190610307565b506040805184815290517f2db24179b782aab7c5ab64add7f84d4f6c845d0779695371f29be1f658d043cd9181900360200190a15b50505050565b600054600160a060020a031681565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061034857805160ff1916838001178555610375565b82800160010185558215610375579182015b8281111561037557825182559160200191906001019061035a565b5b50610382929150610386565b5090565b6103a491905b80821115610382576000815560010161038c565b5090565b905600a165627a7a723058209522849948e8cc25a7d6717d5c10836c97c36425936be5edf399206b3e5d7fa30029"}

总结

通过J2EE组件的接口调用,可以为大多数基于java的区块链应用提供了便利,可以利用J2EE成熟稳定的框架无缝集成到项目中,也是为了以后安卓开发和联盟链提供在线编译智能合约功能,如果是私有的局域网络的企业级联盟链,可以发布该智能合约的J2EE组件到该局域网的机器上去,可以实现联盟中的智能合约统一的编译器,便于快速升级编译器。

感谢HPB团队整理。

HPB45:Solidity概述及基本代码展示

Solidity概述及基本代码展示

作者:HPB团队整理

Solidity是实施智能合约的契约导向的高级语言。它受到C ++,Python和JavaScript的影响,旨在针对以太坊虚拟机(EVM)。

Solidity是静态类型的,支持继承,库和复杂的用户定义类型等功能。

您将会看到,可以创建投票,众筹,盲目拍卖,多重签名钱包等等的合约。

1 Solidity智能合约例子

我们从一个基础的solidity例子开始。开始的时候,你可能看不懂每一行具体的意思,但是没关系,我们会在后续的讲解中介绍每一个细节。

1
2
3
4
5
6
7
8
9
10
11
pragma solidity ^0.4.0;
contract SimpleStorage {
    uint storedData;
    function set(uint x) {
        storedData = x;
    }
    function get() constant returns (uint) {
        return storedData;
    }
}

第一行告诉该合约用的是0.4.0版本的solidity编写,并且这些代码具有向上兼容性。保证不会在不同solidity编译版本下编译会出现不同的行为。

  从Solidity角度来看,合约就是存在于以太坊区块链中的一个特定地址中的代码和数据集合。uint storedData 声明了一个类型为 uint(256位的无符号整型)的变量,变量名称为 storedData。你可以把它想象为数据库中的一个字段,该字段是可以被数据库中的方法进行查询,修改。在以太坊中,这个字段是属于一个合约字段。在这个例子中,该变量可以通过提供的get,set方法进行获取或是修改。在Solidity中,访问一个变量是不需要通过this来引用的。

这个合约很简单,只是允许以太坊上的任何人来存储一个数据到某个节点,同时把这个操作发布到以太坊中,当然,以太坊上的其他节点同样可以通过调用set方法来修改你已经存储好的值。虽然有被修改,但是对该值操作的任何历史记录都是保存在以太坊中的。不用担心你的存储记录或是修改记录会丢失。后面我们会将到如何对合约进行限制,只允许你一个人修改这个数据。

2 Solidity子货币例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pragma solidity ^0.4.0;
contract Coin {
    //public关键字可以让外部访问该变量
    address public minter;
    mapping (address => uint) public balances;
    //事件可以让轻客户端快速的响应变化
    event Sent(address from, address to, uint amount);
    // 构造方法
    function Coin() {
        minter = msg.sender;
    }
    function mint(address receiver, uint amount) {
        if (msg.sender != minter) return;
        balances[receiver] += amount;
    }
    function send(address receiver, uint amount) {
        if (balances[msg.sender] < amount) return;
        balances[msg.sender] -= amount;
        balances[receiver] += amount;
        Sent(msg.sender, receiver, amount);
    }
}

下面的例子将实现一个简单的加密货币例子。无中生币不在是梦想,当然只有合约的创建人才有这个特权。此外,任何人只要有一个以太坊密钥对就可以进行货币交易,根本不需要注册用户名和密码。这个合约引入了一些新的概念,让我们一个个都过一遍。

  address public minter;

  声明了一个public,类型为address的状态变量。Address类型是一个160位的值,不允许任何的算术操作。它适合于存储合约地址或是其他人的密钥对。Public关键字会自动产生用于外部访问该变量值的方法。如果不声明public,其他的合约是无法访问该变量的。自动产生的方法类似于:

  function minter() returns (address) { return minter; }

  当然如果你增加了一个和上面完全一样的方法是没有任何作用的,我们需要变量和产生的方法名完全一致。这块其实编译器会帮助我们完成,不需要我们自己动手编写,我们只要知道这个概念就可以。

  mapping (address => uint) public balances;

  还是创建了一个公有状态变量,这是一个比address更复杂的数据类型,类似java里的Map<address,uint>,它描述了一个地址和一个uinit数据类型的map关系。Mappings的关系可以看成是一个hash表,所有可能的key都对应了一个0的值。当然在map里不存在只有key值或是只有value值的情况。所以我们需要记住添加了一个什么样的map关系或是像这个例子一样,如何使用它。因为这是个public变量,所以系统会自动为它生成一个get方法,类似于:

  function balances(address _account) returns (uint) {

     return balances[_account];

  }

  通过上面的方法我们可以很容易的查询一个账号的余额。

  event Sent(address from, address to, uint amount);

  这一行创建了一个名为event 的事件。该事件会在该示例的最后一行被触发。用户或是server应用可以花很低的代价(后面会讲代价是什么)来监听事件的触发。一旦这个事件被触发了,监听者接收到三个参数:from, to,amount.也是说从哪个账号,到哪个账号,金额是多少。通过这三个参数可以很容易追踪到具体的交易。为了监听这个事件,我们需要使用如下代码:

1
2
3
4
5
6
7
8
9
10
Coin.Sent().watch({}, '', function(error, result) {
    if (!error) {
        console.log("Coin transfer: " + result.args.amount +
            " coins were sent from " + result.args.from +
            " to " + result.args.to + ".");
        console.log("Balances now:\n" +
            "Sender: " + Coin.balances.call(result.args.from) +
            "Receiver: " + Coin.balances.call(result.args.to));
    }
})

注意 :用户是如何调用系统自动生成的balances方法。

Coin方法是构造方法,是在合约产生的时候系统会调用,而且之后不允许被调用。Msg(以及tx和block)是一个全局变量,保存了可以被区块链访问的一些属性。它持久化了创建合约的节点的地址。 Msg.sender是值该方法调用者的地址。

最后,真正完成合约功能的,并且被其他用户调用的是mint和send方法。如果mint是被不是创建该合约的账号调用,不会起任何作用。但是,send可以被任何账号(必须有以太币的账号)调用并发送以太币给另外一个账号。注意,如果你用合约发送以太币到另外一个账号,通过区块链浏览器查看是查看不到任何变化的,因为发送以太币的过程和金额的变化都被存储在了特殊的以太币合约里。而不是体现在账号上。通过使用事件,可以很容易的创建一个区块链浏览器,用来查看交易和账号余额。

感谢HPB团队整理。

HPB44:以太坊虚拟机的基本介绍

以太坊虚拟机的基本介绍

作者:HPB团队整理

此文简要的介绍了以太坊虚拟机的基本要素,在以后的文章我们会向大家展示如何安装应用调试等基本技术。

1.1 概述

 以太坊虚拟机(EVM)是以太网上智能合约的运行环境。这不仅仅是个沙盒,更确实的是一个完全独立的环境,也就是说代码运行在EVM里是没有网络,文件系统或是其他进程的。智能合约甚至被限制访问其他的智能合约

1.2 账号

​ 在以太坊中有两种账号共享地址空间:外部账号和合约账号。外部账号是由公钥和私钥控制的(如人),合约账号是由账号存储的代码所控制。

外部账号的地址是由公钥决定的,而合约地址是在智能合约被创建的时候决定的(这个地址由创建者的地址和发送方发送过来的交易数字衍生而来,这个数字通常被叫做“nonce”)

不管是否账号存有代码(合约账号存储了代码,而外部账号没有),对于EVM来说这两种账号是相等的。

每一个账号都有持久化存储一个key和value长度都为256位字的键值对,被称为“storage”

而且,在以太坊中,每个账号都有一个余额(确切的是用“Wei”来作为基本单位),该余额可以被发送方发送过来带有以太币的交易所更改。

1.3 交易

   交易是一个账号和另外一个账号之间的信息交换。它包含了二进制数据(消费数据)和以太数据。如果目标账号包含了代码,这个代码一旦被执行,那么它的消费数据就会作为一个输入数据。如果目标账号是一个0账号(地址为0的账号),交易会生成一个新的合约。这个合约的地址不为0,但是是来源于发送方,之后这个账号的交易数据会被发送。这个合约消费会被编译为EVM的二进制代码,并执行。这次的执行会被作为这个合约的代码持久化。这就是说:为了创建一个合约,你不需要发送真正的代码到这个合约上,事实上是代码的返回作为合约代码。

1.4 Gas

   以太坊上的每笔进行一笔交易都会被收取一定数量的Gas.这是为了限制交易的数量,同时对每一笔交易的进行支付额外费用。当EVM执行一个交易,交易发起方就会根据定义的规则消耗对应的Gas。

交易的创造者定义了的Gas 价格。所以交易发起方每次需要支付 gas_price * gas 。如果有gas在执行后有剩余,会以同样的方法返回给交易发起方。如果gas在任何时候消耗完,out-of-gas 异常会被抛出,那当前的这边交易所执行的后的状态全部会被回滚到初始状态。

1.5 存储,主存和栈

   每个账号都有持久化的内存空间叫做存储. 存储是一个key和value长度都为256位的key-value键值对。从一个合约里列举存储是不大可能的。读取存储里的内容是需要一定的代价的,修改storage里的内容代价则会更大。一个合约只能读取或是修改自己的存储内容。

第二内存区域叫做主存。系统会为每个消息的调用分配一个新的,被清空的主存空间。主存是线性并且以字节粒度寻址。读的粒度为32字节(256位),写可以是1个字节(8位)或是32个字节(256字节)。当访问一个字(256位)内存时,主存会按照字的大小来扩展。主存扩展时候,消耗Gas也必须要支付,主存的开销会随着其增长而增大(指数增长)。

EVM不是一个基于寄存器,而是基于栈的。所以所有的计算都是在栈中执行。最大的size为1024个元素,每个元素为256位的字。栈的访问限于顶端,按照如下方式:允许拷贝最上面的16个元素中的一个到栈顶或是栈顶和它下面的16个元素中的一个进行交换。所有其他操作会从栈中取出两个(有可能是1个,多个,取决于操作)元素,把操作结果在放回栈中。当然也有可能把栈中元素放入到存储或是主存中,但是不可能在没有移除上层元素的时候,随意访问下层元素。

1.6 指令集

 为了避免错误的实现而导致的一致性问题,EVM的指令集保留最小集合。所有的指令操作都是基于256位的字。包含有常用的算术,位操作,逻辑操作和比较操作。条件跳转或是非条件跳转都是允许的。而且合约可以访问当前区块的相关属性比如编号和时间戳。

1.7 消息调用

合约可以通过消息调用来实现调用其他合约或是发送以太币到非合约账号。消息调用和交易类似,他们都有一个源,一个目标,数据负载,以太币,gas和返回的数据。事实上,每个交易都包含有一个顶层消息调用,这个顶层消息可以依次创建更多的消息调用。

一个合约可以定义内部消息调用需要消耗多少gas,多少gas需要被保留。如果在内部消息调用中出现out-of-gas异常,合约会被通知,会在栈里用一个错误值来标记。这种情况只是这次调用的gas被消耗完。在Solidity,这种情况下调用合约会引起一个人为异常,这种异常会抛出栈的信息。

上面提到,调用合约会被分配到一个新的,并且是清空的主存,并能访问调用的负载。调用负载时被称为calldata的一个独立区域。调用结束后,返回一个存储在调用主存空间里的数据。这个存储空间是被调用者预先分配好的。调用限制的深度为1024.对于更加复杂的操作,我们更倾向于使用循环而不是递归。

1.8 代理调用/ 代码调用和库

存在一种特殊的消息调用,叫做代理调用。除了目标地址的代码在调用方的上下文中被执行,而且msg.sender和msg.value不会改变他们的值,其他都和消息调用一样。这就意味着合约可以在运行时动态的加载其他地址的代码。存储,当前地址,余额都和调用合约有关系。只有代码是从被调用方中获取。这就使得我们可以在Solidity中使用库。比如为了实现复杂的数据结构,可重用的代码可以应用于合约存储中。

1.9 日志

  我们可以把数据存储在一个特殊索引的数据结构中。这个结构映射到区块层面的各个地方。为了实现这个事件,在Solidity把这个特性称为日志。合约在被创建出来后是不可以访问日志数据的。但是他们可以从区块链外面有效的访问这些数据。因为日志的部分数据是存储在bloom filters上。我们可以用有效并且安全加密的方式来查询这些数据。即使不用下载整个区块链数据(轻客户端)也能找到这些日志

1.10 创建

 合约可以通过特殊的指令来创建其他合约。这些创建调用指令和普通的消息调用唯一区别是:负载数据被执行,结果作为代码被存储,调用者在栈里收到了新合约的地址。

1.11 自毁

从区块链中移除代码的唯一方法是合约在它的地址上执行了selfdestruct操作。这个账号下剩余的以太币会发送给指定的目标,存储和代码从栈中删除。

本文由HPB团队整理.

HPB43:Java Web3J概述

1 Java Web3J概述

Web3j是一个轻量级,Reactive(响应式),类型安全的Java库,用于与Ethereum网络上的客户端(节点)集成,这允许您使用Ethereum块链,而不需要为平台编写自己的集成代码的额外开销。

1.1 Web3J的提供的功能

通过HTTP和IPC 完成Ethereum的JSON-RPC客户端API的实现

1.Ethereum钱包支持

2.使用过滤器的函数式编程功能的API

3.自动生成Java智能合约包装器,以创建、部署、处理和调用来自本地Java代码的智能合约

4.支持Parity的 个人和Geth的 个人客户端API

5.支持Infura,所以您不必自己运行一个Ethereum客户端

6.综合整合测试展示了上述一些场景

7.命令行工具

1.2 Web3J的依赖的库(中间件)

1.RxJava函数式编程的的API中间件

2.Apache HTTP Client中间件

3.Jackson Core 用于快速JSON序列化/反序列化中间件

4.Bouncy Castle加密解密和 Java Scrypt加密中间件

5.生成智能合约java包装器类的java源代码(.java)的JavaPoet中间件

6.Java的UNIX域套接字的*nix系统进程间通信API中间件

1.3 启动Ethereum客户端

1
$ geth --fast --cache = 512 –networkid 2 - -rpcapi “personal,db,eth,net,web3” --rpc --dev

1.4 Web3J的进程间通信IPC

Web3j还支持通过文件套接字快速进行进程间通信(IPC)到在与web3j相同的主机上运行的客户端。在创建服务时,连接只需使用相关的IpcService实现而不是 HttpService

1.5 Web3J的过滤器

Web3j的函数式编程的特性让我们设置观察者很容易,这样通知订阅者在区块链以便知道区块链上设置的事件。

1.5.1 区块过滤器

当所有新的块被添加到块链中的时候,接收到这些区块

如果您希望查看最近的当前的区块,以便于新的后续块的创建:

1.5.2 交易过滤器

当所有新的交易被添加到块链中的时候,接收到这些交易

1.5.3 待处理的交易过滤器

当所有待处理的交易被提交到网络时(也就是说,在它们被分组到一个块之前),接收这些交易

1.5.4 使用Web3J交易

Web3j支持使用Ethereum钱包文件(推荐)和用于发送交易的Ethereum客户端管理命令。

使用您的Ethereum钱包文件将Ether发送到另一方:

如果想自定义交易

  1.  获取可用的nonce
    

  1.   创建交易
    

  1.  签名并发送交易
    

  1.  使用web3j的智能合约包装器要简单得多
    

1.6 使用Web3J智能合约

使用Java智能合约包装器处理智能合约

web3j可以自动生成智能合约包装器代码,以便在不离开Java的情况下部署和与智能合约进行交互,生成包装代码

1.6.1 编译智能合约

$ solc .sol —bin —abi —optimize -o <output-dir>

1.6.2 然后使用Web3J的命令行工具生成包装器代码

Web3j solidity生成/path/to/<smart-contract>.bin /path/to/<smart-contract>.abi -o / path / to / src / main / java -p com.your.organisation.name

1.6.3 创建并部署智能合约

1.6.4 使用已存在的智能合约

2 以太坊的Web3J交易

2.1 Web3J交易类型

从广义上讲,以太坊有三种类型的交易

  1. 从以太坊的一方转移到另一方
    
  2. 创建智能合约
    
  3. 交易智能合约
    

2.2 在TestNet /私有块链上挖矿

在Ethereme测试网络(TestNet)中,采矿难度低于主网络(MainNet)。这意味着您可以使用常规CPU(如笔记本电脑)挖掘新的Ether,您需要做的就是运行一个Ethereum客户端

2.3 以太坊GAS

在使用Ethereum客户端时,这意味着有两个参数(GasPrice,GasLimit)用来决定你想花多少时间来完成一项任务:

2.3.1 Gas Price

这是你在每单位Gas中所准备的量。它的价格是9000 Wei (9 x 10 ^15 Ether)

这是你在交易执行过程中愿意花费的总金额。在以太坊中,一个交易的大小是有上限的,它限制了这个值,通常限制为1,500,000

2.3.2 Gas Limit

这些参数组合在一起,决定了您愿意花费在交易成本上的最大数量。也就是说,你不能再用Gas的价格限制了。GasPrice也会影响交易发生的速度,这取决于其他交易的价格,而这些交易对矿商来说更有利可图,您可能需要对这些参数进行调整,以确保交易能够及时地进行。

2.4 交易机制

当您使用某种Ether创建有效的帐户时,可以使用两种机制来与Ethereum进行交易,这两种机制都通过web3j来支持。

  1. [通过Ethereum客户端进行交易签名](https://docs.web3j.io/transactions.html#signing-via-client)
    
  2. [离线交易签名](https://docs.web3j.io/transactions.html#offline-signing)
    

2.4.1 通过Ethereum客户端进行交易签名

为了通过Ethereum客户端进行交易,您首先需要确保您所交易的客户知道您的钱包地址。为了做到这一点,你最好运行自己的Ethereum客户端,如Geth / Parity。一旦您有客户端运行,您可以通过以下方式创建一个钱包:

  1. Geth Wiki包含了Geth支持的不同机制的良好运行,比如导入私钥文件,并通过它的控制台创建一个新帐户或者,您可以为客户端使用JSON-RPC管理命令,例如,用于[Parity](https://github.com/paritytech/parity/wiki/JSONRPC-personal-module#personal_newaccount) 或[Geth的personal_newAccount](https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_newaccount)
    
  2. 在客户端使用json-rpc管理命令,对于 [Parity](https://github.com/paritytech/parity/wiki/JSONRPC-personal-module#personal_newaccount) or [Geth](https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_newaccount)使用personal_newAccount
    

创建您的钱包文件后,您可以通过web3j解锁帐户,首先创建一个支持Parity / Geth管理命令的web3j实例

2.4.2 脱机交易签名

如果您不想管理自己的Ethereum客户端,或者不想向Ethereum客户端提供电子钱包详细信息(如密码),那么离线交易签名就是要走的路。

离线交易签名允许您使用您在Web3j中的Ethereum Ethereum钱包签署交易,从而可以完全控制您的私人凭据。离线创建的交易可以发送到网络上的任何Ethereum客户端,这将会将交易传播到其他节点,前提是它是一个有效的交易。

2.4.3 创建和使用钱包文件

为了离线签署交易,您需要使用您的Ethereum钱包文件或与Ethereum钱包/帐户相关的公钥和私钥,web3j能够为您生成一个新的安全Ethereum钱包文件,或者使用现有的钱包文件。

要创建一个新的钱包文件:

String fileName = WalletUtils.generateNewWalletFile(“your password”,new File(“/path/to/destination”));

2.4.4 脱机签名交易

如果具有达到脱机签名能力目的交易应该使用 RawTransaction类型。RawTransaction类似于之前提到的交易类型,但是它不需要from地址,因为这可以从签名中推断出来。

为了创建和签名一个raw交易,顺序如下:

确定发件人帐户的下一个可用随机数

  1.  创建RawTransaction对象
    
  2.  编码RawTransaction对象
    
  3.  签名RawTransaction对象
    
  4.  将RawTransaction对象发送到节点进行处理
    
  5. 获取下一个可用的[随机数后](https://docs.web3j.io/transactions.html#nonce),该值就可以用于创建交易对象:
    

  1. 然后可以对交易进行签名和编码:
    

这些凭证是在创建和处理钱包文件时加载的。

  1. 然后使用ethsendrawtransaction发送该交易:
    

2.4.5 交易 Nonce

Nonce是一个递增的数值,它用于惟一地标识交易。一个nonce只能被使用一次,直到一个交易被挖矿确认,它可以用同一个nonce发送多个版本的交易,然而,一旦被挖矿确认,任何后续的提交都将被拒绝,可以通过

2.4.6 Nonce使用规则

为了防止交易重播,ETH(ETC)节点要求每笔交易必须有一个nonce数值。每一个账户从同一个节点发起交易时,这个nonce值从0开始计数,发送一笔nonce对应加1。当前面的nonce处理完成之后才会处理后面的nonce。

注意这里的前提条件是相同的地址在相同的节点发送交易。

  1.  当nonce太小(小于之前已经有交易使用的nonce值),交易会被直接拒绝。
    
  2.  当nonce太大,交易会一直处于队列之中,这也就是导致我们上面描述的问题的原因;
    
  3.  当发送一个比较大的nonce值,然后补齐开始nonce到那个值之间的nonce,那么交易依旧可以被执行。
    
  4.  当交易处于queue中时停止geth客户端,那么交易queue中的交易会被清除掉。
    

2.5 创建智能合约

2.5.1 使用Raw Transaction

2.5.2 获取合约地址

如果智能合约包含构造函数,则相关的构造函数字段值必须编码并附加到编译后的智能合约代码中.

2.5.3 执行智能合约

web3j负责为您提供功能编码,进一步的详细信息可以在Ethereum维基百科的电子合同ABI节中找到。

等待响应就使用EthGetTransactionReceipt…

不管消息签名的返回类型是什么,都不可能从事务性函数调用返回值。但是,可以使用过滤器捕获函数返回的值.

2.5.4 查询智能合约的状态

此功能由ethcall json-rpc调用实现, ethcall允许您在智能合约中调用一个方法来查询一个值。这个函数不存在交易成本,这是因为它不会改变任何智能合约方法的状态,它只是简单地返回它们的值:

注意:如果生成了一个无效的函数调用,或者获得了一个null结果,那么返回值将是一个集合。emptylist的实例.

HPB42:Solidity开发神器Remix

1 功能

这里我们使用在线编译器,打开网址

https://ethereum.github.io/browser-solidity

1.1 文件夹管理

最左边是文件夹管理,里面列出了当前工作区里的文件,remix可以支持从本地文件夹读取文件。

1.2 工作区

正中间是工作区,工作区上半部是代码编辑区,在这里可以写solidity合约。 下半部是日志区,在执行智能合约时,会显示transaction相关的信息。在输出日志的时候还可以查看Details和Debug信息。

1.3 功能区

最右边的是功能区,里面有编译、运行、设置和分析以及调试器和支持。

在编译器点击Details可以查看编译细节,里面有NAME,METADATA,BYTECODE,ABI 等一些相关信息。

在设置里面可以选择我们的编译器版本,和一些IDE的使用设置。

2 实战例子

一个简单的实现对数组增删改查的智能合约

2.1 打开工作区,输入代码

2.2 代码分析

• string[] strArr; 定义了一个字符串的数组变量strArr,且该变量没有public因此不可见。

• strArr.push(str); 其中的push是数组类型的两个member之一,另一个是length. 这里的push就是给该数组增加一个元素。//这里实现了对数组的新增功能。

• getStrAt(uint n) 是一个简单的读取字符串的函数,//这里实现对数组的读取功能。

• updateStrAt(uint n, string str) // 实现对数组的更新功能。

• deleteStrAt(uint index) 这个值得一说,因为solidity默认的数组类型中没有删除功能,因此此处自己写了一个删除代码, 核心方法就是保证删除某项后,后面的元素依次向前,同时删除数据,同时保证数组的member length正确。

2.3 编译

代码写完之后我们到Compile区域点击编译Start to compile, 如果编译成功没有错误可以看到我Details信息。

2.4 运行

我们从Compile切换到Run区域,在Environment里面选择编译器,记住这里一定要选择Java Script VM.然后再At Address里面输入我们的eth服务端地址,可以输入测试网络,也可以输入自己搭建的私有链和联盟链。输入完成之后点击Create.这个时候我们可以看到:

右下角会生成我们的ABI方法。

2.5 测试

好了,这个时候我们可以开始测试了。

比如这里输入一个hello eth, 注意一定要加引号。然后点击add, 控制台会打印信息。

点击Details可以看到详细信息

刚刚我们添加了一个字符串,这里再调用一下get方法,打印出字符串。

本文由HPB芯链团队整理

HPB41:Web3j实现智能合约

1 获取凭证

Credentials是我们钱包的凭证,在我们交易和创建智能合约的时候都需要用到。

1.1 创建新凭证

file=WalletUtils.generateFullNewWalletFile(pwd,dir);

返回的file不是全路径,而是该文件的路径名,比如UTC—2017-10-30T12-10-45.516005546Z—5f38056f45091ee992298e53681b0a60c999ff95。

前面的是创建时间,后面的是账号标识。

1.2 使用旧凭证

每个账号在创建的时候都会生成一个keystore,它是Json格式的。如果要使用旧凭证,首先需要找到我们的keystore。我这里的服务器搭建在linux服务端,所以这里把keystore拷贝到我们windows本地。

然后生成credentials

2 智能合约

2.1 编写智能合约

以太坊编写智能合约有三种语言:

  • Serpent

Serpent是一种类似于Python的语言,可用于开发契约并编译为EVM字节码。它旨在最大限度地简化和清理,将低级语言的许多效率优势与易于使用的编程风格相结合,同时为合同编程添加特定领域特定功能。蛇是使用LLL编译的。

  • Solidity

这是一种类似于JavaScript的语言,它允许你开发合同并编译为EVM字节码。它目前是以太坊的旗舰语言,也是最受欢迎的。

我们这里编写一份Hello World

contract HelloWorld

{

​ address creator;

​ string greeting;

​ function HelloWorld(string _greeting) public

​ {

​ creator = msg.sender;

​ greeting = _greeting;

​ }

​ function greet() constant returns (string)

​ {

​ return greeting;

​ }

​ function setGreeting(string _newgreeting)

​ {

​ greeting = _newgreeting;

​ }

​ /****

​ Standard kill() function to recover funds

​ ****/

​ function kill()

​ {

​ if (msg.sender == creator)

​ suicide(creator); // kills this contract and sends remaining funds back to creator

​ }

}

  • LLL

Lisp Like Language(LLL)是一种类似于Assembly的低级语言,它意味着非常简单和简约,基本上就是直接在EVM中进行编码的一个小包装。

还有一种已经被弃用了,这里我们选用官方标准的Solidity语言编写。

2.2 编译智能合约

这里使用Solidity的在线编译器https://ethereum.github.io/browser-solidity/

把我们上面编写好的智能合约代码贴进去。

然后点击Details

这里的name就是智能合约的名称,ABI文件会暴露合同的所有构造参数,我们需要用ABI文件生成java类。WEB3是生成好的js文件,可以配合web3.js直接调用。

2.3 Solidity类转换成java类

如果想要避免使用智能合约的底层实现细节,web3j提供了Solidity 智能合约包装,使您可以通过生成的包装对象直接与所有智能合约的方法进行交互。

这里我们把ABI文件复制一下,然后粘贴到本地作为.json文件,名字最好和类名一样,比如我这里叫HelloWorld.json。这里有一个大坑,就是编译器生成的JSON文件格式不对,没有合约名字,如果我们执行转换会报错。

这里我们打开json文件手动编译一下

web3j支持从Solidity ABI文件在Java中自动生成智能合约函数包装器。

web3j 命令行工具附带一个用于生成智能合同函数包装器的命令行工具:

先去官方下载一个命令行工具

https://github.com/web3j/web3j/releases/tag/v3.2.0

下载.zip文件解压到本地。

找到web3.bat文件执行 $ web3j truffle generate [—javaTypes|—solidityTypes] /path/to/<truffle-smart-contract-output>.json -o /path/to/src/main/java -p com.your.organisation.name

上面这个是官方的格式不太通俗易懂,我给大家翻译一下:

Web3j truffle generate json文件地址 -o 生成的文件目录 -p 生成的文件包名。这里要注意 -o 和-p也要带上。

如果web3.bat文件点不开,就直接使用cmd命令执行,在执行的时候带上web3j的全路径就行了,执行完成之后控制条会显示:

或者也可以通过Java类进行调整,添加web3j的相关依赖,然后使用maven指定运行。

img

这里把生成好的java类拷贝到我们的代码里面。

2.4 发布智能合约

合约生效时间

private static final String BINARY =null;

这个BINARY代表智能合约的生效时间,生成的类里面默认是NULL. 如果不修改这个时间执行智能合约默认是失效的,所以这里我们修改一下为官方指定的二进制代码。

在编译器里面也可以看到我们生成的合约二进制文件。

就是这一行,我们拷贝过来就好了。

部署智能合约

//部署智能合约

HelloWorld contract=deploy(admin, credentials,ManagedTransaction.GAS_PRICE, Contract.GAS_LIMIT,“Hello,World”).send();

部署合约需要消耗Gas.这里说一下我对以太坊Gas的理解:

交易的过程一般需要支付一定量的手续费(当然也可以选择不支付)。矿工会优先打包交易手续费高的交易,如果没有支付交易手续费,你的交易可能要等很久才会被打包。创建一笔交易的时候不需要显式的指明支付多少交易手续费,它是根据你的 UTXO额 – 交易额 – 找零 来计算的。举个栗子,A有一个10btc的UTXO(未花费的交易输出)的支配权,它给B账户转1BTC,那么在创建交易的时候,需要指明交易额1btc,和找零8.995,那么(10-8.995-1 = 0.005)就是这笔交易的手续费,会奖励给打包包含这笔交易的区块的矿工,如果没有设置找零那么多余的9btc都会被当作交易手续费奖励给矿工,虽然你的交易会很快的被打包,但是这可能不是你想要的。

  • 这样可以鼓励更加高效的合约代码,减少不必要的计算,避免系统遭受攻击,毕竟攻击者要为他们消耗的资源付出一定的代价,包括带宽,CPU,和存储。 gasPrice 是由交易的发起者来设置的,但是矿工可以选择先打包那些gas价格高的交易,gas价格低的可能要等很久或者不会被打包。
  • 例如一笔交易:{ from:web3.eth.accounts[0], data:tokenCompiled.token.code, gas: 1000000 }, gas参数设置这个交易最多能使用多少gas。交易里面还可以再加一个参数gasPrice,gasPrice可以自己设置, geth会默认设置一个大多数矿工可以接受的 gasPrice, 0.05e12 wei,可以调eth.gasPrice来查看当前的gasPrice.
  • 矿工在启动geth的时候可以设置两个参数—ask 和 —bid , —ask是设置一个最低的gas价格,低于这个价格的交易会被忽略,默认值是500000000000,—bid 设置gas价格竞价,默认值是 500000000000。

合约地址

//获取合约地址

String contractAddress = contract.getContractAddress();

合约发布成功之后会在ETH客户端生成一个合约地址,我们在以后调用的时候都需要用到。

验证合同是否有效

使用这种方法,您可能需要确定已经加载的合同地址是您期望的智能合约。为此,您可以使用isValid智能合约方法,只有在合约地址已部署字节码与智能合约包装中的字节码匹配时才会返回true.

System.out.println(contract.isValid());

如果返回false,可能是因为智能合约代码生成的有问题,也有可能是合同生效时间没有设置。

加载智能合约

如果我们想使用已经部署过的智能合约,这个时候我们就需要加载智能合约了,

在加载智能合约的时候你需要拥有该智能合约的地址。

HelloWorld contract =HelloWorld.load(contractAddress,admin,credentials, GAS_PRICE, GAS_LIMIT);

调用智能合约

String value=contract.greet().send();

log.info(value);

调用我们编译好的get方法,会在控制台打印Hello World。代表我们这次合约部署调用成功。

销毁智能合约

contract.kill();

销毁后的智能合约无法调用,但是可以查询到。

本文由HPB芯链团队整理

HPB40:基于Ubuntu Docker环境下进行以太坊实践

本文是指导以太坊技术爱好者,通过基于Ubuntu环境下通过docker来进行以太坊的客户端安装和调试的指导教程。

目录

1、实践环境要求

2、Ubuntu版本说明

3、Docker安装

4、Ethereum安装与实践

5、ZSH小工具推荐

1 实践环境要求

1.1 概述

https://docs.docker.com/engine/installation/linux/ubuntulinux/

上面这篇文章主要指导你去安装使用Docker-managed发布包及其安装机制。使用这些包确保你获得最近的docker官方发布版本。如果你需要安装使用Ubuntu-managed包,查阅Ubuntu文档。

1.2 docker对操作系统支持

Ubuntu Xenial 16.04[LTS]

Ubuntu Trusty 14.04[LTS]  

Ubuntu Precise 12.04[LTS]

1.3 前置需求

不管你是Ubuntu的哪个版本,Docker需要64的操作系统。此外你的kernel内核至少要在3.10版本之上。最近的3.10小版本或者最新的维护版本也是可以接受的。kernel3.10版本之前的系统缺少一些特性来运行docker容器。这些旧版本有些已知的bugs会导致数据丢失并且在一定条件下会频繁的故障。检查你当前的kernel版本,打开终端,输入uname –r

注意:如果你之前使用APT安装过docker,为了新版本的docker仓库,确保你更新了APT源。

1.4 更新apt源

Docker的APT仓库包含1.7.1以及更高的版本。通过设置APT使用来自docker仓库的包。

1) 登陆机器,用户必须使用sudo或者root权限。

2) 打开终端

3) 更新包信息,确保APT能使用https方式工作,并且CA证书已安装了

1
2
3
#sudo apt-get update

#sudo apt-get install apt-transport-https ca-certificates

 

出现这个问题可能是有另一个程序正在运行,导致资源被锁不可用。而导致资源被锁的原因可能是上次运行安装或更新没有正常完成,解决办法就是删掉。

1
2
3
#sudo rm /var/cache/apt/archives/lock

#sudo rm /var/lib/dpkg/lock

4)添加一个新的GPG密钥

1
#sudo apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D 

5)找到合适你的Ubuntu操作系统的键,这个键决定APT将搜索哪个包。可能的键有:

Ubuntu version: Precise 12.04, Trusty 14.04,Cenial 16.04

注意:docker没有为所有的架构提供包,Binary artifacts are built nightly,你可以从以下链接下载。

https://master.dockerproject.org. 在一个多架构的系统上安装docker,为键添加一个[arch=]条款。更多细节参考Debian Multiarch维基百科。

6)运行下面的命令,用占位符为你的操作系统替换键。

#echo “” | sudo tee /etc/apt/sources.list.d/docker.list

比如你是16.04将上面命令的

替换成deb https://apt.dockerproject.org/repoubuntu-xenial main 执行那条命令,就在那个文件夹下创建了一个docker.list文件,里面的内容就是

deb https://apt.dockerproject.org/repoubuntu-xenial main

7)更新APT包索引

1
#sudo apt-get update

8)校验APT是从一个正确的仓库拉取安装包。

当运行下面命令的时候,这个键会返回你目前可以安装的docker版本,每个键都包括URL:https://apt.dockerproject.org/repo/%E3%80%82%E4%B8%8B%E9%9D%A2%E6%98%AF%E6%88%AA%E5%8F%96%E7%9A%84%E9%83%A8%E5%88%86%E8%BE%93%E5%87%BA%E5%86%85%E5%AE%B9%E3%80%82

1
#apt-cache policy docker-engine

现在当你运行apt-get upgrade的时候,APT就会从新的仓库拉安装包。

2 Ubuntu版本说明

2.1 前置准备操作

Ubuntu Xenial 16.04[LTS],Ubuntu Trusty 14.04[LTS]这两个版本记得安装linux-iamge-extra-*的kernel包。这个包允许你使用aufs存储驱动。

# sudo apt-get install linux-image-extra-$(uname -r) linux-image-extra-virtual

Ubuntu Precise 12.04[LTS]对于这个版本,你需要3.13以上的kernel版本,你必须升级。下面表格指导你需要哪些包:

你可以执行以下命令:

1
2
3
#sudo apt-get install linux-image-generic-lts-trusty

#sudo reboot

3 Docker安装

3.1 前置操作

1).登陆系统,用你的账号使用sudo全权限

2).运行命令apt-get install openssh-server安装ssh 查看是否安装成功ps -ef|grep ssh

3).更新APT包索引:sudo apt-get update

4).安装docker:sudo apt-get install docker-engine

5).开启docker后台进程:sudo service docker start

6).校验docker是否安装成功:sudo docker run hello-world

这个命令会下载一个测试镜像,并且运行在一个容器中。当容器运行时,他会打印一些信息,并且退出。

3.2 创建一个docker组

docker后台进程是绑定的Unix的socket而不是TCP端口。默认情况下,Unix的socket属于用户root,其它用户要使用要通过sudo命令。由于这个原因,docker daemon通常使用root用户运行。为了避免使用sudo当你使用docker命令的时候,创建一个Unix组名为docker并且添加用户。当docker daemon启动,它会分配Unix socket读写权限给所属的docker组。

注意:docker组不等价于用户root,如果想要知道的更多关于安全影响,查看docker daemon attack surface。

1
2
3
#sudo groupadd docker

#sudo usermod -aG docker $USER

退出再重进,确保该用户有正确的权限。校验生效,通过运行docker命令不带sudo:docker run hello-world,如果失败会有以下类似的信息:Cannot connect to the Docker daemon. Is ‘docker daemon’ running on this host?确保DOCKER_HOST环境变量没有设置。如果有取消它。

3.3 调整内存和交换区计算

当用户运行docker时,他们可能在使用一个镜像时看见下面的信息:

WARNING: Your kernel does not support cgroup swap limit. WARNING: Your kernel does not support swap limit capabilities. Limitation discarded.

为了阻止这些信息,在你的系统中启用内存和交换区计算。这个操作会导致即便docker没有使用也有内存开销以及性能下降。内存开销大概是总内存的1%。性能降低了大约10%。

修改/etc/default/grub文件。vi或者vim命令都行,设置GRUB_CMDLINE_LINUX的值,如下:

GRUB_CMDLINE_LINUX=“cgroup_enable=memory swapaccount=1”。

保存文件并关闭。

1
#sudo update-grub

更新启动项,reboot重启你的系统。

3.4 启动UFW转发

当你运行docker时,在同一台主机上使用UFW(Uncomplicated Firewall) ,你需要额外的配置。docker使用桥接方式来管理容器的网络。默认情况下,UFW废弃所有的转发流量。因此,docker运行时UFW可以使用,你必须设置合适UFW的转发规则。

UFW默认配置规则拒绝了所有传入流量。如果你想要从另一个主机到达你的容器需要允许连接docker的端口。docker的默认端口是2376如果TLS启用,如果没有启动则是2375,会话是不加密的。默认情况,docker运行在没有TLS启动的情况下。

  • 为了配置UFW并且允许进入的连接docker端口:
  • 检查UFW是否安装并启用:
1
#sudo ufw status
  • 打开/etc/default/ufw文件并编辑:
1
#sudo nano /etc/default/ufw
  • 设置DEFAULT_FORWARD_POLICY:DEFAULT_FORWARD_POLICY=“ACCEPT”
  • 保存退出并重启使用新的设置:
1
#sudo ufw reload
  • 允许所有的连接到docker端口:
1
#sudo ufw allow 2375/tcp

3.5 为docker配置DNS服务器

系统运行桌面的Ubuntu或者Ubuntu衍生产品通常使用127.0.0.1作为默认的nameserver文件/etc/resolv.conf文件中。NetworkManager也通常设置dnsmasq nameserver 127.0.0.1在/etc/resolv.conf。

当在桌面机器运行容器,使用这些配置时,docker的使用者会看见这些警告:

WARNING: Local (127.0.0.1) DNS resolver found in resolv.conf and containers can’t use it. Using default external servers : [8.8.8.8 8.8.4.4]

这个警告发生是因为docker容器不能使用本地DNS命名服务器。此外docker默认使用一个额外的nameserver。

为了避免这个警告,你可以在使用docker容器的时候指定一个DNS服务器。或者你可以禁用dnsmasq在NetworkManager中。但是,禁用会导致DNS协议在某些网络中变慢。

下面的说明描述了如何在Ubuntu14.0或以下版本配置docker守护进程。Ubuntu15.04及之上的使用systemd用于启动项和服务管理。指导通过使用systemd来配置和控制一个守护进程。

  • 设置指定的DNS服务:

打开/etc/default/docker文件并编辑:sudo nano /etc/default/docker,添加配置项:DOCKER_OPTS=“—dns 8.8.8.8”。将8.8.8.8用一个本地的DNS服务例如192.168.1.1替换。你也可以配置多个DNS服务器。

用空格隔开它们,如:—dns 8.8.8.8 —dns 192.168.1.1。

警告:当你在笔记本连接了不同网络的情况时做这些操作,确保选择一个公用的DNS服务器。保存文件并退出,重启docker守护进程:sudo service docker restart。或者另一个选择,禁用dnsmasq在网络管理器中,这可能导致你的网速变慢:

  • 打开/etc/NetworkManager/NetworkManager.conf文件,
  • 编辑它:sudo nano /etc/NetworkManager/NetworkManager.conf。
  • 找到行dns=dnsmasq,注释掉。
  • 保存关闭文件,重启网络管理器和docker.
1
2
3
#sudo restart network-manager  

#sudo restart docker

3.6 配置docker引导启动

Ubuntu15.04之后使用systemd作为引导启动和服务管理,14.10及以下版本是upstart。15.04以上,需要配置docker守护进程boot启动,

  • 运行命令:
1
#sudo systemctl enable docker

14.10及以下版本安装方法会自动配置upstart来启动docke daemon在boot。

3.7 升级卸载docker

  • 升级:
1
#sudo apt-get upgrade docker-engine
  • 卸载:
1
#sudo apt-get purge docker-engine
  • 卸载及依赖:
1
#sudo apt-get autoremove --purge docker-engine
  • 上述命令不会卸载images,containers,volumes或者用户自己创建的配置文件。

你如果想删除这些东西,执行下面的命令:

1
#rm -rf /var/lib/docker
  • 安装最简单的方法是:
1
2
3
#sudo apt-get update  

#sudo apt-get install 

3.8 执行docker-compose安装

docker-compose 是用于定义和运行复杂docker应用的工具,以yaml定义语言在一个docker-compose.yaml文件中定义一个包括多容器的应用,用一条命令即可启动应用中包括的所有docker container,容器启动所有依赖的动作都会被工具自动完成。

1
2
3
#curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose

#chmod +x /usr/local/bin/docker-

4 Ethereum安装与实践

4.1 通过docker安装Ethereum

  • 运行如下命令
1
#docker pull docker.io/ethereum/client-go

4.2 直接安装Ethereum

  • 运行如下命令
1
2
3
#apt-get update

#apt-get install software-properties-common add-apt-repository -y ppa:ethereum/ethereum
  • 安装一个稳定版本的以太坊
1
2
3
4
5
#apt-get update

#apt-get install ethereum

#apt-get update
  • 生成引导节点ID
1
#bootnode --genkey=boot.key
  • 运行引导节点
1
#bootnode --nodekey=boot.key

4.3 新建genesis.json文件

  • 运行以下命令
1
vi genesis.json
  • 输入

4.4 初始化创始区块

  • 运行如下命令
1
#docker run -itd --privileged=true -v /path/docker/dev1:/root/ethdev --name gethDev1 ethereum/client-go --datadir /root/ethdev --networkid 8765639736937780 init /root/ethdev/genesis.json

4.5 创建Ethereum节点的容器

  • 运行如下命令
1
2
3
#docker rm -f gethDev1

#docker run -itd -m 512M --privileged=true --memory-swap -1 --net=host -p 8545:8545 -p 40303:40303 -v /path/docker/dev1:/root/ethdev --name gethDev1 ethereum/client-go --ipcdisable --port 40303 --bootnodes "enode://2039a49989e45bf119ecd21403607ea9f5888b13a6bb7a03ed81687deabb251095e4193a77eca067076f77ed40e4c6fd51539038c440337beffbbb36953d1d75@192.168.3.43:30301" --debug --nodiscover --rpcapi "db,eth,net,web3,personal,admin,miner,txpool" --datadir /root/ethdev --networkid 8765639736937780 --wsapi "db,eth,net,web3,personal,admin,miner,txpool" --ws --wsaddr "0.0.0.0" --rpc --rpcaddr "0.0.0.0" --cache=512 --verbosity 6 --mine --minerthreads=1 --etherbase=0x5f38056f45091ee992298e53681b0a60c999ff95 console

4.6 查看Ethereum节点docker日志

  • 运行如下命令
1
#docker logs -f gethDev1

4.7启动Ethereum节点

  • 运行如下命令
1
#docker startgethDev1

4.8 Attach Ethereum节点

  • 运行如下命令
1
#docker attach gethDev1

4.9 Detach Ethereum节点

先后按下键盘ctrl+p+q退出节点,注意:有先后顺序

4.10 停止Ethereum节点

  • 运行命令如下
1
#docker stopgethDev1

4.11 Ethereum日志释疑

  • 首先,告警提示geth抱怨没有定义etherbase,etherbase是成功挖掘区块,执行智能合约并在区块链内返回结果之后用来接收以太奖励的“默认以太坊地址”。这个帐户,在开发合同时也很方便。

  • 接下来,我们看到blockchain数据被写入/root/.ethereum/chaindata,因为我们已经从我们的主机挂载了这个目录,我们应该可以在本地磁盘上看到出现的数据:

  • docker exec –i ethereum geth account new ls –l /opt/docker/ethereum/keystore/. docker exec ethereum apt-get install –y ntpdate docker exec ethereum ntpdate –s ntp.ubuntu.com

在当前配置中,我们有一个可以挂载到我们的容器中的以太坊数据目录。这不是因为区块链数据只能在任何情况下由一个进程访问,而是访问可由Ethereum节点用于进程间通信的IPC文件描述符。因此,我们可以在这里继续,而不需要访问网络。

5 zsh小工具推荐

zsh是一款小工具,对命令补全功能非常强大,可以补齐路径,补齐命令,补齐参数等。

5.1 修改root用户SHELL

先进入root用户,命令:

1
su root

查看默认SHELL命令:

1
echo $SHELL

然后查看是否安装了zsh,命令:

1
cat /etc/shells

默认没有安装,那么先安装zsh,命令:

1
apt-get install zsh

确认zsh是否安装成功,命令:

1
zsh --version

接下来替换bash为zsh,命令:

1
chsh -s /bin/zsh

然后reboot重启,之后查看默认SHELL,发现修改为/bin/zsh

查看是否安装git,命令:

1
git –version

如果没有安装则安装,命令:

1
apt-get install git

最后下载oh-my-zsh,命令:

1
sh -c "$(curl -fsSL https://raw.github.com/robbyrussell/oh-my-zsh/master/tools/install.sh)"

出现如上则安装成功,重新进入终端即可。

5.2 修改普通用户SHELL

查看默认SHELL,命令:

1
echo $SHELL

如果是/bin/zsh则直接:

1
sh -c "$(curl -fsSL https://raw.github.com/robbyrussell/oh-my-zsh/master/tools/install.sh)"

是/bin/bash则,切换:

1
chsh -s /bin/zsh

然后用root权限reboot,然后:

1
sh -c "$(curl -fsSL https://raw.github.com/robbyrussell/oh-my-zsh/master/tools/install.sh)"

安装成功后可以(vi ~/.zshrc)来修改配置文件以改变样式(注意root和非root都要改,可以选择不一样的样式)。

root的样式配置文件在根目录root下,为隐藏文件;非root在/home/root下。

具体样式选择https://github.com/robbyrussell/oh-my-zsh/wiki/themes

本文由HPB(芯链)团队整理。