From 88bf27ac42cd741da2924dcca2784302e129e67a Mon Sep 17 00:00:00 2001 From: "allen.wu" Date: Wed, 17 Jun 2026 12:35:26 +0800 Subject: [PATCH 1/3] feat: change sequencer upgrade switch from height to timestamp --- bindings/bin/l1sequencer_deployed.hex | 1 + bindings/bindings/l1sequencer.go | 77 ++++------- bindings/go.mod | 2 +- common/go.mod | 2 +- contracts/contracts/l1/L1Sequencer.sol | 26 ++-- contracts/contracts/test/L1Sequencer.t.sol | 121 +++++++++--------- .../contracts/test/base/L1SequencerBase.t.sol | 4 +- contracts/deploy/022-SequencerInit.ts | 2 +- contracts/go.mod | 2 +- node/cmd/node/main.go | 38 +++++- node/derivation/derivation.go | 22 ++-- node/flags/flags.go | 11 ++ node/go.mod | 2 +- node/go.sum | 4 +- node/l1sequencer/verifier.go | 43 ++----- node/types/networks.go | 8 ++ .../docker-compose.ha-override.yml | 13 +- .../docker-compose.override.yml | 17 ++- ops/docker-sequencer-test/run-ha-test.sh | 120 ++++++++++++++--- ops/docker-sequencer-test/run-test.sh | 94 +++++++++----- ops/l2-genesis/go.mod | 2 +- ops/tools/go.mod | 2 +- ops/tools/go.sum | 4 +- tx-submitter/go.mod | 2 +- 24 files changed, 369 insertions(+), 250 deletions(-) create mode 100644 bindings/bin/l1sequencer_deployed.hex diff --git a/bindings/bin/l1sequencer_deployed.hex b/bindings/bin/l1sequencer_deployed.hex new file mode 100644 index 000000000..2d5e93397 --- /dev/null +++ b/bindings/bin/l1sequencer_deployed.hex @@ -0,0 +1 @@ +0x608060405234801561000f575f80fd5b50600436106100c4575f3560e01c8063715018a61161007d578063c4d66de811610058578063c4d66de8146101b6578063f151ce9e146101c9578063f2fde38b146101dc575f80fd5b8063715018a61461017d578063761a90fd146101855780638da5cb5b14610198575f80fd5b80634d96a90a116100ad5780634d96a90a146100f35780636628aea1146101205780636d8ce3d214610135575f80fd5b80630df8955e146100c85780633d5767ce146100dd575b5f80fd5b6100db6100d6366004610df2565b6101ef565b005b6065546040519081526020015b60405180910390f35b6100fb610391565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020016100ea565b610128610434565b6040516100ea9190610e12565b610148610143366004610e80565b6104c1565b6040805167ffffffffffffffff909316835273ffffffffffffffffffffffffffffffffffffffff9091166020830152016100ea565b6100db61050d565b6100db610193366004610eae565b610520565b60335473ffffffffffffffffffffffffffffffffffffffff166100fb565b6100db6101c4366004610df2565b6107a4565b6100fb6101d7366004610edf565b610983565b6100db6101ea366004610df2565b610b4c565b6101f7610be9565b6065541561024c5760405162461bcd60e51b815260206004820152601360248201527f616c726561647920696e697469616c697a65640000000000000000000000000060448201526064015b60405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff81166102af5760405162461bcd60e51b815260206004820152600f60248201527f696e76616c6964206164647265737300000000000000000000000000000000006044820152606401610243565b6040805180820182525f80825273ffffffffffffffffffffffffffffffffffffffff84811660208085018281526065805460018101825590865295517f8ff97419363ffd7000167f130ef7168fbea05faf9251824ca5043f113cc6a7c79096018054915190941668010000000000000000027fffffffff0000000000000000000000000000000000000000000000000000000090911667ffffffffffffffff9096169590951794909417909155925181815290917ffed767db50732333bba543b785430d53a3a836d71064a68ae91809e50eca7bb8910160405180910390a350565b6065545f906103e25760405162461bcd60e51b815260206004820152601760248201527f6e6f2073657175656e63657220636f6e666967757265640000000000000000006044820152606401610243565b606580546103f290600190610f25565b8154811061040257610402610f3e565b5f9182526020909120015468010000000000000000900473ffffffffffffffffffffffffffffffffffffffff16919050565b60606065805480602002602001604051908101604052809291908181526020015f905b828210156104b8575f848152602090819020604080518082019091529084015467ffffffffffffffff8116825268010000000000000000900473ffffffffffffffffffffffffffffffffffffffff1681830152825260019092019101610457565b50505050905090565b606581815481106104d0575f80fd5b5f9182526020909120015467ffffffffffffffff8116915068010000000000000000900473ffffffffffffffffffffffffffffffffffffffff1682565b610515610be9565b61051e5f610c50565b565b610528610be9565b73ffffffffffffffffffffffffffffffffffffffff821661058b5760405162461bcd60e51b815260206004820152600f60248201527f696e76616c6964206164647265737300000000000000000000000000000000006044820152606401610243565b6065546105da5760405162461bcd60e51b815260206004820152600f60248201527f6e6f7420696e697469616c697a656400000000000000000000000000000000006044820152606401610243565b606580546105ea90600190610f25565b815481106105fa576105fa610f3e565b5f9182526020909120015467ffffffffffffffff908116908216116106875760405162461bcd60e51b815260206004820152602d60248201527f73746172744c32426c6f636b206d75737420626520677265617465722074686160448201527f6e206c617374207265636f7264000000000000000000000000000000000000006064820152608401610243565b606580545f919061069a90600190610f25565b815481106106aa576106aa610f3e565b5f9182526020808320919091015460408051808201825267ffffffffffffffff87811680835273ffffffffffffffffffffffffffffffffffffffff8a8116848801818152606580546001810182559a5294517f8ff97419363ffd7000167f130ef7168fbea05faf9251824ca5043f113cc6a7c790990180549551999094167fffffffff00000000000000000000000000000000000000000000000000000000909516949094176801000000000000000098821689021790925592519283529490920490931693509183917ffed767db50732333bba543b785430d53a3a836d71064a68ae91809e50eca7bb8910160405180910390a3505050565b5f54610100900460ff16158080156107c257505f54600160ff909116105b806107db5750303b1580156107db57505f5460ff166001145b61084d5760405162461bcd60e51b815260206004820152602e60248201527f496e697469616c697a61626c653a20636f6e747261637420697320616c72656160448201527f647920696e697469616c697a65640000000000000000000000000000000000006064820152608401610243565b5f80547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0016600117905580156108a9575f80547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff166101001790555b73ffffffffffffffffffffffffffffffffffffffff821661090c5760405162461bcd60e51b815260206004820152600d60248201527f696e76616c6964206f776e6572000000000000000000000000000000000000006044820152606401610243565b610914610cc6565b61091d82610c50565b801561097f575f80547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff169055604051600181527f7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb38474024989060200160405180910390a15b5050565b6065545f90806109d55760405162461bcd60e51b815260206004820152601760248201527f6e6f2073657175656e63657220636f6e666967757265640000000000000000006044820152606401610243565b5f806109e2600184610f25565b90505f5b818311610a81575f60026109fa8486610f6b565b610a049190610f7e565b90508667ffffffffffffffff1660658281548110610a2457610a24610f3e565b5f9182526020909120015467ffffffffffffffff1611610a6057809150828103610a4e5750610a81565b610a59816001610f6b565b9350610a7b565b805f03610a6d5750610a81565b610a78600182610f25565b92505b506109e6565b8567ffffffffffffffff1660658281548110610a9f57610a9f610f3e565b5f9182526020909120015467ffffffffffffffff161115610b025760405162461bcd60e51b815260206004820152601660248201527f6e6f2073657175656e63657220617420686569676874000000000000000000006044820152606401610243565b60658181548110610b1557610b15610f3e565b5f9182526020909120015468010000000000000000900473ffffffffffffffffffffffffffffffffffffffff169695505050505050565b610b54610be9565b73ffffffffffffffffffffffffffffffffffffffff8116610bdd5760405162461bcd60e51b815260206004820152602660248201527f4f776e61626c653a206e6577206f776e657220697320746865207a65726f206160448201527f64647265737300000000000000000000000000000000000000000000000000006064820152608401610243565b610be681610c50565b50565b60335473ffffffffffffffffffffffffffffffffffffffff16331461051e5760405162461bcd60e51b815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e65726044820152606401610243565b6033805473ffffffffffffffffffffffffffffffffffffffff8381167fffffffffffffffffffffffff0000000000000000000000000000000000000000831681179093556040519116919082907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a35050565b5f54610100900460ff16610d425760405162461bcd60e51b815260206004820152602b60248201527f496e697469616c697a61626c653a20636f6e7472616374206973206e6f74206960448201527f6e697469616c697a696e670000000000000000000000000000000000000000006064820152608401610243565b61051e5f54610100900460ff16610dc15760405162461bcd60e51b815260206004820152602b60248201527f496e697469616c697a61626c653a20636f6e7472616374206973206e6f74206960448201527f6e697469616c697a696e670000000000000000000000000000000000000000006064820152608401610243565b61051e33610c50565b803573ffffffffffffffffffffffffffffffffffffffff81168114610ded575f80fd5b919050565b5f60208284031215610e02575f80fd5b610e0b82610dca565b9392505050565b602080825282518282018190525f919060409081850190868401855b82811015610e73578151805167ffffffffffffffff16855286015173ffffffffffffffffffffffffffffffffffffffff16868501529284019290850190600101610e2e565b5091979650505050505050565b5f60208284031215610e90575f80fd5b5035919050565b803567ffffffffffffffff81168114610ded575f80fd5b5f8060408385031215610ebf575f80fd5b610ec883610dca565b9150610ed660208401610e97565b90509250929050565b5f60208284031215610eef575f80fd5b610e0b82610e97565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b81810381811115610f3857610f38610ef8565b92915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b80820180821115610f3857610f38610ef8565b5f82610fb1577f4e487b71000000000000000000000000000000000000000000000000000000005f52601260045260245ffd5b50049056fea164736f6c6343000818000a diff --git a/bindings/bindings/l1sequencer.go b/bindings/bindings/l1sequencer.go index 844dadb84..bc46b98ec 100644 --- a/bindings/bindings/l1sequencer.go +++ b/bindings/bindings/l1sequencer.go @@ -37,8 +37,8 @@ type L1SequencerHistoryRecord struct { // L1SequencerMetaData contains all meta data concerning the L1Sequencer contract. var L1SequencerMetaData = &bind.MetaData{ - ABI: "[{\"type\":\"function\",\"name\":\"activeHeight\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint64\",\"internalType\":\"uint64\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"getSequencer\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"getSequencerAt\",\"inputs\":[{\"name\":\"l2Height\",\"type\":\"uint64\",\"internalType\":\"uint64\"}],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"getSequencerHistory\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"tuple[]\",\"internalType\":\"structL1Sequencer.HistoryRecord[]\",\"components\":[{\"name\":\"startL2Block\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"sequencerAddr\",\"type\":\"address\",\"internalType\":\"address\"}]}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"getSequencerHistoryLength\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"initialize\",\"inputs\":[{\"name\":\"_owner\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"initializeHistory\",\"inputs\":[{\"name\":\"firstSequencer\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"upgradeL2Block\",\"type\":\"uint64\",\"internalType\":\"uint64\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"owner\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"renounceOwnership\",\"inputs\":[],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"sequencerHistory\",\"inputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"outputs\":[{\"name\":\"startL2Block\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"sequencerAddr\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"transferOwnership\",\"inputs\":[{\"name\":\"newOwner\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"updateSequencer\",\"inputs\":[{\"name\":\"newSequencer\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"startL2Block\",\"type\":\"uint64\",\"internalType\":\"uint64\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"event\",\"name\":\"Initialized\",\"inputs\":[{\"name\":\"version\",\"type\":\"uint8\",\"indexed\":false,\"internalType\":\"uint8\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"OwnershipTransferred\",\"inputs\":[{\"name\":\"previousOwner\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"newOwner\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"SequencerUpdated\",\"inputs\":[{\"name\":\"oldSequencer\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"newSequencer\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"startL2Block\",\"type\":\"uint64\",\"indexed\":false,\"internalType\":\"uint64\"}],\"anonymous\":false}]", - Bin: "0x608060405234801561000f575f80fd5b506111968061001d5f395ff3fe608060405234801561000f575f80fd5b50600436106100cf575f3560e01c8063761a90fd1161007d578063f151ce9e11610058578063f151ce9e146101ee578063f198e27f14610201578063f2fde38b14610214575f80fd5b8063761a90fd146101aa5780638da5cb5b146101bd578063c4d66de8146101db575f80fd5b80636628aea1116100ad5780636628aea1146101435780636d8ce3d214610158578063715018a6146101a0575f80fd5b80633d5767ce146100d35780633ef5e8cc146100e95780634d96a90a14610116575b5f80fd5b6065546040519081526020015b60405180910390f35b6066546100fd9067ffffffffffffffff1681565b60405167ffffffffffffffff90911681526020016100e0565b61011e610227565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020016100e0565b61014b6102e9565b6040516100e09190610f9d565b61016b61016636600461100b565b610376565b6040805167ffffffffffffffff909316835273ffffffffffffffffffffffffffffffffffffffff9091166020830152016100e0565b6101a86103c2565b005b6101a86101b8366004611061565b6103d5565b60335473ffffffffffffffffffffffffffffffffffffffff1661011e565b6101a86101e9366004611092565b6106a7565b61011e6101fc3660046110b2565b6108ba565b6101a861020f366004611061565b610ab7565b6101a8610222366004611092565b610cb7565b6065545f90610297576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f6e6f2073657175656e63657220636f6e6669677572656400000000000000000060448201526064015b60405180910390fd5b606580546102a7906001906110f8565b815481106102b7576102b7611111565b5f9182526020909120015468010000000000000000900473ffffffffffffffffffffffffffffffffffffffff16919050565b60606065805480602002602001604051908101604052809291908181526020015f905b8282101561036d575f848152602090819020604080518082019091529084015467ffffffffffffffff8116825268010000000000000000900473ffffffffffffffffffffffffffffffffffffffff168183015282526001909201910161030c565b50505050905090565b60658181548110610385575f80fd5b5f9182526020909120015467ffffffffffffffff8116915068010000000000000000900473ffffffffffffffffffffffffffffffffffffffff1682565b6103ca610d6e565b6103d35f610def565b565b6103dd610d6e565b73ffffffffffffffffffffffffffffffffffffffff821661045a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152600f60248201527f696e76616c696420616464726573730000000000000000000000000000000000604482015260640161028e565b6065546104c3576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152600f60248201527f6e6f7420696e697469616c697a65640000000000000000000000000000000000604482015260640161028e565b606580546104d3906001906110f8565b815481106104e3576104e3611111565b5f9182526020909120015467ffffffffffffffff9081169082161161058a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602d60248201527f73746172744c32426c6f636b206d75737420626520677265617465722074686160448201527f6e206c617374207265636f726400000000000000000000000000000000000000606482015260840161028e565b606580545f919061059d906001906110f8565b815481106105ad576105ad611111565b5f9182526020808320919091015460408051808201825267ffffffffffffffff87811680835273ffffffffffffffffffffffffffffffffffffffff8a8116848801818152606580546001810182559a5294517f8ff97419363ffd7000167f130ef7168fbea05faf9251824ca5043f113cc6a7c790990180549551999094167fffffffff00000000000000000000000000000000000000000000000000000000909516949094176801000000000000000098821689021790925592519283529490920490931693509183917ffed767db50732333bba543b785430d53a3a836d71064a68ae91809e50eca7bb8910160405180910390a3505050565b5f54610100900460ff16158080156106c557505f54600160ff909116105b806106de5750303b1580156106de57505f5460ff166001145b61076a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602e60248201527f496e697469616c697a61626c653a20636f6e747261637420697320616c72656160448201527f647920696e697469616c697a6564000000000000000000000000000000000000606482015260840161028e565b5f80547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0016600117905580156107c6575f80547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff166101001790555b73ffffffffffffffffffffffffffffffffffffffff8216610843576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152600d60248201527f696e76616c6964206f776e657200000000000000000000000000000000000000604482015260640161028e565b61084b610e65565b61085482610def565b80156108b6575f80547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff169055604051600181527f7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb38474024989060200160405180910390a15b5050565b6065545f9080610926576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f6e6f2073657175656e63657220636f6e66696775726564000000000000000000604482015260640161028e565b5f806109336001846110f8565b90505f5b8183116109d2575f600261094b848661113e565b6109559190611151565b90508667ffffffffffffffff166065828154811061097557610975611111565b5f9182526020909120015467ffffffffffffffff16116109b15780915082810361099f57506109d2565b6109aa81600161113e565b93506109cc565b805f036109be57506109d2565b6109c96001826110f8565b92505b50610937565b8567ffffffffffffffff16606582815481106109f0576109f0611111565b5f9182526020909120015467ffffffffffffffff161115610a6d576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601660248201527f6e6f2073657175656e6365722061742068656967687400000000000000000000604482015260640161028e565b60658181548110610a8057610a80611111565b5f9182526020909120015468010000000000000000900473ffffffffffffffffffffffffffffffffffffffff169695505050505050565b610abf610d6e565b60655415610b29576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601360248201527f616c726561647920696e697469616c697a656400000000000000000000000000604482015260640161028e565b73ffffffffffffffffffffffffffffffffffffffff8216610ba6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152600f60248201527f696e76616c696420616464726573730000000000000000000000000000000000604482015260640161028e565b60408051808201825267ffffffffffffffff83811680835273ffffffffffffffffffffffffffffffffffffffff8681166020808601828152606580546001810182555f91825297517f8ff97419363ffd7000167f130ef7168fbea05faf9251824ca5043f113cc6a7c79098018054925190951668010000000000000000027fffffffff00000000000000000000000000000000000000000000000000000000909216979096169690961795909517909155606680547fffffffffffffffffffffffffffffffffffffffffffffffff00000000000000001683179055935190815290917ffed767db50732333bba543b785430d53a3a836d71064a68ae91809e50eca7bb8910160405180910390a35050565b610cbf610d6e565b73ffffffffffffffffffffffffffffffffffffffff8116610d62576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f4f776e61626c653a206e6577206f776e657220697320746865207a65726f206160448201527f6464726573730000000000000000000000000000000000000000000000000000606482015260840161028e565b610d6b81610def565b50565b60335473ffffffffffffffffffffffffffffffffffffffff1633146103d3576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572604482015260640161028e565b6033805473ffffffffffffffffffffffffffffffffffffffff8381167fffffffffffffffffffffffff0000000000000000000000000000000000000000831681179093556040519116919082907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a35050565b5f54610100900460ff16610efb576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602b60248201527f496e697469616c697a61626c653a20636f6e7472616374206973206e6f74206960448201527f6e697469616c697a696e67000000000000000000000000000000000000000000606482015260840161028e565b6103d35f54610100900460ff16610f94576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602b60248201527f496e697469616c697a61626c653a20636f6e7472616374206973206e6f74206960448201527f6e697469616c697a696e67000000000000000000000000000000000000000000606482015260840161028e565b6103d333610def565b602080825282518282018190525f919060409081850190868401855b82811015610ffe578151805167ffffffffffffffff16855286015173ffffffffffffffffffffffffffffffffffffffff16868501529284019290850190600101610fb9565b5091979650505050505050565b5f6020828403121561101b575f80fd5b5035919050565b803573ffffffffffffffffffffffffffffffffffffffff81168114611045575f80fd5b919050565b803567ffffffffffffffff81168114611045575f80fd5b5f8060408385031215611072575f80fd5b61107b83611022565b91506110896020840161104a565b90509250929050565b5f602082840312156110a2575f80fd5b6110ab82611022565b9392505050565b5f602082840312156110c2575f80fd5b6110ab8261104a565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b8181038181111561110b5761110b6110cb565b92915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b8082018082111561110b5761110b6110cb565b5f82611184577f4e487b71000000000000000000000000000000000000000000000000000000005f52601260045260245ffd5b50049056fea164736f6c6343000818000a", + ABI: "[{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint8\",\"name\":\"version\",\"type\":\"uint8\"}],\"name\":\"Initialized\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"previousOwner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"OwnershipTransferred\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"oldSequencer\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"newSequencer\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"startL2Block\",\"type\":\"uint64\"}],\"name\":\"SequencerUpdated\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"getSequencer\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint64\",\"name\":\"l2Height\",\"type\":\"uint64\"}],\"name\":\"getSequencerAt\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getSequencerHistory\",\"outputs\":[{\"components\":[{\"internalType\":\"uint64\",\"name\":\"startL2Block\",\"type\":\"uint64\"},{\"internalType\":\"address\",\"name\":\"sequencerAddr\",\"type\":\"address\"}],\"internalType\":\"structL1Sequencer.HistoryRecord[]\",\"name\":\"\",\"type\":\"tuple[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getSequencerHistoryLength\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_owner\",\"type\":\"address\"}],\"name\":\"initialize\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"owner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"renounceOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"name\":\"sequencerHistory\",\"outputs\":[{\"internalType\":\"uint64\",\"name\":\"startL2Block\",\"type\":\"uint64\"},{\"internalType\":\"address\",\"name\":\"sequencerAddr\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"firstSequencer\",\"type\":\"address\"}],\"name\":\"setFirstSequencer\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"transferOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"newSequencer\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"startL2Block\",\"type\":\"uint64\"}],\"name\":\"updateSequencer\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", + Bin: "0x608060405234801561000f575f80fd5b50610fc38061001d5f395ff3fe608060405234801561000f575f80fd5b50600436106100c4575f3560e01c8063715018a61161007d578063c4d66de811610058578063c4d66de8146101b6578063f151ce9e146101c9578063f2fde38b146101dc575f80fd5b8063715018a61461017d578063761a90fd146101855780638da5cb5b14610198575f80fd5b80634d96a90a116100ad5780634d96a90a146100f35780636628aea1146101205780636d8ce3d214610135575f80fd5b80630df8955e146100c85780633d5767ce146100dd575b5f80fd5b6100db6100d6366004610df2565b6101ef565b005b6065546040519081526020015b60405180910390f35b6100fb610391565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020016100ea565b610128610434565b6040516100ea9190610e12565b610148610143366004610e80565b6104c1565b6040805167ffffffffffffffff909316835273ffffffffffffffffffffffffffffffffffffffff9091166020830152016100ea565b6100db61050d565b6100db610193366004610eae565b610520565b60335473ffffffffffffffffffffffffffffffffffffffff166100fb565b6100db6101c4366004610df2565b6107a4565b6100fb6101d7366004610edf565b610983565b6100db6101ea366004610df2565b610b4c565b6101f7610be9565b6065541561024c5760405162461bcd60e51b815260206004820152601360248201527f616c726561647920696e697469616c697a65640000000000000000000000000060448201526064015b60405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff81166102af5760405162461bcd60e51b815260206004820152600f60248201527f696e76616c6964206164647265737300000000000000000000000000000000006044820152606401610243565b6040805180820182525f80825273ffffffffffffffffffffffffffffffffffffffff84811660208085018281526065805460018101825590865295517f8ff97419363ffd7000167f130ef7168fbea05faf9251824ca5043f113cc6a7c79096018054915190941668010000000000000000027fffffffff0000000000000000000000000000000000000000000000000000000090911667ffffffffffffffff9096169590951794909417909155925181815290917ffed767db50732333bba543b785430d53a3a836d71064a68ae91809e50eca7bb8910160405180910390a350565b6065545f906103e25760405162461bcd60e51b815260206004820152601760248201527f6e6f2073657175656e63657220636f6e666967757265640000000000000000006044820152606401610243565b606580546103f290600190610f25565b8154811061040257610402610f3e565b5f9182526020909120015468010000000000000000900473ffffffffffffffffffffffffffffffffffffffff16919050565b60606065805480602002602001604051908101604052809291908181526020015f905b828210156104b8575f848152602090819020604080518082019091529084015467ffffffffffffffff8116825268010000000000000000900473ffffffffffffffffffffffffffffffffffffffff1681830152825260019092019101610457565b50505050905090565b606581815481106104d0575f80fd5b5f9182526020909120015467ffffffffffffffff8116915068010000000000000000900473ffffffffffffffffffffffffffffffffffffffff1682565b610515610be9565b61051e5f610c50565b565b610528610be9565b73ffffffffffffffffffffffffffffffffffffffff821661058b5760405162461bcd60e51b815260206004820152600f60248201527f696e76616c6964206164647265737300000000000000000000000000000000006044820152606401610243565b6065546105da5760405162461bcd60e51b815260206004820152600f60248201527f6e6f7420696e697469616c697a656400000000000000000000000000000000006044820152606401610243565b606580546105ea90600190610f25565b815481106105fa576105fa610f3e565b5f9182526020909120015467ffffffffffffffff908116908216116106875760405162461bcd60e51b815260206004820152602d60248201527f73746172744c32426c6f636b206d75737420626520677265617465722074686160448201527f6e206c617374207265636f7264000000000000000000000000000000000000006064820152608401610243565b606580545f919061069a90600190610f25565b815481106106aa576106aa610f3e565b5f9182526020808320919091015460408051808201825267ffffffffffffffff87811680835273ffffffffffffffffffffffffffffffffffffffff8a8116848801818152606580546001810182559a5294517f8ff97419363ffd7000167f130ef7168fbea05faf9251824ca5043f113cc6a7c790990180549551999094167fffffffff00000000000000000000000000000000000000000000000000000000909516949094176801000000000000000098821689021790925592519283529490920490931693509183917ffed767db50732333bba543b785430d53a3a836d71064a68ae91809e50eca7bb8910160405180910390a3505050565b5f54610100900460ff16158080156107c257505f54600160ff909116105b806107db5750303b1580156107db57505f5460ff166001145b61084d5760405162461bcd60e51b815260206004820152602e60248201527f496e697469616c697a61626c653a20636f6e747261637420697320616c72656160448201527f647920696e697469616c697a65640000000000000000000000000000000000006064820152608401610243565b5f80547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0016600117905580156108a9575f80547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff166101001790555b73ffffffffffffffffffffffffffffffffffffffff821661090c5760405162461bcd60e51b815260206004820152600d60248201527f696e76616c6964206f776e6572000000000000000000000000000000000000006044820152606401610243565b610914610cc6565b61091d82610c50565b801561097f575f80547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff169055604051600181527f7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb38474024989060200160405180910390a15b5050565b6065545f90806109d55760405162461bcd60e51b815260206004820152601760248201527f6e6f2073657175656e63657220636f6e666967757265640000000000000000006044820152606401610243565b5f806109e2600184610f25565b90505f5b818311610a81575f60026109fa8486610f6b565b610a049190610f7e565b90508667ffffffffffffffff1660658281548110610a2457610a24610f3e565b5f9182526020909120015467ffffffffffffffff1611610a6057809150828103610a4e5750610a81565b610a59816001610f6b565b9350610a7b565b805f03610a6d5750610a81565b610a78600182610f25565b92505b506109e6565b8567ffffffffffffffff1660658281548110610a9f57610a9f610f3e565b5f9182526020909120015467ffffffffffffffff161115610b025760405162461bcd60e51b815260206004820152601660248201527f6e6f2073657175656e63657220617420686569676874000000000000000000006044820152606401610243565b60658181548110610b1557610b15610f3e565b5f9182526020909120015468010000000000000000900473ffffffffffffffffffffffffffffffffffffffff169695505050505050565b610b54610be9565b73ffffffffffffffffffffffffffffffffffffffff8116610bdd5760405162461bcd60e51b815260206004820152602660248201527f4f776e61626c653a206e6577206f776e657220697320746865207a65726f206160448201527f64647265737300000000000000000000000000000000000000000000000000006064820152608401610243565b610be681610c50565b50565b60335473ffffffffffffffffffffffffffffffffffffffff16331461051e5760405162461bcd60e51b815260206004820181905260248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e65726044820152606401610243565b6033805473ffffffffffffffffffffffffffffffffffffffff8381167fffffffffffffffffffffffff0000000000000000000000000000000000000000831681179093556040519116919082907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a35050565b5f54610100900460ff16610d425760405162461bcd60e51b815260206004820152602b60248201527f496e697469616c697a61626c653a20636f6e7472616374206973206e6f74206960448201527f6e697469616c697a696e670000000000000000000000000000000000000000006064820152608401610243565b61051e5f54610100900460ff16610dc15760405162461bcd60e51b815260206004820152602b60248201527f496e697469616c697a61626c653a20636f6e7472616374206973206e6f74206960448201527f6e697469616c697a696e670000000000000000000000000000000000000000006064820152608401610243565b61051e33610c50565b803573ffffffffffffffffffffffffffffffffffffffff81168114610ded575f80fd5b919050565b5f60208284031215610e02575f80fd5b610e0b82610dca565b9392505050565b602080825282518282018190525f919060409081850190868401855b82811015610e73578151805167ffffffffffffffff16855286015173ffffffffffffffffffffffffffffffffffffffff16868501529284019290850190600101610e2e565b5091979650505050505050565b5f60208284031215610e90575f80fd5b5035919050565b803567ffffffffffffffff81168114610ded575f80fd5b5f8060408385031215610ebf575f80fd5b610ec883610dca565b9150610ed660208401610e97565b90509250929050565b5f60208284031215610eef575f80fd5b610e0b82610e97565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b81810381811115610f3857610f38610ef8565b92915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b80820180821115610f3857610f38610ef8565b5f82610fb1577f4e487b71000000000000000000000000000000000000000000000000000000005f52601260045260245ffd5b50049056fea164736f6c6343000818000a", } // L1SequencerABI is the input ABI used to generate the binding from. @@ -208,37 +208,6 @@ func (_L1Sequencer *L1SequencerTransactorRaw) Transact(opts *bind.TransactOpts, return _L1Sequencer.Contract.contract.Transact(opts, method, params...) } -// ActiveHeight is a free data retrieval call binding the contract method 0x3ef5e8cc. -// -// Solidity: function activeHeight() view returns(uint64) -func (_L1Sequencer *L1SequencerCaller) ActiveHeight(opts *bind.CallOpts) (uint64, error) { - var out []interface{} - err := _L1Sequencer.contract.Call(opts, &out, "activeHeight") - - if err != nil { - return *new(uint64), err - } - - out0 := *abi.ConvertType(out[0], new(uint64)).(*uint64) - - return out0, err - -} - -// ActiveHeight is a free data retrieval call binding the contract method 0x3ef5e8cc. -// -// Solidity: function activeHeight() view returns(uint64) -func (_L1Sequencer *L1SequencerSession) ActiveHeight() (uint64, error) { - return _L1Sequencer.Contract.ActiveHeight(&_L1Sequencer.CallOpts) -} - -// ActiveHeight is a free data retrieval call binding the contract method 0x3ef5e8cc. -// -// Solidity: function activeHeight() view returns(uint64) -func (_L1Sequencer *L1SequencerCallerSession) ActiveHeight() (uint64, error) { - return _L1Sequencer.Contract.ActiveHeight(&_L1Sequencer.CallOpts) -} - // GetSequencer is a free data retrieval call binding the contract method 0x4d96a90a. // // Solidity: function getSequencer() view returns(address) @@ -460,27 +429,6 @@ func (_L1Sequencer *L1SequencerTransactorSession) Initialize(_owner common.Addre return _L1Sequencer.Contract.Initialize(&_L1Sequencer.TransactOpts, _owner) } -// InitializeHistory is a paid mutator transaction binding the contract method 0xf198e27f. -// -// Solidity: function initializeHistory(address firstSequencer, uint64 upgradeL2Block) returns() -func (_L1Sequencer *L1SequencerTransactor) InitializeHistory(opts *bind.TransactOpts, firstSequencer common.Address, upgradeL2Block uint64) (*types.Transaction, error) { - return _L1Sequencer.contract.Transact(opts, "initializeHistory", firstSequencer, upgradeL2Block) -} - -// InitializeHistory is a paid mutator transaction binding the contract method 0xf198e27f. -// -// Solidity: function initializeHistory(address firstSequencer, uint64 upgradeL2Block) returns() -func (_L1Sequencer *L1SequencerSession) InitializeHistory(firstSequencer common.Address, upgradeL2Block uint64) (*types.Transaction, error) { - return _L1Sequencer.Contract.InitializeHistory(&_L1Sequencer.TransactOpts, firstSequencer, upgradeL2Block) -} - -// InitializeHistory is a paid mutator transaction binding the contract method 0xf198e27f. -// -// Solidity: function initializeHistory(address firstSequencer, uint64 upgradeL2Block) returns() -func (_L1Sequencer *L1SequencerTransactorSession) InitializeHistory(firstSequencer common.Address, upgradeL2Block uint64) (*types.Transaction, error) { - return _L1Sequencer.Contract.InitializeHistory(&_L1Sequencer.TransactOpts, firstSequencer, upgradeL2Block) -} - // RenounceOwnership is a paid mutator transaction binding the contract method 0x715018a6. // // Solidity: function renounceOwnership() returns() @@ -502,6 +450,27 @@ func (_L1Sequencer *L1SequencerTransactorSession) RenounceOwnership() (*types.Tr return _L1Sequencer.Contract.RenounceOwnership(&_L1Sequencer.TransactOpts) } +// SetFirstSequencer is a paid mutator transaction binding the contract method 0x0df8955e. +// +// Solidity: function setFirstSequencer(address firstSequencer) returns() +func (_L1Sequencer *L1SequencerTransactor) SetFirstSequencer(opts *bind.TransactOpts, firstSequencer common.Address) (*types.Transaction, error) { + return _L1Sequencer.contract.Transact(opts, "setFirstSequencer", firstSequencer) +} + +// SetFirstSequencer is a paid mutator transaction binding the contract method 0x0df8955e. +// +// Solidity: function setFirstSequencer(address firstSequencer) returns() +func (_L1Sequencer *L1SequencerSession) SetFirstSequencer(firstSequencer common.Address) (*types.Transaction, error) { + return _L1Sequencer.Contract.SetFirstSequencer(&_L1Sequencer.TransactOpts, firstSequencer) +} + +// SetFirstSequencer is a paid mutator transaction binding the contract method 0x0df8955e. +// +// Solidity: function setFirstSequencer(address firstSequencer) returns() +func (_L1Sequencer *L1SequencerTransactorSession) SetFirstSequencer(firstSequencer common.Address) (*types.Transaction, error) { + return _L1Sequencer.Contract.SetFirstSequencer(&_L1Sequencer.TransactOpts, firstSequencer) +} + // TransferOwnership is a paid mutator transaction binding the contract method 0xf2fde38b. // // Solidity: function transferOwnership(address newOwner) returns() diff --git a/bindings/go.mod b/bindings/go.mod index 63aaf94f3..7b635908d 100644 --- a/bindings/go.mod +++ b/bindings/go.mod @@ -2,7 +2,7 @@ module morph-l2/bindings go 1.24.0 -replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3.8-0.20260612101929-7222401d6577 +replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3.8-0.20260617032026-015f9f543d32 replace github.com/morph-l2/go-ethereum => github.com/morph-l2/go-ethereum v0.0.0-20260608072528-fe02cc1f10bc diff --git a/common/go.mod b/common/go.mod index 00656ed42..0c4101594 100644 --- a/common/go.mod +++ b/common/go.mod @@ -2,7 +2,7 @@ module morph-l2/common go 1.24.0 -replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3.8-0.20260612101929-7222401d6577 +replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3.8-0.20260617032026-015f9f543d32 replace github.com/morph-l2/go-ethereum => github.com/morph-l2/go-ethereum v0.0.0-20260608072528-fe02cc1f10bc diff --git a/contracts/contracts/l1/L1Sequencer.sol b/contracts/contracts/l1/L1Sequencer.sol index d553cc898..da55ab0f3 100644 --- a/contracts/contracts/l1/L1Sequencer.sol +++ b/contracts/contracts/l1/L1Sequencer.sol @@ -17,13 +17,12 @@ contract L1Sequencer is OwnableUpgradeable { // ============ Storage ============ /// @notice Ordered array of sequencer records (by startL2Block ascending). - /// sequencerHistory[0] is the first sequencer after PBFT → single-sequencer upgrade. + /// sequencerHistory[0] is the first sequencer, active from L2 block 0. + /// This contract only answers "who is the sequencer at height N" (identity); + /// the PBFT → single-sequencer upgrade boundary is decided off-chain by block + /// timestamp, not stored here. HistoryRecord[] public sequencerHistory; - /// @notice The L2 block height at which single-sequencer mode activates. - /// Set by initializeHistory(). Nodes read this to know when to switch consensus. - uint64 public activeHeight; - // ============ Events ============ event SequencerUpdated( @@ -44,23 +43,22 @@ contract L1Sequencer is OwnableUpgradeable { // ============ Admin Functions ============ - /// @notice Initialize sequencer history (called once before the L2 upgrade). - /// @param firstSequencer The first sequencer address after the upgrade. - /// @param upgradeL2Block The L2 block height where single-sequencer mode activates. - function initializeHistory( - address firstSequencer, - uint64 upgradeL2Block + /// @notice Set the first sequencer (called once). The sequencer is active from L2 block 0; + /// the upgrade height is no longer specified here — the PBFT → single-sequencer + /// switch is triggered off-chain by block timestamp. + /// @param firstSequencer The first sequencer address. + function setFirstSequencer( + address firstSequencer ) external onlyOwner { require(sequencerHistory.length == 0, "already initialized"); require(firstSequencer != address(0), "invalid address"); sequencerHistory.push(HistoryRecord({ - startL2Block: upgradeL2Block, + startL2Block: 0, sequencerAddr: firstSequencer })); - activeHeight = upgradeL2Block; - emit SequencerUpdated(address(0), firstSequencer, upgradeL2Block); + emit SequencerUpdated(address(0), firstSequencer, 0); } /// @notice Register a sequencer change at a future L2 block height. diff --git a/contracts/contracts/test/L1Sequencer.t.sol b/contracts/contracts/test/L1Sequencer.t.sol index 24beecc9f..ffea2d6c8 100644 --- a/contracts/contracts/test/L1Sequencer.t.sol +++ b/contracts/contracts/test/L1Sequencer.t.sol @@ -22,65 +22,70 @@ contract L1SequencerTest is L1SequencerBaseTest { impl.initialize(address(0)); } - // ============ initializeHistory ============ + // ============ setFirstSequencer ============ + // The first sequencer is always registered at L2 block 0 (identity from + // genesis); the upgrade height is no longer stored on-chain. - function test_initializeHistory_success() public { - _initHistory(sequencerA, UPGRADE_HEIGHT); + function test_setFirstSequencer_success() public { + _setFirstSequencer(sequencerA); - assertEq(l1Sequencer.activeHeight(), UPGRADE_HEIGHT); assertEq(l1Sequencer.getSequencerHistoryLength(), 1); assertEq(l1Sequencer.getSequencer(), sequencerA); - assertEq(l1Sequencer.getSequencerAt(UPGRADE_HEIGHT), sequencerA); + assertEq(l1Sequencer.getSequencerAt(0), sequencerA); + + L1Sequencer.HistoryRecord[] memory history = l1Sequencer.getSequencerHistory(); + assertEq(history[0].startL2Block, 0); + assertEq(history[0].sequencerAddr, sequencerA); } - function test_initializeHistory_emitsEvent() public { + function test_setFirstSequencer_emitsEvent() public { vm.expectEmit(true, true, false, true); - emit L1Sequencer.SequencerUpdated(address(0), sequencerA, UPGRADE_HEIGHT); + emit L1Sequencer.SequencerUpdated(address(0), sequencerA, 0); vm.prank(owner); - l1Sequencer.initializeHistory(sequencerA, UPGRADE_HEIGHT); + l1Sequencer.setFirstSequencer(sequencerA); } - function test_initializeHistory_revertOnSecondCall() public { - _initHistory(sequencerA, UPGRADE_HEIGHT); + function test_setFirstSequencer_revertOnSecondCall() public { + _setFirstSequencer(sequencerA); vm.expectRevert("already initialized"); vm.prank(owner); - l1Sequencer.initializeHistory(sequencerB, UPGRADE_HEIGHT + 100); + l1Sequencer.setFirstSequencer(sequencerB); } - function test_initializeHistory_revertOnZeroAddress() public { + function test_setFirstSequencer_revertOnZeroAddress() public { vm.expectRevert("invalid address"); vm.prank(owner); - l1Sequencer.initializeHistory(address(0), UPGRADE_HEIGHT); + l1Sequencer.setFirstSequencer(address(0)); } - function test_initializeHistory_revertNonOwner() public { + function test_setFirstSequencer_revertNonOwner() public { vm.expectRevert("Ownable: caller is not the owner"); vm.prank(nonOwner); - l1Sequencer.initializeHistory(sequencerA, UPGRADE_HEIGHT); + l1Sequencer.setFirstSequencer(sequencerA); } // ============ updateSequencer ============ function test_updateSequencer_success() public { - _initHistory(sequencerA, UPGRADE_HEIGHT); + _setFirstSequencer(sequencerA); vm.prank(owner); - l1Sequencer.updateSequencer(sequencerB, UPGRADE_HEIGHT + 100); + l1Sequencer.updateSequencer(sequencerB, UPGRADE_HEIGHT); assertEq(l1Sequencer.getSequencerHistoryLength(), 2); assertEq(l1Sequencer.getSequencer(), sequencerB); } function test_updateSequencer_emitsEvent() public { - _initHistory(sequencerA, UPGRADE_HEIGHT); + _setFirstSequencer(sequencerA); vm.expectEmit(true, true, false, true); - emit L1Sequencer.SequencerUpdated(sequencerA, sequencerB, UPGRADE_HEIGHT + 100); + emit L1Sequencer.SequencerUpdated(sequencerA, sequencerB, UPGRADE_HEIGHT); vm.prank(owner); - l1Sequencer.updateSequencer(sequencerB, UPGRADE_HEIGHT + 100); + l1Sequencer.updateSequencer(sequencerB, UPGRADE_HEIGHT); } function test_updateSequencer_revertNotInitialized() public { @@ -90,58 +95,54 @@ contract L1SequencerTest is L1SequencerBaseTest { } function test_updateSequencer_revertZeroAddress() public { - _initHistory(sequencerA, UPGRADE_HEIGHT); + _setFirstSequencer(sequencerA); vm.expectRevert("invalid address"); vm.prank(owner); - l1Sequencer.updateSequencer(address(0), UPGRADE_HEIGHT + 100); + l1Sequencer.updateSequencer(address(0), UPGRADE_HEIGHT); } function test_updateSequencer_revertStartBlockNotGreater() public { - _initHistory(sequencerA, UPGRADE_HEIGHT); + _setFirstSequencer(sequencerA); // first record at block 0 vm.expectRevert("startL2Block must be greater than last record"); vm.prank(owner); - l1Sequencer.updateSequencer(sequencerB, UPGRADE_HEIGHT); // equal, not greater + l1Sequencer.updateSequencer(sequencerB, 0); // equal to last (0), not greater } function test_updateSequencer_revertStartBlockLessThanLast() public { - _initHistory(sequencerA, UPGRADE_HEIGHT); + _setFirstSequencer(sequencerA); + + vm.prank(owner); + l1Sequencer.updateSequencer(sequencerB, UPGRADE_HEIGHT); // last = 100 vm.expectRevert("startL2Block must be greater than last record"); vm.prank(owner); - l1Sequencer.updateSequencer(sequencerB, UPGRADE_HEIGHT - 1); + l1Sequencer.updateSequencer(sequencerC, UPGRADE_HEIGHT - 1); // 99 < 100 } function test_updateSequencer_revertNonOwner() public { - _initHistory(sequencerA, UPGRADE_HEIGHT); + _setFirstSequencer(sequencerA); vm.expectRevert("Ownable: caller is not the owner"); vm.prank(nonOwner); - l1Sequencer.updateSequencer(sequencerB, UPGRADE_HEIGHT + 100); + l1Sequencer.updateSequencer(sequencerB, UPGRADE_HEIGHT); } // ============ getSequencerAt (binary search) ============ - function test_getSequencerAt_singleRecord_exactHeight() public { - _initHistory(sequencerA, UPGRADE_HEIGHT); - assertEq(l1Sequencer.getSequencerAt(UPGRADE_HEIGHT), sequencerA); + function test_getSequencerAt_singleRecord_atZero() public { + _setFirstSequencer(sequencerA); + assertEq(l1Sequencer.getSequencerAt(0), sequencerA); } - function test_getSequencerAt_singleRecord_aboveHeight() public { - _initHistory(sequencerA, UPGRADE_HEIGHT); - assertEq(l1Sequencer.getSequencerAt(UPGRADE_HEIGHT + 9999), sequencerA); - } - - function test_getSequencerAt_singleRecord_revertBelowHeight() public { - _initHistory(sequencerA, UPGRADE_HEIGHT); - - vm.expectRevert("no sequencer at height"); - l1Sequencer.getSequencerAt(UPGRADE_HEIGHT - 1); + function test_getSequencerAt_singleRecord_aboveZero() public { + _setFirstSequencer(sequencerA); + assertEq(l1Sequencer.getSequencerAt(9999), sequencerA); } function test_getSequencerAt_multipleRecords() public { - _initHistory(sequencerA, 100); + _setFirstSequencer(sequencerA); // start 0 vm.prank(owner); l1Sequencer.updateSequencer(sequencerB, 200); @@ -149,18 +150,15 @@ contract L1SequencerTest is L1SequencerBaseTest { vm.prank(owner); l1Sequencer.updateSequencer(sequencerC, 300); - // Before first record - vm.expectRevert("no sequencer at height"); - l1Sequencer.getSequencerAt(99); + // First record covers from block 0 + assertEq(l1Sequencer.getSequencerAt(0), sequencerA); + assertEq(l1Sequencer.getSequencerAt(199), sequencerA); // Exact boundaries - assertEq(l1Sequencer.getSequencerAt(100), sequencerA); assertEq(l1Sequencer.getSequencerAt(200), sequencerB); assertEq(l1Sequencer.getSequencerAt(300), sequencerC); // Between records - assertEq(l1Sequencer.getSequencerAt(150), sequencerA); - assertEq(l1Sequencer.getSequencerAt(199), sequencerA); assertEq(l1Sequencer.getSequencerAt(250), sequencerB); assertEq(l1Sequencer.getSequencerAt(299), sequencerB); @@ -169,32 +167,33 @@ contract L1SequencerTest is L1SequencerBaseTest { } function test_getSequencerAt_twoRecords_boundary() public { - _initHistory(sequencerA, 100); + _setFirstSequencer(sequencerA); // start 0 vm.prank(owner); l1Sequencer.updateSequencer(sequencerB, 101); + assertEq(l1Sequencer.getSequencerAt(0), sequencerA); assertEq(l1Sequencer.getSequencerAt(100), sequencerA); assertEq(l1Sequencer.getSequencerAt(101), sequencerB); } function test_getSequencerAt_manyRecords_binarySearchStress() public { - _initHistory(sequencerA, 10); + _setFirstSequencer(sequencerA); // start 0 - // Add 9 more records (10 total) + // Add 9 more records (10 total) at 100, 200, ... 900 for (uint64 i = 1; i < 10; i++) { address seq = address(uint160(0xA000 + i)); vm.prank(owner); - l1Sequencer.updateSequencer(seq, 10 + i * 100); + l1Sequencer.updateSequencer(seq, i * 100); } assertEq(l1Sequencer.getSequencerHistoryLength(), 10); // Query each boundary - assertEq(l1Sequencer.getSequencerAt(10), sequencerA); + assertEq(l1Sequencer.getSequencerAt(0), sequencerA); assertEq(l1Sequencer.getSequencerAt(99), sequencerA); - assertEq(l1Sequencer.getSequencerAt(110), address(uint160(0xA001))); - assertEq(l1Sequencer.getSequencerAt(910), address(uint160(0xA009))); + assertEq(l1Sequencer.getSequencerAt(100), address(uint160(0xA001))); + assertEq(l1Sequencer.getSequencerAt(900), address(uint160(0xA009))); assertEq(l1Sequencer.getSequencerAt(99999), address(uint160(0xA009))); } @@ -211,10 +210,10 @@ contract L1SequencerTest is L1SequencerBaseTest { } function test_getSequencer_returnsLatest() public { - _initHistory(sequencerA, UPGRADE_HEIGHT); + _setFirstSequencer(sequencerA); vm.prank(owner); - l1Sequencer.updateSequencer(sequencerB, UPGRADE_HEIGHT + 100); + l1Sequencer.updateSequencer(sequencerB, UPGRADE_HEIGHT); assertEq(l1Sequencer.getSequencer(), sequencerB); } @@ -222,14 +221,14 @@ contract L1SequencerTest is L1SequencerBaseTest { // ============ getSequencerHistory ============ function test_getSequencerHistory_returnsAll() public { - _initHistory(sequencerA, 100); + _setFirstSequencer(sequencerA); // start 0 vm.prank(owner); l1Sequencer.updateSequencer(sequencerB, 200); L1Sequencer.HistoryRecord[] memory history = l1Sequencer.getSequencerHistory(); assertEq(history.length, 2); - assertEq(history[0].startL2Block, 100); + assertEq(history[0].startL2Block, 0); assertEq(history[0].sequencerAddr, sequencerA); assertEq(history[1].startL2Block, 200); assertEq(history[1].sequencerAddr, sequencerB); @@ -244,7 +243,7 @@ contract L1SequencerTest is L1SequencerBaseTest { // New owner can now call admin functions vm.prank(nonOwner); - l1Sequencer.initializeHistory(sequencerA, UPGRADE_HEIGHT); + l1Sequencer.setFirstSequencer(sequencerA); assertEq(l1Sequencer.getSequencerHistoryLength(), 1); } @@ -255,6 +254,6 @@ contract L1SequencerTest is L1SequencerBaseTest { vm.expectRevert("Ownable: caller is not the owner"); vm.prank(owner); - l1Sequencer.initializeHistory(sequencerA, UPGRADE_HEIGHT); + l1Sequencer.setFirstSequencer(sequencerA); } } diff --git a/contracts/contracts/test/base/L1SequencerBase.t.sol b/contracts/contracts/test/base/L1SequencerBase.t.sol index 3cdbb1630..fe1ac31db 100644 --- a/contracts/contracts/test/base/L1SequencerBase.t.sol +++ b/contracts/contracts/test/base/L1SequencerBase.t.sol @@ -35,8 +35,8 @@ contract L1SequencerBaseTest is Test { vm.stopPrank(); } - function _initHistory(address seq, uint64 upgradeHeight) internal { + function _setFirstSequencer(address seq) internal { vm.prank(owner); - l1Sequencer.initializeHistory(seq, upgradeHeight); + l1Sequencer.setFirstSequencer(seq); } } diff --git a/contracts/deploy/022-SequencerInit.ts b/contracts/deploy/022-SequencerInit.ts index d0ff33293..be82cd1e9 100644 --- a/contracts/deploy/022-SequencerInit.ts +++ b/contracts/deploy/022-SequencerInit.ts @@ -36,7 +36,7 @@ export const SequencerInit = async ( const owner = await deployer.getAddress() // Upgrade and initialize the proxy with owner only. - // Sequencer history is initialized separately via initializeHistory(). + // The first sequencer is registered separately via setFirstSequencer(address). await IL1SequencerProxy.upgradeToAndCall( L1SequencerImplAddress, L1SequencerFactory.interface.encodeFunctionData('initialize', [owner]) diff --git a/contracts/go.mod b/contracts/go.mod index d19dfca19..0c282fde0 100644 --- a/contracts/go.mod +++ b/contracts/go.mod @@ -2,7 +2,7 @@ module morph-l2/contract go 1.24.0 -replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3.8-0.20260612101929-7222401d6577 +replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3.8-0.20260617032026-015f9f543d32 replace github.com/morph-l2/go-ethereum => github.com/morph-l2/go-ethereum v0.0.0-20260608072528-fe02cc1f10bc diff --git a/node/cmd/node/main.go b/node/cmd/node/main.go index 80cc11702..ddf7f728c 100644 --- a/node/cmd/node/main.go +++ b/node/cmd/node/main.go @@ -19,6 +19,7 @@ import ( tmnode "github.com/tendermint/tendermint/node" "github.com/tendermint/tendermint/privval" tmsequencer "github.com/tendermint/tendermint/sequencer" + "github.com/tendermint/tendermint/upgrade" "github.com/urfave/cli" "morph-l2/bindings/bindings" @@ -73,6 +74,18 @@ func L2NodeMain(ctx *cli.Context) error { if err = nodeConfig.SetCliContext(ctx); err != nil { return err } + + // Wire the centralized-sequencer upgrade time into the consensus upgrade package before any + // consensus / blocksync routine runs. A user-provided --sequencerUpgradeTime wins. If it is + // absent, --mainnet / --hoodi selects the corresponding network default. With no network and no + // flag, leave the upgrade package's existing default untouched. + if upgradeTime, ok := selectSequencerUpgradeTime(ctx); ok { + upgrade.SetUpgradeBlockTime(upgradeTime) + } + nodeConfig.Logger.Info("centralized sequencer upgrade time set", + "network", sequencerUpgradeNetwork(ctx), + "upgradeTimeMs", upgrade.UpgradeBlockTime()) + home, err := homeDir(ctx) if err != nil { return err @@ -190,7 +203,7 @@ func L2NodeMain(ctx *cli.Context) error { if err != nil { return fmt.Errorf("NewRollup error: %v", err) } - dvNode, err = derivation.NewDerivationClient(context.Background(), derivationCfg, syncer, store, rollup, l1Client, tmNode, verifier, nodeConfig.Logger) + dvNode, err = derivation.NewDerivationClient(context.Background(), derivationCfg, syncer, store, rollup, l1Client, tmNode, nodeConfig.Logger) if err != nil { return fmt.Errorf("new derivation client error: %v", err) } @@ -285,6 +298,29 @@ func initHAService(ctx *cli.Context, home string, logger tmlog.Logger) (*hakeepe return hakeeper.New(cfg, logger.With("module", "hakeeper")) } +func selectSequencerUpgradeTime(ctx *cli.Context) (int64, bool) { + if ctx.GlobalIsSet(flags.SequencerUpgradeTime.Name) { + return ctx.GlobalInt64(flags.SequencerUpgradeTime.Name), true + } + if ctx.GlobalBool(flags.MainnetFlag.Name) { + return types.MainnetCentralizedSequencerUpgradeTime, true + } + if ctx.GlobalBool(flags.HoodiFlag.Name) { + return types.HoodiCentralizedSequencerUpgradeTime, true + } + return 0, false +} + +func sequencerUpgradeNetwork(ctx *cli.Context) string { + if ctx.GlobalBool(flags.MainnetFlag.Name) { + return "mainnet" + } + if ctx.GlobalBool(flags.HoodiFlag.Name) { + return "hoodi" + } + return "dev" +} + // initL1SequencerComponents initializes all L1 sequencer related components: // - L1Tracker: monitors L1 sync status // - SequencerCache: caches L1 sequencer address (nil if contract not configured) diff --git a/node/derivation/derivation.go b/node/derivation/derivation.go index f7e8ef2a2..19b524bb7 100644 --- a/node/derivation/derivation.go +++ b/node/derivation/derivation.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "math/big" - "morph-l2/node/l1sequencer" "time" "github.com/morph-l2/go-ethereum" @@ -23,6 +22,7 @@ import ( "github.com/morph-l2/go-ethereum/rpc" tmlog "github.com/tendermint/tendermint/libs/log" tmnode "github.com/tendermint/tendermint/node" + "github.com/tendermint/tendermint/upgrade" "morph-l2/bindings/bindings" "morph-l2/bindings/predeploys" @@ -75,8 +75,6 @@ type Derivation struct { tagAdvancer *tagAdvancer - l1SequencerVerifier *l1sequencer.SequencerVerifier - stop chan struct{} } @@ -90,7 +88,7 @@ type DeployContractBackend interface { // NewDerivationClient takes a shared l1Client owned by main.go. See // sync.NewSyncer for rationale — every L1-touching component in this // process shares one connection pool / retry / metrics surface. -func NewDerivationClient(ctx context.Context, cfg *Config, syncer *sync.Syncer, db Database, rollup *bindings.Rollup, l1Client *ethclient.Client, node *tmnode.Node, verifier *l1sequencer.SequencerVerifier, logger tmlog.Logger) (*Derivation, error) { +func NewDerivationClient(ctx context.Context, cfg *Config, syncer *sync.Syncer, db Database, rollup *bindings.Rollup, l1Client *ethclient.Client, node *tmnode.Node, logger tmlog.Logger) (*Derivation, error) { if l1Client == nil { return nil, errors.New("l1Client cannot be nil") } @@ -159,7 +157,6 @@ func NewDerivationClient(ctx context.Context, cfg *Config, syncer *sync.Syncer, metrics: metrics, l1BeaconClient: l1BeaconClient, L2ToL1MessagePasser: msgPasser, - l1SequencerVerifier: verifier, } // First-run startHeight default: when DB has no derivation cursor and no @@ -342,16 +339,17 @@ func (d *Derivation) derivationBlock(ctx context.Context) { // at the node layer, so reconstructing them via L1 fill-gap derive // would diverge geth's height from the node's on restart. When such // a block is missing locally we wait for P2P/blocksync to backfill - // instead of deriving. VerificationStartHeight() returns MaxUint64 - // while the verifier history is still empty, so until it loads every - // missing block is treated as pre-upgrade and we wait -- the safe - // default before the upgrade height is known. - if batchInfo.firstBlockNumber < d.l1SequencerVerifier.VerificationStartHeight() { - d.logger.Info("local verify: batch firstBlockNumber below verificationStartHeight (pre-upgrade); "+ + // instead of deriving. The upgrade boundary is timestamp-driven and + // persisted in the upgrade package: while it is not yet known + // (UpgradeBlockHeight < 0) IsUpgraded() returns false for every + // height, so every missing block is treated as pre-upgrade and we + // wait -- the safe default before the boundary is known. + if !upgrade.IsUpgraded(int64(batchInfo.firstBlockNumber)) { + d.logger.Info("local verify: batch firstBlockNumber is pre-upgrade; "+ "waiting for P2P backfill instead of L1 derivation", "batchIndex", batchInfo.batchIndex, "firstBlockNumber", batchInfo.firstBlockNumber, - "verificationStartHeight", d.l1SequencerVerifier.VerificationStartHeight()) + "upgradeBlockHeight", upgrade.UpgradeBlockHeight()) return } if l2Grew { diff --git a/node/flags/flags.go b/node/flags/flags.go index 488295338..3747bf6ee 100644 --- a/node/flags/flags.go +++ b/node/flags/flags.go @@ -200,6 +200,16 @@ var ( EnvVar: prefixEnvVar("L1_SEQUENCER_CONTRACT"), } + // SequencerUpgradeTime overrides the PBFT->single-sequencer upgrade timestamp. + // Unit: Unix milliseconds (matches block.Time.UnixMilli()). If unset, --mainnet / --hoodi + // selects the corresponding built-in network default; without a network flag, the upgrade + // package's existing default is left unchanged. + SequencerUpgradeTime = cli.Int64Flag{ + Name: "sequencerUpgradeTime", + Usage: "Unix timestamp (milliseconds) at which consensus switches to sequencer mode", + EnvVar: prefixEnvVar("SEQUENCER_UPGRADE_TIME"), + } + L1SyncLagThreshold = cli.DurationFlag{ Name: "l1.syncLagThreshold", Usage: "L1 sync lag threshold for warning logs", @@ -374,6 +384,7 @@ var Flags = []cli.Flag{ // L1 Sequencer options L1SequencerContractAddr, L1SyncLagThreshold, + SequencerUpgradeTime, SequencerPrivateKey, SequencerEnclaveSignerAddr, SequencerHAEnabled, diff --git a/node/go.mod b/node/go.mod index a16557b82..6c1ade3f6 100644 --- a/node/go.mod +++ b/node/go.mod @@ -2,7 +2,7 @@ module morph-l2/node go 1.24.0 -replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3.8-0.20260612101929-7222401d6577 +replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3.8-0.20260617032026-015f9f543d32 replace github.com/morph-l2/go-ethereum => github.com/morph-l2/go-ethereum v0.0.0-20260608072528-fe02cc1f10bc diff --git a/node/go.sum b/node/go.sum index 315995fb6..4b00502c7 100644 --- a/node/go.sum +++ b/node/go.sum @@ -416,8 +416,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/morph-l2/go-ethereum v0.0.0-20260608072528-fe02cc1f10bc h1:2Umr8WRDBKwCgGrQQ8yCdhn71bCuMJuecId2ClK80DU= github.com/morph-l2/go-ethereum v0.0.0-20260608072528-fe02cc1f10bc/go.mod h1:nkVzHjQWCOjvukQW8ittlwX+Xz9gmVHrP7mUi7zoHTs= -github.com/morph-l2/tendermint v0.3.8-0.20260612101929-7222401d6577 h1:u1F8xG9X23TIE/zYeVMOY1BiHPGT9pcfpIoMz2kyrJY= -github.com/morph-l2/tendermint v0.3.8-0.20260612101929-7222401d6577/go.mod h1:qpiwqfcCB89dBYfqVJOc/HjGxDp3OdDlthgttJJYyRs= +github.com/morph-l2/tendermint v0.3.8-0.20260617032026-015f9f543d32 h1:yx7ekS6vSf3Jx/N4iMG4aO95BqNTcG8W9qsirTHev/4= +github.com/morph-l2/tendermint v0.3.8-0.20260617032026-015f9f543d32/go.mod h1:qpiwqfcCB89dBYfqVJOc/HjGxDp3OdDlthgttJJYyRs= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= diff --git a/node/l1sequencer/verifier.go b/node/l1sequencer/verifier.go index a5022fb61..3f9f76877 100644 --- a/node/l1sequencer/verifier.go +++ b/node/l1sequencer/verifier.go @@ -13,7 +13,6 @@ import ( "github.com/morph-l2/go-ethereum/common" "github.com/morph-l2/go-ethereum/rpc" tmlog "github.com/tendermint/tendermint/libs/log" - "github.com/tendermint/tendermint/upgrade" "morph-l2/bindings/bindings" ) @@ -44,18 +43,16 @@ type SequencerVerifier struct { } // NewSequencerVerifier creates a new SequencerVerifier, loads the full sequencer -// history from L1 (finalized), and starts a background refresh goroutine. +// identity history from L1 (finalized), and starts a background refresh goroutine. // Call Stop to terminate the background loop. // -// Startup is fail-fast: if the initial syncHistory() fails or returns empty -// history, the global upgrade.UpgradeBlockHeight is never set and stays at its -// -1 sentinel. Running with UpgradeBlockHeight=-1 is unsafe: IsUpgraded() -// returns false for every height, so the PBFT state machine can run past the -// true upgrade height, and on a block-synced fullnode restart the handshake's -// sequencer-mode replay exemption fails (ErrAppBlockHeightTooHigh) while -// reconstructLastCommit runs against a nil commit and panics. We therefore -// refuse to start rather than boot into that state; the operator's supervisor -// restarts us once L1 is reachable. +// This verifier answers "who is the sequencer at L2 height N" for V2 block +// signature verification; it does NOT decide the PBFT → single-sequencer upgrade +// boundary (that is timestamp-driven and persisted in the upgrade package). +// +// Startup is fail-fast: if the initial syncHistory() fails, we refuse to start +// rather than run without a sequencer identity table — IsSequencerAt would then +// reject every V2 block. The operator's supervisor restarts us once L1 is reachable. func NewSequencerVerifier(caller *bindings.L1SequencerCaller, logger tmlog.Logger) (*SequencerVerifier, error) { ctx, cancel := context.WithCancel(context.Background()) v := &SequencerVerifier{ @@ -65,12 +62,8 @@ func NewSequencerVerifier(caller *bindings.L1SequencerCaller, logger tmlog.Logge } if err := v.syncHistory(); err != nil { cancel() - return nil, fmt.Errorf("refusing to start with UpgradeBlockHeight=-1: initial sequencer history sync from L1 failed: %w", err) + return nil, fmt.Errorf("refusing to start: initial sequencer history sync from L1 failed: %w", err) } - //if upgrade.UpgradeBlockHeight < 0 { - // cancel() - // return nil, fmt.Errorf("refusing to start with UpgradeBlockHeight=-1: L1 returned empty sequencer history; upgrade height unknown") - //} v.logCurrentState() go v.refreshLoop(ctx) return v, nil @@ -89,7 +82,6 @@ func (c *SequencerVerifier) logCurrentState() { current := c.history[len(c.history)-1] c.logger.Info("Sequencer contract state loaded", "totalRecords", len(c.history), - "verificationStartHeight", c.history[0].StartL2Block, "currentSequencer", current.SequencerAddr.Hex(), "currentSequencerStartHeight", current.StartL2Block) } @@ -138,12 +130,6 @@ func (c *SequencerVerifier) syncHistory() error { "startL2Block", c.history[i].StartL2Block, "address", c.history[i].SequencerAddr.Hex()) } - // Set upgrade height from L1 contract on first successful load - if prev == 0 && len(c.history) > 0 { - height := int64(c.history[0].StartL2Block) - upgrade.SetUpgradeBlockHeight(height) - c.logger.Info("Upgrade height set from L1 contract", "height", height) - } c.logger.Info("Sequencer history synced", "total", len(c.history), "new", len(c.history)-prev) return nil @@ -236,14 +222,3 @@ func (c *SequencerVerifier) IsSequencerAt(addr common.Address, l2Height uint64) } return addr == histAddr, nil } - -// VerificationStartHeight returns history[0].StartL2Block (= contract activeHeight). -// Returns math.MaxUint64 if history is empty. -func (c *SequencerVerifier) VerificationStartHeight() uint64 { - c.mu.Lock() - defer c.mu.Unlock() - if len(c.history) == 0 { - return math.MaxUint64 - } - return c.history[0].StartL2Block -} diff --git a/node/types/networks.go b/node/types/networks.go index 5d443f57a..39f88159e 100644 --- a/node/types/networks.go +++ b/node/types/networks.go @@ -17,3 +17,11 @@ var ( HoodiSyncDepositContractAddress = common.HexToAddress("0xd7f39d837f4790b215ba67e0ab63665912648dbe") HoodiL1SequencerContractAddress = common.HexToAddress("") ) + +// Network-specific default block timestamps (Unix milliseconds) at which consensus switches to +// centralized sequencer mode. They are used when --mainnet / --hoodi is selected and the generic +// sequencerUpgradeTime flag is unset. A value <= 0 disables timestamp-triggered upgrade. +var ( + MainnetCentralizedSequencerUpgradeTime int64 = 0 // ms + HoodiCentralizedSequencerUpgradeTime int64 = 0 // ms +) diff --git a/ops/docker-sequencer-test/docker-compose.ha-override.yml b/ops/docker-sequencer-test/docker-compose.ha-override.yml index 8bac491ee..d802c9102 100644 --- a/ops/docker-sequencer-test/docker-compose.ha-override.yml +++ b/ops/docker-sequencer-test/docker-compose.ha-override.yml @@ -10,10 +10,10 @@ version: '3.8' # up -d # # DESIGN: -# - PBFT phase (height 0 → UPGRADE_HEIGHT-1): node-0/1/2/3 run 4-node tendermint +# - PBFT phase (before the upgrade time): node-0/1/2/3 run 4-node tendermint # PBFT consensus exactly as in the baseline override. ha-node-0/1/2 join the # P2P network as V1 fullnodes (BlockSync only, no block production). -# - After UPGRADE_HEIGHT: V2 activates. Only ha-node-0/1/2 hold the sequencer +# - After the upgrade time (SEQUENCER_UPGRADE_TIME): V2 activates. Only ha-node-0/1/2 hold the sequencer # private key registered in the L1Sequencer contract, so they form a Raft # cluster (ha-node-0 bootstrap, ha-node-1/2 join) and produce blocks. # node-0/1/2/3 become V2 fullnodes (hasSigner=false). @@ -131,7 +131,8 @@ services: # Sequencer private key — only ha-node-0/1/2 hold this. # Rotated to new key for sequencer migration test (2026-05-11) # New address: 0xAb70B9eAF487d6cfFD3AD420785BE389D5B28390 - - MORPH_NODE_SEQUENCER_PRIVATE_KEY=0x4c60870f2eda2dbb0d8af6d33f3eb1fab4c0171387d784ff7423b4e529980f3c + - MORPH_NODE_SEQUENCER_PRIVATE_KEY=0xd99870855d97327d20c666abc78588f1449b1fac76ed0c86c1afb9ce2db85f32 + - MORPH_NODE_SEQUENCER_UPGRADE_TIME=${SEQUENCER_UPGRADE_TIME:-0} - MORPH_NODE_L2_ETH_RPC=http://ha-geth-0:8545 - MORPH_NODE_L2_ENGINE_RPC=http://ha-geth-0:8551 - MORPH_NODE_L2_ENGINE_AUTH=${JWT_SECRET_PATH} @@ -174,7 +175,8 @@ services: environment: # Rotated to new key for sequencer migration test (2026-05-11) # New address: 0xAb70B9eAF487d6cfFD3AD420785BE389D5B28390 - - MORPH_NODE_SEQUENCER_PRIVATE_KEY=0x4c60870f2eda2dbb0d8af6d33f3eb1fab4c0171387d784ff7423b4e529980f3c + - MORPH_NODE_SEQUENCER_PRIVATE_KEY=0xd99870855d97327d20c666abc78588f1449b1fac76ed0c86c1afb9ce2db85f32 + - MORPH_NODE_SEQUENCER_UPGRADE_TIME=${SEQUENCER_UPGRADE_TIME:-0} - MORPH_NODE_L2_ETH_RPC=http://ha-geth-1:8545 - MORPH_NODE_L2_ENGINE_RPC=http://ha-geth-1:8551 - MORPH_NODE_L2_ENGINE_AUTH=${JWT_SECRET_PATH} @@ -216,7 +218,8 @@ services: environment: # Rotated to new key for sequencer migration test (2026-05-11) # New address: 0xAb70B9eAF487d6cfFD3AD420785BE389D5B28390 - - MORPH_NODE_SEQUENCER_PRIVATE_KEY=0x4c60870f2eda2dbb0d8af6d33f3eb1fab4c0171387d784ff7423b4e529980f3c + - MORPH_NODE_SEQUENCER_PRIVATE_KEY=0xd99870855d97327d20c666abc78588f1449b1fac76ed0c86c1afb9ce2db85f32 + - MORPH_NODE_SEQUENCER_UPGRADE_TIME=${SEQUENCER_UPGRADE_TIME:-0} - MORPH_NODE_L2_ETH_RPC=http://ha-geth-2:8545 - MORPH_NODE_L2_ENGINE_RPC=http://ha-geth-2:8551 - MORPH_NODE_L2_ENGINE_AUTH=${JWT_SECRET_PATH} diff --git a/ops/docker-sequencer-test/docker-compose.override.yml b/ops/docker-sequencer-test/docker-compose.override.yml index 81f714791..b465dc18f 100644 --- a/ops/docker-sequencer-test/docker-compose.override.yml +++ b/ops/docker-sequencer-test/docker-compose.override.yml @@ -24,20 +24,25 @@ services: context: ../.. dockerfile: ops/docker-sequencer-test/Dockerfile.l2-node-test environment: - # Sequencer PK intentionally NOT set on node-0 in isolated-HA-cluster test design. - # After upgrade, node-0 should become a V2 fullnode (hasSigner=false). The sequencer - # private key lives ONLY on ha-node-0/1/2 (see docker-compose.ha-override.yml). + # Sequencer PK intentionally NOT set on node-0: after upgrade node-0 becomes a V2 + # fullnode (hasSigner=false). The single sequencer is node-1 (see below). - MORPH_NODE_L1_SEQUENCER_CONTRACT=${L1_SEQUENCER_CONTRACT} - MORPH_NODE_ROLLUP_ADDRESS=${MORPH_ROLLUP:-0x6900000000000000000000000000000000000010} + # Timestamp-driven PBFT->sequencer upgrade (Unix ms). Same value on every node so all + # agree on the boundary. Computed at start time (now+offset) in run-test.sh start_l2_test. + - MORPH_NODE_SEQUENCER_UPGRADE_TIME=${SEQUENCER_UPGRADE_TIME:-0} node-1: image: morph-node-test:latest environment: - # - MORPH_NODE_SEQUENCER_PRIVATE_KEY=0x0890c388c3bf5e04fee1d8f3c117e5f44f435ced7baf7bfd66c10e1f3a3f4b10 + # node-1 holds the sequencer signing key -> becomes the single sequencer after upgrade. + # Its address must match what setFirstSequencer registered (HA_SEQUENCER_ADDR in run-test.sh). + - MORPH_NODE_SEQUENCER_PRIVATE_KEY=${SEQUENCER_PRIVATE_KEY} - MORPH_NODE_L1_SEQUENCER_CONTRACT=${L1_SEQUENCER_CONTRACT} - MORPH_NODE_ROLLUP_ADDRESS=${MORPH_ROLLUP:-0x6900000000000000000000000000000000000010} - MORPH_NODE_SYNC_DEPOSIT_CONTRACT_ADDRESS=${MORPH_PORTAL:-0x6900000000000000000000000000000000000001} + - MORPH_NODE_SEQUENCER_UPGRADE_TIME=${SEQUENCER_UPGRADE_TIME:-0} node-2: @@ -46,6 +51,7 @@ services: - MORPH_NODE_L1_SEQUENCER_CONTRACT=${L1_SEQUENCER_CONTRACT} - MORPH_NODE_ROLLUP_ADDRESS=${MORPH_ROLLUP:-0x6900000000000000000000000000000000000010} - MORPH_NODE_SYNC_DEPOSIT_CONTRACT_ADDRESS=${MORPH_PORTAL:-0x6900000000000000000000000000000000000001} + - MORPH_NODE_SEQUENCER_UPGRADE_TIME=${SEQUENCER_UPGRADE_TIME:-0} node-3: @@ -54,6 +60,7 @@ services: - MORPH_NODE_L1_SEQUENCER_CONTRACT=${L1_SEQUENCER_CONTRACT} - MORPH_NODE_ROLLUP_ADDRESS=${MORPH_ROLLUP:-0x6900000000000000000000000000000000000010} - MORPH_NODE_SYNC_DEPOSIT_CONTRACT_ADDRESS=${MORPH_PORTAL:-0x6900000000000000000000000000000000000001} + - MORPH_NODE_SEQUENCER_UPGRADE_TIME=${SEQUENCER_UPGRADE_TIME:-0} sentry-el-0: @@ -65,6 +72,7 @@ services: - MORPH_NODE_ROLLUP_ADDRESS=${MORPH_ROLLUP:-0x6900000000000000000000000000000000000010} - MORPH_NODE_SYNC_DEPOSIT_CONTRACT_ADDRESS=${MORPH_PORTAL:-0x6900000000000000000000000000000000000001} - MORPH_NODE_L1_SEQUENCER_CONTRACT=${L1_SEQUENCER_CONTRACT} + - MORPH_NODE_SEQUENCER_UPGRADE_TIME=${SEQUENCER_UPGRADE_TIME:-0} # ========== Malicious Node (P2P security test) ========== # Uses morph-node-malicious:latest built from test/p2p-security branch. @@ -107,4 +115,3 @@ services: volumes: malicious_geth_data: - diff --git a/ops/docker-sequencer-test/run-ha-test.sh b/ops/docker-sequencer-test/run-ha-test.sh index 6a75c48e2..9547fcc42 100755 --- a/ops/docker-sequencer-test/run-ha-test.sh +++ b/ops/docker-sequencer-test/run-ha-test.sh @@ -22,7 +22,8 @@ # failover - Run failover tests only (cluster must be running) # # Environment Variables: -# UPGRADE_HEIGHT - Block height for consensus switch (default: 20) +# UPGRADE_OFFSET_S - Seconds after cluster start to fire the timestamp upgrade (default: 120) +# UPGRADE_WAIT_S - Max seconds to wait for the upgrade to actually fire (default: 240) # HA_FORM_WAIT - Seconds to wait for Raft cluster formation (default: 30) # REPORT_OUTPUT - Where to write test report (default: docs/ha/ha-test-report.md) @@ -36,10 +37,19 @@ DOCKER_DIR="$OPS_DIR/docker" DOCS_DIR="$BITGET_ROOT/docs/ha" # ─── Configuration ──────────────────────────────────────────────────────────── -UPGRADE_HEIGHT=${UPGRADE_HEIGHT:-20} -HA_FORM_WAIT=${HA_FORM_WAIT:-30} # seconds after upgrade to wait for cluster formation +# Upgrade is timestamp-driven (not height). UPGRADE_OFFSET_S sets how many seconds after the +# cluster starts the PBFT->sequencer switch fires; start_ha_cluster turns it into an absolute +# SEQUENCER_UPGRADE_TIME (Unix ms) injected into every node. A pre-set SEQUENCER_UPGRADE_TIME +# (absolute ms) takes precedence over the offset. +UPGRADE_OFFSET_S=${UPGRADE_OFFSET_S:-120} +UPGRADE_WAIT_S=${UPGRADE_WAIT_S:-240} # max seconds to wait for the upgrade to actually fire +HA_FORM_WAIT=${HA_FORM_WAIT:-30} # seconds after upgrade to wait for cluster formation REPORT_OUTPUT="${REPORT_OUTPUT:-$DOCS_DIR/ha-test-report.md}" +# Absolute upgrade timestamp (ms) persisted by start_ha_cluster and reused by the `test` phase so +# recreating ha-node-* keeps the same boundary instead of falling back to 0 (= upgrade disabled). +UPGRADE_TIME_FILE="$DOCKER_DIR/.devnet/.sequencer_upgrade_time_ms" + # L2 Geth RPC endpoints for the PBFT nodes (non-HA, pre-upgrade consensus) L2_RPC_NODE0="http://127.0.0.1:8545" L2_RPC_NODE1="http://127.0.0.1:8645" @@ -157,6 +167,62 @@ wait_for_block() { echo "" } +# ─── Upgrade-time Helpers (timestamp-driven upgrade) ────────────────────────── + +# Persist the absolute upgrade timestamp (ms) so a later, separate invocation (e.g. `test`) +# recreates containers with the SAME boundary instead of the compose default of 0 (= disabled). +persist_upgrade_time() { + mkdir -p "$(dirname "$UPGRADE_TIME_FILE")" + echo "${SEQUENCER_UPGRADE_TIME:-0}" > "$UPGRADE_TIME_FILE" +} + +# Resolve SEQUENCER_UPGRADE_TIME and export it so every subsequent `docker compose up` recreates +# nodes with the correct upgrade boundary. Resolution order: +# 1. already-set env (>0) 2. persisted file 3. value baked into running ha-node-0 +load_upgrade_time() { + if [ "${SEQUENCER_UPGRADE_TIME:-0}" -gt 0 ] 2>/dev/null; then + export SEQUENCER_UPGRADE_TIME + return 0 + fi + if [ -f "$UPGRADE_TIME_FILE" ]; then + SEQUENCER_UPGRADE_TIME=$(cat "$UPGRADE_TIME_FILE" 2>/dev/null | tr -dc '0-9') + fi + if [ "${SEQUENCER_UPGRADE_TIME:-0}" -le 0 ] 2>/dev/null; then + SEQUENCER_UPGRADE_TIME=$(docker inspect ha-node-0 \ + --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null \ + | grep '^MORPH_NODE_SEQUENCER_UPGRADE_TIME=' | cut -d= -f2 | tr -dc '0-9' || true) + fi + if [ "${SEQUENCER_UPGRADE_TIME:-0}" -gt 0 ] 2>/dev/null; then + export SEQUENCER_UPGRADE_TIME + log_info "Using SEQUENCER_UPGRADE_TIME=$SEQUENCER_UPGRADE_TIME ms" + else + export SEQUENCER_UPGRADE_TIME=0 + log_warn "SEQUENCER_UPGRADE_TIME unknown — run 'start' first, else container recreation disables the upgrade." + fi +} + +# Wait until the PBFT->sequencer upgrade actually fires, detected via the consensus log on the +# always-present PBFT nodes (node-0..3). Fully decoupled from block height. +wait_for_upgrade() { + local max_wait="${UPGRADE_WAIT_S:-240}" + local waited=0 + cd "$DOCKER_DIR" + log_info "Waiting for timestamp upgrade to fire (max ${max_wait}s)..." + while [ "$waited" -lt "$max_wait" ]; do + if $COMPOSE_HA logs --tail=3000 node-0 node-1 node-2 node-3 2>/dev/null \ + | grep -q "switching to sequencer mode"; then + log_success "Upgrade fired (timestamp boundary crossed) after ~${waited}s" + return 0 + fi + echo -ne "\r waiting for upgrade... ${waited}s/${max_wait}s" + sleep 5 + waited=$((waited + 5)) + done + echo "" + log_error "Upgrade did not fire within ${max_wait}s" + return 1 +} + # ─── HA-Specific Helpers ────────────────────────────────────────────────────── # Call a hakeeper JSON-RPC method @@ -426,6 +492,18 @@ start_ha_cluster() { # - node-0/1/2/3: PBFT validators (baseline), no HA config. # - ha-node-0 bootstrap, ha-node-1/2 join — isolated Raft cluster. # - sentry-node-0: non-HA V2 fullnode after upgrade. + # Timestamp-driven upgrade (block.Time ~= wall clock): fire the PBFT->sequencer switch + # UPGRADE_OFFSET_S seconds after start. The absolute timestamp is injected into every node via + # the overrides and persisted so the `test` phase reuses the identical boundary. + if [ "${SEQUENCER_UPGRADE_TIME:-0}" -gt 0 ] 2>/dev/null; then + export SEQUENCER_UPGRADE_TIME + log_info "Using pre-set SEQUENCER_UPGRADE_TIME=$SEQUENCER_UPGRADE_TIME ms" + else + export SEQUENCER_UPGRADE_TIME=$(( ($(date +%s) + UPGRADE_OFFSET_S) * 1000 )) + log_info "Sequencer upgrade time set to $SEQUENCER_UPGRADE_TIME ms (now + ${UPGRADE_OFFSET_S}s)" + fi + persist_upgrade_time + log_info "Starting tendermint nodes (node-0..3 PBFT, ha-node-0 bootstrap, ha-node-1/2 join)..." $COMPOSE_HA up -d node-0 node-1 node-2 node-3 ha-node-0 ha-node-1 ha-node-2 sentry-node-0 @@ -440,9 +518,8 @@ start_ha_cluster() { run_config_tests() { log_section "Category 1: 配置验证 (Config Tests)" - # Wait for upgrade height + HA formation before running config tests - log_info "Waiting for upgrade height ($UPGRADE_HEIGHT)..." - wait_for_block "$UPGRADE_HEIGHT" "$L2_RPC_NODE0" + # Wait for the timestamp upgrade to actually fire + HA formation before running config tests. + wait_for_upgrade || log_warn "Proceeding despite upgrade-detection timeout" log_info "Waiting ${HA_FORM_WAIT}s for Raft cluster to form..." sleep "$HA_FORM_WAIT" @@ -633,14 +710,12 @@ run_cluster_tests() { run_block_tests() { log_section "Category 3: 出块验证 (Block Production Tests)" - # Ensure we are past upgrade height with blocks flowing + # Ensure blocks are flowing post-upgrade (a handful of fresh blocks past the current head). local current current=$(get_block_number "$L2_RPC_NODE0") - local target=$((UPGRADE_HEIGHT + 15)) - if [ "$current" -lt "$target" ]; then - log_info "Waiting for block $target (current: $current)..." - wait_for_block "$target" "$L2_RPC_NODE0" - fi + local target=$((current + 5)) + log_info "Waiting for a few post-upgrade blocks (target $target, current: $current)..." + wait_for_block "$target" "$L2_RPC_NODE0" local leader_rpc leader_rpc=$(find_leader_rpc) @@ -1320,7 +1395,7 @@ generate_report() { echo "# Sequencer HA V2 集成测试报告" echo "" echo "> 生成时间: $timestamp" - echo "> 升级高度: $UPGRADE_HEIGHT" + echo "> 升级时间(ms): ${SEQUENCER_UPGRADE_TIME:-unset}" echo "> 环境: docker-sequencer-test (3节点 Raft HA 集群)" echo "" echo "---" @@ -1524,7 +1599,11 @@ print_summary() { run_full_ha_test() { log_section "Sequencer HA V2 Integration Test" - log_info "UPGRADE_HEIGHT=$UPGRADE_HEIGHT HA_FORM_WAIT=${HA_FORM_WAIT}s" + + # Recover the upgrade timestamp set by `start` so the cluster reset below recreates ha-node-* + # with the SAME boundary (an unset value resolves to 0 = upgrade disabled). + load_upgrade_time + log_info "SEQUENCER_UPGRADE_TIME=${SEQUENCER_UPGRADE_TIME:-unset} HA_FORM_WAIT=${HA_FORM_WAIT}s" # Reset HA cluster (ha-node-0/1/2) for clean state — makes the test idempotent. log_info "Resetting isolated HA cluster for clean test state..." @@ -1546,7 +1625,7 @@ run_full_ha_test() { # Init report mkdir -p "$DOCS_DIR" REPORT_LINES=() - REPORT_LINES+=("## Environment\n\n- Upgrade Height: $UPGRADE_HEIGHT\n- HA Form Wait: ${HA_FORM_WAIT}s\n- PBFT nodes (pre-upgrade validators, post-upgrade V2 fullnodes): node-0/1/2/3\n- Isolated HA cluster (post-upgrade sequencer): ha-node-0 (bootstrap), ha-node-1 (join), ha-node-2 (join)\n- sentry-node-0: non-HA V2 fullnode\n\n---\n") + REPORT_LINES+=("## Environment\n\n- Upgrade Time (ms): ${SEQUENCER_UPGRADE_TIME:-unset}\n- HA Form Wait: ${HA_FORM_WAIT}s\n- PBFT nodes (pre-upgrade validators, post-upgrade V2 fullnodes): node-0/1/2/3\n- Isolated HA cluster (post-upgrade sequencer): ha-node-0 (bootstrap), ha-node-1 (join), ha-node-2 (join)\n- sentry-node-0: non-HA V2 fullnode\n\n---\n") run_config_tests run_cluster_tests @@ -1603,7 +1682,7 @@ case "${1:-}" in ;; setup) log_info "Setting up devnet (delegating to run-test.sh)..." - UPGRADE_HEIGHT=$UPGRADE_HEIGHT "$SCRIPT_DIR/run-test.sh" setup + "$SCRIPT_DIR/run-test.sh" setup ;; start) start_ha_cluster @@ -1665,12 +1744,13 @@ Commands: failover Run failover tests only (cluster must be running) Environment Variables: - UPGRADE_HEIGHT Block height for V2 mode switch (default: 20) + UPGRADE_OFFSET_S Seconds after start to fire the timestamp upgrade (default: 120) + UPGRADE_WAIT_S Max seconds to wait for the upgrade to fire (default: 240) HA_FORM_WAIT Seconds to wait for Raft cluster formation (default: 30) REPORT_OUTPUT Path for test report markdown file Node Roles: - node-0/1/2/3 PBFT validators (pre-upgrade). After UPGRADE_HEIGHT they + node-0/1/2/3 PBFT validators (pre-upgrade). After the upgrade time they become V2 fullnodes (no sequencer key → hasSigner=false). ha-node-0 Isolated HA cluster: bootstrap leader candidate (MORPH_NODE_HA_BOOTSTRAP=true, SEQUENCER_PRIVATE_KEY set) @@ -1685,8 +1765,8 @@ Host Ports: Quick Start: ./run-ha-test.sh build - UPGRADE_HEIGHT=20 ./run-ha-test.sh setup - ./run-ha-test.sh start + ./run-ha-test.sh setup + UPGRADE_OFFSET_S=120 ./run-ha-test.sh start ./run-ha-test.sh test EOF ;; diff --git a/ops/docker-sequencer-test/run-test.sh b/ops/docker-sequencer-test/run-test.sh index 68484129f..11d668b20 100755 --- a/ops/docker-sequencer-test/run-test.sh +++ b/ops/docker-sequencer-test/run-test.sh @@ -24,7 +24,9 @@ log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } log_error() { echo -e "${RED}[ERROR]${NC} $1"; } # Configuration -UPGRADE_HEIGHT=${UPGRADE_HEIGHT:-10} +# Upgrade is timestamp-driven (not height): UPGRADE_OFFSET_S controls how many seconds after L2 +# start the PBFT->sequencer switch fires (see start_l2_test, default 120s). +UPGRADE_OFFSET_S=${UPGRADE_OFFSET_S:-120} L2_RPC="http://127.0.0.1:8545" L2_RPC_NODE1="http://127.0.0.1:8645" @@ -121,7 +123,7 @@ setup_devnet() { log_info "Running devnet setup..." cd "$MORPH_ROOT" - # Note: upgrade height should already be set before build_test_images + # Note: the timestamp upgrade (SEQUENCER_UPGRADE_TIME) is configured at start, not at setup. # Step 1: Start L1 and setup tendermint nodes # Note: main.py calls setup_devnet_nodes() before devnet.main() @@ -204,29 +206,30 @@ for i in range(4): '--private-key', deploy_config['l2StakingPks'][i] ]) -# Initialize L1Sequencer history for V2 mode -# Register the first sequencer (node-0's staking address) at upgrade height +# Initialize L1Sequencer for V2 mode +# Register the first sequencer (node-0's staking address). The sequencer is +# active from L2 block 0; the PBFT->single-sequencer switch is triggered by +# block timestamp (network-specific sequencerUpgradeTime flag), not by an on-chain height. l1_sequencer_addr = addresses.get('Proxy__L1Sequencer', '') if l1_sequencer_addr: - upgrade_height = os.environ.get('UPGRADE_HEIGHT', '10') # Override for whitelist integration test: register the HA cluster's # signer key (0xAb70...) so that ha-node-X can produce blocks after the # PBFT->HA upgrade. Without this, isSequencerAt() always returns false # for the HA leader and no blocks are produced. sequencer_addr = os.environ.get('HA_SEQUENCER_ADDR', deploy_config['l2StakingAddresses'][0]) deployer_pk = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' - log.info(f'Initializing L1Sequencer history: sequencer={sequencer_addr}, startL2Block={upgrade_height}') + log.info(f'Setting first L1Sequencer: sequencer={sequencer_addr} (active from block 0)') try: run_command([ 'cast', 'send', l1_sequencer_addr, - 'initializeHistory(address,uint64)', - sequencer_addr, str(upgrade_height), + 'setFirstSequencer(address)', + sequencer_addr, '--rpc-url', 'http://127.0.0.1:9545', '--private-key', deployer_pk ]) - log.info('L1Sequencer history initialized successfully') + log.info('L1Sequencer first sequencer set successfully') except Exception as e: - log.info(f'L1Sequencer initializeHistory failed (may already be initialized): {e}') + log.info(f'L1Sequencer setFirstSequencer failed (may already be initialized): {e}') # Update .env file log.info('Updating .env file...') @@ -275,7 +278,7 @@ remove_override() { } # Wait for L1 finalized block to reach at least the given height. -# This ensures contract data (e.g., initializeHistory) is visible via +# This ensures contract data (e.g., setFirstSequencer) is visible via # the finalized block tag when L2 nodes start their verifier sync. wait_for_l1_finalized() { local min_block=${1:-1} @@ -320,7 +323,7 @@ start_l2_test() { # Wait for L1 to finalize past the contract deployment block. # The verifier reads history via finalized tag; if L1 hasn't finalized - # the initializeHistory tx yet, the initial sync will miss it. + # the setFirstSequencer tx yet, the initial sync will miss it. local l1_latest l1_latest=$(curl -s -X POST -H "Content-Type: application/json" \ --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \ @@ -344,6 +347,13 @@ start_l2_test() { sleep 5 + # Timestamp-driven upgrade: tendermint block.Time ~= wall clock, so set the upgrade time + # to (now + offset) ms. The chain runs PBFT for ~offset seconds, then the first block whose + # time crosses this value becomes the last PBFT block and node-1 takes over as sequencer. + local upgrade_offset_s=${UPGRADE_OFFSET_S:-120} + export SEQUENCER_UPGRADE_TIME=$(( ($(date +%s) + upgrade_offset_s) * 1000 )) + log_info "Sequencer upgrade time set to $SEQUENCER_UPGRADE_TIME ms (now + ${upgrade_offset_s}s)" + # Start L2 tendermint nodes log_info "Starting L2 tendermint nodes..." $COMPOSE_CMD up -d node-0 node-1 node-2 node-3 sentry-node-0 @@ -378,16 +388,33 @@ test_pbft_mode() { } test_upgrade() { - log_info "========== Phase 2: Waiting for Upgrade ==========" - log_info "Upgrade height: $UPGRADE_HEIGHT" - - wait_for_block $UPGRADE_HEIGHT + log_info "========== Phase 2: Waiting for timestamp-driven Upgrade ==========" + cd "$DOCKER_DIR" + + # Upgrade is triggered by block timestamp, not height: poll node logs for the consensus + # switch instead of waiting for a fixed height. + local max_wait=${UPGRADE_WAIT_S:-240} + local waited=0 + while [ $waited -lt $max_wait ]; do + if $COMPOSE_CMD logs --tail=3000 node-0 node-1 node-2 node-3 2>/dev/null \ + | grep -q "switching to sequencer mode"; then + log_success "Upgrade triggered (timestamp boundary crossed) after ${waited}s" + break + fi + echo -ne "\r waiting for upgrade... ${waited}s/${max_wait}s" + sleep 5 + waited=$((waited + 5)) + done + if [ $waited -ge $max_wait ]; then + log_error "Upgrade did not trigger within ${max_wait}s" + return 1 + fi + sleep 10 - - # Verify network continues + # Verify network continues producing after upgrade (V2 blocks from node-1) local post_upgrade=$(get_block_number) wait_for_block $((post_upgrade + 5)) - + log_success "Upgrade completed! Network continues producing blocks." } @@ -485,11 +512,15 @@ cleanup() { run_full_test() { log_info "==========================================" - log_info " Sequencer Upgrade Test" - log_info " Upgrade Height: $UPGRADE_HEIGHT" + log_info " Sequencer Upgrade Test (timestamp-driven)" log_info "==========================================" - + trap cleanup EXIT + + # Single sequencer after upgrade = node-1, signing with this key. setFirstSequencer must + # register this key's address so V2 blocks verify. 0xac09..ff80 -> 0xf39Fd6..2266 (anvil #0). + export SEQUENCER_PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" + export HA_SEQUENCER_ADDR="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" # Build test images build_test_images @@ -655,9 +686,12 @@ test_p2p_security() { local height height=$(get_block_number "$L2_RPC") - # Check 1: chain must be past upgrade height (read from L1 contract via verifier) - if [ "$height" -le "$UPGRADE_HEIGHT" ]; then - log_error "Chain height ($height) <= UPGRADE_HEIGHT ($UPGRADE_HEIGHT). V2 not active." + # Check 1: the timestamp-driven upgrade must have occurred (a node switched to sequencer mode). + # The boundary is decided by block timestamp, not a fixed height, so check the consensus log. + local upgraded + upgraded=$($COMPOSE_CMD logs node-0 node-1 node-2 node-3 2>/dev/null | grep -c "switching to sequencer mode" || true) + if [ "$upgraded" -lt 1 ]; then + log_error "Upgrade not triggered yet (no 'switching to sequencer mode' in logs). V2 not active." return 1 fi @@ -665,7 +699,7 @@ test_p2p_security() { local node0_v2 node0_v2=$($COMPOSE_CMD logs node-0 2>/dev/null | grep -c "StateV2 initialized.*hasSigner=true" || true) if [ "$node0_v2" -lt 1 ]; then - log_error "node-0 not in V2 mode with signer. Check SEQUENCER_PRIVATE_KEY and L1 initializeHistory." + log_error "node-0 not in V2 mode with signer. Check SEQUENCER_PRIVATE_KEY and L1 setFirstSequencer." return 1 fi @@ -673,11 +707,11 @@ test_p2p_security() { local sentry_v2 sentry_v2=$($COMPOSE_CMD logs sentry-node-0 2>/dev/null | grep -c "Starting block apply routine" || true) if [ "$sentry_v2" -lt 1 ]; then - log_error "sentry-node-0 not in V2 path. Check L1 contract initializeHistory." + log_error "sentry-node-0 not in V2 path. Check L1 contract setFirstSequencer." return 1 fi - log_info "Preconditions OK: height=$height, upgradeHeight=$UPGRADE_HEIGHT, V2 active" + log_info "Preconditions OK: height=$height, upgrade triggered, V2 active" local pass=0 local fail=0 @@ -919,7 +953,7 @@ case "${1:-}" in echo " status - Show current block numbers" echo "" echo "Environment Variables:" - echo " UPGRADE_HEIGHT - Block height for consensus switch (default: 10)" + echo " UPGRADE_OFFSET_S - Seconds after L2 start to trigger the timestamp upgrade (default: 120)" echo " TX_INTERVAL - Seconds between txs (default: 5)" echo " MALICIOUS_MODE - Attack mode for p2p-test (default: all)" echo "" @@ -931,6 +965,6 @@ case "${1:-}" in echo " 5. p2p-test - Run P2P security tests (requires build-malicious)" echo "" echo "Quick Start:" - echo " UPGRADE_HEIGHT=10 $0 test" + echo " UPGRADE_OFFSET_S=120 $0 test" ;; esac diff --git a/ops/l2-genesis/go.mod b/ops/l2-genesis/go.mod index 0b74f8370..cc1d14fd8 100644 --- a/ops/l2-genesis/go.mod +++ b/ops/l2-genesis/go.mod @@ -2,7 +2,7 @@ module morph-l2/morph-deployer go 1.24.0 -replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3.8-0.20260612101929-7222401d6577 +replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3.8-0.20260617032026-015f9f543d32 replace github.com/morph-l2/go-ethereum => github.com/morph-l2/go-ethereum v0.0.0-20260608072528-fe02cc1f10bc diff --git a/ops/tools/go.mod b/ops/tools/go.mod index 1c1546ea3..9cd471a59 100644 --- a/ops/tools/go.mod +++ b/ops/tools/go.mod @@ -2,7 +2,7 @@ module morph-l2/tools go 1.24.0 -replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3.8-0.20260612101929-7222401d6577 +replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3.8-0.20260617032026-015f9f543d32 replace github.com/morph-l2/go-ethereum => github.com/morph-l2/go-ethereum v0.0.0-20260608072528-fe02cc1f10bc diff --git a/ops/tools/go.sum b/ops/tools/go.sum index f5b0ba312..cbcedbfb3 100644 --- a/ops/tools/go.sum +++ b/ops/tools/go.sum @@ -163,8 +163,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/morph-l2/go-ethereum v0.0.0-20260608072528-fe02cc1f10bc h1:2Umr8WRDBKwCgGrQQ8yCdhn71bCuMJuecId2ClK80DU= github.com/morph-l2/go-ethereum v0.0.0-20260608072528-fe02cc1f10bc/go.mod h1:nkVzHjQWCOjvukQW8ittlwX+Xz9gmVHrP7mUi7zoHTs= -github.com/morph-l2/tendermint v0.3.8-0.20260612101929-7222401d6577 h1:u1F8xG9X23TIE/zYeVMOY1BiHPGT9pcfpIoMz2kyrJY= -github.com/morph-l2/tendermint v0.3.8-0.20260612101929-7222401d6577/go.mod h1:qpiwqfcCB89dBYfqVJOc/HjGxDp3OdDlthgttJJYyRs= +github.com/morph-l2/tendermint v0.3.8-0.20260617032026-015f9f543d32 h1:yx7ekS6vSf3Jx/N4iMG4aO95BqNTcG8W9qsirTHev/4= +github.com/morph-l2/tendermint v0.3.8-0.20260617032026-015f9f543d32/go.mod h1:qpiwqfcCB89dBYfqVJOc/HjGxDp3OdDlthgttJJYyRs= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= diff --git a/tx-submitter/go.mod b/tx-submitter/go.mod index 6f1fd8caa..0035ae2f2 100644 --- a/tx-submitter/go.mod +++ b/tx-submitter/go.mod @@ -2,7 +2,7 @@ module morph-l2/tx-submitter go 1.24.0 -replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3.8-0.20260612101929-7222401d6577 +replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3.8-0.20260617032026-015f9f543d32 replace github.com/morph-l2/go-ethereum => github.com/morph-l2/go-ethereum v0.0.0-20260608072528-fe02cc1f10bc From 862da6324185443d2791f1d0a0ff1ffd4bb13b9e Mon Sep 17 00:00:00 2001 From: "allen.wu" Date: Wed, 17 Jun 2026 15:27:03 +0800 Subject: [PATCH 2/3] build(deps): bump tendermint fork to 29056623 Update the github.com/tendermint/tendermint replace target across all modules to morph-l2/tendermint v0.3.8-0.20260617072029-29056623cdb0. That revision fixes BlockV2.GetTime() to return the block time in milliseconds (multiplying the geth second timestamp by 1000), matching Block.GetTime() and the millisecond upgrade-time comparison. Co-Authored-By: Claude Opus 4.8 (1M context) --- bindings/go.mod | 2 +- common/go.mod | 2 +- contracts/go.mod | 2 +- go.work.sum | 2 ++ node/go.mod | 2 +- node/go.sum | 4 ++-- ops/l2-genesis/go.mod | 2 +- ops/tools/go.mod | 2 +- ops/tools/go.sum | 4 ++-- tx-submitter/go.mod | 2 +- 10 files changed, 13 insertions(+), 11 deletions(-) diff --git a/bindings/go.mod b/bindings/go.mod index 7b635908d..67d4cd834 100644 --- a/bindings/go.mod +++ b/bindings/go.mod @@ -2,7 +2,7 @@ module morph-l2/bindings go 1.24.0 -replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3.8-0.20260617032026-015f9f543d32 +replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3.8-0.20260617072029-29056623cdb0 replace github.com/morph-l2/go-ethereum => github.com/morph-l2/go-ethereum v0.0.0-20260608072528-fe02cc1f10bc diff --git a/common/go.mod b/common/go.mod index 0c4101594..5e8fdceb8 100644 --- a/common/go.mod +++ b/common/go.mod @@ -2,7 +2,7 @@ module morph-l2/common go 1.24.0 -replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3.8-0.20260617032026-015f9f543d32 +replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3.8-0.20260617072029-29056623cdb0 replace github.com/morph-l2/go-ethereum => github.com/morph-l2/go-ethereum v0.0.0-20260608072528-fe02cc1f10bc diff --git a/contracts/go.mod b/contracts/go.mod index 0c282fde0..04e49e149 100644 --- a/contracts/go.mod +++ b/contracts/go.mod @@ -2,7 +2,7 @@ module morph-l2/contract go 1.24.0 -replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3.8-0.20260617032026-015f9f543d32 +replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3.8-0.20260617072029-29056623cdb0 replace github.com/morph-l2/go-ethereum => github.com/morph-l2/go-ethereum v0.0.0-20260608072528-fe02cc1f10bc diff --git a/go.work.sum b/go.work.sum index 5528b5251..c20d48873 100644 --- a/go.work.sum +++ b/go.work.sum @@ -993,6 +993,8 @@ github.com/morph-l2/go-ethereum v0.0.0-20260529141627-eb5fbf8f9748/go.mod h1:nkV github.com/morph-l2/go-ethereum v1.10.14-0.20251125061742-69718a9dcab9/go.mod h1:tiFPeidxjoCmLj18ne9H3KQdIGTCvRC30qlef06Fd9M= github.com/morph-l2/tendermint v0.0.0-20260529095305-b1b3a3a1d806 h1:gJmofzJ0PnCCObETUH02AsSVS1YY4tLpUWYFhEggCmk= github.com/morph-l2/tendermint v0.0.0-20260529095305-b1b3a3a1d806/go.mod h1:qpiwqfcCB89dBYfqVJOc/HjGxDp3OdDlthgttJJYyRs= +github.com/morph-l2/tendermint v0.3.8-0.20260617072029-29056623cdb0 h1:LcRbLo0pQXev4z/6i65mVr/9XoFE9Zf7+tcqKItpE+M= +github.com/morph-l2/tendermint v0.3.8-0.20260617072029-29056623cdb0/go.mod h1:qpiwqfcCB89dBYfqVJOc/HjGxDp3OdDlthgttJJYyRs= github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae h1:VeRdUYdCw49yizlSbMEn2SZ+gT+3IUKx8BqxyQdz+BY= github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= diff --git a/node/go.mod b/node/go.mod index 6c1ade3f6..3c42bd6fd 100644 --- a/node/go.mod +++ b/node/go.mod @@ -2,7 +2,7 @@ module morph-l2/node go 1.24.0 -replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3.8-0.20260617032026-015f9f543d32 +replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3.8-0.20260617072029-29056623cdb0 replace github.com/morph-l2/go-ethereum => github.com/morph-l2/go-ethereum v0.0.0-20260608072528-fe02cc1f10bc diff --git a/node/go.sum b/node/go.sum index 4b00502c7..99b6e15f1 100644 --- a/node/go.sum +++ b/node/go.sum @@ -416,8 +416,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/morph-l2/go-ethereum v0.0.0-20260608072528-fe02cc1f10bc h1:2Umr8WRDBKwCgGrQQ8yCdhn71bCuMJuecId2ClK80DU= github.com/morph-l2/go-ethereum v0.0.0-20260608072528-fe02cc1f10bc/go.mod h1:nkVzHjQWCOjvukQW8ittlwX+Xz9gmVHrP7mUi7zoHTs= -github.com/morph-l2/tendermint v0.3.8-0.20260617032026-015f9f543d32 h1:yx7ekS6vSf3Jx/N4iMG4aO95BqNTcG8W9qsirTHev/4= -github.com/morph-l2/tendermint v0.3.8-0.20260617032026-015f9f543d32/go.mod h1:qpiwqfcCB89dBYfqVJOc/HjGxDp3OdDlthgttJJYyRs= +github.com/morph-l2/tendermint v0.3.8-0.20260617072029-29056623cdb0 h1:LcRbLo0pQXev4z/6i65mVr/9XoFE9Zf7+tcqKItpE+M= +github.com/morph-l2/tendermint v0.3.8-0.20260617072029-29056623cdb0/go.mod h1:qpiwqfcCB89dBYfqVJOc/HjGxDp3OdDlthgttJJYyRs= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= diff --git a/ops/l2-genesis/go.mod b/ops/l2-genesis/go.mod index cc1d14fd8..410643622 100644 --- a/ops/l2-genesis/go.mod +++ b/ops/l2-genesis/go.mod @@ -2,7 +2,7 @@ module morph-l2/morph-deployer go 1.24.0 -replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3.8-0.20260617032026-015f9f543d32 +replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3.8-0.20260617072029-29056623cdb0 replace github.com/morph-l2/go-ethereum => github.com/morph-l2/go-ethereum v0.0.0-20260608072528-fe02cc1f10bc diff --git a/ops/tools/go.mod b/ops/tools/go.mod index 9cd471a59..bfaffcfa6 100644 --- a/ops/tools/go.mod +++ b/ops/tools/go.mod @@ -2,7 +2,7 @@ module morph-l2/tools go 1.24.0 -replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3.8-0.20260617032026-015f9f543d32 +replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3.8-0.20260617072029-29056623cdb0 replace github.com/morph-l2/go-ethereum => github.com/morph-l2/go-ethereum v0.0.0-20260608072528-fe02cc1f10bc diff --git a/ops/tools/go.sum b/ops/tools/go.sum index cbcedbfb3..41263a51f 100644 --- a/ops/tools/go.sum +++ b/ops/tools/go.sum @@ -163,8 +163,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/morph-l2/go-ethereum v0.0.0-20260608072528-fe02cc1f10bc h1:2Umr8WRDBKwCgGrQQ8yCdhn71bCuMJuecId2ClK80DU= github.com/morph-l2/go-ethereum v0.0.0-20260608072528-fe02cc1f10bc/go.mod h1:nkVzHjQWCOjvukQW8ittlwX+Xz9gmVHrP7mUi7zoHTs= -github.com/morph-l2/tendermint v0.3.8-0.20260617032026-015f9f543d32 h1:yx7ekS6vSf3Jx/N4iMG4aO95BqNTcG8W9qsirTHev/4= -github.com/morph-l2/tendermint v0.3.8-0.20260617032026-015f9f543d32/go.mod h1:qpiwqfcCB89dBYfqVJOc/HjGxDp3OdDlthgttJJYyRs= +github.com/morph-l2/tendermint v0.3.8-0.20260617072029-29056623cdb0 h1:LcRbLo0pQXev4z/6i65mVr/9XoFE9Zf7+tcqKItpE+M= +github.com/morph-l2/tendermint v0.3.8-0.20260617072029-29056623cdb0/go.mod h1:qpiwqfcCB89dBYfqVJOc/HjGxDp3OdDlthgttJJYyRs= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= diff --git a/tx-submitter/go.mod b/tx-submitter/go.mod index 0035ae2f2..8f75532d8 100644 --- a/tx-submitter/go.mod +++ b/tx-submitter/go.mod @@ -2,7 +2,7 @@ module morph-l2/tx-submitter go 1.24.0 -replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3.8-0.20260617032026-015f9f543d32 +replace github.com/tendermint/tendermint => github.com/morph-l2/tendermint v0.3.8-0.20260617072029-29056623cdb0 replace github.com/morph-l2/go-ethereum => github.com/morph-l2/go-ethereum v0.0.0-20260608072528-fe02cc1f10bc From 27ddba869ba19d07de9d0d81c92febac1c104235 Mon Sep 17 00:00:00 2001 From: "allen.wu" Date: Wed, 17 Jun 2026 15:27:03 +0800 Subject: [PATCH 3/3] chore(ops): translate run-ha-test.sh to English Translate comments, log/echo strings, record_test labels and the markdown report templates from Chinese to English. No logic, variable, flag, or test-ID changes. Co-Authored-By: Claude Opus 4.8 (1M context) --- ops/docker-sequencer-test/run-ha-test.sh | 334 +++++++++++------------ 1 file changed, 167 insertions(+), 167 deletions(-) diff --git a/ops/docker-sequencer-test/run-ha-test.sh b/ops/docker-sequencer-test/run-ha-test.sh index 9547fcc42..c36cd2183 100755 --- a/ops/docker-sequencer-test/run-ha-test.sh +++ b/ops/docker-sequencer-test/run-ha-test.sh @@ -105,23 +105,23 @@ record_test() { if [ "$result" = "PASS" ]; then PASS=$((PASS + 1)) log_success "[$tc_id] $tc_name" - REPORT_LINES+=("### $tc_id: $tc_name\n\n**状态**: ✅ PASS\n") + REPORT_LINES+=("### $tc_id: $tc_name\n\n**Status**: ✅ PASS\n") elif [ "$result" = "FAIL" ]; then FAIL=$((FAIL + 1)) log_error "[$tc_id] $tc_name" FAILED_TESTS+=("$tc_id: $tc_name") - REPORT_LINES+=("### $tc_id: $tc_name\n\n**状态**: ❌ FAIL\n") + REPORT_LINES+=("### $tc_id: $tc_name\n\n**Status**: ❌ FAIL\n") else SKIP=$((SKIP + 1)) log_warn "[$tc_id] $tc_name (SKIPPED: $notes)" - REPORT_LINES+=("### $tc_id: $tc_name\n\n**状态**: ⏭️ SKIP — $notes\n") + REPORT_LINES+=("### $tc_id: $tc_name\n\n**Status**: ⏭️ SKIP — $notes\n") fi if [ -n "$evidence" ]; then - REPORT_LINES+=("**校验证据**:\n\`\`\`\n$evidence\n\`\`\`\n") + REPORT_LINES+=("**Evidence**:\n\`\`\`\n$evidence\n\`\`\`\n") fi if [ -n "$notes" ] && [ "$result" != "SKIP" ]; then - REPORT_LINES+=("**备注**: $notes\n") + REPORT_LINES+=("**Notes**: $notes\n") fi REPORT_LINES+=("---\n") } @@ -516,21 +516,21 @@ start_ha_cluster() { # ─── Category 1: Config Tests ───────────────────────────────────────────────── run_config_tests() { - log_section "Category 1: 配置验证 (Config Tests)" + log_section "Category 1: Config validation (Config Tests)" # Wait for the timestamp upgrade to actually fire + HA formation before running config tests. wait_for_upgrade || log_warn "Proceeding despite upgrade-detection timeout" log_info "Waiting ${HA_FORM_WAIT}s for Raft cluster to form..." sleep "$HA_FORM_WAIT" - # TC-CFG-01: bootstrap flag 生效 - log_info "--- TC-CFG-01: bootstrap flag 生效 ---" + # TC-CFG-01: bootstrap flag takes effect + log_info "--- TC-CFG-01: bootstrap flag takes effect ---" local node0_leader node0_leader=$(is_ha_leader "$HA_RPC_NODE0") local resp_cfg01 resp_cfg01=$(ha_call "$HA_RPC_NODE0" "ha_leader" "[]") if [ "$node0_leader" -ge 1 ]; then - record_test "TC-CFG-01" "bootstrap flag 生效" "PASS" \ + record_test "TC-CFG-01" "bootstrap flag takes effect" "PASS" \ "ha_leader on ha-node-0: $resp_cfg01" else # ha-node-0 bootstrapped but Raft may have re-elected after restarts; as long as @@ -540,16 +540,16 @@ run_config_tests() { if [ -n "$any_leader_rpc" ]; then local current_leader current_leader=$(rpc_to_container "$any_leader_rpc") - record_test "TC-CFG-01" "bootstrap flag 生效" "PASS" \ + record_test "TC-CFG-01" "bootstrap flag takes effect" "PASS" \ "Current leader=$current_leader (ha-node-0 bootstrapped the cluster, Raft re-elected after restart)\nha-node-0 response: $resp_cfg01" else - record_test "TC-CFG-01" "bootstrap flag 生效" "FAIL" \ + record_test "TC-CFG-01" "bootstrap flag takes effect" "FAIL" \ "ha_leader on ha-node-0: $resp_cfg01\nNo leader found in cluster — bootstrap may have failed" fi fi - # TC-CFG-02: join flag 生效 (3-node cluster formed) - log_info "--- TC-CFG-02: join flag 生效 ---" + # TC-CFG-02: join flag takes effect (3-node cluster formed) + log_info "--- TC-CFG-02: join flag takes effect ---" local leader_rpc leader_rpc=$(find_leader_rpc) local voter_count=0 @@ -559,15 +559,15 @@ run_config_tests() { voter_count=$(count_voters "$leader_rpc") fi if [ "$voter_count" -eq 3 ]; then - record_test "TC-CFG-02" "join flag 生效 — 3节点集群组建" "PASS" \ + record_test "TC-CFG-02" "join flag takes effect — 3-node cluster formed" "PASS" \ "voter_count=$voter_count\nmembership=$membership_resp" else - record_test "TC-CFG-02" "join flag 生效 — 3节点集群组建" "FAIL" \ + record_test "TC-CFG-02" "join flag takes effect — 3-node cluster formed" "FAIL" \ "voter_count=$voter_count (expected 3)\nmembership=$membership_resp" fi - # TC-CFG-03: server-id flag 生效 - log_info "--- TC-CFG-03: server-id flag 生效 ---" + # TC-CFG-03: server-id flag takes effect + log_info "--- TC-CFG-03: server-id flag takes effect ---" local server_ids="" if [ -n "$leader_rpc" ]; then server_ids=$(get_server_ids "$leader_rpc") @@ -575,27 +575,27 @@ run_config_tests() { if echo "$server_ids" | grep -q "ha-node-0" && \ echo "$server_ids" | grep -q "ha-node-1" && \ echo "$server_ids" | grep -q "ha-node-2"; then - record_test "TC-CFG-03" "server-id flag 生效" "PASS" \ + record_test "TC-CFG-03" "server-id flag takes effect" "PASS" \ "server_ids: $server_ids" else - record_test "TC-CFG-03" "server-id flag 生效" "FAIL" \ + record_test "TC-CFG-03" "server-id flag takes effect" "FAIL" \ "server_ids: $server_ids (expected ha-node-0, ha-node-1, ha-node-2)" fi - # TC-CFG-04: 纯 flag 模式(无配置文件) - log_info "--- TC-CFG-04: 纯flag模式(无配置文件)---" + # TC-CFG-04: pure flag mode (no config file) + log_info "--- TC-CFG-04: pure flag mode (no config file) ---" # Verify HA works without ha.toml config file. # If cluster formed and leader elected, pure-flag mode works. if [ -n "$leader_rpc" ] && [ "$voter_count" -ge 2 ]; then - record_test "TC-CFG-04" "纯flag模式(无配置文件)" "PASS" \ + record_test "TC-CFG-04" "pure flag mode (no config file)" "PASS" \ "HA cluster formed with only env var flags (no --ha.config file)\nleader=$leader_rpc voter_count=$voter_count" else - record_test "TC-CFG-04" "纯flag模式(无配置文件)" "FAIL" \ + record_test "TC-CFG-04" "pure flag mode (no config file)" "FAIL" \ "Cluster did not form — flag-only mode may not work\nleader_rpc='$leader_rpc' voter_count=$voter_count" fi - # TC-CFG-05: advertised_addr 自动检测(非 0.0.0.0) - log_info "--- TC-CFG-05: advertised_addr 自动检测 ---" + # TC-CFG-05: advertised_addr auto-detection (non-0.0.0.0) + log_info "--- TC-CFG-05: advertised_addr auto-detection ---" local addrs="" if [ -n "$leader_rpc" ]; then addrs=$(get_server_addrs "$leader_rpc") @@ -608,10 +608,10 @@ run_config_tests() { fi done if [ -n "$addrs" ] && [ "$bad_addr" -eq 0 ]; then - record_test "TC-CFG-05" "advertised_addr 自动检测(非0.0.0.0)" "PASS" \ + record_test "TC-CFG-05" "advertised_addr auto-detection (non-0.0.0.0)" "PASS" \ "server addrs: $addrs\nAll addrs are non-wildcard IPs" else - record_test "TC-CFG-05" "advertised_addr 自动检测(非0.0.0.0)" "FAIL" \ + record_test "TC-CFG-05" "advertised_addr auto-detection (non-0.0.0.0)" "FAIL" \ "server addrs: $addrs\nbad_addr=$bad_addr (found 0.0.0.0 or empty)" fi } @@ -619,36 +619,36 @@ run_config_tests() { # ─── Category 2: Cluster Formation Tests ───────────────────────────────────── run_cluster_tests() { - log_section "Category 2: 集群组建 (Cluster Tests)" + log_section "Category 2: Cluster formation (Cluster Tests)" local leader_rpc leader_rpc=$(find_leader_rpc) - # TC-CLU-01: ha-node-0 成为第一个 leader(bootstrap 节点) - log_info "--- TC-CLU-01: ha-node-0 成为初始leader ---" + # TC-CLU-01: ha-node-0 becomes the first leader (bootstrap node) + log_info "--- TC-CLU-01: ha-node-0 becomes initial leader ---" cd "$DOCKER_DIR" local node0_leader_log node0_leader_log=$($COMPOSE_HA logs ha-node-0 2>/dev/null | grep -i "leaderReady\|hakeeper: raft\|leader" | tail -5 || true) local node0_is_leader node0_is_leader=$(is_ha_leader "$HA_RPC_NODE0") if [ "$node0_is_leader" -ge 1 ]; then - record_test "TC-CLU-01" "ha-node-0成为初始leader(bootstrap节点)" "PASS" \ + record_test "TC-CLU-01" "ha-node-0 becomes initial leader (bootstrap node)" "PASS" \ "ha_leader on ha-node-0=true\nlog: $node0_leader_log" else # ha-node-0 might have transferred leadership; check if any node is leader if [ -n "$leader_rpc" ]; then local leader_node leader_node=$(rpc_to_container "$leader_rpc") - record_test "TC-CLU-01" "ha-node-0成为初始leader(bootstrap节点)" "PASS" \ + record_test "TC-CLU-01" "ha-node-0 becomes initial leader (bootstrap node)" "PASS" \ "Current leader=$leader_node (ha-node-0 bootstrapped, may have transferred)\nha-node-0 log: $node0_leader_log" else - record_test "TC-CLU-01" "ha-node-0成为初始leader(bootstrap节点)" "FAIL" \ + record_test "TC-CLU-01" "ha-node-0 becomes initial leader (bootstrap node)" "FAIL" \ "No leader found. ha-node-0 logs: $node0_leader_log" fi fi - # TC-CLU-02: 3节点集群完整组建 — all 3 as Voter - log_info "--- TC-CLU-02: 3节点集群完整组建 ---" + # TC-CLU-02: full 3-node cluster formed — all 3 as Voter + log_info "--- TC-CLU-02: full 3-node cluster formed ---" local membership_resp voter_count server_ids if [ -n "$leader_rpc" ]; then membership_resp=$(get_membership "$leader_rpc") @@ -658,35 +658,35 @@ run_cluster_tests() { voter_count=0; server_ids=""; membership_resp="no leader" fi if [ "$voter_count" -eq 3 ]; then - record_test "TC-CLU-02" "3节点集群完整组建(3 Voter)" "PASS" \ + record_test "TC-CLU-02" "full 3-node cluster formed (3 Voters)" "PASS" \ "voter_count=$voter_count\nservers=$server_ids\nmembership=$membership_resp" else - record_test "TC-CLU-02" "3节点集群完整组建(3 Voter)" "FAIL" \ + record_test "TC-CLU-02" "full 3-node cluster formed (3 Voters)" "FAIL" \ "voter_count=$voter_count (expected 3)\nservers=$server_ids" fi - # TC-CLU-03: joinLoop 重试机制(通过日志验证) - log_info "--- TC-CLU-03: joinLoop重试机制 ---" + # TC-CLU-03: joinLoop retry mechanism (verified via logs) + log_info "--- TC-CLU-03: joinLoop retry mechanism ---" cd "$DOCKER_DIR" local join_logs join_logs=$($COMPOSE_HA logs ha-node-1 ha-node-2 2>/dev/null | \ grep -i "joined cluster\|join attempt\|joining cluster\|hakeeper.*join" | head -10 || true) if echo "$join_logs" | grep -qi "joined"; then - record_test "TC-CLU-03" "joinLoop重试机制" "PASS" \ + record_test "TC-CLU-03" "joinLoop retry mechanism" "PASS" \ "Join log evidence:\n$join_logs" else # If membership is 3-node, join succeeded even if log message differs if [ "$voter_count" -eq 3 ]; then - record_test "TC-CLU-03" "joinLoop重试机制" "PASS" \ + record_test "TC-CLU-03" "joinLoop retry mechanism" "PASS" \ "3-node cluster formed (join succeeded); specific retry log not captured\nJoin-related logs: $join_logs" else - record_test "TC-CLU-03" "joinLoop重试机制" "FAIL" \ + record_test "TC-CLU-03" "joinLoop retry mechanism" "FAIL" \ "No join success logs found and cluster is not 3-node\nLogs: $join_logs" fi fi - # TC-CLU-04: 重复 bootstrap 无害 (ErrCantBootstrap ignored) - log_info "--- TC-CLU-04: 重复bootstrap无害(ErrCantBootstrap忽略)---" + # TC-CLU-04: repeated bootstrap is harmless (ErrCantBootstrap ignored) + log_info "--- TC-CLU-04: repeated bootstrap is harmless (ErrCantBootstrap ignored) ---" cd "$DOCKER_DIR" local bootstrap_logs bootstrap_logs=$($COMPOSE_HA logs ha-node-0 2>/dev/null | \ @@ -697,10 +697,10 @@ run_cluster_tests() { fatal_bootstrap_err=$($COMPOSE_HA logs ha-node-0 2>/dev/null | \ grep -i "bootstrap.*error\|fatal.*bootstrap" | grep -v "ErrCantBootstrap" | head -3 || true) if [ -z "$fatal_bootstrap_err" ]; then - record_test "TC-CLU-04" "重复bootstrap无害" "PASS" \ + record_test "TC-CLU-04" "repeated bootstrap is harmless" "PASS" \ "No fatal bootstrap error in logs\nBootstrap-related logs:\n$bootstrap_logs" else - record_test "TC-CLU-04" "重复bootstrap无害" "FAIL" \ + record_test "TC-CLU-04" "repeated bootstrap is harmless" "FAIL" \ "Fatal bootstrap error found:\n$fatal_bootstrap_err" fi } @@ -708,7 +708,7 @@ run_cluster_tests() { # ─── Category 3: Block Production Tests ─────────────────────────────────────── run_block_tests() { - log_section "Category 3: 出块验证 (Block Production Tests)" + log_section "Category 3: Block production (Block Production Tests)" # Ensure blocks are flowing post-upgrade (a handful of fresh blocks past the current head). local current @@ -720,22 +720,22 @@ run_block_tests() { local leader_rpc leader_rpc=$(find_leader_rpc) - # TC-BLK-01: 升级后 leader 出块 - log_info "--- TC-BLK-01: leader出块 ---" + # TC-BLK-01: leader produces blocks after upgrade + log_info "--- TC-BLK-01: leader produces blocks ---" local h1 h2 h1=$(get_block_number "$L2_RPC_NODE0") sleep 10 h2=$(get_block_number "$L2_RPC_NODE0") if [ "$h2" -gt "$h1" ]; then - record_test "TC-BLK-01" "升级后leader出块" "PASS" \ + record_test "TC-BLK-01" "leader produces blocks after upgrade" "PASS" \ "Block height increased: $h1 → $h2 (delta=$((h2-h1)) in 10s)" else - record_test "TC-BLK-01" "升级后leader出块" "FAIL" \ + record_test "TC-BLK-01" "leader produces blocks after upgrade" "FAIL" \ "Block height stuck: $h1 → $h2" fi - # TC-BLK-02: follower 不出块(只有 leader 调用 produceBlock) - log_info "--- TC-BLK-02: follower不出块 ---" + # TC-BLK-02: follower does not produce blocks (only leader calls produceBlock) + log_info "--- TC-BLK-02: follower does not produce blocks ---" cd "$DOCKER_DIR" # Check non-leader HA cluster nodes local follower_produce_logs="" @@ -754,7 +754,7 @@ run_block_tests() { fi done if [ -z "$follower_produce_logs" ]; then - record_test "TC-BLK-02" "follower不出块" "PASS" \ + record_test "TC-BLK-02" "follower does not produce blocks" "PASS" \ "No 'Producing block' or 'Block produced' log found on follower nodes" else # Note: "Block committed via HA" may appear on leader after Commit() returns @@ -762,17 +762,17 @@ run_block_tests() { local real_fail real_fail=$(echo -e "$follower_produce_logs" | grep "Producing block" || true) if [ -z "$real_fail" ]; then - record_test "TC-BLK-02" "follower不出块" "PASS" \ + record_test "TC-BLK-02" "follower does not produce blocks" "PASS" \ "Follower produces no blocks (some commit logs are expected on leader path)\nLogs: $follower_produce_logs" else - record_test "TC-BLK-02" "follower不出块" "FAIL" \ + record_test "TC-BLK-02" "follower does not produce blocks" "FAIL" \ "Follower 'Producing block' log found (should only be on leader):\n$real_fail" fi fi - # TC-BLK-03: follower 同步 — geth heights match across all L2 nodes + # TC-BLK-03: follower sync — geth heights match across all L2 nodes # (PBFT nodes node-0..3, HA cluster ha-node-0..2 via ha-geth-0..2) - log_info "--- TC-BLK-03: follower同步 ---" + log_info "--- TC-BLK-03: follower sync ---" sleep 5 # allow sync to settle local bn0 bn1 bn2 bn3 h0 h1 h2 bn0=$(get_block_number "$L2_RPC_NODE0") @@ -791,13 +791,13 @@ run_block_tests() { done local evidence="PBFT: node-0=$bn0 node-1=$bn1 node-2=$bn2 node-3=$bn3\nHA: ha-node-0=$h0 ha-node-1=$h1 ha-node-2=$h2\nMax diff allowed: $max_diff" if [ "$all_ok" -eq 1 ]; then - record_test "TC-BLK-03" "follower同步(PBFT + HA 全部齐头)" "PASS" "$evidence" + record_test "TC-BLK-03" "follower sync (PBFT + HA all in lockstep)" "PASS" "$evidence" else - record_test "TC-BLK-03" "follower同步(PBFT + HA 全部齐头)" "FAIL" "$evidence" + record_test "TC-BLK-03" "follower sync (PBFT + HA all in lockstep)" "FAIL" "$evidence" fi - # TC-BLK-04: 已存在 block 幂等跳过(ApplyBlock idempotent) - log_info "--- TC-BLK-04: 已存在block幂等跳过 ---" + # TC-BLK-04: existing block idempotently skipped (ApplyBlock idempotent) + log_info "--- TC-BLK-04: existing block idempotently skipped ---" cd "$DOCKER_DIR" # Check no "duplicate block" or reorg error logs on HA followers local dup_errors @@ -808,10 +808,10 @@ run_block_tests() { apply_errors=$($COMPOSE_HA logs ha-node-1 ha-node-2 2>/dev/null | \ grep -i "FSM apply.*error\|ApplyBlock.*error" | head -3 || true) if [ -z "$apply_errors" ]; then - record_test "TC-BLK-04" "已存在block幂等跳过" "PASS" \ + record_test "TC-BLK-04" "existing block idempotently skipped" "PASS" \ "No FSMApplyError logs on followers\nIdempotent skip messages: ${dup_errors:-none}" else - record_test "TC-BLK-04" "已存在block幂等跳过" "FAIL" \ + record_test "TC-BLK-04" "existing block idempotently skipped" "FAIL" \ "FSM apply errors found on followers:\n$apply_errors" fi } @@ -819,18 +819,18 @@ run_block_tests() { # ─── Category 4: HA Failover Tests ──────────────────────────────────────────── run_failover_tests() { - log_section "Category 4: Leader故障转移 (HA Failover Tests)" + log_section "Category 4: Leader failover (HA Failover Tests)" # Record current leader before failover local leader_rpc leader_rpc=$(find_leader_rpc) if [ -z "$leader_rpc" ]; then log_error "No leader found — skipping failover tests" - record_test "TC-HA-01" "kill leader → 自动选举" "SKIP" "" "No leader found before test" - record_test "TC-HA-02" "新leader出块" "SKIP" "" "No leader found before test" - record_test "TC-HA-03" "故障转移出块间隔" "SKIP" "" "No leader found before test" - record_test "TC-HA-04" "旧leader重新加入" "SKIP" "" "No leader found before test" - record_test "TC-HA-05" "二次故障转移" "SKIP" "" "No leader found before test" + record_test "TC-HA-01" "kill leader → auto re-election" "SKIP" "" "No leader found before test" + record_test "TC-HA-02" "new leader produces blocks" "SKIP" "" "No leader found before test" + record_test "TC-HA-03" "failover block interval" "SKIP" "" "No leader found before test" + record_test "TC-HA-04" "old leader rejoins" "SKIP" "" "No leader found before test" + record_test "TC-HA-05" "second failover" "SKIP" "" "No leader found before test" return fi local leader_node @@ -840,8 +840,8 @@ run_failover_tests() { log_info "Current leader: $leader_node ($leader_rpc)" - # TC-HA-01: kill leader → 自动选举 - log_info "--- TC-HA-01: kill leader → 自动选举 ---" + # TC-HA-01: kill leader → auto re-election + log_info "--- TC-HA-01: kill leader → auto re-election ---" local pre_kill_height pre_kill_height=$(get_block_number "$leader_geth_rpc") local kill_time @@ -873,16 +873,16 @@ run_failover_tests() { if [ -n "$new_leader_rpc" ]; then local new_leader_node new_leader_node=$(rpc_to_container "$new_leader_rpc") - record_test "TC-HA-01" "kill leader → 自动选举" "PASS" \ + record_test "TC-HA-01" "kill leader → auto re-election" "PASS" \ "Killed: $leader_node\nNew leader: $new_leader_node ($new_leader_rpc)\nElection time: ${election_time}s" else - record_test "TC-HA-01" "kill leader → 自动选举" "FAIL" \ + record_test "TC-HA-01" "kill leader → auto re-election" "FAIL" \ "No new leader elected after 30s\nKilled: $leader_node" # Skip remaining failover tests - record_test "TC-HA-02" "新leader出块" "SKIP" "" "No new leader elected" - record_test "TC-HA-03" "故障转移出块间隔" "SKIP" "" "No new leader elected" - record_test "TC-HA-04" "旧leader重新加入" "SKIP" "" "No new leader elected" - record_test "TC-HA-05" "二次故障转移" "SKIP" "" "No new leader elected" + record_test "TC-HA-02" "new leader produces blocks" "SKIP" "" "No new leader elected" + record_test "TC-HA-03" "failover block interval" "SKIP" "" "No new leader elected" + record_test "TC-HA-04" "old leader rejoins" "SKIP" "" "No new leader elected" + record_test "TC-HA-05" "second failover" "SKIP" "" "No new leader elected" return fi local new_leader_node @@ -890,33 +890,33 @@ run_failover_tests() { local new_leader_geth new_leader_geth=$(ha_rpc_to_geth_rpc "$new_leader_rpc") - # TC-HA-02: 新 leader 出块 - log_info "--- TC-HA-02: 新leader出块 ---" + # TC-HA-02: new leader produces blocks + log_info "--- TC-HA-02: new leader produces blocks ---" local h1 h2 h1=$(get_block_number "$new_leader_geth") log_info "Waiting 15s for new leader ($new_leader_node) to produce blocks..." sleep 15 h2=$(get_block_number "$new_leader_geth") if [ "$h2" -gt "$h1" ]; then - record_test "TC-HA-02" "新leader出块" "PASS" \ + record_test "TC-HA-02" "new leader produces blocks" "PASS" \ "New leader ($new_leader_node) produced blocks: $h1 → $h2 (+$((h2-h1)) in 15s)" else - record_test "TC-HA-02" "新leader出块" "FAIL" \ + record_test "TC-HA-02" "new leader produces blocks" "FAIL" \ "New leader ($new_leader_node) not producing blocks: $h1 → $h2" fi - # TC-HA-03: 故障转移出块间隔 (< 10s) - log_info "--- TC-HA-03: 故障转移出块间隔 ---" + # TC-HA-03: failover block interval (< 10s) + log_info "--- TC-HA-03: failover block interval ---" if [ "$election_time" -le 10 ]; then - record_test "TC-HA-03" "故障转移出块间隔(目标<10s)" "PASS" \ + record_test "TC-HA-03" "failover block interval (target <10s)" "PASS" \ "Kill to new leader detected: ${election_time}s (≤ 10s target)" else - record_test "TC-HA-03" "故障转移出块间隔(目标<10s)" "FAIL" \ + record_test "TC-HA-03" "failover block interval (target <10s)" "FAIL" \ "Kill to new leader detected: ${election_time}s (> 10s target)\nNote: actual first block may come later due to Barrier" fi - # TC-HA-04: 旧 leader 重新加入(以 follower 身份) - log_info "--- TC-HA-04: 旧leader重新加入 ---" + # TC-HA-04: old leader rejoins (as a follower) + log_info "--- TC-HA-04: old leader rejoins ---" log_info "Restarting old leader ($leader_node)..." cd "$DOCKER_DIR" $COMPOSE_HA start "$leader_node" 2>/dev/null || $COMPOSE_HA up -d "$leader_node" @@ -940,22 +940,22 @@ run_failover_tests() { new_voter_count=$(count_voters "$new_leader_rpc") if [ "$old_leader_is_follower" -eq 1 ] && [ "$new_voter_count" -eq 3 ]; then - record_test "TC-HA-04" "旧leader重新加入(follower身份)" "PASS" \ + record_test "TC-HA-04" "old leader rejoins (as follower)" "PASS" \ "Old leader ($leader_node) is now follower (leader=false)\nCluster size: $new_voter_count voters\nHeight sync: old=$old_height, new=$new_height, diff=$rejoin_diff" elif [ "$old_leader_is_follower" -eq 1 ]; then - record_test "TC-HA-04" "旧leader重新加入(follower身份)" "PASS" \ + record_test "TC-HA-04" "old leader rejoins (as follower)" "PASS" \ "Old leader ($leader_node) is follower (leader=false)\nCluster may still be re-forming (voter_count=$new_voter_count)" else - record_test "TC-HA-04" "旧leader重新加入(follower身份)" "FAIL" \ + record_test "TC-HA-04" "old leader rejoins (as follower)" "FAIL" \ "Old leader ($leader_node) still reports as leader OR HA RPC not reachable\nha_leader=$(ha_call "$old_leader_rpc" "ha_leader" "[]")\nvoter_count=$new_voter_count" fi - # TC-HA-05: 二次故障转移 — kill new leader, 第三个节点接管 - log_info "--- TC-HA-05: 二次故障转移 ---" + # TC-HA-05: second failover — kill new leader, third node takes over + log_info "--- TC-HA-05: second failover ---" local current_leader_rpc current_leader_rpc=$(find_leader_rpc) if [ -z "$current_leader_rpc" ]; then - record_test "TC-HA-05" "二次故障转移" "SKIP" "" "Could not find current leader for 2nd failover" + record_test "TC-HA-05" "second failover" "SKIP" "" "Could not find current leader for 2nd failover" return fi local current_leader_node @@ -998,14 +998,14 @@ run_failover_tests() { sleep 10 h3b=$(get_block_number "$third_geth") if [ "$h3b" -gt "$h3a" ]; then - record_test "TC-HA-05" "二次故障转移" "PASS" \ + record_test "TC-HA-05" "second failover" "PASS" \ "2nd leader killed: $current_leader_node\n3rd leader: $third_leader_node, election: ${failover2_time}s\nBlocks: $h3a → $h3b" else - record_test "TC-HA-05" "二次故障转移" "FAIL" \ + record_test "TC-HA-05" "second failover" "FAIL" \ "3rd leader ($third_leader_node) not producing blocks: $h3a → $h3b" fi else - record_test "TC-HA-05" "二次故障转移" "FAIL" \ + record_test "TC-HA-05" "second failover" "FAIL" \ "No 3rd leader elected after 30s (killed: $current_leader_node)" fi @@ -1020,7 +1020,7 @@ run_failover_tests() { # ─── Category 5: Admin API Tests ────────────────────────────────────────────── run_api_tests() { - log_section "Category 5: Admin API 测试 (8 endpoints)" + log_section "Category 5: Admin API tests (8 endpoints)" local leader_rpc leader_rpc=$(find_leader_rpc) @@ -1228,8 +1228,8 @@ run_api_tests() { record_test "TC-API-07" "ha_transferLeaderToServer" "SKIP" "" "No leader available" fi - # TC-API-08: 乐观锁版本校验 — old version rejected - log_info "--- TC-API-08: 乐观锁版本校验 ---" + # TC-API-08: optimistic-lock version check — old version rejected + log_info "--- TC-API-08: optimistic-lock version check ---" leader_rpc=$(find_leader_rpc) if [ -n "$leader_rpc" ]; then wait_for_ha_leader 15 || true @@ -1245,32 +1245,32 @@ run_api_tests() { resp08=$(ha_call "$leader_rpc" "ha_addServerAsVoter" "[\"fake-node\",\"1.2.3.4:9400\",$stale_version_high]") # Should return error (wrong index / mismatch) if echo "$resp08" | grep -q '"error"'; then - record_test "TC-API-08" "乐观锁版本校验(旧版本被拒)" "PASS" \ + record_test "TC-API-08" "optimistic-lock version check (stale version rejected)" "PASS" \ "Used stale version=$stale_version_high (current=$current_version)\nResponse: $resp08 (contains error as expected)" else # Some Raft implementations may accept future versions; check if member was actually added local post_version post_version=$(get_membership_version "$leader_rpc") if echo "$resp08" | grep -q '"result":null'; then - record_test "TC-API-08" "乐观锁版本校验(旧版本被拒)" "FAIL" \ + record_test "TC-API-08" "optimistic-lock version check (stale version rejected)" "FAIL" \ "Stale version not rejected! version=$stale_version_high response=$resp08" else - record_test "TC-API-08" "乐观锁版本校验(旧版本被拒)" "PASS" \ + record_test "TC-API-08" "optimistic-lock version check (stale version rejected)" "PASS" \ "Response: $resp08\nNote: hashicorp/raft uses index as 'prevIndex'; future version may still work in some cases" fi fi else - record_test "TC-API-08" "乐观锁版本校验" "SKIP" "" "No leader available" + record_test "TC-API-08" "optimistic-lock version check" "SKIP" "" "No leader available" fi } # ─── Category 6: Lifecycle Tests ────────────────────────────────────────────── run_lifecycle_tests() { - log_section "Category 6: 生命周期 (Lifecycle Tests)" + log_section "Category 6: Lifecycle (Lifecycle Tests)" - # TC-LIF-01: follower Stop/Start 循环 - log_info "--- TC-LIF-01: follower Stop/Start循环 ---" + # TC-LIF-01: follower Stop/Start cycle + log_info "--- TC-LIF-01: follower Stop/Start cycle ---" # Find a non-leader follower local follower_rpc="" local follower_node="" @@ -1283,7 +1283,7 @@ run_lifecycle_tests() { done if [ -z "$follower_node" ]; then - record_test "TC-LIF-01" "follower Stop/Start循环" "SKIP" "" "No non-leader follower found" + record_test "TC-LIF-01" "follower Stop/Start cycle" "SKIP" "" "No non-leader follower found" else cd "$DOCKER_DIR" log_info "Stopping follower: $follower_node" @@ -1319,16 +1319,16 @@ run_lifecycle_tests() { local height_diff=$((leader_height - follower_height)); height_diff=${height_diff#-} if [ "$still_producing" -eq 1 ] && [ "$rejoin_voter_count" -eq 3 ]; then - record_test "TC-LIF-01" "follower Stop/Start循环" "PASS" \ + record_test "TC-LIF-01" "follower Stop/Start cycle" "PASS" \ "Stopped: $follower_node; cluster continued producing (quorum OK)\nAfter rejoin: voter_count=$rejoin_voter_count, height_diff=$height_diff" else - record_test "TC-LIF-01" "follower Stop/Start循环" "FAIL" \ + record_test "TC-LIF-01" "follower Stop/Start cycle" "FAIL" \ "still_producing=$still_producing voter_count_after_rejoin=$rejoin_voter_count" fi fi - # TC-LIF-02: 全集群重启 - log_info "--- TC-LIF-02: 全集群重启 ---" + # TC-LIF-02: full cluster restart + log_info "--- TC-LIF-02: full cluster restart ---" cd "$DOCKER_DIR" log_info "Stopping all HA nodes..." $COMPOSE_HA stop ha-node-0 ha-node-1 ha-node-2 2>/dev/null || true @@ -1353,19 +1353,19 @@ run_lifecycle_tests() { sleep 10 h2=$(get_block_number "$new_geth") if [ "$h2" -gt "$h1" ]; then - record_test "TC-LIF-02" "全集群重启后恢复" "PASS" \ + record_test "TC-LIF-02" "recovery after full cluster restart" "PASS" \ "New leader after restart: $new_leader\nBlocks: $h1 → $h2" else - record_test "TC-LIF-02" "全集群重启后恢复" "FAIL" \ + record_test "TC-LIF-02" "recovery after full cluster restart" "FAIL" \ "Leader elected ($new_leader) but not producing blocks: $h1 → $h2" fi else - record_test "TC-LIF-02" "全集群重启后恢复" "FAIL" \ + record_test "TC-LIF-02" "recovery after full cluster restart" "FAIL" \ "No leader elected within 45s after full cluster restart" fi - # TC-LIF-03: Barrier 机制 — leader ready 延迟验证 - log_info "--- TC-LIF-03: Barrier机制(日志验证)---" + # TC-LIF-03: Barrier mechanism — leader-ready delay verification + log_info "--- TC-LIF-03: Barrier mechanism (log verification) ---" cd "$DOCKER_DIR" # After the full restart above, check logs for HA startup sequence local ha_start_logs @@ -1374,10 +1374,10 @@ run_lifecycle_tests() { tail -10 || true) # Check that HA startup log appears (including 'became leader', 'Barrier', 'leader ready') if echo "$ha_start_logs" | grep -qi "hakeeper"; then - record_test "TC-LIF-03" "Barrier机制" "PASS" \ + record_test "TC-LIF-03" "Barrier mechanism" "PASS" \ "HA logs confirm Barrier flow:\n$ha_start_logs\nKey messages: 'became leader, running Barrier' → 'leader ready'" else - record_test "TC-LIF-03" "Barrier机制" "FAIL" \ + record_test "TC-LIF-03" "Barrier mechanism" "FAIL" \ "No HA startup logs found — hakeeper may not have started\nLogs: $ha_start_logs" fi } @@ -1392,25 +1392,25 @@ generate_report() { timestamp=$(date "+%Y-%m-%d %H:%M:%S") { - echo "# Sequencer HA V2 集成测试报告" + echo "# Sequencer HA V2 Integration Test Report" echo "" - echo "> 生成时间: $timestamp" - echo "> 升级时间(ms): ${SEQUENCER_UPGRADE_TIME:-unset}" - echo "> 环境: docker-sequencer-test (3节点 Raft HA 集群)" + echo "> Generated at: $timestamp" + echo "> Upgrade time (ms): ${SEQUENCER_UPGRADE_TIME:-unset}" + echo "> Environment: docker-sequencer-test (3-node Raft HA cluster)" echo "" echo "---" echo "" - echo "## 总览" + echo "## Overview" echo "" - echo "| 状态 | 数量 |" + echo "| Status | Count |" echo "|------|------|" - echo "| ✅ 通过 | $PASS |" - echo "| ❌ 失败 | $FAIL |" - echo "| ⏭️ 跳过 | $SKIP |" - echo "| **总计** | **$total** |" + echo "| ✅ Passed | $PASS |" + echo "| ❌ Failed | $FAIL |" + echo "| ⏭️ Skipped | $SKIP |" + echo "| **Total** | **$total** |" echo "" if [ ${#FAILED_TESTS[@]} -gt 0 ]; then - echo "## 失败用例" + echo "## Failed cases" echo "" for t in "${FAILED_TESTS[@]}"; do echo "- ❌ $t" @@ -1419,28 +1419,28 @@ generate_report() { fi echo "---" echo "" - echo "## 测试矩阵" + echo "## Test matrix" echo "" - echo "| ID | 类别 | 测试项 | 状态 |" + echo "| ID | Category | Test item | Status |" echo "|-----|------|-------|------|" - echo "| TC-CFG-01 | 配置验证 | bootstrap flag 生效 | - |" - echo "| TC-CFG-02 | 配置验证 | join flag 生效 | - |" - echo "| TC-CFG-03 | 配置验证 | server-id flag 生效 | - |" - echo "| TC-CFG-04 | 配置验证 | 纯flag模式(无配置文件) | - |" - echo "| TC-CFG-05 | 配置验证 | advertised_addr 自动检测 | - |" - echo "| TC-CLU-01 | 集群组建 | ha-node-0 成为初始 leader | - |" - echo "| TC-CLU-02 | 集群组建 | 3节点集群完整组建 | - |" - echo "| TC-CLU-03 | 集群组建 | joinLoop 重试机制 | - |" - echo "| TC-CLU-04 | 集群组建 | 重复 bootstrap 无害 | - |" - echo "| TC-BLK-01 | 出块验证 | 升级后 leader 出块 | - |" - echo "| TC-BLK-02 | 出块验证 | follower 不出块 | - |" - echo "| TC-BLK-03 | 出块验证 | follower 同步 | - |" - echo "| TC-BLK-04 | 出块验证 | 已存在 block 幂等跳过 | - |" - echo "| TC-HA-01 | 故障转移 | kill leader → 自动选举 | - |" - echo "| TC-HA-02 | 故障转移 | 新 leader 出块 | - |" - echo "| TC-HA-03 | 故障转移 | 故障转移出块间隔(<10s) | - |" - echo "| TC-HA-04 | 故障转移 | 旧 leader 重新加入 | - |" - echo "| TC-HA-05 | 故障转移 | 二次故障转移 | - |" + echo "| TC-CFG-01 | Config validation | bootstrap flag takes effect | - |" + echo "| TC-CFG-02 | Config validation | join flag takes effect | - |" + echo "| TC-CFG-03 | Config validation | server-id flag takes effect | - |" + echo "| TC-CFG-04 | Config validation | pure flag mode (no config file) | - |" + echo "| TC-CFG-05 | Config validation | advertised_addr auto-detection | - |" + echo "| TC-CLU-01 | Cluster formation | ha-node-0 becomes initial leader | - |" + echo "| TC-CLU-02 | Cluster formation | full 3-node cluster formed | - |" + echo "| TC-CLU-03 | Cluster formation | joinLoop retry mechanism | - |" + echo "| TC-CLU-04 | Cluster formation | repeated bootstrap is harmless | - |" + echo "| TC-BLK-01 | Block production | leader produces blocks after upgrade | - |" + echo "| TC-BLK-02 | Block production | follower does not produce blocks | - |" + echo "| TC-BLK-03 | Block production | follower sync | - |" + echo "| TC-BLK-04 | Block production | existing block idempotently skipped | - |" + echo "| TC-HA-01 | Failover | kill leader → auto re-election | - |" + echo "| TC-HA-02 | Failover | new leader produces blocks | - |" + echo "| TC-HA-03 | Failover | failover block interval (<10s) | - |" + echo "| TC-HA-04 | Failover | old leader rejoins | - |" + echo "| TC-HA-05 | Failover | second failover | - |" echo "| TC-API-01 | Admin API | ha_leader | - |" echo "| TC-API-02 | Admin API | ha_leaderWithID | - |" echo "| TC-API-03 | Admin API | ha_clusterMembership | - |" @@ -1448,14 +1448,14 @@ generate_report() { echo "| TC-API-05 | Admin API | ha_removeServer | - |" echo "| TC-API-06 | Admin API | ha_transferLeader | - |" echo "| TC-API-07 | Admin API | ha_transferLeaderToServer | - |" - echo "| TC-API-08 | Admin API | 乐观锁版本校验 | - |" - echo "| TC-LIF-01 | 生命周期 | follower Stop/Start 循环 | - |" - echo "| TC-LIF-02 | 生命周期 | 全集群重启后恢复 | - |" - echo "| TC-LIF-03 | 生命周期 | Barrier 机制日志验证 | - |" + echo "| TC-API-08 | Admin API | optimistic-lock version check | - |" + echo "| TC-LIF-01 | Lifecycle | follower Stop/Start cycle | - |" + echo "| TC-LIF-02 | Lifecycle | recovery after full cluster restart | - |" + echo "| TC-LIF-03 | Lifecycle | Barrier mechanism log verification | - |" echo "" echo "---" echo "" - echo "## 详细结果" + echo "## Detailed results" echo "" for line in "${REPORT_LINES[@]}"; do echo -e "$line" @@ -1493,10 +1493,10 @@ run_p2p_opt_tests() { local follower_delta=$((follower_height_after - follower_height_before)) local gap=$((leader_height_after - follower_height_after)) if [ "$follower_delta" -ge 1 ] && [ "$gap" -lt 10 ]; then - record_test "TC-P2P-01" "fullnode通过P2P同步块" "PASS" \ + record_test "TC-P2P-01" "fullnode syncs blocks via P2P" "PASS" \ "Fullnode(node-0) advanced $follower_delta blocks in 10s, gap to leader=$gap" else - record_test "TC-P2P-01" "fullnode通过P2P同步块" "FAIL" \ + record_test "TC-P2P-01" "fullnode syncs blocks via P2P" "FAIL" \ "Fullnode delta=$follower_delta, gap=$gap (expected delta>=1, gap<10)" fi @@ -1510,10 +1510,10 @@ run_p2p_opt_tests() { apply_log=$($COMPOSE_HA logs --tail 2000 node-0 2>&1 | \ grep -c "Starting block apply routine" || true) if [ "$apply_log" -ge 1 ]; then - record_test "TC-P2P-02" "fullnode启动apply routine" "PASS" \ + record_test "TC-P2P-02" "fullnode starts apply routine" "PASS" \ "Found 'Starting block apply routine' log on node-0" else - record_test "TC-P2P-02" "fullnode启动apply routine" "FAIL" \ + record_test "TC-P2P-02" "fullnode starts apply routine" "FAIL" \ "No apply routine startup log found on node-0" fi @@ -1525,10 +1525,10 @@ run_p2p_opt_tests() { applied_count=$($COMPOSE_HA logs --tail 5000 node-0 2>&1 | \ grep -c "Applied block" || true) if [ "$applied_count" -ge 1 ]; then - record_test "TC-P2P-03" "fullnode成功apply块" "PASS" \ + record_test "TC-P2P-03" "fullnode successfully applies blocks" "PASS" \ "Found $applied_count 'Applied block' entries in node-0 logs" else - record_test "TC-P2P-03" "fullnode成功apply块" "FAIL" \ + record_test "TC-P2P-03" "fullnode successfully applies blocks" "FAIL" \ "No 'Applied block' logs on node-0 (sync path may be broken)" fi @@ -1542,10 +1542,10 @@ run_p2p_opt_tests() { grep -c "Unsolicited sync response" || true) # Allow a small number due to race conditions at startup; require < 5. if [ "$unsolicited_count" -lt 5 ]; then - record_test "TC-P2P-04" "无误报unsolicited响应" "PASS" \ + record_test "TC-P2P-04" "no spurious unsolicited responses" "PASS" \ "Unsolicited response count: $unsolicited_count (threshold <5)" else - record_test "TC-P2P-04" "无误报unsolicited响应" "FAIL" \ + record_test "TC-P2P-04" "no spurious unsolicited responses" "FAIL" \ "Too many unsolicited response errors: $unsolicited_count" fi @@ -1556,10 +1556,10 @@ run_p2p_opt_tests() { ban_count=$($COMPOSE_HA logs --tail 5000 node-0 node-1 node-2 node-3 sentry-node-0 2>&1 | \ grep -c "Banning peer" || true) if [ "$ban_count" -eq 0 ]; then - record_test "TC-P2P-05" "正常运行无误ban" "PASS" \ + record_test "TC-P2P-05" "no false-positive bans in normal operation" "PASS" \ "No 'Banning peer' logs in normal operation" else - record_test "TC-P2P-05" "正常运行无误ban" "FAIL" \ + record_test "TC-P2P-05" "no false-positive bans in normal operation" "FAIL" \ "Unexpected bans in normal operation: $ban_count entries" fi @@ -1571,10 +1571,10 @@ run_p2p_opt_tests() { rl_count=$($COMPOSE_HA logs --tail 5000 node-0 node-1 node-2 node-3 sentry-node-0 ha-node-0 ha-node-1 ha-node-2 2>&1 | \ grep -c "BlockRequest rate limited" || true) if [ "$rl_count" -eq 0 ]; then - record_test "TC-P2P-06" "正常流量无误限流" "PASS" \ + record_test "TC-P2P-06" "no false-positive rate limiting" "PASS" \ "No rate-limit hits during normal sync" else - record_test "TC-P2P-06" "正常流量无误限流" "FAIL" \ + record_test "TC-P2P-06" "no false-positive rate limiting" "FAIL" \ "Legitimate peers tripped rate limit: $rl_count entries" fi }