From 19012ae8c4369360627a0acf42e7be22fede418b Mon Sep 17 00:00:00 2001 From: zhaoguangwei Date: Mon, 13 May 2019 10:15:15 +0800 Subject: [PATCH] intf-contract dev first edition. --- .../contract/jvm/JavaContractCode.java | 70 +++++++++--------- .../transaction/ContractInvocationProxy.java | 8 +- .../ContractInvocationProxyBuilder.java | 53 ++++++++++++- .../blockchain/transaction/ContractType.java | 46 ++++++++++-- .../sdk/samples/SDKDemo_Contract.java | 10 +-- .../jd/blockchain/intgr/IntegrationBase.java | 10 +-- .../jd/blockchain/intgr/IntegrationTest2.java | 6 +- .../intgr/IntegrationTestAll4Redis.java | 6 +- .../intgr/contract/AssetContract.java | 39 ++++++++++ .../src/test/resources/contract.jar | Bin 0 -> 5602 bytes 10 files changed, 188 insertions(+), 60 deletions(-) create mode 100644 source/test/test-integration/src/test/java/test/com/jd/blockchain/intgr/contract/AssetContract.java create mode 100644 source/test/test-integration/src/test/resources/contract.jar diff --git a/source/contract/contract-jvm/src/main/java/com/jd/blockchain/contract/jvm/JavaContractCode.java b/source/contract/contract-jvm/src/main/java/com/jd/blockchain/contract/jvm/JavaContractCode.java index 0d219a65..b51184de 100644 --- a/source/contract/contract-jvm/src/main/java/com/jd/blockchain/contract/jvm/JavaContractCode.java +++ b/source/contract/contract-jvm/src/main/java/com/jd/blockchain/contract/jvm/JavaContractCode.java @@ -74,7 +74,7 @@ public class JavaContractCode implements ContractCode { ReflectionUtils.invokeMethod(beforeMth_, contractMainClassObj, contractEventContext); LOGGER.info("beforeEvent,耗时:" + (System.currentTimeMillis() - startTime)); - Method eventMethod = this.getMethodByAnno(contractMainClassObj, contractEventContext.getEvent()); +// Method eventMethod = this.getMethodByAnno(contractMainClassObj, contractEventContext.getEvent()); startTime = System.currentTimeMillis(); // 反序列化参数; @@ -97,40 +97,40 @@ public class JavaContractCode implements ContractCode { } // 得到当前类中相关方法和注解对应关系; - Method getMethodByAnno(Object classObj, String eventName) { - Class c = classObj.getClass(); - Class contractEventClass = null; - try { - contractEventClass = (Class) c.getClassLoader().loadClass(ContractEvent.class.getName()); - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } - Method[] classMethods = c.getMethods(); - Map methodAnnoMap = new HashMap(); - Map annoMethodMap = new HashMap(); - for (int i = 0; i < classMethods.length; i++) { - Annotation[] a = classMethods[i].getDeclaredAnnotations(); - methodAnnoMap.put(classMethods[i], a); - // 如果当前方法中包含@ContractEvent注解,则将其放入Map; - for (Annotation annotation_ : a) { - // 如果是合同事件类型,则放入map; - if (classMethods[i].isAnnotationPresent(contractEventClass)) { - Object obj = classMethods[i].getAnnotation(contractEventClass); - String annoAllName = obj.toString(); - // format:@com.jd.blockchain.contract.model.ContractEvent(name=transfer-asset) - String eventName_ = obj.toString().substring(BaseConstant.CONTRACT_EVENT_PREFIX.length(), - annoAllName.length() - 1); - annoMethodMap.put(eventName_, classMethods[i]); - break; - } - } - } - if (annoMethodMap.containsKey(eventName)) { - return annoMethodMap.get(eventName); - } else { - return null; - } - } +// Method getMethodByAnno(Object classObj, String eventName) { +// Class c = classObj.getClass(); +// Class contractEventClass = null; +// try { +// contractEventClass = (Class) c.getClassLoader().loadClass(ContractEvent.class.getName()); +// } catch (ClassNotFoundException e) { +// e.printStackTrace(); +// } +// Method[] classMethods = c.getMethods(); +// Map methodAnnoMap = new HashMap(); +// Map annoMethodMap = new HashMap(); +// for (int i = 0; i < classMethods.length; i++) { +// Annotation[] a = classMethods[i].getDeclaredAnnotations(); +// methodAnnoMap.put(classMethods[i], a); +// // 如果当前方法中包含@ContractEvent注解,则将其放入Map; +// for (Annotation annotation_ : a) { +// // 如果是合同事件类型,则放入map; +// if (classMethods[i].isAnnotationPresent(contractEventClass)) { +// Object obj = classMethods[i].getAnnotation(contractEventClass); +// String annoAllName = obj.toString(); +// // format:@com.jd.blockchain.contract.model.ContractEvent(name=transfer-asset) +// String eventName_ = obj.toString().substring(BaseConstant.CONTRACT_EVENT_PREFIX.length(), +// annoAllName.length() - 1); +// annoMethodMap.put(eventName_, classMethods[i]); +// break; +// } +// } +// } +// if (annoMethodMap.containsKey(eventName)) { +// return annoMethodMap.get(eventName); +// } else { +// return null; +// } +// } } } diff --git a/source/ledger/ledger-model/src/main/java/com/jd/blockchain/transaction/ContractInvocationProxy.java b/source/ledger/ledger-model/src/main/java/com/jd/blockchain/transaction/ContractInvocationProxy.java index 47b73020..843643bc 100644 --- a/source/ledger/ledger-model/src/main/java/com/jd/blockchain/transaction/ContractInvocationProxy.java +++ b/source/ledger/ledger-model/src/main/java/com/jd/blockchain/transaction/ContractInvocationProxy.java @@ -4,6 +4,7 @@ import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import com.jd.blockchain.utils.Bytes; +import com.jd.blockchain.utils.serialize.binary.BinarySerializeUtils; public class ContractInvocationProxy implements InvocationHandler { @@ -25,6 +26,10 @@ public class ContractInvocationProxy implements InvocationHandler { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if(contractType == null){ + return "contractType == null, no invoke really."; + } + String event = contractType.getEvent(method); if (event == null) { // 适配 Object 对象的方法; @@ -43,7 +48,6 @@ public class ContractInvocationProxy implements InvocationHandler { private byte[] serializeArgs(Object[] args) { // TODO 根据方法参数的定义序列化参数; - return null; + return BinarySerializeUtils.serialize(args); } - } diff --git a/source/ledger/ledger-model/src/main/java/com/jd/blockchain/transaction/ContractInvocationProxyBuilder.java b/source/ledger/ledger-model/src/main/java/com/jd/blockchain/transaction/ContractInvocationProxyBuilder.java index 195ed348..3dd073d7 100644 --- a/source/ledger/ledger-model/src/main/java/com/jd/blockchain/transaction/ContractInvocationProxyBuilder.java +++ b/source/ledger/ledger-model/src/main/java/com/jd/blockchain/transaction/ContractInvocationProxyBuilder.java @@ -1,9 +1,16 @@ package com.jd.blockchain.transaction; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; import java.lang.reflect.Proxy; +import java.util.HashMap; import java.util.Map; +import com.jd.blockchain.contract.ContractEvent; +import com.jd.blockchain.contract.ContractException; +import com.jd.blockchain.utils.BaseConstant; import com.jd.blockchain.utils.Bytes; +import org.apache.http.annotation.Contract; public class ContractInvocationProxyBuilder { @@ -32,14 +39,58 @@ public class ContractInvocationProxyBuilder { } // 判断是否是标注了合约的接口类型; + if (!isContractType(contractIntf)){ + return null; + } // 解析合约事件处理方法,检查是否有重名; + if(!isUniqueEvent(contractIntf)){ + return null; + } // TODO 检查是否不支持的参数类型; // TODO 检查返回值类型; - return null; + return ContractType.resolve(contractIntf); + } + + + private boolean isUniqueEvent(Class contractIntf) { + boolean isUnique = true; + Method[] classMethods = contractIntf.getMethods(); + Map methodAnnoMap = new HashMap(); + Map annoMethodMap = new HashMap(); + for (int i = 0; i < classMethods.length; i++) { + Annotation[] a = classMethods[i].getDeclaredAnnotations(); + methodAnnoMap.put(classMethods[i], a); + // if current method contains @ContractEvent,then put it in this map; + for (Annotation annotation_ : a) { + if (classMethods[i].isAnnotationPresent(ContractEvent.class)) { + Object obj = classMethods[i].getAnnotation(ContractEvent.class); + String annoAllName = obj.toString(); + // format:@com.jd.blockchain.contract.model.ContractEvent(name=transfer-asset) + String eventName_ = obj.toString().substring(BaseConstant.CONTRACT_EVENT_PREFIX.length(), + annoAllName.length() - 1); + //if annoMethodMap has contained the eventName, too many same eventNames exists probably, say NO! + if(annoMethodMap.containsKey(eventName_)){ + isUnique = false; + } + annoMethodMap.put(eventName_, classMethods[i]); + } + } + } + + return isUnique; } + /** + * is contractType really? identified by @Contract; + * @param contractIntf + * @return + */ + private boolean isContractType(Class contractIntf) { + Annotation annotation = contractIntf.getDeclaredAnnotation(Contract.class); + return annotation != null ? true : false; + } } diff --git a/source/ledger/ledger-model/src/main/java/com/jd/blockchain/transaction/ContractType.java b/source/ledger/ledger-model/src/main/java/com/jd/blockchain/transaction/ContractType.java index c57db077..dddcb789 100644 --- a/source/ledger/ledger-model/src/main/java/com/jd/blockchain/transaction/ContractType.java +++ b/source/ledger/ledger-model/src/main/java/com/jd/blockchain/transaction/ContractType.java @@ -1,8 +1,12 @@ package com.jd.blockchain.transaction; +import com.jd.blockchain.contract.ContractEvent; +import com.jd.blockchain.contract.ContractException; +import com.jd.blockchain.utils.BaseConstant; + +import java.lang.annotation.Annotation; import java.lang.reflect.Method; -import java.util.Set; -import java.util.SortedMap; +import java.util.*; public class ContractType { @@ -48,8 +52,40 @@ public class ContractType { private ContractType() { } - // public static ContractType resolve(Class contractIntf) { - // - // } + public static ContractType resolve(Class contractIntf) { + ContractType contractType = new ContractType(); + //contractIntf contains @Contract and @ContractEvent; + Method[] classMethods = contractIntf.getMethods(); + Map methodAnnoMap = new HashMap(); + for (int i = 0; i < classMethods.length; i++) { + Annotation[] a = classMethods[i].getDeclaredAnnotations(); + methodAnnoMap.put(classMethods[i], a); + // if current method contains @ContractEvent,then put it in this map; + for (Annotation annotation_ : a) { + if (classMethods[i].isAnnotationPresent(ContractEvent.class)) { + Object obj = classMethods[i].getAnnotation(ContractEvent.class); + String annoAllName = obj.toString(); + // format:@com.jd.blockchain.contract.model.ContractEvent(name=transfer-asset) + String eventName_ = obj.toString().substring(BaseConstant.CONTRACT_EVENT_PREFIX.length(), + annoAllName.length() - 1); + //if annoMethodMap has contained the eventName, too many same eventNames exists probably, say NO! + if(contractType.events.containsKey(eventName_)){ + throw new ContractException("too many same eventNames exists in the contract, check it."); + } + contractType.events.put(eventName_, classMethods[i]); + contractType.handleMethods.put(classMethods[i],eventName_); + } + } + } + return contractType; + } + @Override + public String toString() { + return "ContractType{" + + "name='" + name + '\'' + + ", events=" + events + + ", handleMethods=" + handleMethods + + '}'; + } } diff --git a/source/sdk/sdk-samples/src/main/java/com/jd/blockchain/sdk/samples/SDKDemo_Contract.java b/source/sdk/sdk-samples/src/main/java/com/jd/blockchain/sdk/samples/SDKDemo_Contract.java index 6811fd2f..3cff2d52 100644 --- a/source/sdk/sdk-samples/src/main/java/com/jd/blockchain/sdk/samples/SDKDemo_Contract.java +++ b/source/sdk/sdk-samples/src/main/java/com/jd/blockchain/sdk/samples/SDKDemo_Contract.java @@ -30,14 +30,14 @@ public class SDKDemo_Contract { */ public static void demoContract() { // 账本地址; - String ledgerAddress = "ffkjhkeqwiuhivnsh3298josijdocaijsda=="; + String ledgerAddress = "6GgNS3YgtxvZDBMvHEoqDiNZvWdiJ3MMpvRS9kL4DYwr4"; // 节点地址列表; - NetworkAddress[] peerAddrs = { new NetworkAddress("192.168.10.10", 8080), - new NetworkAddress("192.168.10.11", 8080), new NetworkAddress("192.168.10.12", 8080), - new NetworkAddress("192.168.10.13", 8080) }; +// NetworkAddress[] peerAddrs = { new NetworkAddress("192.168.10.10", 8080), +// new NetworkAddress("192.168.10.11", 8080), new NetworkAddress("192.168.10.12", 8080), +// new NetworkAddress("192.168.10.13", 8080) }; // 创建服务代理; - final String GATEWAY_IP = "127.0.0.1"; + final String GATEWAY_IP = "192.168.151.39"; final int GATEWAY_PORT = 80; final boolean SECURE = false; GatewayServiceFactory serviceFactory = GatewayServiceFactory.connect(GATEWAY_IP, GATEWAY_PORT, SECURE, diff --git a/source/test/test-integration/src/test/java/test/com/jd/blockchain/intgr/IntegrationBase.java b/source/test/test-integration/src/test/java/test/com/jd/blockchain/intgr/IntegrationBase.java index e7f7345f..9bf8a31c 100644 --- a/source/test/test-integration/src/test/java/test/com/jd/blockchain/intgr/IntegrationBase.java +++ b/source/test/test-integration/src/test/java/test/com/jd/blockchain/intgr/IntegrationBase.java @@ -28,6 +28,7 @@ import com.jd.blockchain.utils.net.NetworkAddress; import org.apache.commons.io.FileUtils; import org.junit.Assert; import org.springframework.core.io.ClassPathResource; +import test.com.jd.blockchain.intgr.contract.AssetContract; import java.io.File; import java.io.FileInputStream; @@ -470,21 +471,20 @@ public class IntegrationBase { txContentHash = ptx.getHash(); // execute the contract; - testContractExe(adminKey, ledgerHash, userKey, blockchainService, ledgerRepository); + testContractExe(adminKey, ledgerHash, userKey, blockchainService, ledgerRepository, AssetContract.class); return block; } - private void testContractExe(AsymmetricKeypair adminKey, HashDigest ledgerHash, BlockchainKeypair userKey, - BlockchainService blockchainService,LedgerRepository ledgerRepository) { + private void testContractExe(AsymmetricKeypair adminKey, HashDigest ledgerHash, BlockchainKeypair userKey, + BlockchainService blockchainService,LedgerRepository ledgerRepository,Class contractIntf) { LedgerInfo ledgerInfo = blockchainService.getLedger(ledgerHash); LedgerBlock previousBlock = blockchainService.getBlock(ledgerHash, ledgerInfo.getLatestBlockHeight() - 1); // 定义交易; TransactionTemplate txTpl = blockchainService.newTransaction(ledgerHash); - txTpl.contractEvents().send(contractDeployKey.getAddress(), eventName, - ("888##123##" + contractDataKey.getAddress()).getBytes()); + txTpl.contract(contractDeployKey.getAddress(),AssetContract.class).issue(10,"abc"); // 签名; PreparedTransaction ptx = txTpl.prepare(); diff --git a/source/test/test-integration/src/test/java/test/com/jd/blockchain/intgr/IntegrationTest2.java b/source/test/test-integration/src/test/java/test/com/jd/blockchain/intgr/IntegrationTest2.java index 4e8d317b..ec968e54 100644 --- a/source/test/test-integration/src/test/java/test/com/jd/blockchain/intgr/IntegrationTest2.java +++ b/source/test/test-integration/src/test/java/test/com/jd/blockchain/intgr/IntegrationTest2.java @@ -41,6 +41,7 @@ import com.jd.blockchain.utils.concurrent.ThreadInvoker.AsyncCallback; import com.jd.blockchain.utils.net.NetworkAddress; import test.com.jd.blockchain.intgr.IntegratedContext.Node; +import test.com.jd.blockchain.intgr.contract.AssetContract; import test.com.jd.blockchain.intgr.initializer.LedgerInitializeWeb4SingleStepsTest; import test.com.jd.blockchain.intgr.initializer.LedgerInitializeWeb4SingleStepsTest.NodeWebContext; @@ -50,7 +51,7 @@ import test.com.jd.blockchain.intgr.initializer.LedgerInitializeWeb4SingleStepsT public class IntegrationTest2 { // 合约测试使用的初始化数据; BlockchainKeypair contractDeployKey = BlockchainKeyGenerator.getInstance().generate(); - private String contractZipName = "AssetContract3.contract"; + private String contractZipName = "contract.jar"; private String eventName = "issue-asset"; @Test @@ -315,8 +316,7 @@ public class IntegrationTest2 { // 定义交易; TransactionTemplate txTpl = blockchainService.newTransaction(ledgerHash); - txTpl.contractEvents().send(contractDeployKey.getAddress(), eventName, - ("888##999##abc").getBytes()); + txTpl.contract(contractDeployKey.getAddress(), AssetContract.class).issue(10,"abc"); // 签名; PreparedTransaction ptx = txTpl.prepare(); diff --git a/source/test/test-integration/src/test/java/test/com/jd/blockchain/intgr/IntegrationTestAll4Redis.java b/source/test/test-integration/src/test/java/test/com/jd/blockchain/intgr/IntegrationTestAll4Redis.java index bcd2ca80..1339b960 100644 --- a/source/test/test-integration/src/test/java/test/com/jd/blockchain/intgr/IntegrationTestAll4Redis.java +++ b/source/test/test-integration/src/test/java/test/com/jd/blockchain/intgr/IntegrationTestAll4Redis.java @@ -48,6 +48,7 @@ import com.jd.blockchain.utils.codec.HexUtils; import com.jd.blockchain.utils.concurrent.ThreadInvoker.AsyncCallback; import com.jd.blockchain.utils.net.NetworkAddress; +import test.com.jd.blockchain.intgr.contract.AssetContract; import test.com.jd.blockchain.intgr.initializer.LedgerInitializeWeb4SingleStepsTest; public class IntegrationTestAll4Redis { @@ -450,10 +451,7 @@ public class IntegrationTestAll4Redis { // 定义交易; TransactionTemplate txTpl = blockchainService.newTransaction(ledgerHash); - txTpl.contractEvents().send(contractDeployKey.getAddress(), eventName, - ("888##abc##" + contractDataKey.getAddress() + "##" + previousBlock.getHash().toBase58() + "##" - + userKey.getAddress() + "##" + contractDeployKey.getAddress() + "##" + txContentHash.toBase58() - + "##" + pubKeyVal).getBytes()); + txTpl.contract(contractDeployKey.getAddress(), AssetContract.class).issue(10,"abc"); // 签名; PreparedTransaction ptx = txTpl.prepare(); diff --git a/source/test/test-integration/src/test/java/test/com/jd/blockchain/intgr/contract/AssetContract.java b/source/test/test-integration/src/test/java/test/com/jd/blockchain/intgr/contract/AssetContract.java new file mode 100644 index 00000000..68d223fb --- /dev/null +++ b/source/test/test-integration/src/test/java/test/com/jd/blockchain/intgr/contract/AssetContract.java @@ -0,0 +1,39 @@ +package test.com.jd.blockchain.intgr.contract; + +import com.jd.blockchain.contract.Contract; +import com.jd.blockchain.contract.ContractEvent; + +/** + * 示例:一个“资产管理”智能合约; + * + * @author huanghaiquan + * + */ +@Contract +public interface AssetContract { + + /** + * 发行资产; + * + * @param amount + * 新发行的资产数量; + * @param assetHolderAddress + * 新发行的资产的持有账户; + */ + @ContractEvent(name = "issue-asset") + void issue(long amount, String assetHolderAddress); + + /** + * 转移资产 + * + * @param fromAddress + * 转出账户; + * @param toAddress + * 转入账户; + * @param amount + * 转移的资产数额; + */ + @ContractEvent(name = "transfer-asset") + void transfer(String fromAddress, String toAddress, long amount); + +} \ No newline at end of file diff --git a/source/test/test-integration/src/test/resources/contract.jar b/source/test/test-integration/src/test/resources/contract.jar new file mode 100644 index 0000000000000000000000000000000000000000..8cdfebfb565f56ed425cf48ad3c280955ab8d660 GIT binary patch literal 5602 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 ZY-y;V-OgwL0M_l#@AkzsZW$K<@P7fKsw@Bi literal 0 HcmV?d00001