Merge branch 'master' into inpage-provider-fixes

This commit is contained in:
kumavis 2017-08-28 11:29:47 -07:00 committed by GitHub
commit 76de053b0b
8 changed files with 189 additions and 41 deletions

View File

@ -5,6 +5,11 @@
- Make eth_sign deprecation warning less noisy - Make eth_sign deprecation warning less noisy
- Fix bug with network version serialization over synchronous RPC - Fix bug with network version serialization over synchronous RPC
## 3.9.11 2017-8-24
- Fix nonce calculation bug that would sometimes generate very wrong nonces.
- Give up resubmitting a transaction after 3500 blocks.
## 3.9.10 2017-8-23 ## 3.9.10 2017-8-23
- Improve nonce calculation, to prevent bug where people are unable to send transactions reliably. - Improve nonce calculation, to prevent bug where people are unable to send transactions reliably.

View File

@ -1,7 +1,7 @@
{ {
"name": "MetaMask", "name": "MetaMask",
"short_name": "Metamask", "short_name": "Metamask",
"version": "3.9.10", "version": "3.9.11",
"manifest_version": 2, "manifest_version": 2,
"author": "https://metamask.io", "author": "https://metamask.io",
"description": "Ethereum Browser Extension", "description": "Ethereum Browser Extension",

View File

@ -40,6 +40,10 @@ module.exports = class TransactionController extends EventEmitter {
err: undefined, err: undefined,
}) })
}, },
giveUpOnTransaction: (txId) => {
const msg = `Gave up submitting after 3500 blocks un-mined.`
this.setTxStatusFailed(txId, msg)
},
}) })
this.query = new EthQuery(this.provider) this.query = new EthQuery(this.provider)
this.txProviderUtil = new TxProviderUtil(this.provider) this.txProviderUtil = new TxProviderUtil(this.provider)
@ -451,4 +455,4 @@ module.exports = class TransactionController extends EventEmitter {
}) })
this.memStore.updateState({ unapprovedTxs, selectedAddressTxList }) this.memStore.updateState({ unapprovedTxs, selectedAddressTxList })
} }
} }

View File

