Skip to content

Commit 6554f07

Browse files
authored
Merge pull request #1321 from JoinColony/fix/mtx-broadcaster-rebroadcasts
Adjust robustness of MTX broadcaster
2 parents b9b20f6 + c9f3c19 commit 6554f07

File tree

5 files changed

+175
-88
lines changed

5 files changed

+175
-88
lines changed

hardhat.config.js

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,20 @@ task("test", "Run tests").setAction(async () => {
4444
app.use(bodyParser.json());
4545
app.post("/", async function (req, res) {
4646
try {
47+
// console.log(">>>>>>", req.body);
4748
const response = await hre.network.provider.request(req.body);
49+
// console.log("<<<<<<", response);
4850
res.send({ jsonrpc: "2.0", result: response, id: req.body.id });
4951
} catch (error) {
50-
const abiCoder = new ethers.utils.AbiCoder();
51-
const decoded = abiCoder.decode(["string"], `0x${error.data.slice(10)}`);
52-
res.send({
53-
jsonrpc: "2.0",
54-
error: { message: `Error: VM Exception while processing transaction: reverted with reason string '${decoded[0]}'` },
55-
id: req.body.id,
56-
});
52+
console.log(error);
53+
try {
54+
const abiCoder = new ethers.utils.AbiCoder();
55+
const decoded = abiCoder.decode(["string"], `0x${error.data.slice(10)}`);
56+
const message = `Error: VM Exception while processing transaction: reverted with reason string '${decoded[0]}'`;
57+
res.send({ jsonrpc: "2.0", error: { message }, id: req.body.id });
58+
} catch (e) {
59+
res.send({ jsonrpc: "2.0", error: { message: error.message }, id: req.body.id });
60+
}
5761
}
5862
});
5963

helpers/test-helper.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,25 @@ exports.revert = async function revert(provider, snapshotId) {
579579
});
580580
};
581581

