Skip to content

Commit e2485b8

Browse files
committedNov 17, 2023
More robust FallbackProvider broadcast (#4186, #4297, #4442).
1 parent 93fb138 commit e2485b8

File tree

2 files changed

+131
-6
lines changed

2 files changed

+131
-6
lines changed
 
+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import assert from "assert";
2+
3+
import {
4+
isError, makeError,
5+
6+
AbstractProvider, FallbackProvider, Network,
7+
} from "../index.js";
8+
9+
import type {
10+
PerformActionRequest
11+
} from "../index.js";
12+
13+
14+
15+
const network = Network.from("mainnet");
16+
17+
function stall(duration: number): Promise<void> {
18+
return new Promise((resolve) => { setTimeout(resolve, duration); });
19+
}
20+
21+
22+
export type Performer = (req: PerformActionRequest) => Promise<any>;
23+
24+
export class MockProvider extends AbstractProvider {
25+
readonly _perform: Performer;
26+
27+
constructor(perform: Performer) {
28+
super(network, { cacheTimeout: -1 });
29+
this._perform = perform;
30+
}
31+
32+
async _detectNetwork(): Promise<Network> { return network; }
33+
34+
async perform(req: PerformActionRequest): Promise<any> {
35+
return await this._perform(req);
36+
}
37+
}
38+
39+
describe("Test Fallback broadcast", function() {
40+
41+
const txHash = "0x33017397ef7c7943dee3b422aec52b0a210de58d73d49c1b3ce455970f01c83a";
42+
43+
async function test(actions: Array<{ timeout: number, error?: Error }>): Promise<any> {
44+
// https://sepolia.etherscan.io/tx/0x33017397ef7c7943dee3b422aec52b0a210de58d73d49c1b3ce455970f01c83a
45+
const tx = "0x02f87683aa36a7048459682f00845d899ef982520894b5bdaa442bb34f27e793861c456cd5bdc527ac8c89056bc75e2d6310000080c001a07503893743e94445b2361a444343757e6f59d52e19e9b3f65eb138d802eaa972a06e4e9bc10ff55474f9aac0a4c284733b4195cb7b273de5e7465ce75a168e0c38";
46+
47+
const providers: Array<MockProvider> = actions.map(({ timeout, error }) => {
48+
return new MockProvider(async (r) => {
49+
if (r.method === "getBlockNumber") { return 1; }
50+
if (r.method === "broadcastTransaction") {
51+
await stall(timeout);
52+
if (error) { throw error; }
53+
return txHash;
54+
}
55+
throw new Error(`unhandled method: ${ r.method }`);
56+
});
57+
});;
58+
59+
const provider = new FallbackProvider(providers);
60+
return await provider.broadcastTransaction(tx);
61+
}
62+
63+
it("picks late non-failed broadcasts", async function() {
64+
const result = await test([
65+
{ timeout: 200, error: makeError("already seen", "UNKNOWN_ERROR") },
66+
{ timeout: 4000, error: makeError("already seen", "UNKNOWN_ERROR") },
67+
{ timeout: 400 },
68+
]);
69+
assert(result.hash === txHash, "result.hash === txHash");
70+
});
71+
72+
it("picks late non-failed broadcasts with quorum-met red-herrings", async function() {
73+
const result = await test([
74+
{ timeout: 200, error: makeError("bad nonce", "NONCE_EXPIRED") },
75+
{ timeout: 400, error: makeError("bad nonce", "NONCE_EXPIRED") },
76+
{ timeout: 1000 },
77+
]);
78+
assert(result.hash === txHash, "result.hash === txHash");
79+
});
80+
81+
it("insufficient funds short-circuit broadcast", async function() {
82+
await assert.rejects(async function() {
83+
const result = await test([
84+
{ timeout: 200, error: makeError("is broke", "INSUFFICIENT_FUNDS") },
85+
{ timeout: 400, error: makeError("is broke", "INSUFFICIENT_FUNDS") },
86+
{ timeout: 800 },
87+
{ timeout: 1000 },
88+
]);
89+
console.log(result);
90+
}, function(error: unknown) {
91+
assert(isError(error, "INSUFFICIENT_FUNDS"));
92+
return true;
93+
});
94+
});
95+
});

‎src.ts/providers/provider-fallback.ts

+36-6
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* @_section: api/providers/fallback-provider:Fallback Provider [about-fallback-provider]
66
*/
77
import {
8-
getBigInt, getNumber, assert, assertArgument
8+
assert, assertArgument, getBigInt, getNumber, isError
99
} from "../utils/index.js";
1010

1111
import { AbstractProvider } from "./abstract-provider.js";
@@ -707,16 +707,46 @@ export class FallbackProvider extends AbstractProvider {
707707
// a cost on the user, so spamming is safe-ish. Just send it to
708708
// every backend.
709709
if (req.method === "broadcastTransaction") {
710-
const results = await Promise.all(this.#configs.map(async ({ provider, weight }) => {
710+
// Once any broadcast provides a positive result, use it. No
711+
// need to wait for anyone else
712+
const results: Array<null | TallyResult> = this.#configs.map((c) => null);
713+
const broadcasts = this.#configs.map(async ({ provider, weight }, index) => {
711714
try {
712715
const result = await provider._perform(req);
713-
return Object.assign(normalizeResult({ result }), { weight });
716+
results[index] = Object.assign(normalizeResult({ result }), { weight });
714717
} catch (error: any) {
715-
return Object.assign(normalizeResult({ error }), { weight });
718+
results[index] = Object.assign(normalizeResult({ error }), { weight });
716719
}
717-
}));
720+
});
721+
722+
// As each promise finishes...
723+
while (true) {
724+
// Check for a valid broadcast result
725+
const done = <Array<any>>results.filter((r) => (r != null));
726+
for (const { value } of done) {
727+
if (!(value instanceof Error)) { return value; }
728+
}
729+
730+
// Check for a legit broadcast error (one which we cannot
731+
// recover from; some nodes may return the following red
732+
// herring events:
733+
// - alredy seend (UNKNOWN_ERROR)
734+
// - NONCE_EXPIRED
735+
// - REPLACEMENT_UNDERPRICED
736+
const result = checkQuorum(this.quorum, <Array<any>>results.filter((r) => (r != null)));
737+
if (isError(result, "INSUFFICIENT_FUNDS")) {
738+
throw result;
739+
}
740+
741+
// Kick off the next provider (if any)
742+
const waiting = broadcasts.filter((b, i) => (results[i] == null));
743+
if (waiting.length === 0) { break; }
744+
await Promise.race(waiting);
745+
}
718746

719-
const result = getAnyResult(this.quorum, results);
747+
// Use standard quorum results; any result was returned above,
748+
// so this will find any error that met quorum if any
749+
const result = getAnyResult(this.quorum, <Array<any>>results);
720750
assert(result !== undefined, "problem multi-broadcasting", "SERVER_ERROR", {
721751
request: "%sub-requests",
722752
info: { request: req, results: results.map(stringify) }

0 commit comments

Comments
 (0)
Please sign in to comment.