diff --git a/README.md b/README.md index ec7387a..63d08f8 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,9 @@ It answers: - Is every version I have ever released still live, with the code I compiled, on every network I support? - Which operational migrations have actually been applied on this chain, and - when, so a test can assert the state they imply instead of guessing from a - date? + when they ran — including the ones that ran before there was anywhere to write + them down — so a test can assert the state they imply instead of guessing from + a date? - Can a migration be skipped, repeated or applied out of order on one chain and not another? @@ -39,8 +40,8 @@ Approach: - An address registry, read at run time rather than compiled into creation code, and a post-deploy check that every target network's deployment took the address it was supposed to. -- A migration registry, so operational scripts record what they applied and - when, each onto the head it is applying to, and tests assert the state that +- A migration registry, so operational scripts record what they applied and when + it ran, each onto the head it is applying to, and tests assert the state that implies rather than branching on a deadline. - One inherited deploy-pin verification, parameterized over versions, rather than assertions hand-enumerated per version and per chain in every deploy @@ -205,12 +206,21 @@ library supplies the fork loop and the comparison. ## Migration registry -`MigrationRegistry` records that a migration has been applied, and when: a -writer applies one of its own onto the migration it believes ran last -(`applyMigration`), anyone reads when a given writer applied a given one -(`applied`), and anyone reads where a given writer's sequence has got to +`MigrationRegistry` records that a migration has been applied, when, and onto +what: a writer applies one of its own onto the migration it believes ran last +(`applyMigration`, or `applyMigrationHistory` for one that already ran), anyone +reads when a given writer applied a given one (`applied`), what that writer +applied it onto (`appliedOnto`), and where a given writer's sequence has got to (`head`). There is no removal and no upgrade. +The two writes differ in exactly one thing: where the recorded moment comes +from. `applyMigration` stamps the block the record lands in, for a script +applying its own migration in the same atomic unit as the migration itself. +`applyMigrationHistory` takes the moment as an argument, so a migration that ran +before the registry reached the chain is recordable with the time it actually +ran. They write the same record, into the same namespace, and make the same +refusals. + It exists because prod-state tests otherwise decide what to assert by reading the **clock**. The pattern that emerges without it is a dual-state invariant — accept either the pre- or the post-migration value until a hardcoded deadline, @@ -237,9 +247,51 @@ frequently "which invariant applies _yet_": a cliff that starts at the migration, a rate that changes a week after it. A flag sends a consumer that needs the moment back to a hardcoded date, which is the thing this registry exists to delete. Zero and nonzero carry the same two distinct facts a flag did, -with the nonzero case saying more — and zero stays unambiguous because a record -is refused outright in a block whose timestamp is zero rather than written as -one that reads back as no record. +with the nonzero case saying more — and zero stays unambiguous because a zero +moment is refused outright rather than written as a record that reads back as no +record. + +**The moment is the caller's, inside a window the registry enforces.** The fact +being recorded is that a migration RAN, and the moment it ran is not in general +the moment anybody gets to write it down. A registry that could only stamp its +own block offers a writer with history two options and no third: record a time +that is false for every past migration, or record nothing — and recording +nothing strands the namespace, because the registry refuses anything not applied +onto the current head, so a writer that skipped its past migrations cannot +record its next one either. + +What a reader gives up is **not** authenticity. A record is namespaced by the +account that wrote it and no authority checks it, so every entry is already +exactly as trustworthy as its writer and no more; a writer free to invent an id +was always free to invent the fact. What a reader gives up is precisely that +`appliedAt` is the block the record landed in. Everything else is kept, by three +refusals: + +- **Never zero** (`ZeroTimestamp`), or the record would read back through + `applied` as no record while the head had moved and the migration could never + be applied again. +- **Never after the block it is written in** (`FutureTimestamp`). A migration + that has run has run, so a moment still to come is not a late record of + anything, and a consumer measuring an interval since the migration — a cliff, + a grace period, a rate that changes a week later — can subtract it from the + current block without underflowing. +- **Never before the record it is applied onto** (`TimestampBeforeHead`), so a + namespace's moments never go backwards along its chain and the gap between two + of its migrations subtracts in chain order without underflowing either. The + first migration in a namespace is applied onto `MIGRATION_HEAD_GENESIS`, which + holds no record and so bounds nothing. + +Equal is allowed wherever there is a neighbour. A moment may be exactly the +block it is written in, and two records may carry the same moment: two +migrations applied in one transaction share a block, and two backfilled to the +same day share a moment, so forcing them apart would demand a precision the +moments do not have. Which of them ran first is the chain, not the moments — +total order comes from `appliedOnto`, and the bound above only stops a record +claiming to predate the one it is chained onto. + +Both writes get all three, `block.timestamp` included: a block whose timestamp +is zero is `ZeroTimestamp` on `applyMigration`, which a test that warps to zero +and a chain configured from a zero genesis both reach. **A set of applied migrations, not a high-water mark.** A mark needs a total order consumers do not have: two migrations authored on one day collide, and one @@ -251,18 +303,25 @@ dependency actually lives. **A head, so a step cannot be skipped or repeated.** A namespace has a head: the migration it applied most recently, or `MIGRATION_HEAD_GENESIS` if it has -applied none. `applyMigration` names the head it is applying onto, so a chain -that never got the predecessor fails at the moment of applying rather than -diverging silently, and two migrations dispatched at once cannot land in the -wrong order. +applied none. Both writes name the head they are applying onto, so a chain that +never got the predecessor fails at the moment of applying rather than diverging +silently, and two migrations dispatched at once cannot land in the wrong order. ```solidity -// The first migration in a namespace. +// The first migration in a namespace, applied in this transaction. LibMigrationRegistry.applyMigration(MIGRATION_HEAD_GENESIS, MIGRATION_V1); // Every later one names its predecessor. LibMigrationRegistry.applyMigration(MIGRATION_V1, MIGRATION_V2); +// One that ran before the registry reached this chain names the moment it ran. +LibMigrationRegistry.applyMigrationHistory(MIGRATION_V2, MIGRATION_V3, 1750000000); ``` +Each record also keeps the head it was applied onto, which `appliedOnto` reads +back, so a namespace is a chain in storage rather than a set of moments to sort: +from `head`, each answer names the record before it, down to +`MIGRATION_HEAD_GENESIS`. That chain is the order the migrations ran in whatever +moments the records carry. + Genesis is deliberately **not zero**. Zero is what an uninitialised `bytes32` constant reads as, and a zero genesis would make a mis-set predecessor constant a _successful_ first application on any namespace that happens to be empty — the @@ -297,11 +356,12 @@ say the invariant holds — a multisig can act out of band and nothing here move Keep both layers: this selects, codehash and bytecode pins verify. Replacing the pins with it trades a clock-guess for a bookkeeping-guess. -`LibMigrationRegistry` is the surface — `applied`, `head` and `applyMigration`, -all verifying the registry's code hash first. There is deliberately **no -broadcast runner**: the dominant real shape is a Safe executing a bundle that -never broadcasts, and such a script appends `applyMigration` to the bundle it is -already emitting, which makes the record atomic with the migration it describes. +`LibMigrationRegistry` is the surface — `applied`, `appliedOnto`, `head`, +`applyMigration` and `applyMigrationHistory`, each verifying the registry's code +hash before it reads or writes. There is deliberately **no broadcast runner**: +the dominant real shape is a Safe executing a bundle that never broadcasts, and +such a script appends `applyMigration` to the bundle it is already emitting, +which makes the record atomic with the migration it describes. ## Deploying, and then releasing diff --git a/src/concrete/MigrationRegistry.sol b/src/concrete/MigrationRegistry.sol index 7effdee..c99355d 100644 --- a/src/concrete/MigrationRegistry.sol +++ b/src/concrete/MigrationRegistry.sol @@ -4,11 +4,23 @@ pragma solidity =0.8.25; import {IMigrationRegistryV1, MIGRATION_HEAD_GENESIS} from "../interface/IMigrationRegistryV1.sol"; +/// @dev One writer's record of one migration. Written whole, so a record can +/// never hold one half of itself. +struct MigrationRecord { + /// The moment recorded against the migration. Zero means never applied, + /// which neither write records. + uint256 appliedAt; + /// The head the namespace was at when the record was written. Zero means + /// never applied: a head is genesis or an applied id, both nonzero. + bytes32 appliedOnto; +} + /// @title MigrationRegistry /// @notice The whole of `IMigrationRegistryV1`: a writer applies one of its own -/// migrations onto the head it believes its namespace is at, and anyone reads -/// when a given writer applied a given migration, or where that writer's -/// namespace has got to. +/// migrations onto the head it believes its namespace is at, at the moment it +/// says the migration ran, and anyone reads when a given writer applied a given +/// migration, what it applied it onto, or where that writer's namespace has got +/// to. /// /// There is deliberately nothing else. No removal, no upgrade, no pause, and no /// authority at all — which is the difference from `AddressRegistry`, and the @@ -35,23 +47,33 @@ import {IMigrationRegistryV1, MIGRATION_HEAD_GENESIS} from "../interface/IMigrat /// on a chain. /// /// A record is append-only per writer, and a head only ever moves forward onto -/// something new. `applyMigration` refuses a migration the caller has already -/// applied, which is what makes re-running a migration fail rather than repeat, -/// and refuses one applied onto anything but the namespace's current head, which -/// is what makes a skipped or out-of-order migration fail rather than diverge. -/// There is no way to unrecord one — a record describes something that happened, -/// and nothing that happened stops having happened. +/// something new. Both writes refuse a migration the caller has already applied, +/// which is what makes re-running a migration fail rather than repeat, and +/// refuse one applied onto anything but the namespace's current head, which is +/// what makes a skipped or out-of-order migration fail rather than diverge. +/// There is no way to unrecord one, and no way to rewrite one — a record +/// describes something that happened, and nothing that happened stops having +/// happened. +/// +/// The moment is the CALLER's on `applyMigrationHistory`, so a migration that +/// ran before this contract reached the chain is recordable with the time it +/// actually ran. The order is not the caller's: each record keeps the head it +/// was applied onto, so a namespace's records are a chain from `head` back to +/// `MIGRATION_HEAD_GENESIS` whatever moments they carry. The moments run with +/// that chain rather than against it — a record is never earlier than the one +/// it was applied onto, though it may be equal to it. /// -/// Neither storage mapping is `public`. `applied` and `head` refuse the zero -/// writer, `applied` refuses the two ids a migration can never be, and a public -/// mapping's generated getter would answer all of them with zero — which for -/// `applied` is "not applied" and for `head` is a value no head can ever hold, -/// i.e. exactly the silent wrong-branch this contract reverts to prevent. +/// Neither storage mapping is `public`. `applied`, `appliedOnto` and `head` +/// refuse the zero writer, the two record readers refuse the two ids a +/// migration can never be, and a public mapping's generated getter would answer +/// all of them with zero — which for a record is "not applied" and for `head` +/// is a value no head can ever hold, i.e. exactly the silent wrong-branch this +/// contract reverts to prevent. contract MigrationRegistry is IMigrationRegistryV1 { - /// When each record landed, namespaced by writer. Zero means never. Not - /// `public`: the only reader is `applied`, which refuses the two inputs that - /// can only be mistakes. - mapping(address writer => mapping(bytes32 migration => uint256 appliedAt)) internal sApplied; + /// Every record, namespaced by writer. A zero `appliedAt` means never + /// applied. Not `public`: the only readers are `applied` and `appliedOnto`, + /// which refuse the inputs that can only be mistakes. + mapping(address writer => mapping(bytes32 migration => MigrationRecord record)) internal sRecords; /// The most recent migration applied under each writer. Zero means the /// namespace is empty, which reads out as `MIGRATION_HEAD_GENESIS` — the @@ -61,10 +83,30 @@ contract MigrationRegistry is IMigrationRegistryV1 { mapping(address writer => bytes32 head) internal sHead; /// @inheritdoc IMigrationRegistryV1 - /// @dev The refusals run caller-input first and environment last: the two - /// that describe a mistake in the call are true whatever block this lands - /// in, so they are what a caller is told about first. + /// @dev The block is the moment, so a caller that has nothing to say about + /// when its migration ran does not have to say it. + // slither-disable-next-line timestamp + // forge-lint: disable-next-line(block-timestamp) function applyMigration(bytes32 expectedHead, bytes32 migration) external { + applyMigrationRecord(expectedHead, migration, block.timestamp); + } + + /// @inheritdoc IMigrationRegistryV1 + function applyMigrationHistory(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) external { + applyMigrationRecord(expectedHead, migration, appliedAt); + } + + /// Reached by `applyMigration` and by `applyMigrationHistory`, so there is + /// one record and one set of refusals whichever of them supplied the moment. + /// + /// The refusals run from the ones that describe the call alone, through the + /// ones that describe the namespace it arrives at, to the one that + /// describes the block it lands in — which is the order in which a caller + /// can do something about them. + /// @param expectedHead The head the caller believes its namespace is at. + /// @param migration The migration to apply. + /// @param appliedAt The moment to record against it. + function applyMigrationRecord(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) internal { // Checked before everything else, so an uninitialised id is reported as // the mistake it is rather than as a first record of zero. if (migration == bytes32(0)) { @@ -78,6 +120,24 @@ contract MigrationRegistry is IMigrationRegistryV1 { if (migration == MIGRATION_HEAD_GENESIS) { revert GenesisMigration(); } + // Beside the two id refusals because it is the same kind of mistake in + // the same kind of value: an uninitialised `uint256`, refused whatever + // namespace it arrives at and whatever block it lands in. Zero is the + // one moment a record cannot carry — `applied` would answer it as + // "never applied" while the head had moved and the migration could + // never be applied again. Reached by `applyMigration` as well, in a + // block whose timestamp is zero: a test can warp to zero and a chain + // can be configured from a zero genesis. + // + // Slither flags a strict equality on anything reaching it from + // `block.timestamp`, which `applyMigration` does. Zero is the only + // value this refuses and the only one it can refuse, so there is no + // window for a validator to nudge the clock across. Suppressed on this + // comparison rather than turned off for the repo. + // slither-disable-next-line incorrect-equality + if (appliedAt == 0) { + revert ZeroTimestamp(); + } // There is deliberately no zero-writer case here. `msg.sender` cannot // be the zero address, so the zero namespace is unreachable for writes // and a guard on it would be unreachable code pretending to be a check. @@ -90,50 +150,85 @@ contract MigrationRegistry is IMigrationRegistryV1 { // say to a re-dispatched script. It is also not implied by the head // check: re-applying a migration whose successor has landed presents a // matching head, and would drag the head backwards and overwrite the - // original timestamp. - if (sApplied[msg.sender][migration] != 0) { + // original record. + if (sRecords[msg.sender][migration].appliedAt != 0) { revert MigrationAlreadyApplied(msg.sender, migration); } bytes32 actualHead = head(msg.sender); if (expectedHead != actualHead) { revert UnexpectedMigrationHead(msg.sender, expectedHead, actualHead); } - // A zero timestamp is the one value a record cannot carry: `applied` - // would answer it as "never applied" while the head had moved and the - // migration could never be applied again. Not unreachable — a test can - // warp to zero and a chain can be configured from a zero genesis — so - // this is a real check rather than a decorative one. + // Last of the refusals about the namespace, because it is the only one + // that reads a RECORD rather than a key, and the record it reads is the + // one at the head the check above has just confirmed. + // + // At genesis there is nothing to be before. Genesis can never be + // applied, so the record at it is empty in every namespace forever and + // this comparison against zero can only pass — which is the same + // statement a genesis branch would make, made by the value itself. // - // The usual hazard behind a `block.timestamp` comparison — the one both - // the static analysers flag here — is a validator nudging the clock - // across a threshold. There is no threshold here and no nudge - // available: zero is not a value a validator on a live chain can - // produce at all, which is why this is an equality against it rather - // than a window around it, and why all three warnings are suppressed on - // this one comparison rather than turned off for the repo. + // Neither the head nor its own moment is restated in the error: the + // caller named the head, and was told above if it named the wrong one. + uint256 headAppliedAt = sRecords[msg.sender][actualHead].appliedAt; + if (appliedAt < headAppliedAt) { + revert TimestampBeforeHead(appliedAt, headAppliedAt); + } + // Last, because it is the only refusal here that time itself resolves: + // every other one describes something wrong with the call or with where + // the namespace is, and this one describes a moment that has not + // arrived yet. // - // Slither's two are a start/end pair rather than a next-line because - // only one comment fits immediately above the `if`, `forge fmt` moves a - // trailing one inside the braces, and forge-lint has no pair form. - // slither-disable-start incorrect-equality,timestamp + // The usual hazard behind a `block.timestamp` comparison — the one the + // static analysers flag here — is a validator nudging the clock across a + // threshold. The nudge is available and it is harmless: a validator that + // moves the clock forward admits a record a second earlier than it would + // otherwise have been admitted, of a migration that has run either way, + // and moving it backwards is not something a chain lets a proposer do. + // So the warnings are suppressed on this one comparison rather than + // turned off for the repo. + // + // Slither's is a start/end pair rather than a next-line because only one + // comment fits immediately above the `if`, `forge fmt` moves a trailing + // one inside the braces, and forge-lint has no pair form. + // slither-disable-start timestamp // forge-lint: disable-next-line(block-timestamp) - if (block.timestamp == 0) { - revert ZeroTimestamp(); + if (appliedAt > block.timestamp) { + revert FutureTimestamp(appliedAt, block.timestamp); } - // slither-disable-end incorrect-equality,timestamp - sApplied[msg.sender][migration] = block.timestamp; + // slither-disable-end timestamp + // Written whole, so the moment and the predecessor cannot land apart. + sRecords[msg.sender][migration] = MigrationRecord({appliedAt: appliedAt, appliedOnto: actualHead}); sHead[msg.sender] = migration; - emit Migrated(msg.sender, migration); + emit Migrated(msg.sender, migration, appliedAt); } /// @inheritdoc IMigrationRegistryV1 /// @dev All three refusals are about a caller that has not supplied what it /// thinks it has. None can ever be a real record: nothing originates from - /// the zero address, and `applyMigration` will write neither the zero id nor - /// the genesis one — so answering zero for any of them would be answering a + /// the zero address, and neither write records the zero id or the genesis + /// one — so answering zero for any of them would be answering a /// question the caller did not mean to ask, and answering it with the value /// that sends it down its pre-migration branch. function applied(address writer, bytes32 migration) external view returns (uint256) { + checkRecordKey(writer, migration); + return sRecords[writer][migration].appliedAt; + } + + /// @inheritdoc IMigrationRegistryV1 + /// @dev The same three refusals as `applied`, for the same reason and on + /// the same key: a zero answer here reads as "never applied" exactly as a + /// zero moment does. + function appliedOnto(address writer, bytes32 migration) external view returns (bytes32) { + checkRecordKey(writer, migration); + return sRecords[writer][migration].appliedOnto; + } + + /// Refuses the three inputs that can only be a mistake in the caller rather + /// than a record to read. One function, so the two readers of a record + /// cannot drift into refusing different things. + /// @param writer The namespace being read. + /// @param migration The migration being asked about. + function checkRecordKey(address writer, bytes32 migration) internal pure { if (writer == address(0)) { revert ZeroWriter(); } @@ -143,7 +238,6 @@ contract MigrationRegistry is IMigrationRegistryV1 { if (migration == MIGRATION_HEAD_GENESIS) { revert GenesisMigration(); } - return sApplied[writer][migration]; } /// @inheritdoc IMigrationRegistryV1 @@ -154,12 +248,12 @@ contract MigrationRegistry is IMigrationRegistryV1 { /// /// The empty-namespace zero is translated to genesis here and nowhere else, /// which is why this is one `public` function rather than a reader beside an - /// internal helper: `applyMigration` compares against exactly what a caller - /// reads, so the two cannot drift into different ideas of where a namespace - /// that has applied nothing is. + /// internal helper: a write compares against exactly what a caller reads, so + /// the two cannot drift into different ideas of where a namespace that has + /// applied nothing is. /// - /// `applyMigration` reaches it as `head(msg.sender)`, which can never be the - /// zero address, so the refusal is redundant on that path. It is one + /// A write reaches it as `head(msg.sender)`, which can never be the zero + /// address, so the refusal is redundant on that path. It is one /// function, so it is one refusal, and the reachable path is the one it is /// there for. function head(address writer) public view returns (bytes32) { diff --git a/src/generated/candidate/MigrationRegistry.sol b/src/generated/candidate/MigrationRegistry.sol index 2e4f696..a031048 100644 --- a/src/generated/candidate/MigrationRegistry.sol +++ b/src/generated/candidate/MigrationRegistry.sol @@ -5,19 +5,19 @@ pragma solidity ^0.8.25; // THIS FILE IS AUTOGENERATED BY THE BUILD SCRIPT. DO NOT EDIT BY HAND. /// @dev Hash of the known bytecode. -bytes32 constant BYTECODE_HASH = bytes32(0x10624d7ac73d3b4e379fc0af77347144e32f78808fb51795ac7b2613d2f4df53); +bytes32 constant BYTECODE_HASH = bytes32(0xb9a3ed001b8e4acff189216b31f4598759df873dbb9f7c114bf2995941e3723c); /// @dev The deterministic deploy address of the contract when deployed via /// the Zoltu factory. -address constant DEPLOYED_ADDRESS = address(0x6E3aE74aDCd6CF28A1b2F685D5E709ffE44D429D); +address constant DEPLOYED_ADDRESS = address(0x13175E90969fE4977834210F25Fb3ED3ABBA64C7); /// @dev The creation bytecode of the contract. bytes constant CREATION_CODE = - hex"6080604052348015600e575f80fd5b506104a18061001c5f395ff3fe608060405234801561000f575f80fd5b506004361061003f575f3560e01c8063a56d39e014610043578063bda4fec514610058578063e29304161461007d575b5f80fd5b610056610051366004610418565b610090565b005b61006b610066366004610460565b61025f565b60405190815260200160405180910390f35b61006b61008b366004610479565b610307565b806100c7576040517f7704c9bc00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f8103610120576040517f86ba6ff800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b335f908152602081815260408083208484529091529020541561017d576040517fd242f96f000000000000000000000000000000000000000000000000000000008152336004820152602481018290526044015b60405180910390fd5b5f6101873361025f565b90508083146101d2576040517facbe68520000000000000000000000000000000000000000000000000000000081523360048201526024810184905260448101829052606401610174565b425f0361020b576040517fda16d76700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b335f8181526020818152604080832086845282528083204290558383526001909152808220859055518492917f4b783664c1bcc23d06e2e5633252a3fce6e0d115df1940913aecf8082b893de191a3505050565b5f73ffffffffffffffffffffffffffffffffffffffff82166102ad576040517f895c0eb100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff82165f9081526001602052604090205480156102de5780610300565b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f5b9392505050565b5f73ffffffffffffffffffffffffffffffffffffffff8316610355576040517f895c0eb100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b8161038c576040517f7704c9bc00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f82036103e5576040517f86ba6ff800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5073ffffffffffffffffffffffffffffffffffffffff919091165f90815260208181526040808320938352929052205490565b5f8060408385031215610429575f80fd5b50508035926020909101359150565b803573ffffffffffffffffffffffffffffffffffffffff8116811461045b575f80fd5b919050565b5f60208284031215610470575f80fd5b61030082610438565b5f806040838503121561048a575f80fd5b61049383610438565b94602093909301359350505056"; + hex"6080604052348015600e575f80fd5b506106428061001c5f395ff3fe608060405234801561000f575f80fd5b5060043610610064575f3560e01c8063a56d39e01161004d578063a56d39e0146100a2578063bda4fec5146100b5578063e2930416146100c8575f80fd5b8063280a6565146100685780638c3f2ba51461007d575b5f80fd5b61007b610076366004610590565b6100db565b005b61009061008b3660046105e1565b6100eb565b60405190815260200160405180910390f35b61007b6100b0366004610609565b61012c565b6100906100c3366004610629565b61013b565b6100906100d63660046105e1565b6101e3565b6100e6838383610221565b505050565b5f6100f683836104b3565b5073ffffffffffffffffffffffffffffffffffffffff919091165f90815260208181526040808320938352929052206001015490565b610137828242610221565b5050565b5f73ffffffffffffffffffffffffffffffffffffffff8216610189576040517f895c0eb100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff82165f9081526001602052604090205480156101ba57806101dc565b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f5b9392505050565b5f6101ee83836104b3565b5073ffffffffffffffffffffffffffffffffffffffff919091165f90815260208181526040808320938352929052205490565b81610258576040517f7704c9bc00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f82036102b1576040517f86ba6ff800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b805f036102ea576040517fda16d76700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b335f9081526020818152604080832085845290915290205415610347576040517fd242f96f000000000000000000000000000000000000000000000000000000008152336004820152602481018390526044015b60405180910390fd5b5f6103513361013b565b905080841461039c576040517facbe6852000000000000000000000000000000000000000000000000000000008152336004820152602481018590526044810182905260640161033e565b335f90815260208181526040808320848452909152902054808310156103f8576040517f29d1b347000000000000000000000000000000000000000000000000000000008152600481018490526024810182905260440161033e565b4283111561043b576040517f8fdce34f0000000000000000000000000000000000000000000000000000000081526004810184905242602482015260440161033e565b6040805180820182528481526020808201858152335f8181528084528581208a82528452858120945185559151600194850155808252928252839020879055915185815286927f7758a2e9a4f791e6196587ea8cf721b01284de690cbf67ce7a0962b3c80601f6910160405180910390a35050505050565b73ffffffffffffffffffffffffffffffffffffffff8216610500576040517f895c0eb100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b80610537576040517f7704c9bc00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f8103610137576040517f86ba6ff800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f805f606084860312156105a2575f80fd5b505081359360208301359350604090920135919050565b803573ffffffffffffffffffffffffffffffffffffffff811681146105dc575f80fd5b919050565b5f80604083850312156105f2575f80fd5b6105fb836105b9565b946020939093013593505050565b5f806040838503121561061a575f80fd5b50508035926020909101359150565b5f60208284031215610639575f80fd5b6101dc826105b956"; /// @dev The runtime bytecode of the contract. bytes constant RUNTIME_CODE = - hex"608060405234801561000f575f80fd5b506004361061003f575f3560e01c8063a56d39e014610043578063bda4fec514610058578063e29304161461007d575b5f80fd5b610056610051366004610418565b610090565b005b61006b610066366004610460565b61025f565b60405190815260200160405180910390f35b61006b61008b366004610479565b610307565b806100c7576040517f7704c9bc00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f8103610120576040517f86ba6ff800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b335f908152602081815260408083208484529091529020541561017d576040517fd242f96f000000000000000000000000000000000000000000000000000000008152336004820152602481018290526044015b60405180910390fd5b5f6101873361025f565b90508083146101d2576040517facbe68520000000000000000000000000000000000000000000000000000000081523360048201526024810184905260448101829052606401610174565b425f0361020b576040517fda16d76700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b335f8181526020818152604080832086845282528083204290558383526001909152808220859055518492917f4b783664c1bcc23d06e2e5633252a3fce6e0d115df1940913aecf8082b893de191a3505050565b5f73ffffffffffffffffffffffffffffffffffffffff82166102ad576040517f895c0eb100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff82165f9081526001602052604090205480156102de5780610300565b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f5b9392505050565b5f73ffffffffffffffffffffffffffffffffffffffff8316610355576040517f895c0eb100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b8161038c576040517f7704c9bc00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f82036103e5576040517f86ba6ff800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5073ffffffffffffffffffffffffffffffffffffffff919091165f90815260208181526040808320938352929052205490565b5f8060408385031215610429575f80fd5b50508035926020909101359150565b803573ffffffffffffffffffffffffffffffffffffffff8116811461045b575f80fd5b919050565b5f60208284031215610470575f80fd5b61030082610438565b5f806040838503121561048a575f80fd5b61049383610438565b94602093909301359350505056"; + hex"608060405234801561000f575f80fd5b5060043610610064575f3560e01c8063a56d39e01161004d578063a56d39e0146100a2578063bda4fec5146100b5578063e2930416146100c8575f80fd5b8063280a6565146100685780638c3f2ba51461007d575b5f80fd5b61007b610076366004610590565b6100db565b005b61009061008b3660046105e1565b6100eb565b60405190815260200160405180910390f35b61007b6100b0366004610609565b61012c565b6100906100c3366004610629565b61013b565b6100906100d63660046105e1565b6101e3565b6100e6838383610221565b505050565b5f6100f683836104b3565b5073ffffffffffffffffffffffffffffffffffffffff919091165f90815260208181526040808320938352929052206001015490565b610137828242610221565b5050565b5f73ffffffffffffffffffffffffffffffffffffffff8216610189576040517f895c0eb100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff82165f9081526001602052604090205480156101ba57806101dc565b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f5b9392505050565b5f6101ee83836104b3565b5073ffffffffffffffffffffffffffffffffffffffff919091165f90815260208181526040808320938352929052205490565b81610258576040517f7704c9bc00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f82036102b1576040517f86ba6ff800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b805f036102ea576040517fda16d76700000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b335f9081526020818152604080832085845290915290205415610347576040517fd242f96f000000000000000000000000000000000000000000000000000000008152336004820152602481018390526044015b60405180910390fd5b5f6103513361013b565b905080841461039c576040517facbe6852000000000000000000000000000000000000000000000000000000008152336004820152602481018590526044810182905260640161033e565b335f90815260208181526040808320848452909152902054808310156103f8576040517f29d1b347000000000000000000000000000000000000000000000000000000008152600481018490526024810182905260440161033e565b4283111561043b576040517f8fdce34f0000000000000000000000000000000000000000000000000000000081526004810184905242602482015260440161033e565b6040805180820182528481526020808201858152335f8181528084528581208a82528452858120945185559151600194850155808252928252839020879055915185815286927f7758a2e9a4f791e6196587ea8cf721b01284de690cbf67ce7a0962b3c80601f6910160405180910390a35050505050565b73ffffffffffffffffffffffffffffffffffffffff8216610500576040517f895c0eb100000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b80610537576040517f7704c9bc00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7fd85a7f14bb19a07649810ac09029cc9c29667e4059cb83e258bc52e213365d3f8103610137576040517f86ba6ff800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f805f606084860312156105a2575f80fd5b505081359360208301359350604090920135919050565b803573ffffffffffffffffffffffffffffffffffffffff811681146105dc575f80fd5b919050565b5f80604083850312156105f2575f80fd5b6105fb836105b9565b946020939093013593505050565b5f806040838503121561061a575f80fd5b50508035926020909101359150565b5f60208284031215610639575f80fd5b6101dc826105b956"; /// @dev The addresses that MUST already have code on a network before /// this release can be broadcast there, `abi.encode`d as an `address[]` diff --git a/src/interface/IMigrationRegistryV1.sol b/src/interface/IMigrationRegistryV1.sol index 143c9ba..733938c 100644 --- a/src/interface/IMigrationRegistryV1.sol +++ b/src/interface/IMigrationRegistryV1.sol @@ -2,9 +2,9 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity ^0.8.25; -/// @dev The head of a namespace that has never applied a migration. A writer's -/// first `applyMigration` names this, and every later one names the migration -/// before it. +/// @dev The head of a namespace that has never applied a migration. The first +/// migration a writer applies names this, and every later one names the +/// migration before it. /// /// It is deliberately NOT zero. Zero is what an uninitialised `bytes32` constant /// reads as, and a genesis of zero would make an uninitialised predecessor @@ -30,13 +30,22 @@ pragma solidity ^0.8.25; bytes32 constant MIGRATION_HEAD_GENESIS = keccak256("rain.migration-registry.head.genesis"); /// @title IMigrationRegistryV1 -/// @notice A per-writer record of which migrations have been applied and when, -/// with exactly three operations: a writer applies one of its own migrations -/// onto the head it believes its namespace is at (`applyMigration`), anyone -/// reads when a given writer applied a given migration (`applied`), and anyone -/// reads where a given writer's namespace currently is (`head`). There is no -/// removal, no upgrade and no authority beyond the writer over its own -/// namespace, and an implementation MUST NOT add any. +/// @notice A per-writer record of which migrations have been applied, when, and +/// onto what, with exactly five operations: a writer applies one of its own +/// migrations onto the head it believes its namespace is at, either as applied +/// now (`applyMigration`) or as already applied at a moment it supplies +/// (`applyMigrationHistory`), anyone reads when a given writer applied a given +/// migration (`applied`), what that writer applied it onto (`appliedOnto`), and +/// where that writer's namespace currently is (`head`). There is no removal, no +/// upgrade and no authority beyond the writer over its own namespace, and an +/// implementation MUST NOT add any. +/// +/// The two writes differ only in where the recorded moment comes from. +/// `applyMigration` records the block it lands in, for a script applying its +/// own migration in the same atomic unit as the migration. +/// `applyMigrationHistory` takes the moment as an argument, for a migration +/// that already ran — one that ran before this registry reached the chain, or +/// before its writer started recording at all. /// /// It exists so that a test can decide what to assert by reading what happened /// on chain rather than by reading the clock. Without it, a test that spans a @@ -61,11 +70,41 @@ bytes32 constant MIGRATION_HEAD_GENESIS = keccak256("rain.migration-registry.hea /// anything about the state a migration produced — only that it was applied, /// and when. /// -/// The timestamp is a fact about the RECORD, not about the state: it is the -/// block the record landed in, which the record's own log already carries, and -/// it says nothing whatsoever about what the migration did. Reading it back does -/// not become proof of anything, for the same reason reading the record back -/// does not. +/// The timestamp is a fact about the RECORD, not about the state: it says when +/// a migration ran and nothing whatsoever about what it did. Reading it back +/// does not become proof of anything, for the same reason reading the record +/// back does not. +/// +/// ## A moment is data, and it is bounded +/// +/// A moment supplied by the caller is not authenticated, and nothing else in a +/// record is either. A record is namespaced by the account that wrote it and no +/// authority checks it, so every entry is exactly as trustworthy as the writer +/// that wrote it: a writer free to invent a migration id is free to invent the +/// moment. An implementation MUST bound it anyway, however it arrived: +/// +/// - NEVER ZERO (`ZeroTimestamp`). Zero is what `applied` answers for a +/// migration nobody applied, so a record carrying it would read back as no +/// record while the head had moved and the migration could never be applied +/// again. +/// - NEVER AFTER THE BLOCK IT IS WRITTEN IN (`FutureTimestamp`). A record says +/// a migration HAS run. `applied` therefore never answers a moment later than +/// the block asking, and a consumer whose invariant is an interval since the +/// migration — a cliff, a grace period, a rate that changes a week later — +/// subtracts it from the current block without underflowing. +/// - NEVER BEFORE THE RECORD IT IS APPLIED ONTO (`TimestampBeforeHead`). A +/// namespace's moments therefore never go backwards along its chain, so a +/// consumer measuring the gap between two of its migrations subtracts them +/// in chain order without underflowing either. The first migration in a +/// namespace is applied onto `MIGRATION_HEAD_GENESIS`, which holds no record +/// and so bounds nothing. +/// +/// Nothing else constrains it, and EQUAL is accepted at both of the bounds that +/// have a neighbour: a moment may be exactly the block it is written in, and +/// two records may carry the same moment. Two migrations applied in one +/// transaction share a block and two backfilled to the same day share a +/// moment, so forcing them apart would demand a precision the moments do not +/// have. Which of them ran first is the chain, not the moments. /// /// ## `applied` answers WHEN, and zero still means "not applied" /// @@ -81,19 +120,25 @@ bytes32 constant MIGRATION_HEAD_GENESIS = keccak256("rain.migration-registry.hea /// asserts the pre-migration state in. Zero and nonzero are therefore the same /// two distinct facts a flag carried, with the nonzero case saying more. /// -/// That distinction is only sound while a real record can never BE zero, so an -/// implementation MUST refuse to write a record in a block whose timestamp is -/// zero rather than write one that reads back as no record at all. That is not a -/// hypothetical branch: a test can `vm.warp(0)`, and a chain can be configured +/// That distinction is only sound while a real record can never BE zero, which +/// is what `ZeroTimestamp` is for. It refuses a caller that supplies zero, and +/// it refuses `applyMigration` in a block whose timestamp is zero — neither +/// hypothetical, because a test can `vm.warp(0)` and a chain can be configured /// from a zero genesis. /// /// ## The head is what makes an ordered sequence ordered /// /// A namespace has a HEAD: the migration most recently applied under it, or -/// `MIGRATION_HEAD_GENESIS` if it has never applied one. `applyMigration` takes -/// the head the caller believes its namespace is at and refuses to write unless -/// that is where the namespace actually is; on success the applied migration -/// becomes the new head. +/// `MIGRATION_HEAD_GENESIS` if it has never applied one. Both writes take the +/// head the caller believes its namespace is at and refuse to write unless that +/// is where the namespace actually is; on success the applied migration becomes +/// the new head. +/// +/// Each record keeps the head it was applied onto, which `appliedOnto` reads +/// back. A namespace's records are therefore a chain in storage: from `head`, +/// each `appliedOnto` names the record before it, down to +/// `MIGRATION_HEAD_GENESIS`. That chain IS the order the migrations ran in, and +/// it is exact whatever moments the records carry. /// /// This is what blocks a SKIPPED step. A migration script names its predecessor, /// so a chain that never got the predecessor is a loud revert at the moment of @@ -163,8 +208,8 @@ bytes32 constant MIGRATION_HEAD_GENESIS = keccak256("rain.migration-registry.hea /// A head is an id, so the same is true of the head a script names: it is the /// predecessor's named constant, imported, not a second spelling of it. interface IMigrationRegistryV1 { - /// Thrown when `applyMigration` is called with the zero migration id, and - /// by `applied` when it is asked about one. The zero id is what an + /// Thrown when either write is called with the zero migration id, and by + /// `applied` when it is asked about one. The zero id is what an /// uninitialised `bytes32` constant reads as, and an uninitialised id is /// never a migration anybody meant to name. Rejected in both directions /// because the read is the dangerous one: answering zero would silently @@ -177,8 +222,8 @@ interface IMigrationRegistryV1 { /// which names the zero it was handed, so nothing about the mistake is lost. error ZeroMigration(); - /// Thrown when `applyMigration` is called with `MIGRATION_HEAD_GENESIS` as - /// the migration, and by `applied` when it is asked about it. Genesis is a + /// Thrown when either write is called with `MIGRATION_HEAD_GENESIS` as the + /// migration, and by `applied` when it is asked about it. Genesis is a /// head, not a migration: applying it would leave a namespace that has /// applied something at a head no different from one that has applied /// nothing, and asking `applied` about it would answer zero forever for a @@ -197,8 +242,8 @@ interface IMigrationRegistryV1 { /// genesis" — an unresolved or unset writer constant would therefore read as /// a pristine namespace rather than as the mistake it is. /// - /// There is no matching case on `applyMigration`: `msg.sender` is never - /// zero, so the zero namespace cannot be written to in the first place. + /// There is no matching case on either write: `msg.sender` is never zero, + /// so the zero namespace cannot be written to in the first place. error ZeroWriter(); /// Thrown when a writer applies a migration it has already applied. This @@ -225,49 +270,81 @@ interface IMigrationRegistryV1 { /// @param actualHead The head the namespace is actually at. error UnexpectedMigrationHead(address writer, bytes32 expectedHead, bytes32 actualHead); - /// Thrown when `applyMigration` is called in a block whose timestamp is - /// zero. A record IS its timestamp, so a zero one would read back through + /// Thrown when a write would record a zero moment: `applyMigrationHistory` + /// given zero, or `applyMigration` in a block whose timestamp is zero. A + /// record IS its moment, so a zero one would read back through /// `applied` as no record at all, while the head moved and the migration - /// cannot be re-applied — the worst of every branch at once. Refusing to - /// write is the only outcome that leaves the namespace describing something - /// true. + /// cannot be re-applied — the worst of every branch at once. Zero is also + /// what an uninitialised `uint256` holds, so it is refused for the same + /// reason `ZeroMigration` is. error ZeroTimestamp(); + /// Thrown when `applyMigrationHistory` is given an `appliedAt` after the + /// timestamp of the block it is called in. A record says a migration HAS + /// run, so a moment that has not arrived is not a record of anything — and a + /// consumer measuring an interval since the migration would be subtracting a + /// future moment from the present one. + /// @param appliedAt The moment supplied. + /// @param blockTimestamp The timestamp of the block the call landed in. + error FutureTimestamp(uint256 appliedAt, uint256 blockTimestamp); + + /// Thrown when `applyMigrationHistory` is given an `appliedAt` before the + /// moment recorded against the head it is being applied onto. A record says + /// its migration ran after the one before it in the chain, so an earlier + /// moment contradicts the sequence the same call just named, and a consumer + /// measuring the gap between two migrations would be subtracting the later + /// moment from the earlier one. + /// + /// Equal is accepted. Two migrations applied in one transaction share a + /// block, and two backfilled migrations known only to the same day share a + /// moment; the chain is what tells those apart, so refusing them would + /// demand a precision the moments do not have. + /// + /// The first migration in a namespace is never refused this way: it is + /// applied onto `MIGRATION_HEAD_GENESIS`, which is a head rather than a + /// migration and so holds no record and no moment to be before. + /// + /// Neither the head nor the caller is named, because both are values the + /// caller handed in and was told about first: `msg.sender` is the + /// namespace, and a wrong head is `UnexpectedMigrationHead`. + /// @param appliedAt The moment supplied. + /// @param headAppliedAt The moment recorded against the head it is being + /// applied onto. + error TimestampBeforeHead(uint256 appliedAt, uint256 headAppliedAt); + /// Emitted every time a migration is applied. A migration is applied at /// most once per writer, so the log is the complete history of the registry /// and the only way to discover a record without already knowing the id. /// - /// It carries neither the head nor the timestamp because both are already - /// there: the log is ordered, and one writer's entries in order ARE that - /// writer's chain of heads — each entry's migration is the head the next one - /// was applied onto, and the first was applied onto - /// `MIGRATION_HEAD_GENESIS`. The timestamp is the block's. + /// It carries no head, because the log is ordered and one writer's entries + /// in order ARE that writer's chain of heads — each entry's migration is the + /// head the next one was applied onto, and the first was applied onto + /// `MIGRATION_HEAD_GENESIS`. + /// + /// It does carry `appliedAt`, which the log does not otherwise hold: the + /// block a log entry sits in says when the record was written, and + /// `appliedAt` says when the migration ran. /// @param writer The namespace, which is the caller. /// @param migration The migration applied. - event Migrated(address indexed writer, bytes32 indexed migration); + /// @param appliedAt The moment recorded against it. + event Migrated(address indexed writer, bytes32 indexed migration, uint256 appliedAt); - /// Applies `migration` under the caller's namespace, onto `expectedHead`. - /// - /// The implementation MUST revert `ZeroMigration` if `migration` is zero, - /// `GenesisMigration` if it is `MIGRATION_HEAD_GENESIS`, - /// `MigrationAlreadyApplied` if the caller has already applied it, - /// `UnexpectedMigrationHead` if the caller's namespace is not at - /// `expectedHead`, and `ZeroTimestamp` if `block.timestamp` is zero. It MUST - /// NOT provide any way to unrecord a migration or to move a head backwards. - /// On success it MUST record the current block timestamp against - /// `migration`, make `migration` the caller's new head, and emit `Migrated`. - /// - /// Nothing is returned: the new head is the `migration` just passed in and - /// the timestamp is the block's, so both are already in the caller's hand. + /// Applies `migration` under the caller's namespace, onto `expectedHead`, + /// as having been applied in the block this call lands in. /// - /// A caller SHOULD call this in the same atomic unit as the migration - /// itself where it can — a Safe appends this call to the bundle it is - /// already executing — so that the record and the change it describes + /// This is for a script applying its own migration, so the record lands in + /// the same atomic unit as the change it describes — a Safe + /// appends this call to the bundle it is already executing — and the two /// cannot land apart. Where they cannot be atomic, call it LAST: a record /// that never landed leaves a reader asserting the pre-migration state, /// which the verification layer then catches loudly, and leaves a re-run /// possible. A record that landed for a migration that did not is the /// harder state to get out of. + /// + /// It records the same record as `applyMigrationHistory` and makes the same + /// refusals, against `block.timestamp` as the moment — so a block whose + /// timestamp is zero is `ZeroTimestamp`, and the moment can never be in the + /// future. /// @param expectedHead The head the caller believes its namespace is at: /// the migration it is applying onto, or `MIGRATION_HEAD_GENESIS` for the /// first migration in a namespace. Never zero, which can never match. @@ -275,8 +352,36 @@ interface IMigrationRegistryV1 { /// `MIGRATION_HEAD_GENESIS`. function applyMigration(bytes32 expectedHead, bytes32 migration) external; - /// When `writer` applied `migration`, as the timestamp of the block the - /// record landed in. Zero if it never did. + /// Applies `migration` under the caller's namespace, onto `expectedHead`, as + /// having been applied at `appliedAt`. + /// + /// This is for a migration that ALREADY ran, which records the moment it ran + /// rather than the moment it was written down. + /// + /// The implementation MUST revert `ZeroMigration` if `migration` is zero, + /// `GenesisMigration` if it is `MIGRATION_HEAD_GENESIS`, `ZeroTimestamp` if + /// `appliedAt` is zero, `MigrationAlreadyApplied` if the caller has already + /// applied it, `UnexpectedMigrationHead` if the caller's namespace is not at + /// `expectedHead`, `TimestampBeforeHead` if `appliedAt` is before the moment + /// recorded against `expectedHead`, and `FutureTimestamp` if `appliedAt` is + /// after `block.timestamp`. It MUST NOT provide any way to unrecord a + /// migration, to move a head backwards, or to move a record once written. On + /// success it MUST record `appliedAt` and `expectedHead` against + /// `migration`, make `migration` the caller's new head, and emit `Migrated`. + /// + /// Nothing is returned: every part of the record is an argument the caller + /// just handed in. + /// @param expectedHead The head the caller believes its namespace is at: + /// the migration it is applying onto, or `MIGRATION_HEAD_GENESIS` for the + /// first migration in a namespace. Never zero, which can never match. + /// @param migration The migration to apply. Never zero, never + /// `MIGRATION_HEAD_GENESIS`. + /// @param appliedAt The moment `migration` was applied. Never zero, never + /// after the block this call lands in. + function applyMigrationHistory(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) external; + + /// When `writer` applied `migration`, as the moment recorded with the + /// record. Zero if it never did. /// /// The implementation MUST revert `ZeroWriter`, `ZeroMigration` or /// `GenesisMigration` rather than answering about any of them, and MUST @@ -291,15 +396,37 @@ interface IMigrationRegistryV1 { /// revert there would leave a caller with nothing to say about the state it /// is actually looking at, which is the whole failure this registry removes. /// - /// Zero is unambiguous because `applyMigration` refuses to write a zero - /// timestamp, so no applied migration can present as an unapplied one. + /// Zero is unambiguous because neither write records a zero moment, so no + /// applied migration can present as an unapplied one. /// @param writer The namespace to read. Never the zero address. /// @param migration The migration to ask about. Never zero, never /// `MIGRATION_HEAD_GENESIS`. - /// @return The block timestamp `writer` applied `migration` at, or zero if - /// it has not. + /// @return The moment `writer` applied `migration` at, or zero if it has + /// not. function applied(address writer, bytes32 migration) external view returns (uint256); + /// What `writer` applied `migration` ONTO: the head that namespace was at + /// when the record was written. Zero if `writer` never applied `migration`. + /// + /// The implementation MUST revert `ZeroWriter`, `ZeroMigration` or + /// `GenesisMigration` rather than answering about any of them, and MUST + /// answer zero — not revert — for a real writer that has simply not applied + /// a real migration. + /// + /// Zero is unambiguous: a head is either `MIGRATION_HEAD_GENESIS` or an + /// applied id, both nonzero, so zero is never something a record holds. + /// + /// This is what makes a namespace's records a chain rather than a set. + /// Walked from `head` back, each answer names the record before it and the + /// walk ends at `MIGRATION_HEAD_GENESIS`, which is the order the migrations + /// ran in whatever moments they carry. + /// @param writer The namespace to read. Never the zero address. + /// @param migration The migration to ask about. Never zero, never + /// `MIGRATION_HEAD_GENESIS`. + /// @return The head `writer` applied `migration` onto, or zero if it has + /// not applied it. + function appliedOnto(address writer, bytes32 migration) external view returns (bytes32); + /// Where `writer`'s namespace currently is: the migration it applied most /// recently, or `MIGRATION_HEAD_GENESIS` if it has never applied one. /// diff --git a/src/lib/LibMigrationRegistry.sol b/src/lib/LibMigrationRegistry.sol index 9c17e84..5e909a3 100644 --- a/src/lib/LibMigrationRegistry.sol +++ b/src/lib/LibMigrationRegistry.sol @@ -13,11 +13,11 @@ import {LibMigrationRegistryDeploy} from "./LibMigrationRegistryDeploy.sol"; /// a chain the caller has not audited; the address plus the code hash says the /// caller is talking to the registry it compiled against. /// -/// That is the whole library. It answers when a writer applied a migration and -/// where that writer's namespace has got to, and it applies one under the -/// caller. Which writer a test trusts, which invariant each answer selects, and -/// how an id is derived are entirely the consumer's business and none of this -/// library's. +/// That is the whole library. It answers when a writer applied a migration, +/// what that writer applied it onto and where that writer's namespace has got +/// to, and it applies one under the caller. Which writer a test trusts, which +/// invariant each answer selects, and how an id is derived are entirely the +/// consumer's business and none of this library's. /// /// ## There is deliberately no broadcast runner here /// @@ -59,10 +59,13 @@ import {LibMigrationRegistryDeploy} from "./LibMigrationRegistryDeploy.sol"; /// /// ## Writing names the head it is applying onto /// -/// `applyMigration` takes the migration the caller believes ran last in its -/// namespace, so a chain that never got that predecessor refuses the write -/// instead of silently skipping a step, and two migrations dispatched at once -/// cannot land in the wrong order. The first migration in a namespace names +/// `applyMigration` and `applyMigrationHistory` both take the migration the +/// caller believes ran last in its namespace, so a chain that never got that +/// predecessor refuses the write instead of silently skipping a step, and two +/// migrations dispatched at once cannot land in the wrong order. They differ +/// only in where the recorded moment comes from: the block this lands in, or +/// the moment the caller supplies for a migration that already ran. The first +/// migration in a namespace names /// `MIGRATION_HEAD_GENESIS`, imported from the interface — never a zero, which /// is what an uninitialised constant would be and is refused everywhere. /// @@ -71,6 +74,11 @@ import {LibMigrationRegistryDeploy} from "./LibMigrationRegistryDeploy.sol"; /// how a script tests that its predecessor ran: a head says what was LAST, and /// `applied` is what says whether a particular migration ever ran at all. /// +/// `appliedOnto` reads back the head a record was applied onto, so a namespace +/// walked from `head` back is the order its migrations ran in — which is a +/// stronger statement than the moments make, because a moment is whatever the +/// writer supplied and the chain is what the registry enforced. +/// /// The registry is an INDEX, not proof. It says which invariant applies; it does /// not say the invariant holds. A multisig can act out of band and nothing here /// moves. Codehash and bytecode pins are what verify the state itself, and this @@ -85,12 +93,12 @@ library LibMigrationRegistry { /// Reverts unless the pinned registry address holds the pinned code. /// - /// All three entry points check, and they check the same way, because each - /// is worse than useless against unknown code: `applied` would branch a - /// test on whatever timestamp that code returned, `head` would hand back a - /// value that is not a head, and `applyMigration` would record a migration - /// somewhere nothing will ever read it. The check is one function so the - /// three cannot drift into checking different things, and an entry point + /// Every entry point checks, and they check the same way, because each is + /// worse than useless against unknown code: `applied` would branch a test on + /// whatever timestamp that code returned, `appliedOnto` and `head` would + /// hand back values that are not heads, and either write would record a + /// migration somewhere nothing will ever read it. The check is one function + /// so they cannot drift into checking different things, and an entry point /// added later has one place to call rather than a rule to remember. function checkCodeHash() internal view { bytes32 actualCodeHash = LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_ADDRESS.codehash; @@ -129,6 +137,25 @@ library LibMigrationRegistry { .applied(writer, migration); } + /// What `writer` applied `migration` onto, or zero if it never applied it. + /// + /// Verifies the registry's code hash before reading, for the same reason + /// `applied` does: occupying code is free to answer zero to every migration, + /// which here reads as "never applied" exactly as a zero moment does. + /// + /// This is the step that walks a namespace. From `head`, each answer names + /// the record before it, ending at `MIGRATION_HEAD_GENESIS`. + /// @param writer The namespace to read. Never the zero address. + /// @param migration The migration to ask about. Never zero, never + /// `MIGRATION_HEAD_GENESIS`. + /// @return The head `writer` applied `migration` onto, or zero if it has + /// not applied it. + function appliedOnto(address writer, bytes32 migration) internal view returns (bytes32) { + checkCodeHash(); + return IMigrationRegistryV1(LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_ADDRESS) + .appliedOnto(writer, migration); + } + /// The migration `writer` applied most recently, or `MIGRATION_HEAD_GENESIS` /// if it has never applied one. /// @@ -166,7 +193,9 @@ library LibMigrationRegistry { /// The registry refuses the zero id, refuses a migration this caller has /// already applied, and refuses one applied onto anything but the /// namespace's actual head — which between them make a re-dispatched, a - /// skipped and an out-of-order migration all fail rather than land. + /// skipped and an out-of-order migration all fail rather than land. The + /// moment is the block this lands in, which the registry refuses if it is + /// zero. /// @param expectedHead The migration the caller believes it applied last, /// or `MIGRATION_HEAD_GENESIS` for the first in this namespace. /// @param migration The migration to apply. Never zero, never @@ -176,4 +205,29 @@ library LibMigrationRegistry { IMigrationRegistryV1(LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_ADDRESS) .applyMigration(expectedHead, migration); } + + /// Applies `migration` under the CALLER's namespace, onto `expectedHead`, as + /// having been applied at `appliedAt`. + /// + /// This is for a migration that already ran — one that ran before this + /// registry reached the chain, or before its writer started recording at + /// all — so the record carries the moment it ran rather than the moment it + /// was written down. + /// + /// Everything `applyMigration` says about the namespace, the code-hash + /// check and the registry's refusals holds here unchanged. The registry + /// refuses the three moments a record cannot carry as well: zero, one after + /// the block this lands in, and one before the record at the head it is + /// applied onto. + /// @param expectedHead The migration the caller believes it applied last, + /// or `MIGRATION_HEAD_GENESIS` for the first in this namespace. + /// @param migration The migration to apply. Never zero, never + /// `MIGRATION_HEAD_GENESIS`. + /// @param appliedAt The moment `migration` was applied. Never zero, never + /// after the block this lands in. + function applyMigrationHistory(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) internal { + checkCodeHash(); + IMigrationRegistryV1(LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_ADDRESS) + .applyMigrationHistory(expectedHead, migration, appliedAt); + } } diff --git a/test/concrete/MockMigrationApplier.sol b/test/concrete/MockMigrationApplier.sol index 0db70b0..e8ee312 100644 --- a/test/concrete/MockMigrationApplier.sol +++ b/test/concrete/MockMigrationApplier.sol @@ -5,9 +5,9 @@ pragma solidity =0.8.25; import {LibMigrationRegistry} from "../../src/lib/LibMigrationRegistry.sol"; /// @title MockMigrationApplier -/// @notice A consumer in the shape `LibMigrationRegistry.applyMigration` is -/// designed for: it calls the library and nothing else, so the record lands -/// under THIS contract's address. +/// @notice A consumer in the shape `LibMigrationRegistry`'s writes are designed +/// for: it calls the library and nothing else, so the record lands under THIS +/// contract's address. /// /// It exists so the namespace can be exercised as the property it is. The /// library's functions are `internal` and inline into whatever executes them, @@ -28,14 +28,31 @@ contract MockMigrationApplier { LibMigrationRegistry.applyMigration(expectedHead, migration); } + /// Applies `migration` under this contract, onto `expectedHead`, at + /// `appliedAt`. + /// @param expectedHead The head this contract believes it is at. + /// @param migration The migration to apply. + /// @param appliedAt The moment the migration was applied. + function applyMigrationHistory(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) external { + LibMigrationRegistry.applyMigrationHistory(expectedHead, migration, appliedAt); + } + /// When `writer` applied `migration`. /// @param writer The namespace to read. /// @param migration The migration to ask about. - /// @return The timestamp it was applied at, or zero. + /// @return The moment it was applied at, or zero. function applied(address writer, bytes32 migration) external view returns (uint256) { return LibMigrationRegistry.applied(writer, migration); } + /// What `writer` applied `migration` onto. + /// @param writer The namespace to read. + /// @param migration The migration to ask about. + /// @return The head it was applied onto, or zero. + function appliedOnto(address writer, bytes32 migration) external view returns (bytes32) { + return LibMigrationRegistry.appliedOnto(writer, migration); + } + /// The head of `writer`'s namespace. /// @param writer The namespace to read. /// @return The head. diff --git a/test/src/concrete/MigrationRegistryApplied.t.sol b/test/src/concrete/MigrationRegistryApplied.t.sol index 2e1771b..e049fbf 100644 --- a/test/src/concrete/MigrationRegistryApplied.t.sol +++ b/test/src/concrete/MigrationRegistryApplied.t.sol @@ -10,9 +10,9 @@ import {LibMigrationFuzz} from "../../lib/LibMigrationFuzz.sol"; /// @title MigrationRegistryAppliedTest /// @notice A test suite for `MigrationRegistry.applied`: it answers an applied -/// migration with the moment it was applied, an unapplied one with zero, +/// migration with the moment recorded against it, an unapplied one with zero, /// refuses the three inputs that can only be mistakes, and is the only reader of -/// the records. +/// a record's moment. contract MigrationRegistryAppliedTest is Test { /// The registry under test. Stateful, so a fresh one per test. MigrationRegistry internal sRegistry; @@ -35,26 +35,56 @@ contract MigrationRegistryAppliedTest is Test { assertEq(sRegistry.applied(writer, migration), 0); } - /// An applied migration answers the timestamp of the block it was applied - /// in, and keeps answering it as time moves on. The value is when the - /// migration was applied, not how long ago or how recently anything was - /// asked. - function testAppliedIsTheApplicationTimestamp(address writer, bytes32 migration, uint32 appliedAt, uint32 readAt) - external - { + /// An applied migration answers the moment recorded against it, and keeps + /// answering it as time moves on. The value is when the migration was + /// applied, not when the record was written and not how long ago or how + /// recently anything was asked. + function testAppliedIsTheRecordedMoment( + address writer, + bytes32 migration, + uint32 appliedAt, + uint32 writtenAt, + uint32 readAt + ) external { vm.assume(writer != address(0)); LibMigrationFuzz.assumeMigration(vm, migration); vm.assume(appliedAt != 0); - vm.assume(readAt >= appliedAt); + vm.assume(writtenAt >= appliedAt); + vm.assume(readAt >= writtenAt); - vm.warp(appliedAt); + vm.warp(writtenAt); vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migration, appliedAt); vm.warp(readAt); assertEq(sRegistry.applied(writer, migration), appliedAt); } + /// The moment `applied` answers never exceeds the block that asks, whichever + /// form wrote it. That is what lets a consumer measuring an interval since a + /// migration subtract the answer from the current block without + /// underflowing. + function testAppliedNeverExceedsTheReadingBlock( + address writer, + bytes32 migration, + uint32 appliedAt, + uint32 writtenAt, + uint32 readAt + ) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migration); + vm.assume(appliedAt != 0); + vm.assume(writtenAt >= appliedAt); + vm.assume(readAt >= writtenAt); + + vm.warp(writtenAt); + vm.prank(writer); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migration, appliedAt); + + vm.warp(readAt); + assertLe(sRegistry.applied(writer, migration), block.timestamp); + } + /// Reading does not consume or alter a record, so the same question asked /// twice answers the same way. function testAppliedIsIdempotent(address writer, bytes32 migration) external { @@ -81,8 +111,7 @@ contract MigrationRegistryAppliedTest is Test { } /// The zero migration id is refused for the same reason in the other - /// direction: `applyMigration` will not write it, so it can never be a real - /// record. + /// direction: neither write records it, so it can never be a real record. function testAppliedZeroMigrationReverts(address writer) external { vm.assume(writer != address(0)); @@ -91,7 +120,7 @@ contract MigrationRegistryAppliedTest is Test { } /// The genesis head is refused as a migration for the same reason again: - /// `applyMigration` will not write it either, so asking about it would + /// neither write records it either, so asking about it would /// answer zero forever to a caller that has confused a head for a migration /// — and that caller reads zero as its pre-migration branch. function testAppliedGenesisMigrationReverts(address writer) external { @@ -135,13 +164,14 @@ contract MigrationRegistryAppliedTest is Test { assertEq(sRegistry.head(writer), migration); } - /// `applied` is the only reader of the records. The records mapping is not - /// `public`, so the getter a `public` mapping would generate — which answers - /// the zero writer and both refused ids with zero, the exact silent - /// wrong-branch these refusals exist to prevent — does not exist. + /// `applied` and `appliedOnto` are the only readers of the records. The + /// records mapping is not `public`, so the getter a `public` mapping would + /// generate — which answers the zero writer and both refused ids with zero, + /// the exact silent wrong-branch these refusals exist to prevent — does not + /// exist. function testAppliedNoGeneratedMappingGetter(address writer, bytes32 migration) external { (bool success,) = - address(sRegistry).call(abi.encodeWithSignature("sApplied(address,bytes32)", writer, migration)); + address(sRegistry).call(abi.encodeWithSignature("sRecords(address,bytes32)", writer, migration)); assertFalse(success); } @@ -154,11 +184,13 @@ contract MigrationRegistryAppliedTest is Test { } /// There is no other entry point at all: no fallback, no receive, and - /// nothing beyond the three `IMigrationRegistryV1` functions, so an unknown + /// nothing beyond the `IMigrationRegistryV1` functions, so an unknown /// selector reverts instead of being silently absorbed. function testAppliedNoOtherEntryPoint(bytes4 selector, bytes32 migration) external { vm.assume(selector != IMigrationRegistryV1.applied.selector); + vm.assume(selector != IMigrationRegistryV1.appliedOnto.selector); vm.assume(selector != IMigrationRegistryV1.applyMigration.selector); + vm.assume(selector != IMigrationRegistryV1.applyMigrationHistory.selector); vm.assume(selector != IMigrationRegistryV1.head.selector); (bool success,) = address(sRegistry).call(abi.encodeWithSelector(selector, address(this), migration)); diff --git a/test/src/concrete/MigrationRegistryAppliedOnto.t.sol b/test/src/concrete/MigrationRegistryAppliedOnto.t.sol new file mode 100644 index 0000000..65f31f6 --- /dev/null +++ b/test/src/concrete/MigrationRegistryAppliedOnto.t.sol @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std-1.16.2/src/Test.sol"; + +import {IMigrationRegistryV1, MIGRATION_HEAD_GENESIS} from "../../../src/interface/IMigrationRegistryV1.sol"; +import {MigrationRegistry} from "../../../src/concrete/MigrationRegistry.sol"; +import {LibMigrationFuzz} from "../../lib/LibMigrationFuzz.sol"; + +/// @title MigrationRegistryAppliedOntoTest +/// @notice A test suite for `MigrationRegistry.appliedOnto`: it answers an +/// applied migration with the head it was applied onto, an unapplied one with +/// zero, refuses the three inputs that can only be mistakes, and is the step +/// that walks a namespace back to genesis. +contract MigrationRegistryAppliedOntoTest is Test { + /// The registry under test. Stateful, so a fresh one per test. + MigrationRegistry internal sRegistry; + + function setUp() external { + sRegistry = new MigrationRegistry(); + } + + /// An unapplied migration answers zero rather than reverting, exactly as + /// `applied` does. Zero is not a head — a head is genesis or an applied id, + /// both nonzero — so it says "no record" and nothing else. + function testAppliedOntoUnappliedIsZero(address writer, bytes32 migration) external view { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migration); + + assertEq(sRegistry.appliedOnto(writer, migration), bytes32(0)); + } + + /// The first migration in a namespace answers `MIGRATION_HEAD_GENESIS`, so + /// the walk back has a terminator that is not the "no record" zero. + function testAppliedOntoFirstRecordIsGenesis(address writer, bytes32 migration) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migration); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration); + + assertEq(sRegistry.appliedOnto(writer, migration), MIGRATION_HEAD_GENESIS); + } + + /// A later migration answers the migration before it, which is the head the + /// registry itself checked rather than a value the caller was free to + /// choose: a caller that names anything else is refused, so the record can + /// only ever hold where the namespace actually was. + function testAppliedOntoIsTheCheckedHead(address writer, bytes32 migrationA, bytes32 migrationB, bytes32 wrongHead) + external + { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migrationA); + LibMigrationFuzz.assumeMigration(vm, migrationB); + LibMigrationFuzz.assumeMigration(vm, wrongHead); + vm.assume(migrationA != migrationB); + vm.assume(wrongHead != migrationA); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA); + + vm.expectRevert( + abi.encodeWithSelector(IMigrationRegistryV1.UnexpectedMigrationHead.selector, writer, wrongHead, migrationA) + ); + vm.prank(writer); + sRegistry.applyMigration(wrongHead, migrationB); + + vm.prank(writer); + sRegistry.applyMigration(migrationA, migrationB); + + assertEq(sRegistry.appliedOnto(writer, migrationB), migrationA); + } + + /// A record never moves. The answer for an earlier migration is the same + /// after a later one lands, so a chain read at any moment describes the same + /// history. + function testAppliedOntoRecordsAreImmutable(address writer, bytes32 migrationA, bytes32 migrationB) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migrationA); + LibMigrationFuzz.assumeMigration(vm, migrationB); + vm.assume(migrationA != migrationB); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA); + assertEq(sRegistry.appliedOnto(writer, migrationA), MIGRATION_HEAD_GENESIS); + + vm.prank(writer); + sRegistry.applyMigration(migrationA, migrationB); + + assertEq(sRegistry.appliedOnto(writer, migrationA), MIGRATION_HEAD_GENESIS); + assertEq(sRegistry.appliedOnto(writer, migrationB), migrationA); + } + + /// Reading twice answers the same way. + function testAppliedOntoIsIdempotent(address writer, bytes32 migration) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migration); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration); + + assertEq(sRegistry.appliedOnto(writer, migration), MIGRATION_HEAD_GENESIS); + assertEq(sRegistry.appliedOnto(writer, migration), MIGRATION_HEAD_GENESIS); + } + + /// A record is confined to the caller's namespace here as everywhere else: + /// one writer's chain says nothing about another's. + function testAppliedOntoIsPerWriter(address writer, address other, bytes32 migration) external { + vm.assume(writer != address(0)); + vm.assume(other != address(0)); + vm.assume(writer != other); + LibMigrationFuzz.assumeMigration(vm, migration); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration); + + assertEq(sRegistry.appliedOnto(writer, migration), MIGRATION_HEAD_GENESIS); + assertEq(sRegistry.appliedOnto(other, migration), bytes32(0)); + } + + /// The zero writer is refused rather than answered, for the reason `applied` + /// refuses it: the zero namespace is provably empty, so an unresolved writer + /// constant would read as "nothing has been applied" rather than as the + /// mistake it is. + function testAppliedOntoZeroWriterReverts(bytes32 migration) external { + LibMigrationFuzz.assumeMigration(vm, migration); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroWriter.selector)); + sRegistry.appliedOnto(address(0), migration); + } + + /// The zero migration id is refused: neither write records it, so it can + /// never be a real record. + function testAppliedOntoZeroMigrationReverts(address writer) external { + vm.assume(writer != address(0)); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroMigration.selector)); + sRegistry.appliedOnto(writer, bytes32(0)); + } + + /// The genesis head is refused as a migration. It is where a walk ENDS, so a + /// caller that carried on asking about it has confused the terminator for a + /// record and would read zero as one. + function testAppliedOntoGenesisMigrationReverts(address writer) external { + vm.assume(writer != address(0)); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.GenesisMigration.selector)); + sRegistry.appliedOnto(writer, MIGRATION_HEAD_GENESIS); + } + + /// The writer is checked before the migration, so a caller that has zeroed + /// both gets one stable answer rather than one that depends on which check + /// happens to run — the same order `applied` uses, from the same check. + function testAppliedOntoZeroWriterCheckedFirst() external { + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroWriter.selector)); + sRegistry.appliedOnto(address(0), bytes32(0)); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroWriter.selector)); + sRegistry.appliedOnto(address(0), MIGRATION_HEAD_GENESIS); + } + + /// A refusal is not a state change: the refused cases revert on a registry + /// that holds records exactly as they do on an empty one, and leave those + /// records intact. + function testAppliedOntoRefusalLeavesRecordsIntact(address writer, bytes32 migration) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migration); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroWriter.selector)); + sRegistry.appliedOnto(address(0), migration); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroMigration.selector)); + sRegistry.appliedOnto(writer, bytes32(0)); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.GenesisMigration.selector)); + sRegistry.appliedOnto(writer, MIGRATION_HEAD_GENESIS); + + assertEq(sRegistry.appliedOnto(writer, migration), MIGRATION_HEAD_GENESIS); + assertEq(sRegistry.head(writer), migration); + } + + /// The two readers of a record agree about whether it exists. `applied` + /// answering zero and `appliedOnto` answering zero are the same fact, and a + /// nonzero answer from either comes with a nonzero answer from the other — + /// which is what a single whole-struct write buys. + function testAppliedOntoAgreesWithApplied(address writer, bytes32 migrationA, bytes32 migrationB) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migrationA); + LibMigrationFuzz.assumeMigration(vm, migrationB); + vm.assume(migrationA != migrationB); + + assertEq(sRegistry.applied(writer, migrationA), 0); + assertEq(sRegistry.appliedOnto(writer, migrationA), bytes32(0)); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA); + + assertTrue(sRegistry.applied(writer, migrationA) != 0); + assertTrue(sRegistry.appliedOnto(writer, migrationA) != bytes32(0)); + + assertEq(sRegistry.applied(writer, migrationB), 0); + assertEq(sRegistry.appliedOnto(writer, migrationB), bytes32(0)); + } +} diff --git a/test/src/concrete/MigrationRegistryApplyMigration.t.sol b/test/src/concrete/MigrationRegistryApplyMigration.t.sol index d6ce0ac..df0ebe7 100644 --- a/test/src/concrete/MigrationRegistryApplyMigration.t.sol +++ b/test/src/concrete/MigrationRegistryApplyMigration.t.sol @@ -9,9 +9,11 @@ import {MigrationRegistry} from "../../../src/concrete/MigrationRegistry.sol"; import {LibMigrationFuzz} from "../../lib/LibMigrationFuzz.sol"; /// @title MigrationRegistryApplyMigrationTest -/// @notice A test suite for `MigrationRegistry.applyMigration`: who a record -/// belongs to, that a migration is applied at most once and only onto the head -/// its caller named, what a record carries, and what it may never become. +/// @notice A test suite for `MigrationRegistry.applyMigration` and +/// `MigrationRegistry.applyMigrationHistory`: who a record belongs to, that a +/// migration is applied at most once and only onto the head its caller named, +/// which moments a record may carry, what a record carries, and what it may +/// never become. contract MigrationRegistryApplyMigrationTest is Test { /// The registry under test. Stateful, so a fresh one per test. MigrationRegistry internal sRegistry; @@ -33,7 +35,7 @@ contract MigrationRegistryApplyMigrationTest is Test { assertEq(sRegistry.applied(writer, migration), block.timestamp); } - /// A record IS the block timestamp it landed in, which is the whole + /// `applyMigration` records the block it landed in, which is the whole /// difference from a flag: a consumer whose invariant starts AT the /// migration — a cliff, a rate change, a grace period — reads the moment /// from the chain rather than from a constant somebody guessed. @@ -72,11 +74,12 @@ contract MigrationRegistryApplyMigrationTest is Test { assertEq(sRegistry.applied(writer, migrationB), 2000); } - /// A record refuses to be written at all in a block whose timestamp is zero, - /// rather than write one that `applied` would read back as no record. The - /// head does not move and the migration can still be applied, which is the - /// only outcome that leaves the namespace describing something true. - function testApplyMigrationZeroTimestampReverts(address writer, bytes32 migration) external { + /// `applyMigration` refuses to write at all in a block whose timestamp is + /// zero, rather than write a record that `applied` would read back as no + /// record. The head does not move and the migration can still be applied, + /// which is the only outcome that leaves the namespace describing something + /// true. + function testApplyMigrationZeroBlockReverts(address writer, bytes32 migration) external { vm.assume(writer != address(0)); LibMigrationFuzz.assumeMigration(vm, migration); vm.warp(0); @@ -94,13 +97,12 @@ contract MigrationRegistryApplyMigrationTest is Test { assertEq(sRegistry.applied(writer, migration), 1); } - /// The zero timestamp is checked LAST, after every refusal that describes a - /// mistake in the call. Those are true whatever block the call lands in, so - /// a caller in a zero-timestamp block is told which of its arguments is - /// wrong rather than told to come back later — and only a caller whose - /// arguments are all right is told about the block, which is the one - /// refusal that goes away on its own. - function testApplyMigrationZeroTimestampCheckedLast( + /// The zero moment is checked after the two id refusals and before anything + /// about the namespace, on `applyMigration` as on `applyMigrationHistory`. An id is + /// what a record is ABOUT, so a call with no subject has nothing to say a + /// moment for; everything after describes a namespace no writable record + /// will reach. + function testApplyMigrationZeroBlockCheckedAfterIdsAndBeforeTheNamespace( address writer, bytes32 migrationA, bytes32 migrationB, @@ -125,26 +127,18 @@ contract MigrationRegistryApplyMigrationTest is Test { vm.prank(writer); sRegistry.applyMigration(anyHead, MIGRATION_HEAD_GENESIS); - vm.expectRevert( - abi.encodeWithSelector(IMigrationRegistryV1.MigrationAlreadyApplied.selector, writer, migrationA) - ); + // Already applied, in a block that can hold no record: told about the + // moment, because the record could not be written whatever namespace it + // arrived at. + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroTimestamp.selector)); vm.prank(writer); sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA); - vm.expectRevert( - abi.encodeWithSelector( - IMigrationRegistryV1.UnexpectedMigrationHead.selector, writer, MIGRATION_HEAD_GENESIS, migrationA - ) - ); - vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationB); - - // With nothing left to say about the call, the block. This is what - // makes the four refusals above statements about the ORDER rather than - // about a check that was not live in this block at all. + // A head the namespace has moved on from, in the same block: told about + // the moment. vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroTimestamp.selector)); vm.prank(writer); - sRegistry.applyMigration(migrationA, migrationB); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationB); } /// A record is confined to the caller's namespace. Applying under one @@ -564,36 +558,62 @@ contract MigrationRegistryApplyMigrationTest is Test { } /// `Migrated` is emitted with the writer and migration both indexed, so the - /// log can be filtered by either. The log is the only enumeration of the - /// registry, so a record that does not emit is a record nobody can find. + /// log can be filtered by either, and carries the moment as data. The log is + /// the only enumeration of the registry, so a record that does not emit is a + /// record nobody can find. /// - /// It carries no head and no timestamp because the log already holds both: - /// one writer's entries in order ARE its chain of heads, and the timestamp - /// is the block's. - function testApplyMigrationEvent(address writer, bytes32 migration) external { + /// It carries no head because the log already holds it: one writer's entries + /// in order ARE its chain of heads. It does carry the moment, which the + /// block a log entry sits in does not — that block says when the record was + /// written, and the moment says when the migration ran. + function testApplyMigrationHistoryEvent(address writer, bytes32 migration, uint32 appliedAt, uint32 writtenAt) + external + { vm.assume(writer != address(0)); LibMigrationFuzz.assumeMigration(vm, migration); + vm.assume(appliedAt != 0); + vm.assume(writtenAt > appliedAt); + vm.warp(writtenAt); vm.recordLogs(); vm.prank(writer); - sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migration, appliedAt); Vm.Log[] memory entries = vm.getRecordedLogs(); assertEq(entries.length, 1); assertEq(entries[0].emitter, address(sRegistry)); assertEq(entries[0].topics.length, 3); - assertEq(entries[0].topics[0], keccak256("Migrated(address,bytes32)")); + assertEq(entries[0].topics[0], keccak256("Migrated(address,bytes32,uint256)")); assertEq(entries[0].topics[1], bytes32(uint256(uint160(writer)))); assertEq(entries[0].topics[2], migration); - assertEq(entries[0].data.length, 0); + assertEq(entries[0].data, abi.encode(uint256(appliedAt))); + } + + /// `applyMigration` emits the same event, carrying the block it stamped — so + /// a reader of the log never has to know which of the two wrote a record. + function testApplyMigrationEvent(address writer, bytes32 migration, uint32 now_) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migration); + vm.assume(now_ != 0); + vm.warp(now_); + + vm.recordLogs(); + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration); + Vm.Log[] memory entries = vm.getRecordedLogs(); + + assertEq(entries.length, 1); + assertEq(entries[0].topics[0], keccak256("Migrated(address,bytes32,uint256)")); + assertEq(entries[0].data, abi.encode(uint256(now_))); } - /// A refused `applyMigration` emits nothing, so a failed apply can never be + /// A refused write emits nothing, so a failed apply can never be /// mistaken for a record by anything reading the logs — which for a /// re-dispatched migration is exactly the mistake that matters. function testApplyMigrationNoEventOnRevert(address writer, bytes32 migration) external { vm.assume(writer != address(0)); LibMigrationFuzz.assumeMigration(vm, migration); + vm.warp(1000); vm.prank(writer); sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migration); @@ -627,5 +647,594 @@ contract MigrationRegistryApplyMigrationTest is Test { vm.prank(writer); sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, keccak256(abi.encode(migration))); assertEq(vm.getRecordedLogs().length, 0); + + vm.recordLogs(); + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroTimestamp.selector)); + vm.prank(writer); + sRegistry.applyMigrationHistory(migration, keccak256(abi.encode(migration)), 0); + assertEq(vm.getRecordedLogs().length, 0); + + vm.recordLogs(); + vm.expectRevert( + abi.encodeWithSelector(IMigrationRegistryV1.FutureTimestamp.selector, block.timestamp + 1, block.timestamp) + ); + vm.prank(writer); + sRegistry.applyMigrationHistory(migration, keccak256(abi.encode(migration)), block.timestamp + 1); + assertEq(vm.getRecordedLogs().length, 0); + + vm.recordLogs(); + vm.expectRevert( + abi.encodeWithSelector(IMigrationRegistryV1.TimestampBeforeHead.selector, block.timestamp - 1, 1000) + ); + vm.prank(writer); + sRegistry.applyMigrationHistory(migration, keccak256(abi.encode(migration)), block.timestamp - 1); + assertEq(vm.getRecordedLogs().length, 0); + } + + /// `applyMigrationHistory` records the moment the CALLER supplied, which is + /// what lets a migration that already ran be recorded with the time it ran + /// rather than the time it was written down. The block the record lands in + /// is not the value, and a record written long after the fact says so. + function testApplyMigrationHistoryRecordsTheSuppliedMoment( + address writer, + bytes32 migration, + uint32 appliedAt, + uint32 writtenAt + ) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migration); + vm.assume(appliedAt != 0); + vm.assume(writtenAt > appliedAt); + vm.warp(writtenAt); + + vm.prank(writer); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migration, appliedAt); + + assertEq(sRegistry.applied(writer, migration), appliedAt); + assertTrue(sRegistry.applied(writer, migration) != block.timestamp); + } + + /// The moment of the current block is an ordinary value for the parameter, + /// which is what a caller reaching for `applyMigrationHistory` to record a + /// migration running now passes. + function testApplyMigrationHistoryCurrentBlockIsAccepted(address writer, bytes32 migration, uint32 now_) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migration); + vm.assume(now_ != 0); + vm.warp(now_); + + vm.prank(writer); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migration, block.timestamp); + + assertEq(sRegistry.applied(writer, migration), now_); + } + + /// The two writes make the SAME record when the moment is this block, down + /// to the head each was applied onto — which is what makes `applyMigration` + /// `applyMigrationHistory` with today's moment rather than a second way to + /// write a record. + function testApplyMigrationAndHistoryWriteTheSameRecord( + address writer, + bytes32 migrationA, + bytes32 migrationB, + uint32 now_ + ) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migrationA); + LibMigrationFuzz.assumeMigration(vm, migrationB); + vm.assume(migrationA != migrationB); + vm.assume(now_ != 0); + vm.warp(now_); + + MigrationRegistry stamping = new MigrationRegistry(); + MigrationRegistry supplied = new MigrationRegistry(); + + vm.prank(writer); + stamping.applyMigration(MIGRATION_HEAD_GENESIS, migrationA); + vm.prank(writer); + supplied.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migrationA, block.timestamp); + + vm.prank(writer); + stamping.applyMigration(migrationA, migrationB); + vm.prank(writer); + supplied.applyMigrationHistory(migrationA, migrationB, block.timestamp); + + assertEq(stamping.applied(writer, migrationB), supplied.applied(writer, migrationB)); + assertEq(stamping.appliedOnto(writer, migrationB), supplied.appliedOnto(writer, migrationB)); + assertEq(stamping.head(writer), supplied.head(writer)); + } + + /// A moment that has not arrived is refused. A record says a migration HAS + /// run, so a future one is not a late record of anything, and a consumer + /// measuring an interval since the migration would be subtracting a moment + /// later than the one it is measuring from. + function testApplyMigrationHistoryFutureTimestampReverts( + address writer, + bytes32 migration, + uint32 now_, + uint256 appliedAt + ) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migration); + vm.warp(now_); + appliedAt = bound(appliedAt, uint256(now_) + 1, type(uint256).max); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.FutureTimestamp.selector, appliedAt, uint256(now_))); + vm.prank(writer); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migration, appliedAt); + + assertEq(sRegistry.applied(writer, migration), 0); + assertEq(sRegistry.appliedOnto(writer, migration), bytes32(0)); + assertEq(sRegistry.head(writer), MIGRATION_HEAD_GENESIS); + } + + /// One second past the current block is refused, and the current block is + /// not: the boundary is the block's own timestamp, inclusive. + function testApplyMigrationHistoryFutureBoundary(address writer, bytes32 migration, uint32 now_) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migration); + vm.assume(now_ != 0); + vm.warp(now_); + + vm.expectRevert( + abi.encodeWithSelector(IMigrationRegistryV1.FutureTimestamp.selector, uint256(now_) + 1, uint256(now_)) + ); + vm.prank(writer); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migration, uint256(now_) + 1); + + vm.prank(writer); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migration, uint256(now_)); + assertEq(sRegistry.applied(writer, migration), now_); + } + + /// A supplied zero is refused, rather than written as a record that + /// `applied` would read back as no record. The head does not move and the + /// migration can still be applied. + function testApplyMigrationHistoryZeroTimestampReverts(address writer, bytes32 migration, uint32 now_) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migration); + vm.assume(now_ != 0); + vm.warp(now_); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroTimestamp.selector)); + vm.prank(writer); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migration, 0); + + assertEq(sRegistry.applied(writer, migration), 0); + assertEq(sRegistry.head(writer), MIGRATION_HEAD_GENESIS); + + vm.prank(writer); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migration, 1); + assertEq(sRegistry.applied(writer, migration), 1); + } + + /// A block whose timestamp is zero can hold no record at all: zero is + /// refused as a moment, and every other moment is still in the future. The + /// head does not move, so the namespace goes on describing something true + /// and the migration is still applicable once the clock has moved. + function testApplyMigrationHistoryZeroBlockRecordsNothing(address writer, bytes32 migration, uint256 appliedAt) + external + { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migration); + vm.assume(appliedAt != 0); + vm.warp(0); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroTimestamp.selector)); + vm.prank(writer); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migration, 0); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.FutureTimestamp.selector, appliedAt, 0)); + vm.prank(writer); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migration, appliedAt); + + assertEq(sRegistry.applied(writer, migration), 0); + assertEq(sRegistry.head(writer), MIGRATION_HEAD_GENESIS); + + vm.warp(1); + vm.prank(writer); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migration, 1); + assertEq(sRegistry.applied(writer, migration), 1); + } + + /// The zero moment is refused BEFORE anything about the namespace is read, + /// so an uninitialised argument is reported as itself rather than as + /// whatever the namespace happens to make of it. Fuzzed over the head and + /// checked against a namespace that has moved on, because a head the + /// namespace happens to be at is accepted whichever check runs first. + function testApplyMigrationHistoryZeroTimestampCheckedBeforeTheNamespace( + address writer, + bytes32 migrationA, + bytes32 migrationB, + bytes32 anyHead + ) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migrationA); + LibMigrationFuzz.assumeMigration(vm, migrationB); + vm.assume(migrationA != migrationB); + + vm.prank(writer); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migrationA, block.timestamp); + + // Already applied, and a zero moment: told about the moment. + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroTimestamp.selector)); + vm.prank(writer); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migrationA, 0); + + // A head that has moved on, and a zero moment: told about the moment. + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroTimestamp.selector)); + vm.prank(writer); + sRegistry.applyMigrationHistory(anyHead, migrationB, 0); + } + + /// The two id refusals come before the moment, so a caller that has zeroed + /// both an id and a moment is told about the id: an id is what the record is + /// ABOUT, and a call with no subject has nothing to say a moment for. + function testApplyMigrationHistoryIdCheckedBeforeTimestamp(address writer, bytes32 anyHead) external { + vm.assume(writer != address(0)); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroMigration.selector)); + vm.prank(writer); + sRegistry.applyMigrationHistory(anyHead, bytes32(0), 0); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.GenesisMigration.selector)); + vm.prank(writer); + sRegistry.applyMigrationHistory(anyHead, MIGRATION_HEAD_GENESIS, 0); + } + + /// The refusals that describe the NAMESPACE come before the future-moment + /// one, so a re-dispatched script is told its migration already ran, and a + /// script at the wrong point in the sequence is told where the namespace is, + /// rather than either of them being sent to look at a clock. + function testApplyMigrationHistoryNamespaceCheckedBeforeTheFuture( + address writer, + bytes32 migrationA, + bytes32 migrationB, + bytes32 skipped + ) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migrationA); + LibMigrationFuzz.assumeMigration(vm, migrationB); + LibMigrationFuzz.assumeMigration(vm, skipped); + vm.assume(migrationA != migrationB); + vm.assume(skipped != migrationA); + vm.assume(skipped != migrationB); + + vm.warp(9000); + vm.prank(writer); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migrationA, 5000); + + // Already applied, and in the future: told it already ran. + vm.expectRevert( + abi.encodeWithSelector(IMigrationRegistryV1.MigrationAlreadyApplied.selector, writer, migrationA) + ); + vm.prank(writer); + sRegistry.applyMigrationHistory(migrationA, migrationA, 9001); + + // The wrong head, and in the future: told where the namespace is. + vm.expectRevert( + abi.encodeWithSelector(IMigrationRegistryV1.UnexpectedMigrationHead.selector, writer, skipped, migrationA) + ); + vm.prank(writer); + sRegistry.applyMigrationHistory(skipped, migrationB, 9001); + } + + /// A record may NOT carry a moment earlier than the record it is applied + /// onto. Nothing is written and the head does not move, so the migration is + /// still applicable with a moment the chain admits. + function testApplyMigrationHistoryMomentBeforeHeadReverts( + address writer, + bytes32 migrationA, + bytes32 migrationB, + uint32 now_, + uint256 earlier + ) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migrationA); + LibMigrationFuzz.assumeMigration(vm, migrationB); + vm.assume(migrationA != migrationB); + vm.assume(now_ > 1); + vm.warp(now_); + earlier = bound(earlier, 1, uint256(now_) - 1); + + vm.prank(writer); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migrationA, uint256(now_)); + + vm.expectRevert( + abi.encodeWithSelector(IMigrationRegistryV1.TimestampBeforeHead.selector, earlier, uint256(now_)) + ); + vm.prank(writer); + sRegistry.applyMigrationHistory(migrationA, migrationB, earlier); + + assertEq(sRegistry.applied(writer, migrationB), 0); + assertEq(sRegistry.appliedOnto(writer, migrationB), bytes32(0)); + assertEq(sRegistry.head(writer), migrationA); + + vm.prank(writer); + sRegistry.applyMigrationHistory(migrationA, migrationB, uint256(now_)); + assertEq(sRegistry.applied(writer, migrationB), uint256(now_)); + assertEq(sRegistry.head(writer), migrationB); + } + + /// The boundary is the head's own moment, inclusive: exactly it is + /// accepted, and one second below it is refused. + function testApplyMigrationHistoryBeforeHeadBoundary( + address writer, + bytes32 migrationA, + bytes32 migrationB, + uint32 headAppliedAt + ) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migrationA); + LibMigrationFuzz.assumeMigration(vm, migrationB); + vm.assume(migrationA != migrationB); + vm.assume(headAppliedAt > 1); + vm.warp(headAppliedAt); + + vm.prank(writer); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migrationA, uint256(headAppliedAt)); + + vm.expectRevert( + abi.encodeWithSelector( + IMigrationRegistryV1.TimestampBeforeHead.selector, uint256(headAppliedAt) - 1, uint256(headAppliedAt) + ) + ); + vm.prank(writer); + sRegistry.applyMigrationHistory(migrationA, migrationB, uint256(headAppliedAt) - 1); + + vm.prank(writer); + sRegistry.applyMigrationHistory(migrationA, migrationB, uint256(headAppliedAt)); + assertEq(sRegistry.applied(writer, migrationB), uint256(headAppliedAt)); + } + + /// A moment AFTER the head's is the ordinary case, and it is the moment the + /// record carries rather than anything derived from the one before it. + function testApplyMigrationHistoryMomentAfterHeadIsAccepted( + address writer, + bytes32 migrationA, + bytes32 migrationB, + uint32 first, + uint32 second + ) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migrationA); + LibMigrationFuzz.assumeMigration(vm, migrationB); + vm.assume(migrationA != migrationB); + vm.assume(first != 0); + vm.assume(second > first); + vm.warp(second); + + vm.prank(writer); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migrationA, uint256(first)); + vm.prank(writer); + sRegistry.applyMigrationHistory(migrationA, migrationB, uint256(second)); + + assertEq(sRegistry.applied(writer, migrationA), uint256(first)); + assertEq(sRegistry.applied(writer, migrationB), uint256(second)); + assertEq(sRegistry.head(writer), migrationB); + } + + /// The first migration in a namespace is compared to nothing. It is applied + /// onto `MIGRATION_HEAD_GENESIS`, which is refused as a migration and so + /// holds no record in any namespace ever — which is why the smallest moment + /// a record may carry is accepted at genesis in the latest block. + function testApplyMigrationHistoryNoMomentBoundAtGenesis(address writer, bytes32 migration, uint32 now_) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migration); + vm.assume(now_ != 0); + vm.warp(now_); + + // Nothing can put a record at genesis to be bounded by. + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.GenesisMigration.selector)); + vm.prank(writer); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, MIGRATION_HEAD_GENESIS, uint256(now_)); + + vm.prank(writer); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migration, 1); + + assertEq(sRegistry.applied(writer, migration), 1); + assertEq(sRegistry.appliedOnto(writer, migration), MIGRATION_HEAD_GENESIS); + } + + /// The refusals that read the namespace's KEYS come before the one that + /// reads its RECORD, so a re-dispatched script is told its migration already + /// ran and a script at the wrong point is told where the namespace is, + /// rather than either being told about the moment of a record it was never + /// going to be chained onto. The zero moment still comes before all of them. + function testApplyMigrationHistoryNamespaceCheckedBeforeTheHeadsMoment( + address writer, + bytes32 migrationA, + bytes32 migrationB, + bytes32 skipped + ) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migrationA); + LibMigrationFuzz.assumeMigration(vm, migrationB); + LibMigrationFuzz.assumeMigration(vm, skipped); + vm.assume(migrationA != migrationB); + vm.assume(skipped != migrationA); + vm.assume(skipped != migrationB); + + vm.warp(9000); + vm.prank(writer); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migrationA, 9000); + + // Already applied, and before the head's moment: told it already ran. + vm.expectRevert( + abi.encodeWithSelector(IMigrationRegistryV1.MigrationAlreadyApplied.selector, writer, migrationA) + ); + vm.prank(writer); + sRegistry.applyMigrationHistory(migrationA, migrationA, 1); + + // The wrong head, and before the head's moment: told where the + // namespace is. + vm.expectRevert( + abi.encodeWithSelector(IMigrationRegistryV1.UnexpectedMigrationHead.selector, writer, skipped, migrationA) + ); + vm.prank(writer); + sRegistry.applyMigrationHistory(skipped, migrationB, 1); + + // A zero moment, which is also before the head's: told about the zero. + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroTimestamp.selector)); + vm.prank(writer); + sRegistry.applyMigrationHistory(migrationA, migrationB, 0); + + // With nothing else wrong, the head's moment is what refuses it. + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.TimestampBeforeHead.selector, 1, 9000)); + vm.prank(writer); + sRegistry.applyMigrationHistory(migrationA, migrationB, 1); + } + + /// A head's moment bounds only its own namespace. Another writer at genesis + /// is bounded by nothing, whatever moment the first namespace recorded. + function testApplyMigrationHistoryHeadMomentIsPerWriter( + address writer, + address other, + bytes32 migrationA, + bytes32 migrationB + ) external { + vm.assume(writer != address(0)); + vm.assume(other != address(0)); + vm.assume(writer != other); + LibMigrationFuzz.assumeMigration(vm, migrationA); + LibMigrationFuzz.assumeMigration(vm, migrationB); + vm.assume(migrationA != migrationB); + + vm.warp(9000); + vm.prank(writer); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migrationA, 9000); + + vm.prank(other); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migrationB, 1); + + assertEq(sRegistry.applied(other, migrationB), 1); + + // And the other namespace's own head bounds it from there. + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.TimestampBeforeHead.selector, 1, 9000)); + vm.prank(writer); + sRegistry.applyMigrationHistory(migrationA, migrationB, 1); + } + + /// Two records may carry the SAME moment. Two migrations applied in one + /// transaction share a block, and two backfilled migrations known only to + /// the same day share a moment; the chain is what orders them, so the + /// moments are not asked to. + function testApplyMigrationHistoryMomentsMayBeEqual( + address writer, + bytes32 migrationA, + bytes32 migrationB, + uint32 appliedAt + ) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migrationA); + LibMigrationFuzz.assumeMigration(vm, migrationB); + vm.assume(migrationA != migrationB); + vm.assume(appliedAt != 0); + vm.warp(appliedAt); + + vm.prank(writer); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migrationA, appliedAt); + vm.prank(writer); + sRegistry.applyMigrationHistory(migrationA, migrationB, appliedAt); + + assertEq(sRegistry.applied(writer, migrationA), appliedAt); + assertEq(sRegistry.applied(writer, migrationB), appliedAt); + } + + /// A record keeps the head it was applied onto, which is what makes the + /// order structural. The first record in a namespace holds + /// `MIGRATION_HEAD_GENESIS`, and each later one holds the migration before + /// it — the value the caller named and the registry checked, not one the + /// caller could have chosen freely. + function testApplyMigrationRecordsTheHeadItWasAppliedOnto(address writer, bytes32 migrationA, bytes32 migrationB) + external + { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migrationA); + LibMigrationFuzz.assumeMigration(vm, migrationB); + vm.assume(migrationA != migrationB); + + assertEq(sRegistry.appliedOnto(writer, migrationA), bytes32(0)); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA); + assertEq(sRegistry.appliedOnto(writer, migrationA), MIGRATION_HEAD_GENESIS); + + vm.prank(writer); + sRegistry.applyMigration(migrationA, migrationB); + assertEq(sRegistry.appliedOnto(writer, migrationB), migrationA); + assertEq(sRegistry.appliedOnto(writer, migrationA), MIGRATION_HEAD_GENESIS); + } + + /// The chain is the order the migrations ran in, and it says so where the + /// moments cannot. Three records carrying one moment walk back from the head + /// in the order they were APPLIED, ending at genesis. + function testApplyMigrationHistoryChainIsTheOrderWhateverTheMoments( + address writer, + bytes32 migrationA, + bytes32 migrationB, + bytes32 migrationC + ) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migrationA); + LibMigrationFuzz.assumeMigration(vm, migrationB); + LibMigrationFuzz.assumeMigration(vm, migrationC); + vm.assume(migrationA != migrationB); + vm.assume(migrationB != migrationC); + vm.assume(migrationA != migrationC); + + vm.warp(9000); + vm.prank(writer); + sRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migrationA, 3000); + vm.prank(writer); + sRegistry.applyMigrationHistory(migrationA, migrationB, 3000); + vm.prank(writer); + sRegistry.applyMigrationHistory(migrationB, migrationC, 3000); + + // Every moment is the same as the one before it, so nothing about the + // order can be read out of them. + assertEq(sRegistry.applied(writer, migrationA), 3000); + assertEq(sRegistry.applied(writer, migrationB), 3000); + assertEq(sRegistry.applied(writer, migrationC), 3000); + + // The chain still says exactly what happened. + bytes32 cursor = sRegistry.head(writer); + assertEq(cursor, migrationC); + cursor = sRegistry.appliedOnto(writer, cursor); + assertEq(cursor, migrationB); + cursor = sRegistry.appliedOnto(writer, cursor); + assertEq(cursor, migrationA); + cursor = sRegistry.appliedOnto(writer, cursor); + assertEq(cursor, MIGRATION_HEAD_GENESIS); + } + + /// A chain belongs to one namespace. Another writer applying the same + /// migrations builds its own chain, and neither reaches the other. + function testApplyMigrationChainIsPerWriter(address writer, address other, bytes32 migrationA, bytes32 migrationB) + external + { + vm.assume(writer != address(0)); + vm.assume(other != address(0)); + vm.assume(writer != other); + LibMigrationFuzz.assumeMigration(vm, migrationA); + LibMigrationFuzz.assumeMigration(vm, migrationB); + vm.assume(migrationA != migrationB); + + vm.prank(writer); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationA); + vm.prank(writer); + sRegistry.applyMigration(migrationA, migrationB); + + // The other namespace applies them in the opposite order, so a chain + // that leaked would be visibly the first one's. + vm.prank(other); + sRegistry.applyMigration(MIGRATION_HEAD_GENESIS, migrationB); + vm.prank(other); + sRegistry.applyMigration(migrationB, migrationA); + + assertEq(sRegistry.appliedOnto(writer, migrationA), MIGRATION_HEAD_GENESIS); + assertEq(sRegistry.appliedOnto(writer, migrationB), migrationA); + assertEq(sRegistry.appliedOnto(other, migrationB), MIGRATION_HEAD_GENESIS); + assertEq(sRegistry.appliedOnto(other, migrationA), migrationB); } } diff --git a/test/src/concrete/MigrationRegistryHead.t.sol b/test/src/concrete/MigrationRegistryHead.t.sol index 4702534..f3f594e 100644 --- a/test/src/concrete/MigrationRegistryHead.t.sol +++ b/test/src/concrete/MigrationRegistryHead.t.sol @@ -11,7 +11,7 @@ import {LibMigrationFuzz} from "../../lib/LibMigrationFuzz.sol"; /// @title MigrationRegistryHeadTest /// @notice A test suite for `MigrationRegistry.head`: where a namespace is, what /// an empty one answers, that the answer is never a value that is not a head, -/// and that it is the same answer `applyMigration` checks against. +/// and that it is the same answer a write checks against. contract MigrationRegistryHeadTest is Test { /// The registry under test. Stateful, so a fresh one per test. MigrationRegistry internal sRegistry; @@ -70,8 +70,8 @@ contract MigrationRegistryHeadTest is Test { assertEq(sRegistry.head(other), MIGRATION_HEAD_GENESIS); } - /// The head `head` reports is exactly the head `applyMigration` demands: - /// whatever this answers is accepted, and it is the only value that is. The + /// The head `head` reports is exactly the head a write demands: whatever + /// this answers is accepted, and it is the only value that is. The /// two go through one translation of an empty namespace, so they cannot /// disagree about where one is. function testHeadIsWhatApplyMigrationAccepts(address writer, bytes32 migrationA, bytes32 migrationB) external { diff --git a/test/src/lib/LibMigrationRegistry.t.sol b/test/src/lib/LibMigrationRegistry.t.sol index 41bb030..0956c92 100644 --- a/test/src/lib/LibMigrationRegistry.t.sol +++ b/test/src/lib/LibMigrationRegistry.t.sol @@ -62,6 +62,15 @@ contract LibMigrationRegistryTest is Test { return LibMigrationRegistry.applied(writer, migration); } + /// External wrapper for `appliedOnto` so that `vm.expectRevert` works at + /// the correct call depth. + /// @param writer The namespace to read. + /// @param migration The migration to ask about. + /// @return What `writer` applied `migration` onto, or zero. + function externalAppliedOnto(address writer, bytes32 migration) external view returns (bytes32) { + return LibMigrationRegistry.appliedOnto(writer, migration); + } + /// External wrapper for `head` so that `vm.expectRevert` works at the /// correct call depth. /// @param writer The namespace to read. @@ -78,6 +87,15 @@ contract LibMigrationRegistryTest is Test { LibMigrationRegistry.applyMigration(expectedHead, migration); } + /// External wrapper for `applyMigrationHistory` so that `vm.expectRevert` + /// works at the correct call depth. + /// @param expectedHead The head this contract believes it is at. + /// @param migration The migration to apply. + /// @param appliedAt The moment to record against it. + function externalApplyMigrationHistory(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) external { + LibMigrationRegistry.applyMigrationHistory(expectedHead, migration, appliedAt); + } + /// The Zoltu deploy really does land the registry on its pinned address /// with its pinned code hash. Every other test here depends on that, and a /// pin that had gone stale would otherwise show up as an unrelated @@ -431,4 +449,263 @@ contract LibMigrationRegistryTest is Test { ); this.externalApplyMigration(expectedHead, migration); } + + /// A migration recorded with a supplied moment reads back as that moment + /// through the library, so what `applyMigrationHistory` writes is what + /// `applied` finds — and it is not the block the write landed in. + function testApplyMigrationHistoryThenApplied(bytes32 migration, uint32 appliedAt, uint32 writtenAt) external { + LibMigrationFuzz.assumeMigration(vm, migration); + vm.assume(appliedAt != 0); + vm.assume(writtenAt > appliedAt); + deployRegistry(); + vm.warp(writtenAt); + + LibMigrationRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migration, appliedAt); + + assertEq(LibMigrationRegistry.applied(address(this), migration), appliedAt); + } + + /// A namespace backfilled in head order keeps every moment it was given, and + /// the chain reads back as the order it was applied in. This is a consumer + /// whose migrations ran before this registry reached the chain recording + /// what actually happened rather than the day it got round to writing it + /// down. + function testApplyMigrationHistoryBackfillsASequence(bytes32 migrationA, bytes32 migrationB, bytes32 migrationC) + external + { + LibMigrationFuzz.assumeMigration(vm, migrationA); + LibMigrationFuzz.assumeMigration(vm, migrationB); + LibMigrationFuzz.assumeMigration(vm, migrationC); + vm.assume(migrationA != migrationB); + vm.assume(migrationB != migrationC); + vm.assume(migrationA != migrationC); + deployRegistry(); + vm.warp(9000); + + LibMigrationRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migrationA, 1000); + LibMigrationRegistry.applyMigrationHistory(migrationA, migrationB, 2000); + LibMigrationRegistry.applyMigrationHistory(migrationB, migrationC, 3000); + + assertEq(LibMigrationRegistry.applied(address(this), migrationA), 1000); + assertEq(LibMigrationRegistry.applied(address(this), migrationB), 2000); + assertEq(LibMigrationRegistry.applied(address(this), migrationC), 3000); + assertEq(LibMigrationRegistry.head(address(this)), migrationC); + + bytes32 cursor = LibMigrationRegistry.head(address(this)); + cursor = LibMigrationRegistry.appliedOnto(address(this), cursor); + assertEq(cursor, migrationB); + cursor = LibMigrationRegistry.appliedOnto(address(this), cursor); + assertEq(cursor, migrationA); + cursor = LibMigrationRegistry.appliedOnto(address(this), cursor); + assertEq(cursor, MIGRATION_HEAD_GENESIS); + } + + /// The registry's zero-moment refusal arrives unmodified through + /// `applyMigrationHistory`, so a consumer that left its `appliedAt` + /// uninitialised is told so rather than writing a record that reads back as + /// none. + function testApplyMigrationHistoryZeroTimestampReverts(bytes32 migration) external { + LibMigrationFuzz.assumeMigration(vm, migration); + deployRegistry(); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroTimestamp.selector)); + this.externalApplyMigrationHistory(MIGRATION_HEAD_GENESIS, migration, 0); + } + + /// The registry's future-moment refusal arrives unmodified through + /// `applyMigrationHistory`. + function testApplyMigrationHistoryFutureTimestampReverts(bytes32 migration, uint32 now_) external { + LibMigrationFuzz.assumeMigration(vm, migration); + deployRegistry(); + vm.warp(now_); + + vm.expectRevert( + abi.encodeWithSelector(IMigrationRegistryV1.FutureTimestamp.selector, uint256(now_) + 1, uint256(now_)) + ); + this.externalApplyMigrationHistory(MIGRATION_HEAD_GENESIS, migration, uint256(now_) + 1); + } + + /// The registry's before-the-head refusal arrives unmodified through + /// `applyMigrationHistory`, so a consumer backfilling its history out of + /// order is told which moment it contradicted. + function testApplyMigrationHistoryTimestampBeforeHeadReverts(bytes32 migrationA, bytes32 migrationB) external { + LibMigrationFuzz.assumeMigration(vm, migrationA); + LibMigrationFuzz.assumeMigration(vm, migrationB); + vm.assume(migrationA != migrationB); + deployRegistry(); + vm.warp(9000); + + LibMigrationRegistry.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migrationA, 2000); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.TimestampBeforeHead.selector, 1999, 2000)); + this.externalApplyMigrationHistory(migrationA, migrationB, 1999); + } + + /// The namespace of `applyMigrationHistory` is the calling CONTRACT too, so + /// a consumer backfilling its history writes its own namespace and nobody + /// else's. + function testApplyMigrationHistoryLandsUnderTheCallingContract(bytes32 migration, uint32 appliedAt) external { + LibMigrationFuzz.assumeMigration(vm, migration); + vm.assume(appliedAt != 0); + deployRegistry(); + vm.warp(appliedAt); + MockMigrationApplier applier = new MockMigrationApplier(); + + applier.applyMigrationHistory(MIGRATION_HEAD_GENESIS, migration, appliedAt); + + assertEq(LibMigrationRegistry.applied(address(applier), migration), appliedAt); + assertEq(LibMigrationRegistry.appliedOnto(address(applier), migration), MIGRATION_HEAD_GENESIS); + assertEq(LibMigrationRegistry.applied(address(this), migration), 0); + } + + /// An unapplied migration answers a zero head, which is the same "no record" + /// answer `applied` gives as a zero moment. + function testAppliedOntoUnappliedIsZero(address writer, bytes32 migration) external { + vm.assume(writer != address(0)); + LibMigrationFuzz.assumeMigration(vm, migration); + deployRegistry(); + + assertEq(LibMigrationRegistry.appliedOnto(writer, migration), bytes32(0)); + } + + /// The registry's zero-writer refusal arrives unmodified through + /// `appliedOnto`. + function testAppliedOntoZeroWriterReverts(bytes32 migration) external { + LibMigrationFuzz.assumeMigration(vm, migration); + deployRegistry(); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroWriter.selector)); + this.externalAppliedOnto(address(0), migration); + } + + /// The registry's zero-id refusal arrives unmodified through `appliedOnto`. + function testAppliedOntoZeroMigrationReverts(address writer) external { + vm.assume(writer != address(0)); + deployRegistry(); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.ZeroMigration.selector)); + this.externalAppliedOnto(writer, bytes32(0)); + } + + /// The registry's genesis-id refusal arrives unmodified through + /// `appliedOnto`. + function testAppliedOntoGenesisMigrationReverts(address writer) external { + vm.assume(writer != address(0)); + deployRegistry(); + + vm.expectRevert(abi.encodeWithSelector(IMigrationRegistryV1.GenesisMigration.selector)); + this.externalAppliedOnto(writer, MIGRATION_HEAD_GENESIS); + } + + /// Reading a chain step off a chain with no registry is refused by the code + /// hash, with the same named error as every other read. + function testAppliedOntoNoRegistry(address writer, bytes32 migration) external { + assertEq(LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_ADDRESS.code.length, 0); + + vm.expectRevert( + abi.encodeWithSelector( + LibMigrationRegistry.UnexpectedMigrationRegistryCodeHash.selector, + LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_CODEHASH, + bytes32(0) + ) + ); + this.externalAppliedOnto(writer, migration); + } + + /// Nor is a chain step read out of ordinary occupying code, which is free to + /// answer a head that was never applied and send a walk anywhere it likes. + function testAppliedOntoWrongCode(address writer, bytes32 migration, bytes memory code) external { + assumeOrdinaryCode(code); + vm.assume(keccak256(code) != LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_CODEHASH); + vm.etch(LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_ADDRESS, code); + + vm.expectRevert( + abi.encodeWithSelector( + LibMigrationRegistry.UnexpectedMigrationRegistryCodeHash.selector, + LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_CODEHASH, + keccak256(code) + ) + ); + this.externalAppliedOnto(writer, migration); + } + + /// Nor out of a delegated account. + /// @param writer The namespace a reader would ask about. + /// @param migration The migration a reader would ask about. + /// @param delegate The account the registry address is delegated to. + function testAppliedOntoDelegatedCode(address writer, bytes32 migration, address delegate) external { + bytes memory designator = assumedDesignator(delegate); + + vm.etch(LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_ADDRESS, designator); + + vm.expectRevert( + abi.encodeWithSelector( + LibMigrationRegistry.UnexpectedMigrationRegistryCodeHash.selector, + LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_CODEHASH, + keccak256(designator) + ) + ); + this.externalAppliedOnto(writer, migration); + } + + /// `applyMigrationHistory` checks the code hash too, so a backfill is never + /// written to a chain with no registry. + function testApplyMigrationHistoryNoRegistry(bytes32 expectedHead, bytes32 migration, uint256 appliedAt) external { + assertEq(LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_ADDRESS.code.length, 0); + + vm.expectRevert( + abi.encodeWithSelector( + LibMigrationRegistry.UnexpectedMigrationRegistryCodeHash.selector, + LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_CODEHASH, + bytes32(0) + ) + ); + this.externalApplyMigrationHistory(expectedHead, migration, appliedAt); + } + + /// Nor into ordinary occupying code. + function testApplyMigrationHistoryWrongCode( + bytes32 expectedHead, + bytes32 migration, + uint256 appliedAt, + bytes memory code + ) external { + assumeOrdinaryCode(code); + vm.assume(keccak256(code) != LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_CODEHASH); + vm.etch(LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_ADDRESS, code); + + vm.expectRevert( + abi.encodeWithSelector( + LibMigrationRegistry.UnexpectedMigrationRegistryCodeHash.selector, + LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_CODEHASH, + keccak256(code) + ) + ); + this.externalApplyMigrationHistory(expectedHead, migration, appliedAt); + } + + /// Nor into a delegated account. + /// @param expectedHead The head the writer believes it is at. + /// @param migration The migration being applied. + /// @param appliedAt The moment being recorded. + /// @param delegate The account the registry address is delegated to. + function testApplyMigrationHistoryDelegatedCode( + bytes32 expectedHead, + bytes32 migration, + uint256 appliedAt, + address delegate + ) external { + bytes memory designator = assumedDesignator(delegate); + + vm.etch(LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_ADDRESS, designator); + + vm.expectRevert( + abi.encodeWithSelector( + LibMigrationRegistry.UnexpectedMigrationRegistryCodeHash.selector, + LibMigrationRegistryDeploy.MIGRATION_REGISTRY_DEPLOYED_CODEHASH, + keccak256(designator) + ) + ); + this.externalApplyMigrationHistory(expectedHead, migration, appliedAt); + } }