一、如何对1个交易加速
所谓加速,是指让矿工尽早打包该事务。
对事务加速的方式即是:使用更高的gasPrice,将事务发送出去,并且必须使用原事务的nonce值。
为什么必须要使用原事务的nonce值?如果不使用原事务的nonce值,则原事务在条件达到时,仍旧会被执行。如果合约没有设计重入防护,将引入bug;即使合约设计了重入防护,也会导致无畏的gas消耗。
如果将原事务的nonce值占用,则原事务在被矿工打包时,将因为nonce已被使用而被矿工丢弃。
已知txHash,取出原nonce值
1 2 3 4 5 6 7 | String txHash = ethSendTransaction.getTransactionHash(); EthTransaction transaction = web3j.ethGetTransactionByHash(txHash).send(); if (!transaction.getTransaction().isPresent()) { //错误处理 } else { BigInteger nonce = transaction.getTransaction().get().getNonce(); } |
如果继续使用合约生成的java类,而不把web3j代码剖开,自己合成transaction的话,可以使用下面的方法,简单得更改新事务的nonce值
返回固定nonce值的 transactionManager
1 2 3 4 5 6 7 8 9 | // 创建返回固定值的transactionManager,可以结合上面的代码一起使用; RawTransactionManager rawTransactionManager = new RawTransactionManager(web3j, adminCredential, chainId, queryReceiptAttempts, queryReceiptSleepDuration){ protected BigInteger getNonce() throws IOException { return BigInteger.valueOf( 155 ); } }; //将transactionManager用于创建合约类中,用该临时类创建事务, YoloFoxProxy proxy = YoloFoxProxy.load(proxyAddress, web3j, rawTransactionManager, defaultGasProvider); TransactionReceipt receipt = proxy.setImplementAddress(nftAddress).send(); |
二、如何取消1个事务
取消1个事务的原理和上面加速事务的原理类似:将被取消事务的nonce值挪做它用。
最简单的做法:给自己发送1笔1wei的转账,给予较高的gasPrice即可。
三、只有事务的哈希,事务执行失败,如何得到revert信息?
后端代码执行事务时,通常只记录了事务的hash,并定时去查询事务的receipt,如果发现receipt中事务失败了,但是却没有失败原因(revert信息),该如何获取revert信息呢?
这是来自web3j SDK中的做法:
将原事务的函数调用,按ethCall的形式执行,且在事务失败时的区块上执行,即可即时得到revert信息
获取失败事务失败原因
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // org.web3j.utils.RevertReasonExtractor#retrieveRevertReason // data参数就是 函数及其调用参数编码后的数据 public static String retrieveRevertReason(TransactionReceipt transactionReceipt, String data, Web3j web3j) throws IOException { if (transactionReceipt.getBlockNumber() == null ) { return null ; } //这的原理是:将原事务的函数调用,按ethCall的形式执行,且在事务失败时的区块上执行,即可即时得到revert信息 return web3j.ethCall( Transaction.createEthCallTransaction(transactionReceipt.getFrom(), transactionReceipt.getTo(), data), DefaultBlockParameter.valueOf(transactionReceipt.getBlockNumber())) .send() .getRevertReason(); } |
四、abi.encode/abi.decode 和 abi.encodePacked 对应的链下代码
4.1 合约代码对数据进行abi.decode,链下该如何构造数据上传?
合约代码 折叠源码
1 2 3 4 5 | function _becomeImplementation(bytes memory data) public { //... (address daiJoinAddress_, address potAddress_) = abi.decode(data, (address, address)); //... } |
如果该函数在另外的合约调用,只需要用 abi.encode对数据编码即可:
合约abi.encode 折叠源码
1 2 3 | function test(address a1, address a2) public pure returns(bytes memory) { return abi.encode(a1,a2); } |
链下java代码对应到abi.encode:
对应的java编码方式 折叠源码
1 2 3 4 5 6 7 8 9 10 11 12 | //Type的完整类型:org.web3j.abi.datatypes.Type List<Type> typeList = new ArrayList<>(); //Address的完整类型:org.web3j.abi.datatypes.Address typeList.add( new Address( "0xa2bc756f63521e4Fa1d432Aab74AD29431Cb0361" )); typeList.add( new Address( "0xa2bc756f63521e4Fa1d432Aab74AD29431Cb0362" )); //DefaultFunctionEncoder的完整类型:org.web3j.abi.DefaultFunctionEncoder DefaultFunctionEncoder encoder = new DefaultFunctionEncoder(); // reslut 就是要发送到链上的 参数编码结果 String reslut = encoder.encodeParameters(typeList); |
4.2 java代码对应到abi.encodePacked
常用的链下签名,链上验签的技术中,一般都用 abi.encodePacked 对数据进行打包,再使用 keccak256 对数据进行hash,之后再对hash后的数据签名
例如:
//被签名的内容,包含了 代理者,链上nonce值, 超时时间 bytes32 digest = keccak256(abi.encodePacked( "\x19\x01" , domainSeparator, structHash)); //从签名中恢复出地址 address signatory = ecrecover(digest, v, r, s); |
关于abi.encodePacked的详细说明位于这里
abi.encodePacked编码的两个特点:
非数组参数,不需要pad,即不需要在后面补0补足为32字节;数组参数,不编码数组长度,但是每个元素编码时要pad,补足为32字节;
假设链上的编码数据为:
1 2 3 | //address a1, //address[] memory addrArray, addrArray中有2个地址值 abi.encodePacked(a1, addrArray); |
对应的链下编码代码为:
链下java编码行为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | //链上编码:abi.encodePacked(a1, addrArray) Address a1 = new Address( "0xa2bc756f63521e4Fa1d432Aab74AD29431Cb0361" ); List<Address> addressList = new ArrayList<>(); addressList.add( new Address( "0xa2bc756f63521e4Fa1d432Aab74AD29431Cb0361" )); addressList.add( new Address( "0xa2bc756f63521e4Fa1d432Aab74AD29431Cb0362" )); StringBuilder result = new StringBuilder(); //参考了 org.web3j.abi.TypeEncoder.encodeAddress 的实现 //添加第1个参数 result.append(Numeric.toHexStringNoPrefix(a1.toUint().getValue().toByteArray())); //添加第2个参数 for (Address a : addressList) { byte [] paddedRawValue = new byte [MAX_BYTE_LENGTH]; byte [] rawValue = a.toUint().getValue().toByteArray(); System.arraycopy(rawValue, 0 , paddedRawValue, MAX_BYTE_LENGTH - rawValue.length, rawValue.length); result.append(Numeric.toHexStringNoPrefix(paddedRawValue)); } //最终结果 String likeChainResult = result.toString(); |
4.3 对应keccak256的java函数
1 2 | //org.web3j.crypto.Hash#sha3(java.lang.String) String sha3(String hexInput) |
五、链下签名
链下签名的应用是非常广泛的。
主要流程就是:在链下对特定数据进行签名 → 将签名数据和被签名信息发送到链上 → 链上重新合成被签名数据 → 从签名中恢复出签名者的公钥 → 从公钥计算出签名者的地址 → 检查该地址是否是特定账户的地址。
注意:【为了防止重放攻击,签名数据中必须包含nonce。如果要再安全点,把chainId也包含进来,这样就不会跨链重放攻击了】
在SunFlowerLand这款链游的调研中,就发现【链游后台为了控制用户的行为,在关键路径上全部使用链下签名控制用户的行为】。
下面我们对SunFlowerLand中一处链上验签,完成对应的链下签名代码:
链上恢复签名的代码
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 | function mint( // Verification bytes memory signature, bytes32 sessionId, uint256 deadline, // Data uint256 farmId, uint256 mintId ) external payable isReady(farmId) returns(bool success) { ... // sessionId 就是这里的nonce bytes32 txHash = mintSignature(sessionId, farmId, deadline, mintId); //就是封装了一下keccak256 require(!executed[txHash], "SunflowerLand: Tx Executed" ); require(verify(txHash, signature), "SunflowerLand: Unauthorised" ); ... } //封装计算hash的函数 function mintSignature( bytes32 sessionId, uint256 farmId, uint256 deadline, uint256 mintId ) private view returns(bytes32 success) { return keccak256(abi.encode(mintId, deadline, _msgSender(), sessionId, farmId)); } //从签名数据中恢复出地址 function verify(bytes32 hash, bytes memory signature) private view returns (bool) { //toEthSignedMessageHash 定义在 ECDSA.sol中,为 hash 拼接了特定的头,再次计算hash //这个功能在web3j SDK中也有对应的函数 bytes32 ethSignedHash = hash.toEthSignedMessageHash(); //recover 也定义在 ECDSA.sol中,这里使用EVM的汇编代码,完成了从签名中恢复地址的操作 return ethSignedHash.recover(signature) == signer; } |
结合上面说的知识点,我们完成链下签名的代码:
链下签名代码
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 | //参与计算的变量 Bytes32 sessionId = new Bytes32( new byte [ 32 ]); Uint256 farmId = new Uint256( 0 ); Uint256 deadline = new Uint256( 0 ); Uint256 mintId = new Uint256( 0 ); Address msgSender = new Address( "0x00" ); Credentials msgSenderCred = Credentials.create(Keys.createEcKeyPair()); //计算abi.encode List<Type> params = new ArrayList<Type>(); params.add(mintId); params.add(deadline); params.add(msgSender); params.add(sessionId); params.add(farmId); DefaultFunctionEncoder encoder = new DefaultFunctionEncoder(); //对应到 mintSignature String txHash = Hash.sha3(encoder.encodeParameters(params)); // //对数据进行签名,得到 v, r, s Sign.SignatureData signature = Sign.signPrefixedMessage(Numeric.hexStringToByteArray(txHash), msgSenderCred.getEcKeyPair()); //将v, r, s 拼接成 bytes[] StringBuilder tmpBuilder = new StringBuilder(); tmpBuilder.append(Numeric.toHexStringNoPrefix(signature.getV())); tmpBuilder.append(Numeric.toHexStringNoPrefix(signature.getR())); tmpBuilder.append(Numeric.toHexStringNoPrefix(signature.getS())); //bytes 总是对应到 String 类型 String realSignatureData = tmpBuilder.toString(); //就是最终的签名数据 |
六、监听链上事件
典型的链上事件监听是创建好1个filter后,持续查询该filter对应的变化,但是这种方式在后端有可能宕机的场景下不适用。
此时,我们需要用 org.web3j.protocol.core.Ethereum#ethGetLogs 主动定时查询某段区块内指定的事件,并把查询过的最高区块高度记录下来。