forked from tronprotocol/java-tron
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request tronprotocol#2882 from guoquanwu/custom_actuator_doc
add docs: modularization intro & implement a Actuator
- Loading branch information
Showing
3 changed files
with
389 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,321 @@ | ||
# 自定义 SumActuator | ||
|
||
基于java-tron搭建一条自定义公链时,实现一个定制的actuator是不可缺少的一环,本文演示如何基于 java-tron 开发一个 `SumActuator`。 | ||
|
||
Actuator 模块抽象出4个方法并定义在 Actuator 接口中: | ||
|
||
1. `execute()`: 负责交易执行的逻辑,如状态修改、流程跳转、逻辑判断等 | ||
2. `validate()`: 定义交易校验逻辑 | ||
3. `getOwnerAddress()`: 获取交易发起方的地址 | ||
4. `calcFee()`: 定义手续费计算逻辑 | ||
|
||
|
||
|
||
## 定义并注册合约 | ||
|
||
目前 java-tron 支持的合约定义在 Protocol 模块的 src/main/protos/core/contract 目录中,在这个目录下新建一个 math_contract.proto 文件并声明 `SumContract`。基于篇幅有限本文只提供 sum 的实现,用户也可以实现 minus 等实现。 | ||
|
||
`SumContract` 的逻辑是将两个数值相加求和: | ||
|
||
```protobuf | ||
syntax = "proto3"; | ||
package protocol; | ||
option java_package = "org.tron.protos.contract"; //Specify the name of the package that generated the Java file | ||
option go_package = "github.com/tronprotocol/grpc-gateway/core"; | ||
message SumContract { | ||
int64 param1 = 1; | ||
int64 param2 = 2; | ||
bytes owner_address = 3; | ||
} | ||
``` | ||
|
||
同时将新的合约类型注册在 src/main/protos/core/Tron.proto 文件的 `Transaction.Contract.ContractType` 枚举中,交易、账号、区块等重要的数据结构都定义在 Tron.proto 文件中: | ||
|
||
```protobuf | ||
message Transaction { | ||
message Contract { | ||
enum ContractType { | ||
AccountCreateContract = 0; | ||
TransferContract = 1; | ||
........ | ||
SumContract = 52; | ||
} | ||
... | ||
} | ||
``` | ||
|
||
然后还需要注册一个方法来保证 gRPC 能够接收并识别该类型合约的请求,目前 gRPC 协议统一定义在 src/main/protos/api/api.proto,在 api.proto 中的 Wallet Service 新增 `InvokeSum` 接口: | ||
|
||
```protobuf | ||
service Wallet { | ||
rpc InvokeSum (SumContract) returns (Transaction) { | ||
option (google.api.http) = { | ||
post: "/wallet/invokesum" | ||
body: "*" | ||
additional_bindings { | ||
get: "/wallet/invokesum" | ||
} | ||
}; | ||
}; | ||
... | ||
}; | ||
``` | ||
最后重新编译修改过 proto 文件,可自行编译也可直接通过编译 java-tron 项目来编译 proto 文件: | ||
|
||
*目前 java-tron 采用的是 protoc v3.4.0,自行编译时确保 protoc 版本一致。* | ||
|
||
```shell | ||
# recommended | ||
./gradlew build -x test | ||
|
||
# or build via protoc | ||
protoc -I=src/main/protos -I=src/main/protos/core --java_out=src/main/java Tron.proto | ||
protoc -I=src/main/protos/core/contract --java_out=src/main/java math_contract.proto | ||
protoc -I=src/main/protos/api -I=src/main/protos/core -I=src/main/protos --java_out=src/main/java api.proto | ||
``` | ||
|
||
编译之后会更新 java_out 目录中对应的 java 文件。 | ||
|
||
## 实现 SumActuator | ||
|
||
目前 java-tron 默认支持的 Actuator 存放在该模块的 org.tron.core.actuator 目录下,同样在该目录下创建 `SumActuator` : | ||
|
||
```java | ||
public class SumActuator extends AbstractActuator { | ||
|
||
public SumActuator() { | ||
super(ContractType.SumContract, SumContract.class); | ||
} | ||
|
||
/** | ||
* define the contract logic in this method | ||
* e.g.: do some calculate / transfer asset / trigger a contract / or something else | ||
* | ||
* SumActuator just sum(param1+param2) and put the result into logs. | ||
* also a new chainbase could be created to store the generated data(how to create a chainbase will be released in future.) | ||
* | ||
* @param object instanceof(TransactionResultCapsule), store the result of contract | ||
*/ | ||
@Override | ||
public boolean execute(Object object) throws ContractExeException { | ||
TransactionResultCapsule ret = (TransactionResultCapsule) object; | ||
if (Objects.isNull(ret)) { | ||
throw new RuntimeException("TransactionResultCapsule is null"); | ||
} | ||
|
||
long fee = calcFee(); | ||
try { | ||
SumContract sumContract = any.unpack(SumContract.class); | ||
long param1 = sumContract.getParam1(); | ||
long param2 = sumContract.getParam2(); | ||
long sum = param1 + param2; | ||
|
||
logger.info(String.format("\n\n" + | ||
"-------------------------------------------------\n" + | ||
"|\n" + | ||
"| SumActuator: param1 = %d, param2 = %d, sum = %d\n" + | ||
"|\n" + | ||
"-------------------------------------------------\n\n", | ||
param1, param2, sum)); | ||
ret.setStatus(fee, code.SUCESS); | ||
} catch (ArithmeticException | InvalidProtocolBufferException e) { | ||
logger.debug(e.getMessage(), e); | ||
ret.setStatus(fee, code.FAILED); | ||
throw new ContractExeException(e.getMessage()); | ||
} | ||
return true; | ||
} | ||
|
||
/** | ||
* define the rule to validate the contract | ||
* | ||
* this demo first checks whether contract is null, then checks whether ${any} is a instanceof SumContract, | ||
* then validates the ownerAddress, finally checks params are not less than 0. | ||
*/ | ||
@Override | ||
public boolean validate() throws ContractValidateException { | ||
if (this.any == null) { | ||
throw new ContractValidateException("No contract!"); | ||
} | ||
final SumContract sumContract; | ||
try { | ||
sumContract = any.unpack(SumContract.class); | ||
} catch (InvalidProtocolBufferException e) { | ||
logger.debug(e.getMessage(), e); | ||
throw new ContractValidateException(e.getMessage()); | ||
} | ||
byte[] ownerAddress = sumContract.getOwnerAddress().toByteArray(); | ||
if (!DecodeUtil.addressValid(ownerAddress)) { | ||
throw new ContractValidateException("Invalid ownerAddress!"); | ||
} | ||
long param1 = sumContract.getParam1(); | ||
long param2 = sumContract.getParam2(); | ||
if(param1 < 0 || param2 < 0){ | ||
logger.debug("negative number is not supported"); | ||
return false; | ||
} | ||
return true; | ||
} | ||
|
||
/** | ||
* this method returns the ownerAddress | ||
* @return | ||
* @throws InvalidProtocolBufferException | ||
*/ | ||
@Override | ||
public ByteString getOwnerAddress() throws InvalidProtocolBufferException { | ||
return any.unpack(SumContract.class).getOwnerAddress(); | ||
} | ||
|
||
/** | ||
* burning fee for a contract can reduce attacks like DDoS. | ||
* choose the best strategy according to the business logic | ||
* | ||
* here return a contant just for demo | ||
* @return | ||
*/ | ||
@Override | ||
public long calcFee() { | ||
return TRANSFER_FEE; | ||
} | ||
} | ||
``` | ||
|
||
为了简单起见 `SumActuator` 的执行结果直接输出至 log 文件中,对于有存储需求的情况,可以考虑创建一个新的 chainbase 来存储相应的数据。(创建 chainbase 的方法将在后续相关文章中发布) | ||
|
||
确定 `SumActuator` 的实现后,还需要在 RpcApiService 的子类 WalletApi 中继承并实现 `invokeSum(MathContract.SumContract req, StreamObserver<Transaction> responseObserver)` 方法用于接收并处理 `SumContract` | ||
|
||
```java | ||
public class WalletApi extends WalletImplBase { | ||
... | ||
@Override | ||
public void invokeSum(MathContract.SumContract req, StreamObserver<Transaction> responseObserver){ | ||
try { | ||
responseObserver | ||
.onNext( | ||
createTransactionCapsule(req, ContractType.SumContract).getInstance()); | ||
} catch (ContractValidateException e) { | ||
responseObserver | ||
.onNext(null); | ||
logger.debug(CONTRACT_VALIDATE_EXCEPTION, e.getMessage()); | ||
} | ||
responseObserver.onCompleted(); | ||
} | ||
... | ||
} | ||
``` | ||
|
||
## 验证 SumActuator | ||
|
||
最后实现一个测试类来验证上述步骤的正确性: | ||
|
||
```java | ||
public class SumActuatorTest { | ||
private static final Logger logger = LoggerFactory.getLogger("Test"); | ||
private String serviceNode = "127.0.0.1:50051"; | ||
private String confFile = "config-localtest.conf"; | ||
private String dbPath = "output-directory"; | ||
private TronApplicationContext context; | ||
private Application appTest; | ||
private ManagedChannel channelFull = null; | ||
private WalletGrpc.WalletBlockingStub blockingStubFull = null; | ||
|
||
/** | ||
* init the application. | ||
*/ | ||
@Before | ||
public void init() { | ||
CommonParameter argsTest = Args.getInstance(); | ||
Args.setParam(new String[]{"--output-directory", dbPath}, | ||
confFile); | ||
context = new TronApplicationContext(DefaultConfig.class); | ||
RpcApiService rpcApiService = context.getBean(RpcApiService.class); | ||
appTest = ApplicationFactory.create(context); | ||
appTest.addService(rpcApiService); | ||
appTest.initServices(argsTest); | ||
appTest.startServices(); | ||
appTest.startup(); | ||
channelFull = ManagedChannelBuilder.forTarget(serviceNode) | ||
.usePlaintext(true) | ||
.build(); | ||
blockingStubFull = WalletGrpc.newBlockingStub(channelFull); | ||
} | ||
|
||
/** | ||
* destroy the context. | ||
*/ | ||
@After | ||
public void destroy() throws InterruptedException { | ||
if (channelFull != null) { | ||
channelFull.shutdown().awaitTermination(5, TimeUnit.SECONDS); | ||
} | ||
Args.clearParam(); | ||
appTest.shutdownServices(); | ||
appTest.shutdown(); | ||
context.destroy(); | ||
FileUtil.deleteDir(new File(dbPath)); | ||
} | ||
|
||
@Test | ||
public void sumActuatorTest() { | ||
// this key is defined in config-localtest.conf as accountName=Sun | ||
String key = "cba92a516ea09f620a16ff7ee95ce0df1d56550a8babe9964981a7144c8a784a"; | ||
byte[] address = PublicMethed.getFinalAddress(key); | ||
ECKey ecKey = null; | ||
try { | ||
BigInteger priK = new BigInteger(key, 16); | ||
ecKey = ECKey.fromPrivate(priK); | ||
} catch (Exception ex) { | ||
ex.printStackTrace(); | ||
} | ||
|
||
// build contract | ||
MathContract.SumContract.Builder builder = MathContract.SumContract.newBuilder(); | ||
builder.setParam1(1); | ||
builder.setParam2(2); | ||
builder.setOwnerAddress(ByteString.copyFrom(address)); | ||
MathContract.SumContract contract = builder.build(); | ||
|
||
// send contract and return transaction | ||
Protocol.Transaction transaction = blockingStubFull.invokeSum(contract); | ||
// sign trx | ||
transaction = signTransaction(ecKey, transaction); | ||
// broadcast transaction | ||
GrpcAPI.Return response = blockingStubFull.broadcastTransaction(transaction); | ||
Assert.assertNotNull(response); | ||
} | ||
|
||
private Protocol.Transaction signTransaction(ECKey ecKey, Protocol.Transaction transaction) { | ||
if (ecKey == null || ecKey.getPrivKey() == null) { | ||
logger.warn("Warning: Can't sign,there is no private key !!"); | ||
return null; | ||
} | ||
transaction = TransactionUtils.setTimestamp(transaction); | ||
return TransactionUtils.sign(transaction, ecKey); | ||
} | ||
} | ||
``` | ||
|
||
运行 SumActuatorTest 测试类即可在log文件中看到 `SumActuator: param1 = 1, param2 = 2, sum = 3` 类似的输出字样,得到如下输出: | ||
|
||
```text | ||
INFO [o.r.Reflections] Reflections took 420 ms to scan 9 urls, producing 381 keys and 2047 values | ||
INFO [discover] homeNode : Node{ host='0.0.0.0', port=6666, id=1d4bbab782f4021586b4dd2027da2d8438a10297ade13b1e33c3e83354a7cfaf608dfe23677757921c38068a4baf3ce6a9deedaa2f43696f8441f683246a7083} | ||
INFO [net] start the PeerConnectionCheckService | ||
INFO [API] RpcApiService has started, listening on 50051 | ||
INFO [net] Node config, trust 0, active 0, forward 0. | ||
INFO [discover] Discovery server started, bind port 6666 | ||
INFO [net] Fast forward config, isWitness: false, keySize: 1, fastForwardNodes: 0 | ||
INFO [net] TronNetService start successfully. | ||
INFO [net] TCP listener started, bind port 6666 | ||
INFO [Configuration] user defined config file doesn't exists, use default config file in jar | ||
INFO [actuator] | ||
------------------------------------------------- | ||
| | ||
| SumActuator: param1 = 1, param2 = 2, sum = 3 | ||
| | ||
------------------------------------------------- | ||
``` | ||
|
||
至此,SumActuator 已基本实现完毕,这只是一个最简单的例子,真正的业务场景还需要做一些额外的工作,比如在 wallet-cli 中提供对应的合约支持、定制合适的 chainbase 存储等。 |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
# 模块化介绍 | ||
|
||
## 模块化的初衷 | ||
|
||
基于以太坊上的CryptoKitties游戏高峰时期甚至占据了以太坊16%的流量,造成严重的网络拥堵;即便波场的性能约是以太坊的100倍,但依然存在极限,为了更好的进行水平扩展需要对 java-tron 进行模块化拆分,模块化后的 java-tron 可以让应用开发者能够轻易的研发并部署一条区块链,而不仅仅是研发部署一个链上的应用(One Dapp is One Chain),这将会降低区块链基础设施开发的成本,而且模块化可以帮助开发者更有效的定制符合自身业务的模块,比如抽象后的共识模块可以帮助业务针对具体的场景来选择合适的共识机制。模块后的 java-tron 将区块链本身的底层实现细节对开发者屏蔽,让应用开发者更加专注于业务场景。 | ||
|
||
java-tron 模块化的目的是为了帮助开发者方便地构建出特定应用的区块链,一个应用即是一条链。有以下几点优势: | ||
|
||
1. 代码层面上的模块化将使系统架构更清晰,代码更加易于维护扩展 | ||
2. 各个模块皆是独立组件,模块化后有利于组件产品化和提高产品成熟度 | ||
3. 面向接口的开发模式使模块更加解耦,实现模块可插拔,满足不同业务需求 | ||
|
||
## 模块化的 java-tron 架构介绍 | ||
|
||
![modular-structure](https://github.com/tronprotocol/java-tron/tree/develop/docs/images/module.png) | ||
|
||
模块化后的 java-tron 目前分为6个模块:framework、protocol、common、chainbase、consensus、actuator,下面分别简单介绍一下各个模块的作用。 | ||
|
||
### framework | ||
|
||
framework 是 java-tron 的核心模块,不仅是整个链的入口模块,同时也充当粘合剂的作用将其他模块有机地组织起来,framework 模块负责各个模块的初始化、流程的跳转。 | ||
|
||
### protocol | ||
|
||
对于区块链这种分布式网络,简洁高效的数据交换协议尤为重要,protocol 模块定义了外界与 java-tron 交互的二进制协议格式,是 java-tron 实现跨语言跨平台的基础,该模块同时定义了: | ||
1. java-tron 内部节点间的通信协议 | ||
2. java-tron 对外提供的服务协议 | ||
|
||
### common | ||
|
||
common 模块对公共组件和一些工具类进行了封装,以方便其他模块调用。 | ||
|
||
### chainbase | ||
|
||
chainbase 模块是数据库层面的抽象,像 PoW、PoS、DPoS 这类基于概率性的共识算法不可避免的会以一定的概率发生切链,因此 chainbase 定义了一个支持可回退数据库的接口标准,该接口要求数据库实现状态回滚机制、checkpoint容灾机制等。 | ||
另外 chainbase 模块具有良好的接口抽象设计,任何满足接口实现的数据库都可以作为区块链的底层存储,赋予开发者更多的灵活性,LevelDB和RocksDB是默认提供的两种具体实现。 | ||
|
||
下面简单的为大家介绍一下 chainbase 模块中重要的几个实现类和接口: | ||
1. RevokingDatabase: 是数据库容器的接口,用于所有可回退数据库的管理,SnapshotManager 是该接口的一个实现 | ||
2. TronStoreWithRevoking: 支持可回退的数据库的基类,Chainbase 类是它的具体实现 | ||
|
||
### consensus | ||
|
||
共识机制是区块链中非常重要的模块,常见的有 PoW、PoS、DPoS、PBFT 等,联盟链以及其他一些可信网络中也会采用 Paxos、Raft 等共识机制,共识的选择需要和业务场景相匹配,比如对共识效率敏感实时游戏类就不适合采用 PoW,而对实时性要求极高的交易所来说 PBFT 可能是首选。所以支持可替换的共识将是一个有非常有想象力的创造,同时也是实现特定应用区块链的重要一环,即便像 Cosmos SDK 这样的明星区块链项目依然也停留在应用层为开发者提供一些自主性,底层的共识依然受限于 Tendermint 共识。 | ||
consensus 模块最终目标是能够让应用开发者能够像配置参数那样简单的切换共识机制。 | ||
|
||
consensus 模块将共识过程抽象成几个重要的部分,定义在 ConsensusInterface 接口中: | ||
1. start: 启动共识服务,可以自定制启动参数 | ||
2. stop: 停止共识服务 | ||
3. receiveBlock: 定义接收区块的共识逻辑 | ||
4. validBlock: 定义验证区块的共识逻辑 | ||
5. applyBlock: 定义处理区块的共识逻辑 | ||
|
||
应用开发者可以通过实现 ConsensusInterface 接口来定制共识,同时社区也正在探索 PBFT 和 DPoS 组合共识的方案,进一步降低区块校验延时来满足对实时性要求更高的场景,届时这个混合共识将成为可替换共识的具体案例。 | ||
|
||
### actuator | ||
|
||
以太坊初创性的引入了虚拟机并定义了智能合约这种开发方式,但对于一些复杂的应用,智能合约不够灵活且受限于性能,这也是 java-tron 提供创建应用链的一个原因。为此 java-tron 独立出来了 actuator 模块,该模块为应用开发者提供一种新的开发范式:可以将应用代码直接植入链中而不再将应用代码跑在虚拟机中。 | ||
actuator 是交易的执行器,可以将应用看成是不同交易类型组成的交易集,每类交易都由对应的 actuator 负责执行。 | ||
|
||
actuator模块定义了 Actuator 接口,该接口有4个方法: | ||
1. execute: 负责交易具体需要执行的动作,可以是状态修改、流程跳转、逻辑判断... | ||
2. validate: 负责验证交易的正确性 | ||
3. getOwnerAddress: 获取交易发起方的地址 | ||
4. calcFee: 定义交易手续费计算逻辑 | ||
|
||
开发者可以根据自身业务实现 Actuator 接口,就能实现自定义交易类型的处理。 | ||
|