582+
exports.hardhatDropTransaction = async function hardhatDropTransaction(provider, txHash) {
583+
return new Promise((resolve, reject) => {
584+
provider.send(
585+
{
586+
jsonrpc: "2.0",
587+
method: "hardhat_dropTransaction",
588+
params: [txHash],
589+
id: new Date().getTime(),
590+
},
591+
(err) => {
592+
if (err) {
593+
return reject(err);
594+
}
595+
return resolve();
596+
},
597+
);
598+
});
599+
};
600+
582601
exports.hardhatSnapshot = async function hardhatSnapshot(provider) {
583602
const res = await provider.request({
584603
method: "evm_snapshot",

packages/package-utils/ExtendedNonceManager.js

Lines changed: 13 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,21 @@
11
const { NonceManager } = require("@ethersproject/experimental");
22

33
class ExtendedNonceManager extends NonceManager {
4-
constructor(signer) {
5-
super(signer);
6-
this.signedTransactions = {};
7-
this.nonce = 0;
8-
this.signer.provider.on("block", async () => {
9-
Object.keys(this.signedTransactions).map(async (txHash) => {
10-
const nodeTx = await this.signer.provider.getTransaction(txHash);
11-
if (!nodeTx) {
12-
this.signer.provider.sendTransaction(this.signedTransactions[txHash]);
13-
return;
14-
}
15-
if (nodeTx.blockNumber) {
16-
// It's been mined, so forget it.
17-
delete this.signedTransactions[txHash];
18-
}
19-
// Otherwise it's known, but not mined yet. No action required.
20-
});
21-
});
22-
}
23-
244
async sendTransaction(transactionRequest) {
25-
// What nonce are we going to attach to this?
26-
// Definitely not any we've sent and are pending
27-
const pendingNonces = Object.keys(this.signedTransactions).map((txhash) => this.signedTransactions[txhash].nonce);
28-
29-
// At least whatever the endpoint says, or whatever we've already sent if higher
30-
let nonce = await this.signer.getTransactionCount();
31-
32-
// Note the order we did the above two lines in - if a tx is mined between these two lines,
33-
// and got removed by the `on block` handler above, by doing it in this order we won't be tripped up
34-
// And we'll skip any nonces we've already used
35-
while (pendingNonces.includes(nonce)) {
36-
nonce += 1;
5+
try {
6+
const txCount = await this.signer.getTransactionCount("latest");
7+
this.setTransactionCount(txCount);
8+
const response = super.sendTransaction(transactionRequest);
9+
const tx = await response;
10+
await tx.wait();
11+
return response;
12+
} catch (e) {
13+
if (e.code === "NONCE_EXPIRED" || e.code === "TRANSACTION_REPLACED") {
14+
// The nonce has expired, so try again
15+
return this.sendTransaction(transactionRequest);
16+
}
17+
throw e;
3718
}
38-
transactionRequest.nonce = nonce; // eslint-disable-line no-param-reassign
39-
this.nonce = nonce + 1;
40-
const response = super.sendTransaction(transactionRequest);
41-
const tx = await response;
42-
this.signedTransactions[tx.hash] = transactionRequest;
43-
return response;
4419
}
4520
}
4621

packages/package-utils/RetryProvider.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ class RetryProvider extends ethers.providers.StaticJsonRpcProvider {
88
}
99

1010
static attemptCheck(err, attemptNumber) {
11-
const allowedErrorCodes = ["CALL_EXCEPTION", "UNPREDICTABLE_GAS_LIMIT"];
11+
const allowedErrorCodes = ["CALL_EXCEPTION", "UNPREDICTABLE_GAS_LIMIT", "NONCE_EXPIRED"];
1212
if (allowedErrorCodes.includes(err.code)) {
1313
console.log(`Got a ${err.code}, no retrying`);
1414
return false;

test/packages/metaTransactionBroadcaster.js

Lines changed: 131 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,15 @@ const axios = require("axios");
99
const { TruffleLoader, RetryProvider } = require("../../packages/package-utils");
1010
const { setupEtherRouter } = require("../../helpers/upgradable-contracts");
1111
const { UINT256_MAX, CURR_VERSION } = require("../../helpers/constants");
12-
const { web3GetTransaction, currentBlockTime } = require("../../helpers/test-helper");
12+
const {
13+
web3GetTransaction,
14+
currentBlockTime,
15+
stopMining,
16+
startMining,
17+
sleep,
18+
hardhatDropTransaction,
19+
mineBlock,
20+
} = require("../../helpers/test-helper");
1321

1422
const MetatransactionBroadcaster = require("../../packages/metatransaction-broadcaster/MetatransactionBroadcaster");
1523
const { getMetaTransactionParameters, getPermitParameters, setupColony } = require("../../helpers/test-data-generator");
@@ -379,17 +387,21 @@ contract("Metatransaction broadcaster", (accounts) => {
379387
expect(balanceAccount2).to.eq.BN(1200000);
380388
const balanceColony = await metaTxToken.balanceOf(colony.address);
381389
expect(balanceColony).to.eq.BN(600000);
390+
391+
// Check the transactions happened with the hashes returned
392+
const tx1 = await web3GetTransaction(txHash);
393+
expect(tx1.gas).to.be.lt.BN(500000);
394+
const tx2 = await web3GetTransaction(txHash2);
395+
expect(tx2.gas).to.be.lt.BN(500000);
382396
});
383397

384-
it("a valid transaction is broadcast and mined, even if the broadcaster's nonce manager fell behind", async function () {
398+
it("a nonce collision is resolved correctly", async function () {
385399
await metaTxToken.unlock();
386400
await metaTxToken.mint(USER0, 1500000, { from: USER0 });
387-
await metaTxToken.mint(USER1, 1500000, { from: USER0 });
388401

389402
const txData = await metaTxToken.contract.methods.transfer(colony.address, 300000).encodeABI();
390403

391404
const { r, s, v } = await getMetaTransactionParameters(txData, USER0, metaTxToken.address);
392-
const { r: r2, s: s2, v: v2 } = await getMetaTransactionParameters(txData, USER1, metaTxToken.address);
393405

394406
// Send to endpoint
395407

@@ -402,21 +414,98 @@ contract("Metatransaction broadcaster", (accounts) => {
402414
v,
403415
};
404416

405-
const res = await axios.post("http://127.0.0.1:3000/broadcast", jsonData, {
417+
await stopMining();
418+
const p = new web3.eth.providers.HttpProvider("http://localhost:8545");
419+
420+
const postRequest = axios.post("http://127.0.0.1:3000/broadcast", jsonData, {
406421
headers: {
407422
"Content-Type": "application/json",
408423
},
409424
});
410425

426+
let firstTxHash = null;
427+
while (!firstTxHash) {
428+
const block = await provider.send("eth_getBlockByNumber", ["pending", true]);
429+
if (block.transactions.length > 0) {
430+
const [firstTx] = block.transactions;
431+
firstTxHash = firstTx.hash;
432+
}
433+
await sleep(1000);
434+
}
435+
436+
await hardhatDropTransaction(p, firstTxHash);
437+
438+
const currentNonce = await provider.getTransactionCount(USER0);
439+
440+
await startMining();
441+
442+
// This will have the nonce of the dropped transaction, which the broadcaster will try to
443+
// rebroadcast when it sees a block, but it will fail because the nonce is too low
444+
// it should then rebroadcast with the correct nonce
445+
await metaTxToken.mint(USER1, 1, { from: USER0 });
446+
447+
let newNonce = 0;
448+
while (newNonce < currentNonce + 2) {
449+
newNonce = await provider.getTransactionCount(USER0);
450+
await mineBlock();
451+
await sleep(1000);
452+
}
453+
const res = await postRequest;
454+
455+
const { txHash } = res.data.data;
456+
expect(txHash.length).to.be.equal(66);
457+
458+
expect(res.data).to.be.deep.equal({
459+
status: "success",
460+
data: {
461+
txHash,
462+
},
463+
});
464+
465+
// Check the transaction happened
466+
const balanceAccount1 = await metaTxToken.balanceOf(USER0);
467+
expect(balanceAccount1).to.eq.BN(1200000);
468+
const balanceColony = await metaTxToken.balanceOf(colony.address);
469+
expect(balanceColony).to.eq.BN(300000);
470+
471+
// Check the transactions happened with the hashes returned
472+
const tx1 = await web3GetTransaction(txHash);
473+
if (!tx1) {
474+
throw new Error("Transaction not found");
475+
}
476+
expect(tx1.gas).to.be.lt.BN(500000);
477+
478+
const originalTx = await web3GetTransaction(firstTxHash);
479+
expect(originalTx).to.be.null;
480+
});
481+
482+
it("a valid transaction is broadcast and mined, even if the broadcaster's nonce manager fell behind", async function () {
483+
await metaTxToken.unlock();
484+
await metaTxToken.mint(USER0, 1500000, { from: USER0 });
485+
await metaTxToken.mint(USER1, 1500000, { from: USER0 });
486+
487+
const txData = await metaTxToken.contract.methods.transfer(colony.address, 300000).encodeABI();
488+
489+
const { r, s, v } = await getMetaTransactionParameters(txData, USER0, metaTxToken.address);
490+
491+
const txCount = await provider.getTransactionCount(accounts[0]);
492+
await broadcaster.nonceManager.setTransactionCount(txCount);
493+
411494
// Make an unexpected transaction
412495
await metaTxToken.mint(USER2, 1500000, { from: USER0 });
413496

414-
jsonData.r = r2;
415-
jsonData.s = s2;
416-
jsonData.v = v2;
417-
jsonData.userAddress = USER1;
497+
// Send to endpoint
498+
499+
const jsonData = {
500+
target: metaTxToken.address,
501+
payload: txData,
502+
userAddress: USER0,
503+
r,
504+
s,
505+
v,
506+
};
418507

419-
await axios.post("http://127.0.0.1:3000/broadcast", jsonData, {
508+
const res = await axios.post("http://127.0.0.1:3000/broadcast", jsonData, {
420509
headers: {
421510
"Content-Type": "application/json",
422511
},
@@ -437,7 +526,11 @@ contract("Metatransaction broadcaster", (accounts) => {
437526
const balanceAccount1 = await metaTxToken.balanceOf(USER0);
438527
expect(balanceAccount1).to.eq.BN(1200000);
439528
const balanceAccount2 = await metaTxToken.balanceOf(colony.address);
440-
expect(balanceAccount2).to.eq.BN(600000);
529+
expect(balanceAccount2).to.eq.BN(300000);
530+
531+
// Check the transactions happened with the hashes returned
532+
const tx1 = await web3GetTransaction(txHash);
533+
expect(tx1.gas).to.be.lt.BN(500000);
441534
});
442535

443536
it("a valid transaction is broadcast and mined, even if the broadcaster's nonce manager got ahead", async function () {
@@ -448,7 +541,6 @@ contract("Metatransaction broadcaster", (accounts) => {
448541
const txData = await metaTxToken.contract.methods.transfer(colony.address, 300000).encodeABI();
449542

450543
const { r, s, v } = await getMetaTransactionParameters(txData, USER0, metaTxToken.address);
451-
const { r: r2, s: s2, v: v2 } = await getMetaTransactionParameters(txData, USER1, metaTxToken.address);
452544

453545
// Send to endpoint
454546

@@ -461,22 +553,11 @@ contract("Metatransaction broadcaster", (accounts) => {
461553
v,
462554
};
463555

464-
let res = await axios.post("http://127.0.0.1:3000/broadcast", jsonData, {
465-
headers: {
466-
"Content-Type": "application/json",
467-
},
468-
});
469-
470556
// Set the nonce
471557
const txCount = await provider.getTransactionCount(accounts[0]);
472558
await broadcaster.nonceManager.setTransactionCount(txCount + 1);
473559

474-
jsonData.r = r2;
475-
jsonData.s = s2;
476-
jsonData.v = v2;
477-
jsonData.userAddress = USER1;
478-
479-
res = await axios.post("http://127.0.0.1:3000/broadcast", jsonData, {
560+
const res = await axios.post("http://127.0.0.1:3000/broadcast", jsonData, {
480561
headers: {
481562
"Content-Type": "application/json",
482563
},
@@ -497,7 +578,11 @@ contract("Metatransaction broadcaster", (accounts) => {
497578
const balanceAccount1 = await metaTxToken.balanceOf(USER0);
498579
expect(balanceAccount1).to.eq.BN(1200000);
499580
const balanceAccount2 = await metaTxToken.balanceOf(colony.address);
500-
expect(balanceAccount2).to.eq.BN(600000);
581+
expect(balanceAccount2).to.eq.BN(300000);
582+
583+
// Check the transaction happened with the hash returned
584+
const tx = await web3GetTransaction(txHash);
585+
expect(tx.gas).to.be.lt.BN(500000);
501586
});
502587

503588
it("a valid EIP712 transaction is broadcast and mined", async function () {
@@ -539,6 +624,10 @@ contract("Metatransaction broadcaster", (accounts) => {
539624
// Check the transaction happened
540625
const allowed = await metaTxToken.allowance(USER0, colony.address);
541626
expect(allowed).to.eq.BN(1);
627+
628+
// With the expected hash
629+
const tx = await web3GetTransaction(txHash);
630+
expect(tx.gas).to.be.lt.BN(500000);
542631
});
543632

544633
it("an EIP712 transaction with an invalid spender is not broadcast and mined", async function () {
@@ -600,26 +689,22 @@ contract("Metatransaction broadcaster", (accounts) => {
600689
s,
601690
v,
602691
};
603-
try {
604-
const res = await axios.post("http://127.0.0.1:3000/broadcast", jsonData, {
605-
headers: {
606-
"Content-Type": "application/json",
607-
},
608-
});
692+
const res = await axios.post("http://127.0.0.1:3000/broadcast", jsonData, {
693+
headers: {
694+
"Content-Type": "application/json",
695+
},
696+
});
609697

610-
const { txHash } = res.data.data;
698+
const { txHash } = res.data.data;
611699

612-
expect(txHash.length).to.be.equal(66);
700+
expect(txHash.length).to.be.equal(66);
613701

614-
expect(res.data).to.be.deep.equal({
615-
status: "success",
616-
data: {
617-
txHash,
618-
},
619-
});
620-
} catch (err) {
621-
console.log(err.response.data);
622-
}
702+
expect(res.data).to.be.deep.equal({
703+
status: "success",
704+
data: {
705+
txHash,
706+
},
707+
});
623708

624709
// Check the transaction happened
625710
const roles = await colony.getUserRoles(USER1, 1);
@@ -628,6 +713,10 @@ contract("Metatransaction broadcaster", (accounts) => {
628713

629714
const expectedRoles = roleArchitecture | roleFunding; // eslint-disable-line no-bitwise
630715
expect(roles).to.equal(ethers.utils.hexZeroPad(ethers.BigNumber.from(expectedRoles).toHexString(), 32));
716+
717+
// With the expected hash
718+
const tx = await web3GetTransaction(txHash);
719+
expect(tx.gas).to.be.lt.BN(500000);
631720
});
632721

633722
it("a multicall transaction that calls something invalid is rejected", async function () {

0 commit comments

Comments
 (0)