mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
commit
d836e281de
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
## Current Master
|
## Current Master
|
||||||
|
|
||||||
|
- Improve nonce calculation, to prevent bug where people are unable to send transactions reliably.
|
||||||
- Remove link to eth-tx-viz from identicons in tx history.
|
- Remove link to eth-tx-viz from identicons in tx history.
|
||||||
|
|
||||||
## 3.9.9 2017-8-18
|
## 3.9.9 2017-8-18
|
||||||
|
@ -33,6 +33,13 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
err: undefined,
|
err: undefined,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
getConfirmedTransactions: (address) => {
|
||||||
|
return this.getFilteredTxList({
|
||||||
|
from: address,
|
||||||
|
status: 'confirmed',
|
||||||
|
err: undefined,
|
||||||
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
this.query = new EthQuery(this.provider)
|
this.query = new EthQuery(this.provider)
|
||||||
this.txProviderUtil = new TxProviderUtil(this.provider)
|
this.txProviderUtil = new TxProviderUtil(this.provider)
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
const EthQuery = require('eth-query')
|
const EthQuery = require('ethjs-query')
|
||||||
const assert = require('assert')
|
const assert = require('assert')
|
||||||
const Mutex = require('await-semaphore').Mutex
|
const Mutex = require('await-semaphore').Mutex
|
||||||
|
|
||||||
class NonceTracker {
|
class NonceTracker {
|
||||||
|
|
||||||
constructor ({ provider, getPendingTransactions }) {
|
constructor ({ provider, getPendingTransactions, getConfirmedTransactions }) {
|
||||||
this.provider = provider
|
this.provider = provider
|
||||||
this.ethQuery = new EthQuery(provider)
|
this.ethQuery = new EthQuery(provider)
|
||||||
this.getPendingTransactions = getPendingTransactions
|
this.getPendingTransactions = getPendingTransactions
|
||||||
|
this.getConfirmedTransactions = getConfirmedTransactions
|
||||||
this.lockMap = {}
|
this.lockMap = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,21 +26,14 @@ class NonceTracker {
|
|||||||
await this._globalMutexFree()
|
await this._globalMutexFree()
|
||||||
// await lock free, then take lock
|
// await lock free, then take lock
|
||||||
const releaseLock = await this._takeMutex(address)
|
const releaseLock = await this._takeMutex(address)
|
||||||
// calculate next nonce
|
// evaluate multiple nextNonce strategies
|
||||||
// we need to make sure our base count
|
const nonceDetails = {}
|
||||||
// and pending count are from the same block
|
const localNonceResult = await this._getlocalNextNonce(address)
|
||||||
const currentBlock = await this._getCurrentBlock()
|
nonceDetails.local = localNonceResult.details
|
||||||
const pendingTransactions = this.getPendingTransactions(address)
|
const networkNonceResult = await this._getNetworkNextNonce(address)
|
||||||
const pendingCount = pendingTransactions.length
|
nonceDetails.network = networkNonceResult.details
|
||||||
assert(Number.isInteger(pendingCount), `nonce-tracker - pendingCount is not an integer - got: (${typeof pendingCount}) "${pendingCount}"`)
|
const nextNonce = Math.max(networkNonceResult.nonce, localNonceResult.nonce)
|
||||||
const baseCountHex = await this._getTxCount(address, currentBlock)
|
|
||||||
const baseCount = parseInt(baseCountHex, 16)
|
|
||||||
assert(Number.isInteger(baseCount), `nonce-tracker - baseCount is not an integer - got: (${typeof baseCount}) "${baseCount}"`)
|
|
||||||
const nextNonce = baseCount + pendingCount
|
|
||||||
assert(Number.isInteger(nextNonce), `nonce-tracker - nextNonce is not an integer - got: (${typeof nextNonce}) "${nextNonce}"`)
|
assert(Number.isInteger(nextNonce), `nonce-tracker - nextNonce is not an integer - got: (${typeof nextNonce}) "${nextNonce}"`)
|
||||||
// collect the numbers used to calculate the nonce for debugging
|
|
||||||
const blockNumber = currentBlock.number
|
|
||||||
const nonceDetails = { blockNumber, baseCount, baseCountHex, pendingCount }
|
|
||||||
// return nonce and release cb
|
// return nonce and release cb
|
||||||
return { nextNonce, nonceDetails, releaseLock }
|
return { nextNonce, nonceDetails, releaseLock }
|
||||||
}
|
}
|
||||||
@ -53,15 +47,6 @@ class NonceTracker {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async _getTxCount (address, currentBlock) {
|
|
||||||
const blockNumber = currentBlock.number
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.ethQuery.getTransactionCount(address, blockNumber, (err, result) => {
|
|
||||||
err ? reject(err) : resolve(result)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async _globalMutexFree () {
|
async _globalMutexFree () {
|
||||||
const globalMutex = this._lookupMutex('global')
|
const globalMutex = this._lookupMutex('global')
|
||||||
const release = await globalMutex.acquire()
|
const release = await globalMutex.acquire()
|
||||||
@ -83,12 +68,70 @@ class NonceTracker {
|
|||||||
return mutex
|
return mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _getNetworkNextNonce (address) {
|
||||||
|
// calculate next nonce
|
||||||
|
// we need to make sure our base count
|
||||||
|
// and pending count are from the same block
|
||||||
|
const currentBlock = await this._getCurrentBlock()
|
||||||
|
const blockNumber = currentBlock.blockNumber
|
||||||
|
const baseCountHex = await this.ethQuery.getTransactionCount(address, blockNumber)
|
||||||
|
const baseCount = parseInt(baseCountHex, 16)
|
||||||
|
assert(Number.isInteger(baseCount), `nonce-tracker - baseCount is not an integer - got: (${typeof baseCount}) "${baseCount}"`)
|
||||||
|
const nonceDetails = { blockNumber, baseCountHex, baseCount }
|
||||||
|
return { name: 'network', nonce: baseCount, details: nonceDetails }
|
||||||
|
}
|
||||||
|
|
||||||
|
async _getlocalNextNonce (address) {
|
||||||
|
let nextNonce
|
||||||
|
// check our local tx history for the highest nonce (if any)
|
||||||
|
const confirmedTransactions = this.getConfirmedTransactions(address)
|
||||||
|
const pendingTransactions = this.getPendingTransactions(address)
|
||||||
|
const transactions = confirmedTransactions.concat(pendingTransactions)
|
||||||
|
const highestConfirmedNonce = this._getHighestNonce(confirmedTransactions)
|
||||||
|
const highestPendingNonce = this._getHighestNonce(pendingTransactions)
|
||||||
|
const highestNonce = this._getHighestNonce(transactions)
|
||||||
|
|
||||||
|
const haveHighestNonce = Number.isInteger(highestNonce)
|
||||||
|
if (haveHighestNonce) {
|
||||||
|
// next nonce is the nonce after our last
|
||||||
|
nextNonce = highestNonce + 1
|
||||||
|
} else {
|
||||||
|
// no local tx history so next must be first (zero)
|
||||||
|
nextNonce = 0
|
||||||
|
}
|
||||||
|
const nonceDetails = { highestNonce, haveHighestNonce, highestConfirmedNonce, highestPendingNonce }
|
||||||
|
return { name: 'local', nonce: nextNonce, details: nonceDetails }
|
||||||
|
}
|
||||||
|
|
||||||
|
_getPendingTransactionCount (address) {
|
||||||
|
const pendingTransactions = this.getPendingTransactions(address)
|
||||||
|
return this._reduceTxListToUniqueNonces(pendingTransactions).length
|
||||||
|
}
|
||||||
|
|
||||||
|
_reduceTxListToUniqueNonces (txList) {
|
||||||
|
const reducedTxList = txList.reduce((reducedList, txMeta, index) => {
|
||||||
|
if (!index) return [txMeta]
|
||||||
|
const nonceMatches = txList.filter((txData) => {
|
||||||
|
return txMeta.txParams.nonce === txData.txParams.nonce
|
||||||
|
})
|
||||||
|
if (nonceMatches.length > 1) return reducedList
|
||||||
|
reducedList.push(txMeta)
|
||||||
|
return reducedList
|
||||||
|
}, [])
|
||||||
|
return reducedTxList
|
||||||
|
}
|
||||||
|
|
||||||
|
_getHighestNonce (txList) {
|
||||||
|
const nonces = txList.map((txMeta) => parseInt(txMeta.txParams.nonce, 16))
|
||||||
|
const highestNonce = Math.max.apply(null, nonces)
|
||||||
|
return highestNonce
|
||||||
|
}
|
||||||
|
|
||||||
// this is a hotfix for the fact that the blockTracker will
|
// this is a hotfix for the fact that the blockTracker will
|
||||||
// change when the network changes
|
// change when the network changes
|
||||||
_getBlockTracker () {
|
_getBlockTracker () {
|
||||||
return this.provider._blockTracker
|
return this.provider._blockTracker
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = NonceTracker
|
module.exports = NonceTracker
|
||||||
|
40
test/lib/mock-tx-gen.js
Normal file
40
test/lib/mock-tx-gen.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
const extend = require('xtend')
|
||||||
|
const BN = require('ethereumjs-util').BN
|
||||||
|
const template = {
|
||||||
|
'status': 'submitted',
|
||||||
|
'txParams': {
|
||||||
|
'from': '0x7d3517b0d011698406d6e0aed8453f0be2697926',
|
||||||
|
'gas': '0x30d40',
|
||||||
|
'value': '0x0',
|
||||||
|
'nonce': '0x3',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
class TxGenerator {
|
||||||
|
|
||||||
|
constructor () {
|
||||||
|
this.txs = []
|
||||||
|
}
|
||||||
|
|
||||||
|
generate (tx = {}, opts = {}) {
|
||||||
|
let { count, fromNonce } = opts
|
||||||
|
let nonce = fromNonce || this.txs.length
|
||||||
|
let txs = []
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
txs.push(extend(template, {
|
||||||
|
txParams: {
|
||||||
|
nonce: hexify(nonce++),
|
||||||
|
}
|
||||||
|
}, tx))
|
||||||
|
}
|
||||||
|
this.txs = this.txs.concat(txs)
|
||||||
|
return txs
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function hexify (number) {
|
||||||
|
return '0x' + (new BN(number)).toString(16)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = TxGenerator
|
@ -1,41 +1,147 @@
|
|||||||
const assert = require('assert')
|
const assert = require('assert')
|
||||||
const NonceTracker = require('../../app/scripts/lib/nonce-tracker')
|
const NonceTracker = require('../../app/scripts/lib/nonce-tracker')
|
||||||
|
const MockTxGen = require('../lib/mock-tx-gen')
|
||||||
|
let providerResultStub = {}
|
||||||
|
|
||||||
describe('Nonce Tracker', function () {
|
describe('Nonce Tracker', function () {
|
||||||
let nonceTracker, provider, getPendingTransactions, pendingTxs
|
let nonceTracker, provider
|
||||||
|
let getPendingTransactions, pendingTxs
|
||||||
|
let getConfirmedTransactions, confirmedTxs
|
||||||
beforeEach(function () {
|
|
||||||
pendingTxs = [{
|
|
||||||
'status': 'submitted',
|
|
||||||
'txParams': {
|
|
||||||
'from': '0x7d3517b0d011698406d6e0aed8453f0be2697926',
|
|
||||||
'gas': '0x30d40',
|
|
||||||
'value': '0x0',
|
|
||||||
'nonce': '0x0',
|
|
||||||
},
|
|
||||||
}]
|
|
||||||
|
|
||||||
|
|
||||||
getPendingTransactions = () => pendingTxs
|
|
||||||
provider = {
|
|
||||||
sendAsync: (_, cb) => { cb(undefined, {result: '0x0'}) },
|
|
||||||
_blockTracker: {
|
|
||||||
getCurrentBlock: () => '0x11b568',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
nonceTracker = new NonceTracker({
|
|
||||||
provider,
|
|
||||||
getPendingTransactions,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#getNonceLock', function () {
|
describe('#getNonceLock', function () {
|
||||||
it('should work', async function () {
|
|
||||||
this.timeout(15000)
|
describe('with 3 confirmed and 1 pending', function () {
|
||||||
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
|
beforeEach(function () {
|
||||||
assert.equal(nonceLock.nextNonce, '1', 'nonce should be 1')
|
const txGen = new MockTxGen()
|
||||||
await nonceLock.releaseLock()
|
confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 3 })
|
||||||
|
pendingTxs = txGen.generate({ status: 'submitted' }, { count: 1 })
|
||||||
|
nonceTracker = generateNonceTrackerWith(pendingTxs, confirmedTxs, '0x1')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should work', async function () {
|
||||||
|
this.timeout(15000)
|
||||||
|
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
|
||||||
|
assert.equal(nonceLock.nextNonce, '4', 'nonce should be 4')
|
||||||
|
await nonceLock.releaseLock()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should use localNonce if network returns a nonce lower then a confirmed tx in state', async function () {
|
||||||
|
this.timeout(15000)
|
||||||
|
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
|
||||||
|
assert.equal(nonceLock.nextNonce, '4', 'nonce should be 4')
|
||||||
|
await nonceLock.releaseLock()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with no previous txs', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
nonceTracker = generateNonceTrackerWith([], [])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return 0', async function () {
|
||||||
|
this.timeout(15000)
|
||||||
|
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
|
||||||
|
assert.equal(nonceLock.nextNonce, '0', 'nonce should be 0')
|
||||||
|
await nonceLock.releaseLock()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with multiple previous txs with same nonce', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
const txGen = new MockTxGen()
|
||||||
|
confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 1 })
|
||||||
|
pendingTxs = txGen.generate({
|
||||||
|
status: 'submitted',
|
||||||
|
txParams: { nonce: '0x01' },
|
||||||
|
}, { count: 5 })
|
||||||
|
|
||||||
|
nonceTracker = generateNonceTrackerWith(pendingTxs, confirmedTxs)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return nonce after those', async function () {
|
||||||
|
this.timeout(15000)
|
||||||
|
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
|
||||||
|
assert.equal(nonceLock.nextNonce, '2', `nonce should be 2 got ${nonceLock.nextNonce}`)
|
||||||
|
await nonceLock.releaseLock()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when local confirmed count is higher than network nonce', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
const txGen = new MockTxGen()
|
||||||
|
confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 2 })
|
||||||
|
nonceTracker = generateNonceTrackerWith([], confirmedTxs)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return nonce after those', async function () {
|
||||||
|
this.timeout(15000)
|
||||||
|
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
|
||||||
|
assert.equal(nonceLock.nextNonce, '2', `nonce should be 2 got ${nonceLock.nextNonce}`)
|
||||||
|
await nonceLock.releaseLock()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when local pending count is higher than other metrics', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
const txGen = new MockTxGen()
|
||||||
|
pendingTxs = txGen.generate({ status: 'submitted' }, { count: 2 })
|
||||||
|
nonceTracker = generateNonceTrackerWith(pendingTxs, [])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return nonce after those', async function () {
|
||||||
|
this.timeout(15000)
|
||||||
|
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
|
||||||
|
assert.equal(nonceLock.nextNonce, '2', `nonce should be 2 got ${nonceLock.nextNonce}`)
|
||||||
|
await nonceLock.releaseLock()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when provider nonce is higher than other metrics', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
const txGen = new MockTxGen()
|
||||||
|
pendingTxs = txGen.generate({ status: 'submitted' }, { count: 2 })
|
||||||
|
nonceTracker = generateNonceTrackerWith(pendingTxs, [], '0x05')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return nonce after those', async function () {
|
||||||
|
this.timeout(15000)
|
||||||
|
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
|
||||||
|
assert.equal(nonceLock.nextNonce, '5', `nonce should be 5 got ${nonceLock.nextNonce}`)
|
||||||
|
await nonceLock.releaseLock()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when there are some pending nonces below the remote one and some over.', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
const txGen = new MockTxGen()
|
||||||
|
pendingTxs = txGen.generate({ status: 'submitted' }, { count: 5 })
|
||||||
|
nonceTracker = generateNonceTrackerWith(pendingTxs, [], '0x03')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return nonce after those', async function () {
|
||||||
|
this.timeout(15000)
|
||||||
|
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
|
||||||
|
assert.equal(nonceLock.nextNonce, '5', `nonce should be 5 got ${nonceLock.nextNonce}`)
|
||||||
|
await nonceLock.releaseLock()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function generateNonceTrackerWith (pending, confirmed, providerStub = '0x0') {
|
||||||
|
const getPendingTransactions = () => pending
|
||||||
|
const getConfirmedTransactions = () => confirmed
|
||||||
|
providerResultStub.result = providerStub
|
||||||
|
const provider = {
|
||||||
|
sendAsync: (_, cb) => { cb(undefined, providerResultStub) },
|
||||||
|
_blockTracker: {
|
||||||
|
getCurrentBlock: () => '0x11b568',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return new NonceTracker({
|
||||||
|
provider,
|
||||||
|
getPendingTransactions,
|
||||||
|
getConfirmedTransactions,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user