provider-fallback.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.FallbackProvider = void 0;
  4. /**
  5. * A **FallbackProvider** provides resilience, security and performance
  6. * in a way that is customizable and configurable.
  7. *
  8. * @_section: api/providers/fallback-provider:Fallback Provider [about-fallback-provider]
  9. */
  10. const index_js_1 = require("../utils/index.js");
  11. const abstract_provider_js_1 = require("./abstract-provider.js");
  12. const network_js_1 = require("./network.js");
  13. const BN_1 = BigInt("1");
  14. const BN_2 = BigInt("2");
  15. function shuffle(array) {
  16. for (let i = array.length - 1; i > 0; i--) {
  17. const j = Math.floor(Math.random() * (i + 1));
  18. const tmp = array[i];
  19. array[i] = array[j];
  20. array[j] = tmp;
  21. }
  22. }
  23. function stall(duration) {
  24. return new Promise((resolve) => { setTimeout(resolve, duration); });
  25. }
  26. function getTime() { return (new Date()).getTime(); }
  27. function stringify(value) {
  28. return JSON.stringify(value, (key, value) => {
  29. if (typeof (value) === "bigint") {
  30. return { type: "bigint", value: value.toString() };
  31. }
  32. return value;
  33. });
  34. }
  35. ;
  36. const defaultConfig = { stallTimeout: 400, priority: 1, weight: 1 };
  37. const defaultState = {
  38. blockNumber: -2, requests: 0, lateResponses: 0, errorResponses: 0,
  39. outOfSync: -1, unsupportedEvents: 0, rollingDuration: 0, score: 0,
  40. _network: null, _updateNumber: null, _totalTime: 0,
  41. _lastFatalError: null, _lastFatalErrorTimestamp: 0
  42. };
  43. async function waitForSync(config, blockNumber) {
  44. while (config.blockNumber < 0 || config.blockNumber < blockNumber) {
  45. if (!config._updateNumber) {
  46. config._updateNumber = (async () => {
  47. try {
  48. const blockNumber = await config.provider.getBlockNumber();
  49. if (blockNumber > config.blockNumber) {
  50. config.blockNumber = blockNumber;
  51. }
  52. }
  53. catch (error) {
  54. config.blockNumber = -2;
  55. config._lastFatalError = error;
  56. config._lastFatalErrorTimestamp = getTime();
  57. }
  58. config._updateNumber = null;
  59. })();
  60. }
  61. await config._updateNumber;
  62. config.outOfSync++;
  63. if (config._lastFatalError) {
  64. break;
  65. }
  66. }
  67. }
  68. function _normalize(value) {
  69. if (value == null) {
  70. return "null";
  71. }
  72. if (Array.isArray(value)) {
  73. return "[" + (value.map(_normalize)).join(",") + "]";
  74. }
  75. if (typeof (value) === "object" && typeof (value.toJSON) === "function") {
  76. return _normalize(value.toJSON());
  77. }
  78. switch (typeof (value)) {
  79. case "boolean":
  80. case "symbol":
  81. return value.toString();
  82. case "bigint":
  83. case "number":
  84. return BigInt(value).toString();
  85. case "string":
  86. return JSON.stringify(value);
  87. case "object": {
  88. const keys = Object.keys(value);
  89. keys.sort();
  90. return "{" + keys.map((k) => `${JSON.stringify(k)}:${_normalize(value[k])}`).join(",") + "}";
  91. }
  92. }
  93. console.log("Could not serialize", value);
  94. throw new Error("Hmm...");
  95. }
  96. function normalizeResult(value) {
  97. if ("error" in value) {
  98. const error = value.error;
  99. return { tag: _normalize(error), value: error };
  100. }
  101. const result = value.result;
  102. return { tag: _normalize(result), value: result };
  103. }
  104. // This strategy picks the highest weight result, as long as the weight is
  105. // equal to or greater than quorum
  106. function checkQuorum(quorum, results) {
  107. const tally = new Map();
  108. for (const { value, tag, weight } of results) {
  109. const t = tally.get(tag) || { value, weight: 0 };
  110. t.weight += weight;
  111. tally.set(tag, t);
  112. }
  113. let best = null;
  114. for (const r of tally.values()) {
  115. if (r.weight >= quorum && (!best || r.weight > best.weight)) {
  116. best = r;
  117. }
  118. }
  119. if (best) {
  120. return best.value;
  121. }
  122. return undefined;
  123. }
  124. function getMedian(quorum, results) {
  125. let resultWeight = 0;
  126. const errorMap = new Map();
  127. let bestError = null;
  128. const values = [];
  129. for (const { value, tag, weight } of results) {
  130. if (value instanceof Error) {
  131. const e = errorMap.get(tag) || { value, weight: 0 };
  132. e.weight += weight;
  133. errorMap.set(tag, e);
  134. if (bestError == null || e.weight > bestError.weight) {
  135. bestError = e;
  136. }
  137. }
  138. else {
  139. values.push(BigInt(value));
  140. resultWeight += weight;
  141. }
  142. }
  143. if (resultWeight < quorum) {
  144. // We have quorum for an error
  145. if (bestError && bestError.weight >= quorum) {
  146. return bestError.value;
  147. }
  148. // We do not have quorum for a result
  149. return undefined;
  150. }
  151. // Get the sorted values
  152. values.sort((a, b) => ((a < b) ? -1 : (b > a) ? 1 : 0));
  153. const mid = Math.floor(values.length / 2);
  154. // Odd-length; take the middle value
  155. if (values.length % 2) {
  156. return values[mid];
  157. }
  158. // Even length; take the ceiling of the mean of the center two values
  159. return (values[mid - 1] + values[mid] + BN_1) / BN_2;
  160. }
  161. function getAnyResult(quorum, results) {
  162. // If any value or error meets quorum, that is our preferred result
  163. const result = checkQuorum(quorum, results);
  164. if (result !== undefined) {
  165. return result;
  166. }
  167. // Otherwise, do we have any result?
  168. for (const r of results) {
  169. if (r.value) {
  170. return r.value;
  171. }
  172. }
  173. // Nope!
  174. return undefined;
  175. }
  176. function getFuzzyMode(quorum, results) {
  177. if (quorum === 1) {
  178. return (0, index_js_1.getNumber)(getMedian(quorum, results), "%internal");
  179. }
  180. const tally = new Map();
  181. const add = (result, weight) => {
  182. const t = tally.get(result) || { result, weight: 0 };
  183. t.weight += weight;
  184. tally.set(result, t);
  185. };
  186. for (const { weight, value } of results) {
  187. const r = (0, index_js_1.getNumber)(value);
  188. add(r - 1, weight);
  189. add(r, weight);
  190. add(r + 1, weight);
  191. }
  192. let bestWeight = 0;
  193. let bestResult = undefined;
  194. for (const { weight, result } of tally.values()) {
  195. // Use this result, if this result meets quorum and has either:
  196. // - a better weight
  197. // - or equal weight, but the result is larger
  198. if (weight >= quorum && (weight > bestWeight || (bestResult != null && weight === bestWeight && result > bestResult))) {
  199. bestWeight = weight;
  200. bestResult = result;
  201. }
  202. }
  203. return bestResult;
  204. }
  205. /**
  206. * A **FallbackProvider** manages several [[Providers]] providing
  207. * resilience by switching between slow or misbehaving nodes, security
  208. * by requiring multiple backends to aggree and performance by allowing
  209. * faster backends to respond earlier.
  210. *
  211. */
  212. class FallbackProvider extends abstract_provider_js_1.AbstractProvider {
  213. /**
  214. * The number of backends that must agree on a value before it is
  215. * accpeted.
  216. */
  217. quorum;
  218. /**
  219. * @_ignore:
  220. */
  221. eventQuorum;
  222. /**
  223. * @_ignore:
  224. */
  225. eventWorkers;
  226. #configs;
  227. #height;
  228. #initialSyncPromise;
  229. /**
  230. * Creates a new **FallbackProvider** with %%providers%% connected to
  231. * %%network%%.
  232. *
  233. * If a [[Provider]] is included in %%providers%%, defaults are used
  234. * for the configuration.
  235. */
  236. constructor(providers, network, options) {
  237. super(network, options);
  238. this.#configs = providers.map((p) => {
  239. if (p instanceof abstract_provider_js_1.AbstractProvider) {
  240. return Object.assign({ provider: p }, defaultConfig, defaultState);
  241. }
  242. else {
  243. return Object.assign({}, defaultConfig, p, defaultState);
  244. }
  245. });
  246. this.#height = -2;
  247. this.#initialSyncPromise = null;
  248. if (options && options.quorum != null) {
  249. this.quorum = options.quorum;
  250. }
  251. else {
  252. this.quorum = Math.ceil(this.#configs.reduce((accum, config) => {
  253. accum += config.weight;
  254. return accum;
  255. }, 0) / 2);
  256. }
  257. this.eventQuorum = 1;
  258. this.eventWorkers = 1;
  259. (0, index_js_1.assertArgument)(this.quorum <= this.#configs.reduce((a, c) => (a + c.weight), 0), "quorum exceed provider weight", "quorum", this.quorum);
  260. }
  261. get providerConfigs() {
  262. return this.#configs.map((c) => {
  263. const result = Object.assign({}, c);
  264. for (const key in result) {
  265. if (key[0] === "_") {
  266. delete result[key];
  267. }
  268. }
  269. return result;
  270. });
  271. }
  272. async _detectNetwork() {
  273. return network_js_1.Network.from((0, index_js_1.getBigInt)(await this._perform({ method: "chainId" })));
  274. }
  275. // @TODO: Add support to select providers to be the event subscriber
  276. //_getSubscriber(sub: Subscription): Subscriber {
  277. // throw new Error("@TODO");
  278. //}
  279. /**
  280. * Transforms a %%req%% into the correct method call on %%provider%%.
  281. */
  282. async _translatePerform(provider, req) {
  283. switch (req.method) {
  284. case "broadcastTransaction":
  285. return await provider.broadcastTransaction(req.signedTransaction);
  286. case "call":
  287. return await provider.call(Object.assign({}, req.transaction, { blockTag: req.blockTag }));
  288. case "chainId":
  289. return (await provider.getNetwork()).chainId;
  290. case "estimateGas":
  291. return await provider.estimateGas(req.transaction);
  292. case "getBalance":
  293. return await provider.getBalance(req.address, req.blockTag);
  294. case "getBlock": {
  295. const block = ("blockHash" in req) ? req.blockHash : req.blockTag;
  296. return await provider.getBlock(block, req.includeTransactions);
  297. }
  298. case "getBlockNumber":
  299. return await provider.getBlockNumber();
  300. case "getCode":
  301. return await provider.getCode(req.address, req.blockTag);
  302. case "getGasPrice":
  303. return (await provider.getFeeData()).gasPrice;
  304. case "getPriorityFee":
  305. return (await provider.getFeeData()).maxPriorityFeePerGas;
  306. case "getLogs":
  307. return await provider.getLogs(req.filter);
  308. case "getStorage":
  309. return await provider.getStorage(req.address, req.position, req.blockTag);
  310. case "getTransaction":
  311. return await provider.getTransaction(req.hash);
  312. case "getTransactionCount":
  313. return await provider.getTransactionCount(req.address, req.blockTag);
  314. case "getTransactionReceipt":
  315. return await provider.getTransactionReceipt(req.hash);
  316. case "getTransactionResult":
  317. return await provider.getTransactionResult(req.hash);
  318. }
  319. }
  320. // Grab the next (random) config that is not already part of
  321. // the running set
  322. #getNextConfig(running) {
  323. // @TODO: Maybe do a check here to favour (heavily) providers that
  324. // do not require waitForSync and disfavour providers that
  325. // seem down-ish or are behaving slowly
  326. const configs = Array.from(running).map((r) => r.config);
  327. // Shuffle the states, sorted by priority
  328. const allConfigs = this.#configs.slice();
  329. shuffle(allConfigs);
  330. allConfigs.sort((a, b) => (a.priority - b.priority));
  331. for (const config of allConfigs) {
  332. if (config._lastFatalError) {
  333. continue;
  334. }
  335. if (configs.indexOf(config) === -1) {
  336. return config;
  337. }
  338. }
  339. return null;
  340. }
  341. // Adds a new runner (if available) to running.
  342. #addRunner(running, req) {
  343. const config = this.#getNextConfig(running);
  344. // No runners available
  345. if (config == null) {
  346. return null;
  347. }
  348. // Create a new runner
  349. const runner = {
  350. config, result: null, didBump: false,
  351. perform: null, staller: null
  352. };
  353. const now = getTime();
  354. // Start performing this operation
  355. runner.perform = (async () => {
  356. try {
  357. config.requests++;
  358. const result = await this._translatePerform(config.provider, req);
  359. runner.result = { result };
  360. }
  361. catch (error) {
  362. config.errorResponses++;
  363. runner.result = { error };
  364. }
  365. const dt = (getTime() - now);
  366. config._totalTime += dt;
  367. config.rollingDuration = 0.95 * config.rollingDuration + 0.05 * dt;
  368. runner.perform = null;
  369. })();
  370. // Start a staller; when this times out, it's time to force
  371. // kicking off another runner because we are taking too long
  372. runner.staller = (async () => {
  373. await stall(config.stallTimeout);
  374. runner.staller = null;
  375. })();
  376. running.add(runner);
  377. return runner;
  378. }
  379. // Initializes the blockNumber and network for each runner and
  380. // blocks until initialized
  381. async #initialSync() {
  382. let initialSync = this.#initialSyncPromise;
  383. if (!initialSync) {
  384. const promises = [];
  385. this.#configs.forEach((config) => {
  386. promises.push((async () => {
  387. await waitForSync(config, 0);
  388. if (!config._lastFatalError) {
  389. config._network = await config.provider.getNetwork();
  390. }
  391. })());
  392. });
  393. this.#initialSyncPromise = initialSync = (async () => {
  394. // Wait for all providers to have a block number and network
  395. await Promise.all(promises);
  396. // Check all the networks match
  397. let chainId = null;
  398. for (const config of this.#configs) {
  399. if (config._lastFatalError) {
  400. continue;
  401. }
  402. const network = (config._network);
  403. if (chainId == null) {
  404. chainId = network.chainId;
  405. }
  406. else if (network.chainId !== chainId) {
  407. (0, index_js_1.assert)(false, "cannot mix providers on different networks", "UNSUPPORTED_OPERATION", {
  408. operation: "new FallbackProvider"
  409. });
  410. }
  411. }
  412. })();
  413. }
  414. await initialSync;
  415. }
  416. async #checkQuorum(running, req) {
  417. // Get all the result objects
  418. const results = [];
  419. for (const runner of running) {
  420. if (runner.result != null) {
  421. const { tag, value } = normalizeResult(runner.result);
  422. results.push({ tag, value, weight: runner.config.weight });
  423. }
  424. }
  425. // Are there enough results to event meet quorum?
  426. if (results.reduce((a, r) => (a + r.weight), 0) < this.quorum) {
  427. return undefined;
  428. }
  429. switch (req.method) {
  430. case "getBlockNumber": {
  431. // We need to get the bootstrap block height
  432. if (this.#height === -2) {
  433. this.#height = Math.ceil((0, index_js_1.getNumber)(getMedian(this.quorum, this.#configs.filter((c) => (!c._lastFatalError)).map((c) => ({
  434. value: c.blockNumber,
  435. tag: (0, index_js_1.getNumber)(c.blockNumber).toString(),
  436. weight: c.weight
  437. })))));
  438. }
  439. // Find the mode across all the providers, allowing for
  440. // a little drift between block heights
  441. const mode = getFuzzyMode(this.quorum, results);
  442. if (mode === undefined) {
  443. return undefined;
  444. }
  445. if (mode > this.#height) {
  446. this.#height = mode;
  447. }
  448. return this.#height;
  449. }
  450. case "getGasPrice":
  451. case "getPriorityFee":
  452. case "estimateGas":
  453. return getMedian(this.quorum, results);
  454. case "getBlock":
  455. // Pending blocks are in the mempool and already
  456. // quite untrustworthy; just grab anything
  457. if ("blockTag" in req && req.blockTag === "pending") {
  458. return getAnyResult(this.quorum, results);
  459. }
  460. return checkQuorum(this.quorum, results);
  461. case "call":
  462. case "chainId":
  463. case "getBalance":
  464. case "getTransactionCount":
  465. case "getCode":
  466. case "getStorage":
  467. case "getTransaction":
  468. case "getTransactionReceipt":
  469. case "getLogs":
  470. return checkQuorum(this.quorum, results);
  471. case "broadcastTransaction":
  472. return getAnyResult(this.quorum, results);
  473. }
  474. (0, index_js_1.assert)(false, "unsupported method", "UNSUPPORTED_OPERATION", {
  475. operation: `_perform(${stringify(req.method)})`
  476. });
  477. }
  478. async #waitForQuorum(running, req) {
  479. if (running.size === 0) {
  480. throw new Error("no runners?!");
  481. }
  482. // Any promises that are interesting to watch for; an expired stall
  483. // or a successful perform
  484. const interesting = [];
  485. let newRunners = 0;
  486. for (const runner of running) {
  487. // No responses, yet; keep an eye on it
  488. if (runner.perform) {
  489. interesting.push(runner.perform);
  490. }
  491. // Still stalling...
  492. if (runner.staller) {
  493. interesting.push(runner.staller);
  494. continue;
  495. }
  496. // This runner has already triggered another runner
  497. if (runner.didBump) {
  498. continue;
  499. }
  500. // Got a response (result or error) or stalled; kick off another runner
  501. runner.didBump = true;
  502. newRunners++;
  503. }
  504. // Check if we have reached quorum on a result (or error)
  505. const value = await this.#checkQuorum(running, req);
  506. if (value !== undefined) {
  507. if (value instanceof Error) {
  508. throw value;
  509. }
  510. return value;
  511. }
  512. // Add any new runners, because a staller timed out or a result
  513. // or error response came in.
  514. for (let i = 0; i < newRunners; i++) {
  515. this.#addRunner(running, req);
  516. }
  517. // All providers have returned, and we have no result
  518. (0, index_js_1.assert)(interesting.length > 0, "quorum not met", "SERVER_ERROR", {
  519. request: "%sub-requests",
  520. info: { request: req, results: Array.from(running).map((r) => stringify(r.result)) }
  521. });
  522. // Wait for someone to either complete its perform or stall out
  523. await Promise.race(interesting);
  524. // This is recursive, but at worst case the depth is 2x the
  525. // number of providers (each has a perform and a staller)
  526. return await this.#waitForQuorum(running, req);
  527. }
  528. async _perform(req) {
  529. // Broadcasting a transaction is rare (ish) and already incurs
  530. // a cost on the user, so spamming is safe-ish. Just send it to
  531. // every backend.
  532. if (req.method === "broadcastTransaction") {
  533. // Once any broadcast provides a positive result, use it. No
  534. // need to wait for anyone else
  535. const results = this.#configs.map((c) => null);
  536. const broadcasts = this.#configs.map(async ({ provider, weight }, index) => {
  537. try {
  538. const result = await provider._perform(req);
  539. results[index] = Object.assign(normalizeResult({ result }), { weight });
  540. }
  541. catch (error) {
  542. results[index] = Object.assign(normalizeResult({ error }), { weight });
  543. }
  544. });
  545. // As each promise finishes...
  546. while (true) {
  547. // Check for a valid broadcast result
  548. const done = results.filter((r) => (r != null));
  549. for (const { value } of done) {
  550. if (!(value instanceof Error)) {
  551. return value;
  552. }
  553. }
  554. // Check for a legit broadcast error (one which we cannot
  555. // recover from; some nodes may return the following red
  556. // herring events:
  557. // - alredy seend (UNKNOWN_ERROR)
  558. // - NONCE_EXPIRED
  559. // - REPLACEMENT_UNDERPRICED
  560. const result = checkQuorum(this.quorum, results.filter((r) => (r != null)));
  561. if ((0, index_js_1.isError)(result, "INSUFFICIENT_FUNDS")) {
  562. throw result;
  563. }
  564. // Kick off the next provider (if any)
  565. const waiting = broadcasts.filter((b, i) => (results[i] == null));
  566. if (waiting.length === 0) {
  567. break;
  568. }
  569. await Promise.race(waiting);
  570. }
  571. // Use standard quorum results; any result was returned above,
  572. // so this will find any error that met quorum if any
  573. const result = getAnyResult(this.quorum, results);
  574. (0, index_js_1.assert)(result !== undefined, "problem multi-broadcasting", "SERVER_ERROR", {
  575. request: "%sub-requests",
  576. info: { request: req, results: results.map(stringify) }
  577. });
  578. if (result instanceof Error) {
  579. throw result;
  580. }
  581. return result;
  582. }
  583. await this.#initialSync();
  584. // Bootstrap enough runners to meet quorum
  585. const running = new Set();
  586. let inflightQuorum = 0;
  587. while (true) {
  588. const runner = this.#addRunner(running, req);
  589. if (runner == null) {
  590. break;
  591. }
  592. inflightQuorum += runner.config.weight;
  593. if (inflightQuorum >= this.quorum) {
  594. break;
  595. }
  596. }
  597. const result = await this.#waitForQuorum(running, req);
  598. // Track requests sent to a provider that are still
  599. // outstanding after quorum has been otherwise found
  600. for (const runner of running) {
  601. if (runner.perform && runner.result == null) {
  602. runner.config.lateResponses++;
  603. }
  604. }
  605. return result;
  606. }
  607. async destroy() {
  608. for (const { provider } of this.#configs) {
  609. provider.destroy();
  610. }
  611. super.destroy();
  612. }
  613. }
  614. exports.FallbackProvider = FallbackProvider;
  615. //# sourceMappingURL=provider-fallback.js.map