一、如何对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 主动定时查询某段区块内指定的事件,并把查询过的最高区块高度记录下来。
所有评论(0)