Nonce tracker subprovider

Caches the nonce when a request to getTransactionCount is made
and increments the pending nonce after successful transactions
This commit is contained in:
Jacob Evans
2018-01-31 16:19:47 +11:00
parent d4631e14b2
commit ead990a734
4 changed files with 329 additions and 0 deletions

View File

@@ -1,3 +1,4 @@
declare module 'dirty-chai';
declare module 'es6-promisify';
@@ -13,7 +14,9 @@ declare module 'ethereumjs-tx' {
public r: Buffer;
public s: Buffer;
public v: Buffer;
public nonce: Buffer;
public serialize(): Buffer;
public getSenderAddress(): Buffer;
constructor(txParams: any);
}
export = EthereumTx;
@@ -97,6 +100,24 @@ declare module 'web3-provider-engine' {
}
export = Web3ProviderEngine;
}
declare module 'web3-provider-engine/util/rpc-cache-utils' {
class ProviderEngineRpcUtils {
public static blockTagForPayload(payload: any): string|null;
}
export = ProviderEngineRpcUtils;
}
declare module 'web3-provider-engine/subproviders/fixture' {
import * as Web3 from 'web3';
class FixtureSubprovider {
constructor(staticResponses: any);
public handleRequest(
payload: Web3.JSONRPCRequestPayload,
next: () => void,
end: (err: Error | null, data?: any) => void,
): void;
}
export = FixtureSubprovider;
}
// hdkey declarations
declare module 'hdkey' {

View File

@@ -9,6 +9,7 @@ import { LedgerEthereumClient } from './types';
export { InjectedWeb3Subprovider } from './subproviders/injected_web3';
export { RedundantRPCSubprovider } from './subproviders/redundant_rpc';
export { LedgerSubprovider } from './subproviders/ledger';
export { NonceTrackerSubprovider } from './subproviders/nonce_tracker';
export { ECSignature, LedgerWalletSubprovider, LedgerCommunicationClient } from './types';
/**

View File

@@ -0,0 +1,112 @@
import { promisify } from '@0xproject/utils';
import * as _ from 'lodash';
import EthereumTx = require('ethereumjs-tx');
import ethUtil = require('ethereumjs-util');
import providerEngineUtils = require('web3-provider-engine/util/rpc-cache-utils');
import { JSONRPCPayload } from '../types';
import { Subprovider } from './subprovider';
const NONCE_TOO_LOW_ERROR_MESSAGE = 'Transaction nonce is too low';
export class NonceTrackerSubprovider extends Subprovider {
private _nonceCache: { [address: string]: string } = {};
private static _reconstructTransaction(payload: JSONRPCPayload): EthereumTx {
const raw = payload.params[0];
const transactionData = ethUtil.stripHexPrefix(raw);
const rawData = new Buffer(transactionData, 'hex');
return new EthereumTx(rawData);
}
private static _determineAddress(payload: JSONRPCPayload): string {
switch (payload.method) {
case 'eth_getTransactionCount':
return payload.params[0].toLowerCase();
case 'eth_sendRawTransaction':
const transaction = NonceTrackerSubprovider._reconstructTransaction(payload);
return `0x${transaction.getSenderAddress().toString('hex')}`.toLowerCase();
default:
throw new Error('Invalid Method');
}
}
constructor() {
super();
}
// tslint:disable-next-line:async-suffix
public async handleRequest(
payload: JSONRPCPayload,
next: (callback?: (err: Error | null, result: any, cb: any) => void) => void,
end: (err: Error | null, data?: any) => void,
): Promise<void> {
switch (payload.method) {
case 'eth_getTransactionCount':
const blockTag = providerEngineUtils.blockTagForPayload(payload);
if (!_.isNull(blockTag) && blockTag === 'pending') {
const address = NonceTrackerSubprovider._determineAddress(payload);
const cachedResult = this._nonceCache[address];
if (cachedResult) {
end(null, cachedResult);
return;
} else {
next((requestError: Error | null, requestResult: any, cb: any) => {
if (_.isNull(requestError)) {
this._nonceCache[address] = requestResult as string;
}
cb();
return;
});
return;
}
} else {
next();
return;
}
case 'eth_sendRawTransaction':
return next(async (sendTransactionError: Error | null, txResult: any, cb: any) => {
if (_.isNull(sendTransactionError)) {
this._handleSuccessfulTransaction(payload);
} else {
await this._handleSendTransactionErrorAsync(payload, sendTransactionError);
}
cb();
});
default:
return next();
}
}
private _handleSuccessfulTransaction(payload: JSONRPCPayload): void {
const address = NonceTrackerSubprovider._determineAddress(payload);
const transaction = NonceTrackerSubprovider._reconstructTransaction(payload);
// Increment the nonce from the previous successfully submitted transaction
let nonce = ethUtil.bufferToInt(transaction.nonce);
nonce++;
let nextHexNonce = nonce.toString(16);
if (nextHexNonce.length % 2) {
nextHexNonce = `0${nextHexNonce}`;
}
nextHexNonce = `0x${nextHexNonce}`;
this._nonceCache[address] = nextHexNonce;
}
private async _handleSendTransactionErrorAsync(payload: JSONRPCPayload, err: Error): Promise<void> {
const address = NonceTrackerSubprovider._determineAddress(payload);
if (this._nonceCache[address]) {
if (_.includes(err.message, NONCE_TOO_LOW_ERROR_MESSAGE)) {
await this._handleNonceTooLowErrorAsync(address);
}
}
}
private async _handleNonceTooLowErrorAsync(address: string): Promise<void> {
const oldNonceInt = ethUtil.bufferToInt(new Buffer(this._nonceCache[address], 'hex'));
delete this._nonceCache[address];
const nonceResult = await this.emitPayloadAsync({
method: 'eth_getTransactionCount',
params: [address, 'pending'],
});
const nonce = nonceResult.result;
const latestNonceInt = ethUtil.bufferToInt(new Buffer(nonce, 'hex'));
if (latestNonceInt > oldNonceInt) {
this._nonceCache[address] = nonce;
}
}
}

View File

@@ -0,0 +1,195 @@
import * as chai from 'chai';
import * as _ from 'lodash';
import Web3 = require('web3');
import Web3ProviderEngine = require('web3-provider-engine');
import FixtureSubprovider = require('web3-provider-engine/subproviders/fixture');
import promisify = require('es6-promisify');
import EthereumTx = require('ethereumjs-tx');
import { NonceTrackerSubprovider } from '../../src';
import { DoneCallback } from '../../src/types';
import { chaiSetup } from '../chai_setup';
import { reportCallbackErrors } from '../utils/report_callback_errors';
import { Subprovider } from '../../src/subproviders/subprovider';
const expect = chai.expect;
chaiSetup.configure();
describe('NonceTrackerSubprovider', () => {
let provider: Web3ProviderEngine;
it('successfully caches the transaction count', async () => {
provider = new Web3ProviderEngine();
let called = false;
const nonceTrackerSubprovider = new NonceTrackerSubprovider();
provider.addProvider(nonceTrackerSubprovider);
provider.addProvider(new FixtureSubprovider({
'eth_getBlockByNumber': '0x01',
'eth_getTransactionCount': (data: any, next: any, end: any) => {
if (called) {
return end(null, '0x99');
} else {
called = true;
end(null, '0x01');
}
},
}));
provider.start();
const payload = {
jsonrpc: '2.0',
method: 'eth_getTransactionCount',
params: ['0x0', 'pending'],
id: 1,
};
const response = await promisify(provider.sendAsync, provider)(payload);
expect(response.result).to.be.eq('0x01');
const secondResponse = await promisify(provider.sendAsync, provider)(payload);
expect(secondResponse.result).to.be.eq('0x01');
});
it('does not cache the result for latest transaction count', async () => {
provider = new Web3ProviderEngine();
let called = false;
const nonceTrackerSubprovider = new NonceTrackerSubprovider();
provider.addProvider(nonceTrackerSubprovider);
provider.addProvider(new FixtureSubprovider({
'eth_getBlockByNumber': '0x01',
'eth_getTransactionCount': (data: any, next: any, end: any) => {
if (called) {
return end(null, '0x99');
} else {
called = true;
end(null, '0x01');
}
},
}));
provider.start();
const payload = {
jsonrpc: '2.0',
method: 'eth_getTransactionCount',
params: ['0x0', 'latest'],
id: 1,
};
const response = await promisify(provider.sendAsync, provider)(payload);
expect(response.result).to.be.eq('0x01');
const secondResponse = await promisify(provider.sendAsync, provider)(payload);
expect(secondResponse.result).to.be.eq('0x99');
});
it('clears the cache on a Nonce Too Low Error', async () => {
provider = new Web3ProviderEngine();
let called = false;
const nonceTrackerSubprovider = new NonceTrackerSubprovider();
provider.addProvider(nonceTrackerSubprovider);
provider.addProvider(new FixtureSubprovider({
'eth_getBlockByNumber': '0x01',
'eth_getTransactionCount': (data: any, next: any, end: any) => {
if (called) {
return end(null, '0x99');
} else {
called = true;
end(null, '0x01');
}
},
'eth_sendRawTransaction': (data: any, next: any, end: any) => {
end(new Error('Transaction nonce is too low'));
},
}));
provider.start();
const noncePayload = {
jsonrpc: '2.0',
method: 'eth_getTransactionCount',
params: ['0x1f36f546477cda21bf2296c50976f2740247906f', 'pending'],
id: 1,
};
const txParams = [
'0x',
'0x09184e72a000',
'0x2710',
'0x0000000000000000000000000000000000000000',
'0x',
'0x7f7465737432000000000000000000000000000000000000000000000000000000600057',
'0x1c',
'0x5e1d3a76fbf824220eafc8c79ad578ad2b67d01b0c2425eb1f1347e8f50882ab',
'0x5bd428537f05f9830e93792f90ea6a3e2d1ee84952dd96edbae9f658f831ab13'
];
const transaction = new EthereumTx(txParams);
const txPayload = {
jsonrpc: '2.0',
method: 'eth_sendRawTransaction',
params: [transaction.serialize()],
id: 1,
};
const response = await promisify(provider.sendAsync, provider)(noncePayload);
expect(response.result).to.be.eq('0x01');
const secondResponse = await promisify(provider.sendAsync, provider)(noncePayload);
expect(secondResponse.result).to.be.eq('0x01');
try {
const txResponse = await promisify(provider.sendAsync, provider)(txPayload);
} catch (err) {
const thirdResponse = await promisify(provider.sendAsync, provider)(noncePayload);
expect(thirdResponse.result).to.be.eq('0x99');
}
});
it('increments the used nonce', async () => {
provider = new Web3ProviderEngine();
let called = false;
const nonceTrackerSubprovider = new NonceTrackerSubprovider();
provider.addProvider(nonceTrackerSubprovider);
provider.addProvider(new FixtureSubprovider({
'eth_getBlockByNumber': '0x01',
'eth_getTransactionCount': (data: any, next: any, end: any) => {
if (called) {
return end(null, '0x99');
} else {
called = true;
end(null, '0x00');
}
},
'eth_sendRawTransaction': (data: any, next: any, end: any) => {
end(null);
},
}));
provider.start();
const noncePayload = {
jsonrpc: '2.0',
method: 'eth_getTransactionCount',
params: ['0x1f36f546477cda21bf2296c50976f2740247906f', 'pending'],
id: 1,
};
const txParams = [
'0x',
'0x09184e72a000',
'0x2710',
'0x0000000000000000000000000000000000000000',
'0x',
'0x7f7465737432000000000000000000000000000000000000000000000000000000600057',
'0x1c',
'0x5e1d3a76fbf824220eafc8c79ad578ad2b67d01b0c2425eb1f1347e8f50882ab',
'0x5bd428537f05f9830e93792f90ea6a3e2d1ee84952dd96edbae9f658f831ab13'
];
const transaction = new EthereumTx(txParams);
const txPayload = {
jsonrpc: '2.0',
method: 'eth_sendRawTransaction',
params: [transaction.serialize()],
id: 1,
};
const response = await promisify(provider.sendAsync, provider)(noncePayload);
expect(response.result).to.be.eq('0x00');
const secondResponse = await promisify(provider.sendAsync, provider)(noncePayload);
expect(secondResponse.result).to.be.eq('0x00');
const txResponse = await promisify(provider.sendAsync, provider)(txPayload);
const thirdResponse = await promisify(provider.sendAsync, provider)(noncePayload);
expect(thirdResponse.result).to.be.eq('0x01');
});
});