-
Notifications
You must be signed in to change notification settings - Fork 679
fix: if a timestamp is passed to evm_mine
, subsequent blocks reflect the change in time
#3317
Conversation
evm_mine
, subsequent blocks reflect the change in time. fix:#3265evm_mine
, subsequent blocks reflect the change in time. fix: #3265
8542ba7
to
3f51c9f
Compare
evm_mine
, subsequent blocks reflect the change in time. fix: #3265evm_mine
, subsequent blocks reflect the change in time
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just two small changes!
BTW, I've installed this version and tested against the repro I had set up from #3265, and it fixes the issue as expected!
if (numBlocks == null) { | ||
numBlocks = 1; | ||
} | ||
if (timestamp) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (timestamp) { | |
if (timestamp !== null) { |
I know this is a silly edge case, but what if they set the timestamp to 0? This would get skipped.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I fixed it :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And added an explicit test case
221b482
to
8e0f535
Compare
…equent blocks reflect the change in time
13943c9
to
3ba0cfc
Compare
@@ -0,0 +1,97 @@ | |||
export interface BlockTime { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've extracted the logic for block timestamp into BlockTime, which allows us to more clearly describe the behaviour, and test more thoroughly.
By settings this as a dependency, it will allow us to decouple our tests from the system clock (without deferring to timestampIncrement
), for the likes of #3271 (which I'll pick up after this).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I opened PR #3389 to address #3271 but have also fixed the issue in this PR, and was able to test the issue in this PR (at least on the underlying provider, not so easy at the higher level integration test).
I figured I would leave both PRs open, so that we can get a quick fix in with #3389, but am happy to close that PR if this one goes through quickly.
3ba0cfc
to
e7d4124
Compare
e7d4124
to
19f3f25
Compare
19f3f25
to
8d7d5bd
Compare
I've standardised all timestamp units to milliseconds, with the exceptions:
I ensure that all interactions below |
8d7d5bd
to
194571b
Compare
…and IncrementBasedBlockTime.
194571b
to
58e3a6e
Compare
…epency between start time and the value returned from reference clock causing non-zero offset. fix: #3271
92048a5
to
8d56eae
Compare
} | ||
} | ||
|
||
constructor(getReferenceClockTime: () => number | Date, startTime: number | Date | undefined) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made the constructor explicitly accept a startTime
of value undefined
, and switched the argument order to make it more intuitive. I didn't make the argument optional, because the only time that we don't pass it (except for providing a variable of value undefined
) is in tests.
This allows us to instantiate a ClockBasedBlockTime
with no offset, without jumping through hoops.
} | ||
|
||
export class ClockBasedBlockTime implements BlockTime { | ||
private _getReferenceClockTime: () => number | Date; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed all of the # prefixed private class fields to _ prefixed typescript privates. The former have performance implications, and I don't see the benefit until we can resolve this.
#3240 addresses this more generally.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
quick partial review
} | ||
} | ||
|
||
constructor(startTime: number | Date, getReferenceClockTime: () => number | Date) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use number
here (and elsewhere) instead of number | Date
. The Date
object isn't used internally and we are casting everywhere we used the referenced clock time. Date.now()
is about a bajillion or so times faster than new Date()
const timestampMilliseconds = | ||
timestamp == undefined ? undefined : timestamp * 1000; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We shouldn't compute this in the loop; add this logic to the timestamp != undefined
condition above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah definitely!
@@ -297,33 +297,36 @@ export default class EthereumApi implements Api { | |||
const blockchain = this.#blockchain; | |||
const options = this.#options; | |||
const vmErrorsOnRPCResponse = options.chain.vmErrorsOnRPCResponse; | |||
|
|||
let numBlocks, timestamp; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These need types.
if (numBlocks == undefined) { | ||
numBlocks = 1; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any objection to checking this condition in the if
above?
numBlocks = arg.blocks == null ? 1 : arg.blocks;
and then in the else
assign numBlocks
to 1
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not at all - I think that makes sense. It splits out the two use cases (args vs timestamp) nicely.
|
||
private static validateTimestamp(timestamp: number | Date) { | ||
if (timestamp < 0) { | ||
throw new Error(`Invalid timestamp: ${timestamp}. Value must be positive.`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why must the timestamp be positive? This breaks ganache --time
, and evm_setTime
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As discussed separately, although a negative timestamp is valid, the block timestamp
property must be a positive integer value (it is of type Quantity
). Presently Ganache will accept a negative value (programatically, but not via cli), but will result in undesired block time.
I've added a comment explaining why we are validating. I don't think this should be considered a breaking change, as it's aligning Ganache with the expected behaviour, but it could cause problems for our users, so I'm happy to push that change out to milestone 8 if desired.
|
||
const startTime = this.#options.chain.time || new Date(); | ||
const blockTime = | ||
this.#options.miner.timestampIncrement === "clock" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this.#options.miner.timestampIncrement === "clock" | |
providerOptions.miner.timestampIncrement === "clock" |
const startTime = this.#options.chain.time || new Date(); | ||
const blockTime = | ||
this.#options.miner.timestampIncrement === "clock" | ||
? new ClockBasedBlockTime(startTime, () => new Date()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
? new ClockBasedBlockTime(startTime, () => new Date()) | |
? new ClockBasedBlockTime(startTime, () => Date.now()) |
? new ClockBasedBlockTime(startTime, () => new Date()) | ||
: new IncrementBasedBlockTime( | ||
startTime, | ||
this.#options.miner.timestampIncrement.toNumber() * 1000 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this.#options.miner.timestampIncrement.toNumber() * 1000 | |
providerOptions.miner.timestampIncrement.toNumber() * 1000 |
// whereas in incrementBasedBlockTime we set the time of the _next_ block | ||
// maybe when we call setTime, we increment the value passed in. | ||
const blockTimestampMs = this.block.header.timestamp.toNumber() * 1000; | ||
this.block.header.timestamp = Quantity.from( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
chainOptions.time
is no longer set and should be. The CLI startup output uses this updated time
value when it outputs its startup details.
You can check this by running ganache --fork mainnet --fork.blockNumber 15073145 --miner.timestampIncrement 1
ganache 7.3.2:
Forked Chain
==================
Location: Ethereum Mainnet, via 丕Infura
Block: 15073145
Network ID: 1
Time: Sun Jul 03 2022 21:41:07 GMT-0400 (Eastern Daylight Time)
this PR:
Forked Chain
==================
Location: Ethereum Mainnet, via 丕Infura
Block: 15073145
Network ID: 1
Time: Tue Jul 19 2022 12:41:59 GMT-0400 (Eastern Daylight Time)
We really should add tests for the CLI output one day 😅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah. Good to know.
I've set that value, and validated (note the timezone difference!):
Forked Chain
==================
Location: Ethereum Mainnet, via 丕Infura
Block: 15073145
Network ID: 1
Time: Mon Jul 04 2022 13:41:06 GMT+1200 (New Zealand Standard Time)
Chain Id
==================
1337
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Except the timestamp is now incorrect if ganache is using "clock" mode (since it doesn't use the clock for the first block after the fork block).
Note: I don't particularly like that this is ganache's behavior in "clock" mode.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've fixed and added some explicit tests around this behaviour to forking.test.ts
…w Date()). Set chainOptions.time in case where time comes from forked chain.
blockchain = new Blockchain( | ||
options, | ||
fromAddress, | ||
new ClockBasedBlockTime(Date.now, undefined) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need to pass undefined
here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's intentional - #3317 (comment)
I wanted to make it explicit. In real usage in provider.ts
we explicitly pass a variable with value undefined
. I wanted the tests to explicitly match this, so I didn't make the argument optional.
I'm open to changing that if you feel strongly about it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As per #3317
I added fromSystemClock()
static constructor method which means the only time we call this constructor directly is in tests, so I've done as you suggested.
|
||
let numBlocks: number, timestamp: number; | ||
// Since `typeof null === "object"` we have to guard against that | ||
if (arg !== null && typeof arg === "object") { | ||
let { blocks, timestamp } = arg; | ||
if (blocks == null) { | ||
blocks = 1; | ||
} | ||
// TODO(perf): add an option to mine a bunch of blocks in a batch so | ||
// we can save them all to the database in one go. | ||
// Developers like to move the blockchain forward by thousands of blocks | ||
// at a time and doing this would make it way faster | ||
for (let i = 0; i < blocks; i++) { | ||
const { transactions } = await blockchain.mine( | ||
Capacity.FillBlock, | ||
timestamp, | ||
true | ||
); | ||
|
||
if (vmErrorsOnRPCResponse) { | ||
assertExceptionalTransactions(transactions); | ||
} | ||
} | ||
numBlocks = arg.blocks == null ? 1 : arg.blocks; | ||
timestamp = arg.timestamp; | ||
} else { | ||
numBlocks = 1; | ||
timestamp = arg as number | null; | ||
} | ||
|
||
let timestampMilliseconds: number; | ||
if (timestamp != undefined) { | ||
timestampMilliseconds = timestamp * 1000; | ||
this.#blockchain.setTime(timestampMilliseconds); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we need to setTime
here because blockchain.mine
already does this if a timestamp is passed in. I think this can be simplified further into something like:
let numBlocks: number, timestampMilliseconds: number;
if (arg == null) {
numBlocks = 1;
} else if (typeof arg === "object") {
numBlocks = typeof arg.blocks === "number" ? arg.blocks : 1;
if (typeof arg.timestamp === "number") {
timestampMilliseconds = arg.timestamp * 1000;
}
} else {
numBlocks = 1;
timestampMilliseconds = arg * 1000;
}
That said, we are converting our time from seconds to milliseconds here, just to convert it back to seconds in readyNextBlock
. Maybe this is fine and is the best we can do because the precision of our options in various places is all over the place (some in seconds, some milliseconds).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, you are correct. I've simplified as you suggested.
@@ -107,8 +108,7 @@ describe("provider", () => { | |||
method: "eth_getBlockByNumber", | |||
params: ["latest", false] | |||
}); | |||
const expectedTime = | |||
Math.floor((fastForward + +time) / 1000) + timestampIncrement; | |||
const expectedTime = Math.floor((fastForward + +time) / 1000) + timestampIncrement; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like this may need to be prettieried.
@@ -214,6 +216,7 @@ export default class Blockchain extends Emittery<BlockchainTypedEvents> { | |||
constructor( | |||
options: EthereumInternalOptions, | |||
coinbase: Address, | |||
blockTime: BlockTime, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we give this a default? Something like blockTime: BlockTime = new ClockBasedBlockTime(Date.now),
. I think it could simplify a bunch of tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer to not specify a default in cases such as this, as it encourages more implicit code which is prone to miss-understandings. I also feel that the implementation should be optimised for the "real" use case (while obviously being testable).
I've added a fromSystemClock()
static constructor. I intentionally didn't just give getReferenceClockTime
argument a default, because I wanted to make it explicit that it's using the system clock.
I think this gives a nice balance, and also tidies up the real world consumption - where we can just call fromSystemClock(startTime)
and have the Date.now
part abstracted away.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, there is a default, you just put it in a different place. :-)
I'm fine with it... but I can foresee someone creating a test helper that applies actual defaults in the future (like the getProvider()
helper does already).
/* | ||
A BlockTime implementation that increments it's reference time by the duration specified by incrementMilliseconds, | ||
every time createBlockTimestampInSeconds() is called. The timestamp returned will be impacted by the timestamp offset, | ||
so can be moved forward and backwards by calling .setOffset() independent of the start time, and will increment when |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you check this comment for accuracy/consistency? I'm having a hard time following it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it's a bit mucky, I've made it more concise and easier to follow. 🤞
private readonly _tickReferenceClock: () => void; | ||
|
||
constructor(startTime: number, incrementMilliseconds: number) { | ||
let referenceTime = startTime; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this referenceTime
is reset on evm_revert
, but I think it should be.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a really good point. I rethought the approach and went back and forward a couple of times. I've settled on a slight modification, which I think makes sense. The IncrementBasedBlockTime now holds the reference clock at startTime
and increments it's offset rather than reference clock.
Offset wasn't working correctly with increment based clocks prior to this change, (as blocktime = previousBlocktime + increment + offset
will result in compounding the offset). This change fixes that.
It does mean that the meaning of "offset" wrt increment based clocks is now strictly the elapsed duration (in block time) since the "start time" (plus / minus any modifications made by calls to evm_setTime
/ evm_increaseTime
).
Happy to hear opinions about the above, or we can set up a call to beat out the details.
…e sub and supers of each other. Modify IncrementBasedBlockTime to increment the offset, so that checkpointing and reverting work. Improve comments, and tests.
c348ba6
to
b8a2e85
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since you told me to hold off on the rest of this review I thought I'd just send off the two comments I have so far
|
||
const startTime = | ||
providerOptions.chain.time === undefined | ||
? Date.now() | ||
: +providerOptions.chain.time; | ||
// if startTime is undefined, BlockTime will determine 0 offset from the reference time | ||
const blockTime = | ||
providerOptions.miner.timestampIncrement === "clock" | ||
? BlockTime.fromSystemClock(startTime) | ||
: new IncrementBasedBlockTime( | ||
startTime, | ||
providerOptions.miner.timestampIncrement.toNumber() * 1000 | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A BlockTime
static that abstracts this logic away would really help make the BlockTime logic more cohesive. I'm not a fan of adding all this additional time logic to the provider constructor.
@@ -214,6 +216,7 @@ export default class Blockchain extends Emittery<BlockchainTypedEvents> { | |||
constructor( | |||
options: EthereumInternalOptions, | |||
coinbase: Address, | |||
blockTime: BlockTime, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, there is a default, you just put it in a different place. :-)
I'm fine with it... but I can foresee someone creating a test helper that applies actual defaults in the future (like the getProvider()
helper does already).
This PR is now redundant |
This fixes a regression introduced in Ganche v7, where providing a timestamp in a call to
evm_mine
would result in a block with the correct timestamp, but the internal time-offset would not be updated, so subsequent blocks would not reflect the change in time.Fixes: #3265 #3271