@ -28,12 +28,26 @@ class NonceTracker {
const releaseLock = await this._takeMutex(address) const releaseLock = await this._takeMutex(address)
// evaluate multiple nextNonce strategies // evaluate multiple nextNonce strategies
const nonceDetails = {} const nonceDetails = {}
const localNonceResult = await this._getlocalNextNonce(address)
nonceDetails.local = localNonceResult.details
const networkNonceResult = await this._getNetworkNextNonce(address) const networkNonceResult = await this._getNetworkNextNonce(address)
nonceDetails.network = networkNonceResult.details const highestLocallyConfirmed = this._getHighestLocallyConfirmed(address)
const nextNetworkNonce = networkNonceResult.nonce
const highestLocalNonce = highestLocallyConfirmed
const highestSuggested = Math.max(nextNetworkNonce, highestLocalNonce)
const pendingTxs = this.getPendingTransactions(address)
const localNonceResult = this._getHighestContinuousFrom(pendingTxs, highestSuggested) || 0
nonceDetails.params = {
highestLocalNonce,
highestSuggested,
nextNetworkNonce,
}
nonceDetails.local = localNonceResult
nonceDetails.network = networkNonceResult
const nextNonce = Math.max(networkNonceResult.nonce, localNonceResult.nonce) const nextNonce = Math.max(networkNonceResult.nonce, localNonceResult.nonce)
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}"`)
// return nonce and release cb // return nonce and release cb
return { nextNonce, nonceDetails, releaseLock } return { nextNonce, nonceDetails, releaseLock }
} }
@ -74,38 +88,17 @@ class NonceTracker {
// and pending count are from the same block // and pending count are from the same block
const currentBlock = await this._getCurrentBlock() const currentBlock = await this._getCurrentBlock()
const blockNumber = currentBlock.blockNumber const blockNumber = currentBlock.blockNumber
const baseCountHex = await this.ethQuery.getTransactionCount(address, blockNumber) const baseCountBN = await this.ethQuery.getTransactionCount(address, blockNumber || 'latest')
const baseCount = parseInt(baseCountHex, 16) const baseCount = baseCountBN.toNumber()
assert(Number.isInteger(baseCount), `nonce-tracker - baseCount is not an integer - got: (${typeof baseCount}) "${baseCount}"`) assert(Number.isInteger(baseCount), `nonce-tracker - baseCount is not an integer - got: (${typeof baseCount}) "${baseCount}"`)
const nonceDetails = { blockNumber, baseCountHex, baseCount } const nonceDetails = { blockNumber, baseCount }
return { name: 'network', nonce: baseCount, details: nonceDetails } return { name: 'network', nonce: baseCount, details: nonceDetails }
} }
async _getlocalNextNonce (address) { _getHighestLocallyConfirmed (address) {
let nextNonce
// check our local tx history for the highest nonce (if any)
const confirmedTransactions = this.getConfirmedTransactions(address) const confirmedTransactions = this.getConfirmedTransactions(address)
const pendingTransactions = this.getPendingTransactions(address) const highest = this._getHighestNonce(confirmedTransactions)
const transactions = confirmedTransactions.concat(pendingTransactions) return Number.isInteger(highest) ? highest + 1 : 0
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) { _reduceTxListToUniqueNonces (txList) {
@ -122,11 +115,30 @@ class NonceTracker {
} }
_getHighestNonce (txList) { _getHighestNonce (txList) {
const nonces = txList.map((txMeta) => parseInt(txMeta.txParams.nonce, 16)) const nonces = txList.map((txMeta) => {
const nonce = txMeta.txParams.nonce
assert(typeof nonce, 'string', 'nonces should be hex strings')
return parseInt(nonce, 16)
})
const highestNonce = Math.max.apply(null, nonces) const highestNonce = Math.max.apply(null, nonces)
return highestNonce return highestNonce
} }
_getHighestContinuousFrom (txList, startPoint) {
const nonces = txList.map((txMeta) => {
const nonce = txMeta.txParams.nonce
assert(typeof nonce, 'string', 'nonces should be hex strings')
return parseInt(nonce, 16)
})
let highest = startPoint
while (nonces.includes(highest)) {
highest++
}
return { name: 'local', nonce: highest, details: { startPoint, highest } }
}
// 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 () {

View File

@ -1,6 +1,7 @@
const EventEmitter = require('events') const EventEmitter = require('events')
const EthQuery = require('ethjs-query') const EthQuery = require('ethjs-query')
const sufficientBalance = require('./util').sufficientBalance const sufficientBalance = require('./util').sufficientBalance
const RETRY_LIMIT = 3500 // Retry 3500 blocks, or about 1 day.
/* /*
Utility class for tracking the transactions as they Utility class for tracking the transactions as they
@ -28,6 +29,7 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
this.getBalance = config.getBalance this.getBalance = config.getBalance
this.getPendingTransactions = config.getPendingTransactions this.getPendingTransactions = config.getPendingTransactions
this.publishTransaction = config.publishTransaction this.publishTransaction = config.publishTransaction
this.giveUpOnTransaction = config.giveUpOnTransaction
} }
// checks if a signed tx is in a block and // checks if a signed tx is in a block and
@ -100,6 +102,10 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
if (balance === undefined) return if (balance === undefined) return
if (!('retryCount' in txMeta)) txMeta.retryCount = 0 if (!('retryCount' in txMeta)) txMeta.retryCount = 0
if (txMeta.retryCount > RETRY_LIMIT) {
return this.giveUpOnTransaction(txMeta.id)
}
// if the value of the transaction is greater then the balance, fail. // if the value of the transaction is greater then the balance, fail.
if (!sufficientBalance(txMeta.txParams, balance)) { if (!sufficientBalance(txMeta.txParams, balance)) {
const insufficientFundsError = new Error('Insufficient balance during rebroadcast.') const insufficientFundsError = new Error('Insufficient balance during rebroadcast.')
@ -160,4 +166,4 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
} }
nonceGlobalLock.releaseLock() nonceGlobalLock.releaseLock()
} }
} }

View File

@ -0,0 +1,83 @@
const version = 19
/*
This migration sets transactions as failed
whos nonce is too high
*/
const clone = require('clone')
module.exports = {
version,
migrate: function (originalVersionedData) {
const versionedData = clone(originalVersionedData)
versionedData.meta.version = version
try {
const state = versionedData.data
const newState = transformState(state)
versionedData.data = newState
} catch (err) {
console.warn(`MetaMask Migration #${version}` + err.stack)
}
return Promise.resolve(versionedData)
},
}
function transformState (state) {
const newState = state
const transactions = newState.TransactionController.transactions
newState.TransactionController.transactions = transactions.map((txMeta, _, txList) => {
if (txMeta.status !== 'submitted') return txMeta
const confirmedTxs = txList.filter((tx) => tx.status === 'confirmed')
.filter((tx) => tx.txParams.from === txMeta.txParams.from)
.filter((tx) => tx.metamaskNetworkId.from === txMeta.metamaskNetworkId.from)
const highestConfirmedNonce = getHighestNonce(confirmedTxs)
const pendingTxs = txList.filter((tx) => tx.status === 'submitted')
.filter((tx) => tx.txParams.from === txMeta.txParams.from)
.filter((tx) => tx.metamaskNetworkId.from === txMeta.metamaskNetworkId.from)
const highestContinuousNonce = getHighestContinuousFrom(pendingTxs, highestConfirmedNonce)
const maxNonce = Math.max(highestContinuousNonce, highestConfirmedNonce)
if (parseInt(txMeta.txParams.nonce, 16) > maxNonce + 1) {
txMeta.status = 'failed'
txMeta.err = {
message: 'nonce too high',
note: 'migration 019 custom error',
}
}
return txMeta
})
return newState
}
function getHighestContinuousFrom (txList, startPoint) {
const nonces = txList.map((txMeta) => {
const nonce = txMeta.txParams.nonce
return parseInt(nonce, 16)
})
let highest = startPoint
while (nonces.includes(highest)) {
highest++
}
return highest
}
function getHighestNonce (txList) {
const nonces = txList.map((txMeta) => {
const nonce = txMeta.txParams.nonce
return parseInt(nonce || '0x0', 16)
})
const highestNonce = Math.max.apply(null, nonces)
return highestNonce
}

View File

@ -29,4 +29,5 @@ module.exports = [
require('./016'), require('./016'),
require('./017'), require('./017'),
require('./018'), require('./018'),
require('./019'),
] ]

View File

@ -18,10 +18,10 @@ describe('Nonce Tracker', function () {
nonceTracker = generateNonceTrackerWith(pendingTxs, confirmedTxs, '0x1') nonceTracker = generateNonceTrackerWith(pendingTxs, confirmedTxs, '0x1')
}) })
it('should work', async function () { it('should return 4', async function () {
this.timeout(15000) this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926') const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '4', 'nonce should be 4') assert.equal(nonceLock.nextNonce, '4', `nonce should be 4 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock() await nonceLock.releaseLock()
}) })
@ -41,7 +41,7 @@ describe('Nonce Tracker', function () {
it('should return 0', async function () { it('should return 0', async function () {
this.timeout(15000) this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926') const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '0', 'nonce should be 0') assert.equal(nonceLock.nextNonce, '0', `nonce should be 0 returned ${nonceLock.nextNonce}`)
await nonceLock.releaseLock() await nonceLock.releaseLock()
}) })
}) })
@ -55,7 +55,7 @@ describe('Nonce Tracker', function () {
txParams: { nonce: '0x01' }, txParams: { nonce: '0x01' },
}, { count: 5 }) }, { count: 5 })
nonceTracker = generateNonceTrackerWith(pendingTxs, confirmedTxs) nonceTracker = generateNonceTrackerWith(pendingTxs, confirmedTxs, '0x0')
}) })
it('should return nonce after those', async function () { it('should return nonce after those', async function () {
@ -69,14 +69,14 @@ describe('Nonce Tracker', function () {
describe('when local confirmed count is higher than network nonce', function () { describe('when local confirmed count is higher than network nonce', function () {
beforeEach(function () { beforeEach(function () {
const txGen = new MockTxGen() const txGen = new MockTxGen()
confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 2 }) confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 3 })
nonceTracker = generateNonceTrackerWith([], confirmedTxs) nonceTracker = generateNonceTrackerWith([], confirmedTxs, '0x1')
}) })
it('should return nonce after those', async function () { it('should return nonce after those', async function () {
this.timeout(15000) this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926') const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '2', `nonce should be 2 got ${nonceLock.nextNonce}`) assert.equal(nonceLock.nextNonce, '3', `nonce should be 3 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock() await nonceLock.releaseLock()
}) })
}) })
@ -125,6 +125,43 @@ describe('Nonce Tracker', function () {
await nonceLock.releaseLock() await nonceLock.releaseLock()
}) })
}) })
describe('when there are pending nonces non sequentially over the network nonce.', function () {
beforeEach(function () {
const txGen = new MockTxGen()
txGen.generate({ status: 'submitted' }, { count: 5 })
// 5 over that number
pendingTxs = txGen.generate({ status: 'submitted' }, { count: 5 })
nonceTracker = generateNonceTrackerWith(pendingTxs, [], '0x00')
})
it('should return nonce after network nonce', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '0', `nonce should be 0 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
describe('When all three return different values', function () {
beforeEach(function () {
const txGen = new MockTxGen()
const confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 10 })
const pendingTxs = txGen.generate({
status: 'submitted',
nonce: 100,
}, { count: 1 })
// 0x32 is 50 in hex:
nonceTracker = generateNonceTrackerWith(pendingTxs, confirmedTxs, '0x32')
})
it('should return nonce after network nonce', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '50', `nonce should be 50 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
}) })
}) })