From 85e41918e19c5ca7f9354bf1adc2fc9ff51856d1 Mon Sep 17 00:00:00 2001 From: blockchaindevsh Date: Tue, 8 Jul 2025 00:09:59 +0800 Subject: [PATCH 1/4] cgt --- op-chain-ops/genesis/config.go | 14 +++ op-chain-ops/genesis/genesis.go | 8 +- .../interfaces/L1/IOptimismPortal2.sol | 4 + .../interfaces/L2/IL2ToL1MessagePasser.sol | 1 + .../src/L1/OptimismPortal2.sol | 100 ++++++++++++++++++ .../src/L2/L2ToL1MessagePasser.sol | 40 +++++++ .../src/libraries/Constants.sol | 2 + 7 files changed, 166 insertions(+), 3 deletions(-) diff --git a/op-chain-ops/genesis/config.go b/op-chain-ops/genesis/config.go index ad83381689873..5a2103e30f75a 100644 --- a/op-chain-ops/genesis/config.go +++ b/op-chain-ops/genesis/config.go @@ -718,6 +718,8 @@ type L2InitializationConfig struct { UpgradeScheduleDeployConfig L2CoreDeployConfig AltDADeployConfig + InboxContractConfig + L1ScalarMultiplierConfig } func (d *L2InitializationConfig) Check(log log.Logger) error { @@ -890,6 +892,18 @@ type L1DependenciesConfig struct { ProtocolVersionsProxy common.Address `json:"protocolVersionsProxy"` } +// InboxContractConfig configures whether inbox contract is enabled. +// If enabled, the batcher tx will be further filtered by tx status. +type InboxContractConfig struct { + UseInboxContract bool `json:"useInboxContract,omitempty"` +} + +// L1ScalarMultiplierConfig configures the scalar multipliers for L1 base fee and blob base fee. +type L1ScalarMultiplierConfig struct { + L1BaseFeeScalarMultiplier uint64 `json:"l1BaseFeeScalarMultiplier,omitempty"` + L1BlobBaseFeeScalarMultiplier uint64 `json:"l1BlobBaseFeeScalarMultiplier,omitempty"` +} + // DependencyContext is the contextual configuration needed to verify the L1 dependencies, // used by DeployConfig.CheckAddresses. type DependencyContext struct { diff --git a/op-chain-ops/genesis/genesis.go b/op-chain-ops/genesis/genesis.go index fa55eda6cf21a..f831009b84ea5 100644 --- a/op-chain-ops/genesis/genesis.go +++ b/op-chain-ops/genesis/genesis.go @@ -76,9 +76,11 @@ func NewL2Genesis(config *DeployConfig, l1StartHeader *eth.BlockRef) (*core.Gene PragueTime: config.IsthmusTime(l1StartTime), InteropTime: config.InteropTime(l1StartTime), Optimism: ¶ms.OptimismConfig{ - EIP1559Denominator: eip1559Denom, - EIP1559Elasticity: eip1559Elasticity, - EIP1559DenominatorCanyon: &eip1559DenomCanyon, + EIP1559Denominator: eip1559Denom, + EIP1559Elasticity: eip1559Elasticity, + EIP1559DenominatorCanyon: &eip1559DenomCanyon, + L1BaseFeeScalarMultiplier: config.L1BaseFeeScalarMultiplier, + L1BlobBaseFeeScalarMultiplier: config.L1BlobBaseFeeScalarMultiplier, }, } diff --git a/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol b/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol index eb73b2956cc24..2efb62bcdb1c2 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol @@ -125,5 +125,9 @@ interface IOptimismPortal2 is IProxyAdminOwnedBase { function version() external pure returns (string memory); function migrateLiquidity() external; + function setMinter(address _minter) external; + function mintTransaction(address _to, uint256 _value) external; + function setNativeDeposit(bool _disable) external; + function __constructor__(uint256 _proofMaturityDelaySeconds) external; } diff --git a/packages/contracts-bedrock/interfaces/L2/IL2ToL1MessagePasser.sol b/packages/contracts-bedrock/interfaces/L2/IL2ToL1MessagePasser.sol index 4629dbaba8d09..6da156237feca 100644 --- a/packages/contracts-bedrock/interfaces/L2/IL2ToL1MessagePasser.sol +++ b/packages/contracts-bedrock/interfaces/L2/IL2ToL1MessagePasser.sol @@ -21,6 +21,7 @@ interface IL2ToL1MessagePasser { function messageNonce() external view returns (uint256); function sentMessages(bytes32) external view returns (bool); function version() external view returns (string memory); + function setNativeDeposit(bool _disable) external; function __constructor__() external; } diff --git a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol index ef2f8c8cb0e07..840fe5159ef1f 100644 --- a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol +++ b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol @@ -11,6 +11,7 @@ import { ReinitializableBase } from "src/universal/ReinitializableBase.sol"; import { EOA } from "src/libraries/EOA.sol"; import { SafeCall } from "src/libraries/SafeCall.sol"; import { Constants } from "src/libraries/Constants.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; import { Types } from "src/libraries/Types.sol"; import { Hashing } from "src/libraries/Hashing.sol"; import { SecureMerkleTrie } from "src/libraries/trie/SecureMerkleTrie.sol"; @@ -23,6 +24,7 @@ import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; +import { IL2ToL1MessagePasser } from "interfaces/L2/IL2ToL1MessagePasser.sol"; import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; import { IETHLockbox } from "interfaces/L1/IETHLockbox.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; @@ -50,6 +52,9 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase /// @notice The L2 gas limit set when eth is deposited using the receive() function. uint64 internal constant RECEIVE_DEFAULT_GAS_LIMIT = 100_000; + /// @notice The L2 gas limit for system deposit transactions that are initiated from L1. + uint32 internal constant SYSTEM_DEPOSIT_GAS_LIMIT = 200_000; + /// @notice Address of the L2 account which initiated a withdrawal in this transaction. /// If the value of this variable is the default L2 sender address, then we are NOT /// inside of a call to finalizeWithdrawalTransaction. @@ -125,6 +130,24 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase /// @notice Whether the OptimismPortal is using Super Roots or Output Roots. bool public superRootsActive; + // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.OptimismPortal2.QKCConfigStorage")) - 1)) & + // ~bytes32(uint256(0xff)) + bytes32 private constant _QKC_CONFIG_STORAGE_LOCATION = + 0xb42b8bfdf1143b9dfcdc891f15a039d3c36301d501f5a44f62223d852a602a00; + /// @custom:storage-location erc7201:openzeppelin.storage.OptimismPortal2.QKCConfigStorage + + struct QKCConfigStorage { + /// @notice The minter for migrating existing L1 token to L2 native token. + address minter; + bool disableNativeDeposit; + } + + function _getQKCConfigStorage() private pure returns (QKCConfigStorage storage $) { + assembly { + $.slot := _QKC_CONFIG_STORAGE_LOCATION + } + } + /// @notice Emitted when a transaction is deposited from L1 to L2. The parameters of this event /// are read by the rollup node and used to derive deposit transactions on L2. /// @param from Address that triggered the deposit transaction. @@ -168,6 +191,13 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase IAnchorStateRegistry newAnchorStateRegistry ); + /// @notice Emitted when a minter is set. + event MinterSet(address indexed minter); + /// @notice Emitted when native deposit is disabled. + event NativeDepositDisabled(); + /// @notice Emitted when native deposit is enabled. + event NativeDepositEnabled(); + /// @notice Thrown when a withdrawal has already been finalized. error OptimismPortal_AlreadyFinalized(); @@ -186,6 +216,10 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase /// @notice Thrown when the gas limit for a deposit is too low. error OptimismPortal_GasLimitTooLow(); + // @notice Thrown when native token is deposited to the portal contract when disabled. + // For swc, the native token is actually qkc so we need to disable ETH deposits. + error NativeDepositForbidden(); + /// @notice Thrown when the target of a withdrawal is not a proper dispute game. error OptimismPortal_ImproperDisputeGame(); @@ -726,6 +760,66 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase } } + /// @notice Add a minter to the OptimismPortal contract. To disable, set an empty value. + function setMinter(address _minter) external { + if (msg.sender != proxyAdminOwner()) { + revert OptimismPortal_Unauthorized(); + } + QKCConfigStorage storage $ = _getQKCConfigStorage(); + $.minter = _minter; + emit MinterSet(_minter); + } + + /// @notice Mint a specific amount of L2 native token to an address. + function mintTransaction(address _to, uint256 _value) external metered(RECEIVE_DEFAULT_GAS_LIMIT) { + QKCConfigStorage storage $ = _getQKCConfigStorage(); + if (msg.sender != $.minter) { + revert OptimismPortal_Unauthorized(); + } + + if (_to == address(0)) { + revert OptimismPortal_BadTarget(); + } + // Compute the opaque data that will be emitted as part of the TransactionDeposited event. + // We use opaque data so that we can update the TransactionDeposited event in the future + // without breaking the current interface. + bytes memory opaqueData = abi.encodePacked(_value, _value, RECEIVE_DEFAULT_GAS_LIMIT, false, bytes("")); + + // Emit a TransactionDeposited event so that the rollup node can derive a deposit + // transaction for this deposit. + emit TransactionDeposited(Constants.QKC_DEPOSITOR_ACCOUNT, _to, DEPOSIT_VERSION, opaqueData); + } + + /// @notice set native deposit flag. Pass true to disable. + function setNativeDeposit(bool _disable) external { + if (msg.sender != proxyAdminOwner()) { + revert OptimismPortal_Unauthorized(); + } + QKCConfigStorage storage $ = _getQKCConfigStorage(); + $.disableNativeDeposit = _disable; + if (_disable) { + emit NativeDepositDisabled(); + } else { + emit NativeDepositEnabled(); + } + + // Compute the opaque data that will be emitted as part of the TransactionDeposited event. + // We use opaque data so that we can update the TransactionDeposited event in the future + // without breaking the current interface. + bytes memory opaqueData = abi.encodePacked( + uint256(0), + uint256(0), + uint64(SYSTEM_DEPOSIT_GAS_LIMIT), + false, + abi.encodeCall(IL2ToL1MessagePasser.setNativeDeposit, (_disable)) + ); + // Emit a TransactionDeposited event so that the rollup node can derive a deposit + // transaction for this deposit. + emit TransactionDeposited( + Constants.QKC_DEPOSITOR_ACCOUNT, Predeploys.L2_TO_L1_MESSAGE_PASSER, DEPOSIT_VERSION, opaqueData + ); + } + /// @notice Accepts deposits of ETH and data, and emits a TransactionDeposited event for use in /// deriving deposit transactions. Note that if a deposit is made by a contract, its /// address will be aliased when retrieved using `tx.origin` or `msg.sender`. Consider @@ -748,6 +842,12 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase payable metered(_gasLimit) { + if (msg.value > 0) { + QKCConfigStorage storage $ = _getQKCConfigStorage(); + if ($.disableNativeDeposit) { + revert NativeDepositForbidden(); + } + } // Lock the ETH in the ETHLockbox. if (msg.value > 0) ethLockbox.lockETH{ value: msg.value }(); diff --git a/packages/contracts-bedrock/src/L2/L2ToL1MessagePasser.sol b/packages/contracts-bedrock/src/L2/L2ToL1MessagePasser.sol index b25a2a1248bc5..7f7ecfa7aad56 100644 --- a/packages/contracts-bedrock/src/L2/L2ToL1MessagePasser.sol +++ b/packages/contracts-bedrock/src/L2/L2ToL1MessagePasser.sol @@ -6,6 +6,7 @@ import { Types } from "src/libraries/Types.sol"; import { Hashing } from "src/libraries/Hashing.sol"; import { Encoding } from "src/libraries/Encoding.sol"; import { Burn } from "src/libraries/Burn.sol"; +import { Constants } from "src/libraries/Constants.sol"; // Interfaces import { ISemver } from "interfaces/universal/ISemver.sol"; @@ -29,6 +30,22 @@ contract L2ToL1MessagePasser is ISemver { /// @notice A unique value hashed with each withdrawal. uint240 internal msgNonce; + // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.L2ToL1MessagePasser.QKCConfigStorage")) - 1)) & + // ~bytes32(uint256(0xff)) + bytes32 private constant _QKC_CONFIG_STORAGE_LOCATION = + 0x750f1ab2ed0ba2a4405b31f7b30e394ba3545975e71082ba3e508022a159f900; + /// @custom:storage-location erc7201:openzeppelin.storage.L2ToL1MessagePasser.QKCConfigStorage + + struct QKCConfigStorage { + bool disableNativeDeposit; + } + + function _getQKCConfigStorage() private pure returns (QKCConfigStorage storage $) { + assembly { + $.slot := _QKC_CONFIG_STORAGE_LOCATION + } + } + /// @notice Emitted any time a withdrawal is initiated. /// @param nonce Unique value corresponding to each withdrawal. /// @param sender The L2 account address which initiated the withdrawal. @@ -51,6 +68,11 @@ contract L2ToL1MessagePasser is ISemver { /// @param amount Amount of ETh that was burned. event WithdrawerBalanceBurnt(uint256 indexed amount); + /// @notice Emitted when native deposit is disabled. + event NativeDepositDisabled(); + /// @notice Emitted when native deposit is enabled. + event NativeDepositEnabled(); + /// @custom:semver 1.1.2 string public constant version = "1.1.2"; @@ -74,6 +96,10 @@ contract L2ToL1MessagePasser is ISemver { /// @param _gasLimit Minimum gas limit for executing the message on L1. /// @param _data Data to forward to L1 target. function initiateWithdrawal(address _target, uint256 _gasLimit, bytes memory _data) public payable { + QKCConfigStorage storage $ = _getQKCConfigStorage(); + if ($.disableNativeDeposit && msg.value > 0) { + revert("native deposit/withdraw is disabled"); + } bytes32 withdrawalHash = Hashing.hashWithdrawal( Types.WithdrawalTransaction({ nonce: messageNonce(), @@ -101,4 +127,18 @@ contract L2ToL1MessagePasser is ISemver { function messageNonce() public view returns (uint256) { return Encoding.encodeVersionedNonce(msgNonce, MESSAGE_VERSION); } + + /// @notice set native deposit flag. Pass true to disable. + function setNativeDeposit(bool _disable) external { + if (msg.sender != Constants.QKC_DEPOSITOR_ACCOUNT) { + revert("L2ToL1MessagePasser: Only the depositor #2 can enable/disable native deposits"); + } + QKCConfigStorage storage $ = _getQKCConfigStorage(); + $.disableNativeDeposit = _disable; + if (_disable) { + emit NativeDepositDisabled(); + } else { + emit NativeDepositEnabled(); + } + } } diff --git a/packages/contracts-bedrock/src/libraries/Constants.sol b/packages/contracts-bedrock/src/libraries/Constants.sol index 6dcf3611956af..dc14100a26d50 100644 --- a/packages/contracts-bedrock/src/libraries/Constants.sol +++ b/packages/contracts-bedrock/src/libraries/Constants.sol @@ -37,6 +37,8 @@ library Constants { /// @notice The address that represents the system caller responsible for L1 attributes /// transactions. address internal constant DEPOSITOR_ACCOUNT = 0xDeaDDEaDDeAdDeAdDEAdDEaddeAddEAdDEAd0001; + /// @notice Another fixed address to avoid potential risky calls for deposit tx on L2. + address internal constant QKC_DEPOSITOR_ACCOUNT = 0xDEAdDeaddEAdDeadDEaddeAdDeaddEAd00514b43; /// @notice Returns the default values for the ResourceConfig. These are the recommended values /// for a production network. From 8bb963d1eb9a639560e75807504811325dec9c9c Mon Sep 17 00:00:00 2001 From: blockchaindevsh Date: Mon, 7 Jul 2025 23:33:48 +0800 Subject: [PATCH 2/4] deploy sgt --- .../interfaces/L2/ISoulGasToken.sol | 15 + .../contracts-bedrock/scripts/Artifacts.s.sol | 2 + .../contracts-bedrock/scripts/L2Genesis.s.sol | 22 ++ .../contracts-bedrock/src/L2/SoulGasToken.sol | 266 ++++++++++++++++++ .../src/libraries/Predeploys.sol | 10 +- 5 files changed, 312 insertions(+), 3 deletions(-) create mode 100644 packages/contracts-bedrock/interfaces/L2/ISoulGasToken.sol create mode 100644 packages/contracts-bedrock/src/L2/SoulGasToken.sol diff --git a/packages/contracts-bedrock/interfaces/L2/ISoulGasToken.sol b/packages/contracts-bedrock/interfaces/L2/ISoulGasToken.sol new file mode 100644 index 0000000000000..096b42f7a4117 --- /dev/null +++ b/packages/contracts-bedrock/interfaces/L2/ISoulGasToken.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @title ISoulGasToken +/// @notice The interface for the SoulGasToken. +interface ISoulGasToken { + function initialize(string memory _name, string memory _symbol, address _owner) external; + + function __constructor__(bool _isBackedByNative) external; + + function name() external view returns (string memory); + function symbol() external view returns (string memory); + function owner() external view returns (address); + function admin() external view returns (address); +} diff --git a/packages/contracts-bedrock/scripts/Artifacts.s.sol b/packages/contracts-bedrock/scripts/Artifacts.s.sol index 5858c8aa4c597..7d5471ba49b73 100644 --- a/packages/contracts-bedrock/scripts/Artifacts.s.sol +++ b/packages/contracts-bedrock/scripts/Artifacts.s.sol @@ -80,6 +80,8 @@ contract Artifacts { bytes32 digest = keccak256(bytes(_name)); if (digest == keccak256(bytes("L2CrossDomainMessenger"))) { return payable(Predeploys.L2_CROSS_DOMAIN_MESSENGER); + } else if (digest == keccak256(bytes("SoulGasToken"))) { + return payable(Predeploys.SOUL_GAS_TOKEN); } else if (digest == keccak256(bytes("L2ToL1MessagePasser"))) { return payable(Predeploys.L2_TO_L1_MESSAGE_PASSER); } else if (digest == keccak256(bytes("L2StandardBridge"))) { diff --git a/packages/contracts-bedrock/scripts/L2Genesis.s.sol b/packages/contracts-bedrock/scripts/L2Genesis.s.sol index de75c6b99cf1e..f50c6301955bf 100644 --- a/packages/contracts-bedrock/scripts/L2Genesis.s.sol +++ b/packages/contracts-bedrock/scripts/L2Genesis.s.sol @@ -31,6 +31,8 @@ import { IL2CrossDomainMessenger } from "interfaces/L2/IL2CrossDomainMessenger.s import { IGasPriceOracle } from "interfaces/L2/IGasPriceOracle.sol"; import { IL1Block } from "interfaces/L2/IL1Block.sol"; +import { SoulGasToken } from "src/L2/SoulGasToken.sol"; + /// @title L2Genesis /// @notice Generates the genesis state for the L2 network. /// The following safety invariants are used when setting state: @@ -229,6 +231,7 @@ contract L2Genesis is Script { // 1C,1D,1E,1F: not used. setSchemaRegistry(); // 20 setEAS(); // 21 + setSoulGasToken(_input); // 800 setGovernanceToken(_input); // 42: OP (not behind a proxy) if (_input.fork >= uint256(Fork.INTEROP)) { if (_input.deployCrossL2Inbox) { @@ -256,7 +259,26 @@ contract L2Genesis is Script { _setImplementationCode(Predeploys.L2_TO_L1_MESSAGE_PASSER); } + /// @notice This predeploy is following the safety invariant #2. + function setSoulGasToken(Input memory _input) internal { + address impl = Predeploys.predeployToCodeNamespace(Predeploys.SOUL_GAS_TOKEN); + + SoulGasToken token = new SoulGasToken({ _isBackedByNative: true }); + vm.etch(impl, address(token).code); + + /// Reset so its not included state dump + vm.etch(address(token), ""); + vm.resetNonce(address(token)); + + SoulGasToken(impl).initialize({ _name: "", _symbol: "", _owner: _input.opChainProxyAdminOwner }); + SoulGasToken(Predeploys.SOUL_GAS_TOKEN).initialize({ + _name: "SoulQKC", + _symbol: "SoulQKC", + _owner: _input.opChainProxyAdminOwner + }); + } /// @notice This predeploy is following the safety invariant #1. + function setL2CrossDomainMessenger(address payable _l1CrossDomainMessengerProxy) internal { address impl = _setImplementationCode(Predeploys.L2_CROSS_DOMAIN_MESSENGER); diff --git a/packages/contracts-bedrock/src/L2/SoulGasToken.sol b/packages/contracts-bedrock/src/L2/SoulGasToken.sol new file mode 100644 index 0000000000000..5a23f83bcd0d1 --- /dev/null +++ b/packages/contracts-bedrock/src/L2/SoulGasToken.sol @@ -0,0 +1,266 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import { ERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +/// @title SoulGasToken +/// @notice The SoulGasToken is a soul-bounded ERC20 contract which can be used to pay gas on L2. +/// It has 2 modes: +/// 1. when IS_BACKED_BY_NATIVE(or in other words: SoulQKC mode), the token can be minted by +/// anyone depositing native token into the contract. +/// 2. when !IS_BACKED_BY_NATIVE(or in other words: SoulETH mode), the token can only be +/// minted by whitelist minters specified by contract owner. +contract SoulGasToken is ERC20Upgradeable, OwnableUpgradeable { + /// @custom:storage-location erc7201:openzeppelin.storage.SoulGasToken + struct SoulGasTokenStorage { + // minters are whitelist EOAs, only used when !IS_BACKED_BY_NATIVE + mapping(address => bool) minters; + // burners are whitelist EOAs to burn/withdraw SoulGasToken + mapping(address => bool) burners; + // allowSgtValue are whitelist contracts to consume sgt as msg.value + // when IS_BACKED_BY_NATIVE + mapping(address => bool) allowSgtValue; + } + + /// @notice Emitted when sgt as msg.value is enabled for a contract. + /// @param from Address of the contract for which sgt as msg.value is enabled. + event AllowSgtValue(address indexed from); + /// @notice Emitted when sgt as msg.value is disabled for a contract. + /// @param from Address of the contract for which sgt as msg.value is disabled. + event DisallowSgtValue(address indexed from); + + event BurnerAdded(address indexed burner); + event BurnerDeleted(address indexed burner); + event MinterAdded(address indexed minter); + event MinterDeleted(address indexed minter); + + // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.SoulGasToken")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant _SOULGASTOKEN_STORAGE_LOCATION = + 0x135c38e215d95c59dcdd8fe622dccc30d04cacb8c88c332e4e7441bac172dd00; + + bool internal immutable IS_BACKED_BY_NATIVE; + + function _getSoulGasTokenStorage() private pure returns (SoulGasTokenStorage storage $) { + assembly { + $.slot := _SOULGASTOKEN_STORAGE_LOCATION + } + } + + constructor(bool _isBackedByNative) { + IS_BACKED_BY_NATIVE = _isBackedByNative; + initialize("", "", msg.sender); + } + + /// @notice Initializer. + function initialize(string memory _name, string memory _symbol, address _owner) public initializer { + __Ownable_init(); + transferOwnership(_owner); + + // initialize the inherited ERC20Upgradeable + __ERC20_init(_name, _symbol); + } + + /// @notice deposit can be called by anyone to deposit native token for SoulGasToken when + /// IS_BACKED_BY_NATIVE. + function deposit() external payable { + require(IS_BACKED_BY_NATIVE, "SGT: deposit should only be called when IS_BACKED_BY_NATIVE"); + + _mint(_msgSender(), msg.value); + } + + /// @notice batchDepositFor can be called by anyone to deposit native token for SoulGasToken in batch when + /// IS_BACKED_BY_NATIVE. + function batchDepositFor(address[] calldata _accounts, uint256[] calldata _values) external payable { + require(_accounts.length == _values.length, "SGT: invalid arguments"); + + require(IS_BACKED_BY_NATIVE, "SGT: batchDepositFor should only be called when IS_BACKED_BY_NATIVE"); + + uint256 totalValue = 0; + for (uint256 i = 0; i < _accounts.length; i++) { + _mint(_accounts[i], _values[i]); + totalValue += _values[i]; + } + require(msg.value == totalValue, "SGT: unexpected msg.value"); + } + + /// @notice batchDepositForAll is similar to batchDepositFor, but the value is the same for all accounts. + function batchDepositForAll(address[] calldata _accounts, uint256 _value) external payable { + require(IS_BACKED_BY_NATIVE, "SGT: batchDepositForAll should only be called when IS_BACKED_BY_NATIVE"); + + for (uint256 i = 0; i < _accounts.length; i++) { + _mint(_accounts[i], _value); + } + require(msg.value == _value * _accounts.length, "SGT: unexpected msg.value"); + } + + /// @notice withdrawFrom is called by the burner to burn SoulGasToken and return the native token when + /// IS_BACKED_BY_NATIVE. + function withdrawFrom(address _account, uint256 _value) external { + require(IS_BACKED_BY_NATIVE, "SGT: withdrawFrom should only be called when IS_BACKED_BY_NATIVE"); + + SoulGasTokenStorage storage $ = _getSoulGasTokenStorage(); + require($.burners[_msgSender()], "SGT: not the burner"); + + _burn(_account, _value); + payable(_msgSender()).transfer(_value); + } + + /// @notice batchWithdrawFrom is the batch version of withdrawFrom. + function batchWithdrawFrom(address[] calldata _accounts, uint256[] calldata _values) external { + require(_accounts.length == _values.length, "SGT: invalid arguments"); + + require(IS_BACKED_BY_NATIVE, "SGT: batchWithdrawFrom should only be called when IS_BACKED_BY_NATIVE"); + + SoulGasTokenStorage storage $ = _getSoulGasTokenStorage(); + require($.burners[_msgSender()], "SGT: not the burner"); + + uint256 totalValue = 0; + for (uint256 i = 0; i < _accounts.length; i++) { + _burn(_accounts[i], _values[i]); + totalValue += _values[i]; + } + + payable(_msgSender()).transfer(totalValue); + } + + /// @notice batchMint is called: + /// 1. by EOA minters to mint SoulGasToken in batch when !IS_BACKED_BY_NATIVE. + function batchMint(address[] calldata _accounts, uint256[] calldata _values) external { + // we don't explicitly check !IS_BACKED_BY_NATIVE here, because if IS_BACKED_BY_NATIVE, + // there's no way to add a minter. + require(_accounts.length == _values.length, "SGT: invalid arguments"); + + SoulGasTokenStorage storage $ = _getSoulGasTokenStorage(); + require($.minters[_msgSender()], "SGT: not a minter"); + + for (uint256 i = 0; i < _accounts.length; i++) { + _mint(_accounts[i], _values[i]); + } + } + + /// @notice addMinters is called by the owner to add minters when !IS_BACKED_BY_NATIVE. + function addMinters(address[] calldata _minters) external onlyOwner { + require(!IS_BACKED_BY_NATIVE, "SGT: addMinters should only be called when !IS_BACKED_BY_NATIVE"); + SoulGasTokenStorage storage $ = _getSoulGasTokenStorage(); + uint256 i; + for (i = 0; i < _minters.length; i++) { + $.minters[_minters[i]] = true; + emit MinterAdded(_minters[i]); + } + } + + /// @notice delMinters is called by the owner to delete minters when !IS_BACKED_BY_NATIVE. + function delMinters(address[] calldata _minters) external onlyOwner { + require(!IS_BACKED_BY_NATIVE, "SGT: delMinters should only be called when !IS_BACKED_BY_NATIVE"); + SoulGasTokenStorage storage $ = _getSoulGasTokenStorage(); + uint256 i; + for (i = 0; i < _minters.length; i++) { + delete $.minters[_minters[i]]; + emit MinterDeleted(_minters[i]); + } + } + + /// @notice addBurners is called by the owner to add burners. + function addBurners(address[] calldata _burners) external onlyOwner { + SoulGasTokenStorage storage $ = _getSoulGasTokenStorage(); + uint256 i; + for (i = 0; i < _burners.length; i++) { + $.burners[_burners[i]] = true; + emit BurnerAdded(_burners[i]); + } + } + + /// @notice delBurners is called by the owner to delete burners. + function delBurners(address[] calldata _burners) external onlyOwner { + SoulGasTokenStorage storage $ = _getSoulGasTokenStorage(); + uint256 i; + for (i = 0; i < _burners.length; i++) { + delete $.burners[_burners[i]]; + emit BurnerDeleted(_burners[i]); + } + } + + /// @notice allowSgtValue is called by the owner to enable whitelist contracts to consume sgt as msg.value + function allowSgtValue(address[] calldata _contracts) external onlyOwner { + require(IS_BACKED_BY_NATIVE, "SGT: allowSgtValue should only be called when IS_BACKED_BY_NATIVE"); + SoulGasTokenStorage storage $ = _getSoulGasTokenStorage(); + uint256 i; + for (i = 0; i < _contracts.length; i++) { + $.allowSgtValue[_contracts[i]] = true; + emit AllowSgtValue(_contracts[i]); + } + } + + /// @notice allowSgtValue is called by the owner to disable whitelist contracts to consume sgt as msg.value + function disallowSgtValue(address[] calldata _contracts) external onlyOwner { + require(IS_BACKED_BY_NATIVE, "SGT: disallowSgtValue should only be called when IS_BACKED_BY_NATIVE"); + SoulGasTokenStorage storage $ = _getSoulGasTokenStorage(); + uint256 i; + for (i = 0; i < _contracts.length; i++) { + $.allowSgtValue[_contracts[i]] = false; + emit DisallowSgtValue(_contracts[i]); + } + } + + /// @notice chargeFromOrigin is called when IS_BACKED_BY_NATIVE to charge for native balance + /// from tx.origin if caller is whitelisted. + function chargeFromOrigin(uint256 _amount) external returns (uint256 amountCharged_) { + require(IS_BACKED_BY_NATIVE, "SGT: chargeFromOrigin should only be called when IS_BACKED_BY_NATIVE"); + SoulGasTokenStorage storage $ = _getSoulGasTokenStorage(); + require($.allowSgtValue[_msgSender()], "SGT: caller is not whitelisted"); + uint256 balance = balanceOf(tx.origin); + if (balance == 0) { + amountCharged_ = 0; + return amountCharged_; + } + if (balance >= _amount) { + amountCharged_ = _amount; + } else { + amountCharged_ = balance; + } + _burn(tx.origin, amountCharged_); + payable(_msgSender()).transfer(amountCharged_); + } + + /// @notice burnFrom is called when !IS_BACKED_BY_NATIVE: + /// 1. by the burner to burn SoulGasToken. + function burnFrom(address _account, uint256 _value) external { + require(!IS_BACKED_BY_NATIVE, "SGT: burnFrom should only be called when !IS_BACKED_BY_NATIVE"); + SoulGasTokenStorage storage $ = _getSoulGasTokenStorage(); + require($.burners[_msgSender()], "SGT: not the burner"); + _burn(_account, _value); + } + + /// @notice batchBurnFrom is the batch version of burnFrom. + function batchBurnFrom(address[] calldata _accounts, uint256[] calldata _values) external { + require(_accounts.length == _values.length, "SGT: invalid arguments"); + require(!IS_BACKED_BY_NATIVE, "SGT: batchBurnFrom should only be called when !IS_BACKED_BY_NATIVE"); + SoulGasTokenStorage storage $ = _getSoulGasTokenStorage(); + require($.burners[_msgSender()], "SGT: not the burner"); + + for (uint256 i = 0; i < _accounts.length; i++) { + _burn(_accounts[i], _values[i]); + } + } + + /// @notice transferFrom is disabled for SoulGasToken. + function transfer(address, uint256) public virtual override returns (bool) { + revert("SGT: transfer is disabled for SoulGasToken"); + } + + /// @notice transferFrom is disabled for SoulGasToken. + function transferFrom(address, address, uint256) public virtual override returns (bool) { + revert("SGT: transferFrom is disabled for SoulGasToken"); + } + + /// @notice approve is disabled for SoulGasToken. + function approve(address, uint256) public virtual override returns (bool) { + revert("SGT: approve is disabled for SoulGasToken"); + } + + /// @notice Returns whether SoulGasToken is backed by native token. + function isBackedByNative() external view returns (bool) { + return IS_BACKED_BY_NATIVE; + } +} diff --git a/packages/contracts-bedrock/src/libraries/Predeploys.sol b/packages/contracts-bedrock/src/libraries/Predeploys.sol index baeb6a143575c..3ee2f404466c9 100644 --- a/packages/contracts-bedrock/src/libraries/Predeploys.sol +++ b/packages/contracts-bedrock/src/libraries/Predeploys.sol @@ -8,7 +8,7 @@ import { Fork } from "scripts/libraries/Config.sol"; // This excludes the preinstalls (non-protocol contracts). library Predeploys { /// @notice Number of predeploy-namespace addresses reserved for protocol usage. - uint256 internal constant PREDEPLOY_COUNT = 2048; + uint256 internal constant PREDEPLOY_COUNT = 4096; /// @custom:legacy /// @notice Address of the LegacyMessagePasser predeploy. Deprecate. Use the updated @@ -79,6 +79,9 @@ library Predeploys { /// @notice Address of the EAS predeploy. address internal constant EAS = 0x4200000000000000000000000000000000000021; + /// @notice Address of the SOUL_GAS_TOKEN predeploy. + address internal constant SOUL_GAS_TOKEN = 0x4200000000000000000000000000000000000800; + /// @notice Address of the GovernanceToken predeploy. address internal constant GOVERNANCE_TOKEN = 0x4200000000000000000000000000000000000042; @@ -145,6 +148,7 @@ library Predeploys { if (_addr == OPTIMISM_SUPERCHAIN_ERC20_FACTORY) return "OptimismSuperchainERC20Factory"; if (_addr == OPTIMISM_SUPERCHAIN_ERC20_BEACON) return "OptimismSuperchainERC20Beacon"; if (_addr == SUPERCHAIN_TOKEN_BRIDGE) return "SuperchainTokenBridge"; + if (_addr == SOUL_GAS_TOKEN) return "SoulGasToken"; revert("Predeploys: unnamed predeploy"); } @@ -169,13 +173,13 @@ library Predeploys { || _addr == L2_ERC721_BRIDGE || _addr == L1_BLOCK_ATTRIBUTES || _addr == L2_TO_L1_MESSAGE_PASSER || _addr == OPTIMISM_MINTABLE_ERC721_FACTORY || _addr == PROXY_ADMIN || _addr == BASE_FEE_VAULT || _addr == L1_FEE_VAULT || _addr == OPERATOR_FEE_VAULT || _addr == SCHEMA_REGISTRY || _addr == EAS - || _addr == GOVERNANCE_TOKEN + || _addr == GOVERNANCE_TOKEN || _addr == SOUL_GAS_TOKEN || (_fork >= uint256(Fork.INTEROP) && _enableCrossL2Inbox && _addr == CROSS_L2_INBOX) || (_fork >= uint256(Fork.INTEROP) && _addr == L2_TO_L2_CROSS_DOMAIN_MESSENGER); } function isPredeployNamespace(address _addr) internal pure returns (bool) { - return uint160(_addr) >> 11 == uint160(0x4200000000000000000000000000000000000000) >> 11; + return uint160(_addr) >> 12 == uint160(0x4200000000000000000000000000000000000000) >> 12; } /// @notice Function to compute the expected address of the predeploy implementation From f35bd47105aabea7792154c0c8577eabb249c0f8 Mon Sep 17 00:00:00 2001 From: blockchaindevsh Date: Tue, 8 Jul 2025 00:05:06 +0800 Subject: [PATCH 3/4] inbox contract --- go.mod | 2 +- go.sum | 4 +- op-batcher/batcher/driver.go | 58 ++++++++++++++--- op-chain-ops/genesis/config.go | 5 ++ op-node/rollup/derive/blob_data_source.go | 63 +++++++++++++++++-- .../rollup/derive/blob_data_source_test.go | 49 +++++++++++++-- op-node/rollup/derive/calldata_source.go | 19 +++++- op-node/rollup/derive/calldata_source_test.go | 2 +- op-node/rollup/derive/data_source.go | 2 + op-node/rollup/types.go | 12 ++++ 10 files changed, 190 insertions(+), 26 deletions(-) diff --git a/go.mod b/go.mod index b09102566ed41..6c3ea86b84730 100644 --- a/go.mod +++ b/go.mod @@ -304,7 +304,7 @@ require ( rsc.io/tmplfunc v0.0.3 // indirect ) -replace github.com/ethereum/go-ethereum => github.com/ethereum-optimism/op-geth v1.101511.1-dev.1.0.20250608235258-6005dd53e1b5 +replace github.com/ethereum/go-ethereum => github.com/Quarkchain/op-geth v0.0.0-20250707150543-e235a31d4cc0 //replace github.com/ethereum/go-ethereum => ../op-geth diff --git a/go.sum b/go.sum index 2e7bf88558e4b..1f85a120aa299 100644 --- a/go.sum +++ b/go.sum @@ -37,6 +37,8 @@ github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lpr github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/Quarkchain/op-geth v0.0.0-20250707150543-e235a31d4cc0 h1:PgkV5nv/vftz+9EIabwCPbDFQrRUHiEGBogqJl54mH0= +github.com/Quarkchain/op-geth v0.0.0-20250707150543-e235a31d4cc0/go.mod h1:SkytozVEPtnUeBlquwl0Qv5JKvrN/Y5aqh+VkQo/EOI= github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI= github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI= github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= @@ -228,8 +230,6 @@ github.com/elastic/gosigar v0.14.3 h1:xwkKwPia+hSfg9GqrCUKYdId102m9qTJIIr7egmK/u github.com/elastic/gosigar v0.14.3/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.3 h1:RWHKLhCrQThMfch+QJ1Z8veEq5ZO3DfIhZ7xgRP9WTc= github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.3/go.mod h1:QziizLAiF0KqyLdNJYD7O5cpDlaFMNZzlxYNcWsJUxs= -github.com/ethereum-optimism/op-geth v1.101511.1-dev.1.0.20250608235258-6005dd53e1b5 h1:wczwl6+GChQaDe3no+h1TegOO8J1Cyb+L3BdFXDsMhk= -github.com/ethereum-optimism/op-geth v1.101511.1-dev.1.0.20250608235258-6005dd53e1b5/go.mod h1:SkytozVEPtnUeBlquwl0Qv5JKvrN/Y5aqh+VkQo/EOI= github.com/ethereum-optimism/superchain-registry/validation v0.0.0-20250603144016-9c45ca7d4508 h1:A/3QVFt+Aa9ozpPVXxUTLui8honBjSusAaiCVRbafgs= github.com/ethereum-optimism/superchain-registry/validation v0.0.0-20250603144016-9c45ca7d4508/go.mod h1:NZ816PzLU1TLv1RdAvYAb6KWOj4Zm5aInT0YpDVml2Y= github.com/ethereum/c-kzg-4844/v2 v2.1.0 h1:gQropX9YFBhl3g4HYhwE70zq3IHFRgbbNPw0Shwzf5w= diff --git a/op-batcher/batcher/driver.go b/op-batcher/batcher/driver.go index 5d45ea6c401b4..2d6c48548bdfd 100644 --- a/op-batcher/batcher/driver.go +++ b/op-batcher/batcher/driver.go @@ -31,8 +31,9 @@ import ( ) var ( - ErrBatcherNotRunning = errors.New("batcher is not running") - emptyTxData = txData{ + ErrBatcherNotRunning = errors.New("batcher is not running") + ErrInboxTransactionFailed = errors.New("inbox transaction failed") + emptyTxData = txData{ frames: []frameData{ { data: []byte{}, @@ -70,6 +71,7 @@ func (r txRef) string(txIDStringer func(txID) string) string { type L1Client interface { HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error) + CodeAt(ctx context.Context, account common.Address, blockNumber *big.Int) ([]byte, error) } type L2Client interface { @@ -112,6 +114,7 @@ type BatchSubmitter struct { txpoolState TxPoolState txpoolBlockedBlob bool + inboxIsEOA atomic.Pointer[bool] channelMgrMutex sync.Mutex // guards channelMgr and prevCurrentL1 channelMgr *channelManager prevCurrentL1 eth.L1BlockRef // cached CurrentL1 from the last syncStatus @@ -929,6 +932,9 @@ func (l *BatchSubmitter) sendTransaction(txdata txData, queue *txmgr.Queue[txRef candidate = l.calldataTxCandidate(txdata.CallData()) } + if *candidate.To != l.RollupConfig.BatchInboxAddress { + return fmt.Errorf("candidate.To is not inbox") + } l.sendTx(txdata, false, candidate, queue, receiptsCh) return nil } @@ -940,12 +946,39 @@ type TxSender[T any] interface { // sendTx uses the txmgr queue to send the given transaction candidate after setting its // gaslimit. It will block if the txmgr queue has reached its MaxPendingTransactions limit. func (l *BatchSubmitter) sendTx(txdata txData, isCancel bool, candidate *txmgr.TxCandidate, queue TxSender[txRef], receiptsCh chan txmgr.TxReceipt[txRef]) { - floorDataGas, err := core.FloorDataGas(candidate.TxData) - if err != nil { - // We log instead of return an error here because the txmgr will do its own gas estimation. - l.Log.Warn("Failed to calculate floor data gas", "err", err) - } else { - candidate.GasLimit = floorDataGas + var isEOAPointer *bool + if l.RollupConfig.UseInboxContract() { + // RollupConfig.UseInboxContract() being true just means the batcher's transaction status matters, + // but the actual inbox may still be an EOA. + isEOAPointer = l.inboxIsEOA.Load() + if isEOAPointer == nil { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) + defer cancel() + var code []byte + code, err := l.L1Client.CodeAt(ctx, *candidate.To, nil) + if err != nil { + l.Log.Error("CodeAt failed, assuming code exists", "err", err) + // assume code exist, but don't persist the result + isEOA := false + isEOAPointer = &isEOA + } else { + isEOA := len(code) == 0 + isEOAPointer = &isEOA + l.inboxIsEOA.Store(isEOAPointer) + } + } + } + + // Set GasLimit as intrinstic gas if the inbox is EOA, otherwise + // Leave GasLimit unset when inbox is contract so that later on `EstimateGas` will be called + if !l.RollupConfig.UseInboxContract() || *isEOAPointer { + floorDataGas, err := core.FloorDataGas(candidate.TxData) + if err != nil { + // We log instead of return an error here because the txmgr will do its own gas estimation. + l.Log.Warn("Failed to calculate floor data gas", "err", err) + } else { + candidate.GasLimit = floorDataGas + } } queue.Send(txRef{id: txdata.ID(), isCancel: isCancel, isBlob: txdata.asBlob}, *candidate, receiptsCh) @@ -980,6 +1013,11 @@ func (l *BatchSubmitter) handleReceipt(r txmgr.TxReceipt[txRef]) { if r.Err != nil { l.recordFailedTx(r.ID.id, r.Err) } else if r.Receipt != nil { + // check tx status + if l.RollupConfig.UseInboxContract() && r.Receipt.Status == types.ReceiptStatusFailed { + l.recordFailedTx(r.ID.id, ErrInboxTransactionFailed) + return + } l.recordConfirmedTx(r.ID.id, r.Receipt) } // Both r.Err and r.Receipt can be nil, in which case we do nothing. @@ -997,6 +1035,10 @@ func (l *BatchSubmitter) recordFailedDARequest(id txID, err error) { func (l *BatchSubmitter) recordFailedTx(id txID, err error) { l.channelMgrMutex.Lock() defer l.channelMgrMutex.Unlock() + + if l.RollupConfig.UseInboxContract() { + l.inboxIsEOA.Store(nil) + } l.Log.Warn("Transaction failed to send", logFields(id, err)...) l.channelMgr.TxFailed(id) } diff --git a/op-chain-ops/genesis/config.go b/op-chain-ops/genesis/config.go index 5a2103e30f75a..d428d5bdfa61a 100644 --- a/op-chain-ops/genesis/config.go +++ b/op-chain-ops/genesis/config.go @@ -1076,6 +1076,10 @@ func (d *DeployConfig) RollupConfig(l1StartBlock *eth.BlockRef, l2GenesisBlockHa l1StartTime := l1StartBlock.Time + var inboxContractConfig *rollup.InboxContractConfig + if d.UseInboxContract { + inboxContractConfig = &rollup.InboxContractConfig{UseInboxContract: true} + } return &rollup.Config{ Genesis: rollup.Genesis{ L1: eth.BlockID{ @@ -1111,6 +1115,7 @@ func (d *DeployConfig) RollupConfig(l1StartBlock *eth.BlockRef, l2GenesisBlockHa InteropTime: d.InteropTime(l1StartTime), ProtocolVersionsAddress: d.ProtocolVersionsProxy, AltDAConfig: altDA, + InboxContractConfig: inboxContractConfig, ChainOpConfig: chainOpConfig, }, nil } diff --git a/op-node/rollup/derive/blob_data_source.go b/op-node/rollup/derive/blob_data_source.go index 2c4626941b8b5..fd2f7cef1236e 100644 --- a/op-node/rollup/derive/blob_data_source.go +++ b/op-node/rollup/derive/blob_data_source.go @@ -27,13 +27,13 @@ type BlobDataSource struct { ref eth.L1BlockRef batcherAddr common.Address dsCfg DataSourceConfig - fetcher L1TransactionFetcher + fetcher L1Fetcher blobsFetcher L1BlobsFetcher log log.Logger } // NewBlobDataSource creates a new blob data source. -func NewBlobDataSource(ctx context.Context, log log.Logger, dsCfg DataSourceConfig, fetcher L1TransactionFetcher, blobsFetcher L1BlobsFetcher, ref eth.L1BlockRef, batcherAddr common.Address) DataIter { +func NewBlobDataSource(ctx context.Context, log log.Logger, dsCfg DataSourceConfig, fetcher L1Fetcher, blobsFetcher L1BlobsFetcher, ref eth.L1BlockRef, batcherAddr common.Address) DataIter { return &BlobDataSource{ ref: ref, dsCfg: dsCfg, @@ -73,6 +73,51 @@ func (ds *BlobDataSource) Next(ctx context.Context) (eth.Data, error) { return data, nil } +// getTxSucceedMap returns a map indicating whether tx status is successful if useInboxContract; +// if !useInboxContract, nil map is returned to indicate that no status check is needed. +func getTxSucceedMap(ctx context.Context, useInboxContract bool, fetcher L1Fetcher, hash common.Hash) (txSucceeded map[common.Hash]bool, err error) { + if !useInboxContract { + return + } + _, receipts, err := fetcher.FetchReceipts(ctx, hash) + if err != nil { + return nil, NewTemporaryError(fmt.Errorf("failed to fetch L1 block info and receipts: %w", err)) + } + txSucceeded = make(map[common.Hash]bool) + for _, receipt := range receipts { + if receipt.Status == types.ReceiptStatusSuccessful { + txSucceeded[receipt.TxHash] = true + } + } + return +} + +// getTxSucceed returns all successful txs +func getTxSucceed(ctx context.Context, useInboxContract bool, fetcher L1Fetcher, hash common.Hash, txs types.Transactions) (successTxs types.Transactions, err error) { + if !useInboxContract { + // if !useInboxContract, all txs are considered successful + return txs, nil + } + _, receipts, err := fetcher.FetchReceipts(ctx, hash) + if err != nil { + return nil, NewTemporaryError(fmt.Errorf("failed to fetch L1 block info and receipts: %w", err)) + } + + txSucceeded := make(map[common.Hash]bool) + for _, receipt := range receipts { + if receipt.Status == types.ReceiptStatusSuccessful { + txSucceeded[receipt.TxHash] = true + } + } + successTxs = make(types.Transactions, 0) + for _, tx := range txs { + if _, ok := txSucceeded[tx.Hash()]; ok { + successTxs = append(successTxs, tx) + } + } + return successTxs, nil +} + // open fetches and returns the blob or calldata (as appropriate) from all valid batcher // transactions in the referenced block. Returns an empty (non-nil) array if no batcher // transactions are found. It returns ResetError if it cannot find the referenced block or a @@ -85,8 +130,12 @@ func (ds *BlobDataSource) open(ctx context.Context) ([]blobOrCalldata, error) { } return nil, NewTemporaryError(fmt.Errorf("failed to open blob data source: %w", err)) } + txSucceedMap, err := getTxSucceedMap(ctx, ds.dsCfg.useInboxContract, ds.fetcher, ds.ref.Hash) + if err != nil { + return nil, err + } - data, hashes := dataAndHashesFromTxs(txs, &ds.dsCfg, ds.batcherAddr, ds.log) + data, hashes := dataAndHashesFromTxs(txs, &ds.dsCfg, ds.batcherAddr, ds.log, txSucceedMap) if len(hashes) == 0 { // there are no blobs to fetch so we can return immediately @@ -115,13 +164,15 @@ func (ds *BlobDataSource) open(ctx context.Context) ([]blobOrCalldata, error) { // dataAndHashesFromTxs extracts calldata and datahashes from the input transactions and returns them. It // creates a placeholder blobOrCalldata element for each returned blob hash that must be populated // by fillBlobPointers after blob bodies are retrieved. -func dataAndHashesFromTxs(txs types.Transactions, config *DataSourceConfig, batcherAddr common.Address, logger log.Logger) ([]blobOrCalldata, []eth.IndexedBlobHash) { +func dataAndHashesFromTxs(txs types.Transactions, config *DataSourceConfig, batcherAddr common.Address, logger log.Logger, txSucceedMap map[common.Hash]bool) ([]blobOrCalldata, []eth.IndexedBlobHash) { data := []blobOrCalldata{} var hashes []eth.IndexedBlobHash blobIndex := 0 // index of each blob in the block's blob sidecar for _, tx := range txs { - // skip any non-batcher transactions - if !isValidBatchTx(tx, config.l1Signer, config.batchInboxAddress, batcherAddr, logger) { + // skip any non-batcher transactions or failed transactions + // blobIndex needs to be incremented for both invalid batch tx and failed tx + // if txSucceedMap is nil, it means no status check is needed. + if (!isValidBatchTx(tx, config.l1Signer, config.batchInboxAddress, batcherAddr, logger)) || (txSucceedMap != nil && !txSucceedMap[tx.Hash()]) { blobIndex += len(tx.BlobHashes()) continue } diff --git a/op-node/rollup/derive/blob_data_source_test.go b/op-node/rollup/derive/blob_data_source_test.go index a20205544c478..7c1c3fd7548e1 100644 --- a/op-node/rollup/derive/blob_data_source_test.go +++ b/op-node/rollup/derive/blob_data_source_test.go @@ -45,7 +45,7 @@ func TestDataAndHashesFromTxs(t *testing.T) { } calldataTx, _ := types.SignNewTx(privateKey, signer, txData) txs := types.Transactions{calldataTx} - data, blobHashes := dataAndHashesFromTxs(txs, &config, batcherAddr, logger) + data, blobHashes := dataAndHashesFromTxs(txs, &config, batcherAddr, logger, nil) require.Equal(t, 1, len(data)) require.Equal(t, 0, len(blobHashes)) @@ -60,14 +60,14 @@ func TestDataAndHashesFromTxs(t *testing.T) { } blobTx, _ := types.SignNewTx(privateKey, signer, blobTxData) txs = types.Transactions{blobTx} - data, blobHashes = dataAndHashesFromTxs(txs, &config, batcherAddr, logger) + data, blobHashes = dataAndHashesFromTxs(txs, &config, batcherAddr, logger, nil) require.Equal(t, 1, len(data)) require.Equal(t, 1, len(blobHashes)) require.Nil(t, data[0].calldata) // try again with both the blob & calldata transactions and make sure both are picked up txs = types.Transactions{blobTx, calldataTx} - data, blobHashes = dataAndHashesFromTxs(txs, &config, batcherAddr, logger) + data, blobHashes = dataAndHashesFromTxs(txs, &config, batcherAddr, logger, nil) require.Equal(t, 2, len(data)) require.Equal(t, 1, len(blobHashes)) require.NotNil(t, data[1].calldata) @@ -75,7 +75,7 @@ func TestDataAndHashesFromTxs(t *testing.T) { // make sure blob tx to the batch inbox is ignored if not signed by the batcher blobTx, _ = types.SignNewTx(testutils.RandomKey(), signer, blobTxData) txs = types.Transactions{blobTx} - data, blobHashes = dataAndHashesFromTxs(txs, &config, batcherAddr, logger) + data, blobHashes = dataAndHashesFromTxs(txs, &config, batcherAddr, logger, nil) require.Equal(t, 0, len(data)) require.Equal(t, 0, len(blobHashes)) @@ -84,7 +84,7 @@ func TestDataAndHashesFromTxs(t *testing.T) { blobTxData.To = testutils.RandomAddress(rng) blobTx, _ = types.SignNewTx(privateKey, signer, blobTxData) txs = types.Transactions{blobTx} - data, blobHashes = dataAndHashesFromTxs(txs, &config, batcherAddr, logger) + data, blobHashes = dataAndHashesFromTxs(txs, &config, batcherAddr, logger, nil) require.Equal(t, 0, len(data)) require.Equal(t, 0, len(blobHashes)) @@ -98,11 +98,48 @@ func TestDataAndHashesFromTxs(t *testing.T) { setCodeTx, err := types.SignNewTx(privateKey, signer, setCodeTxData) require.NoError(t, err) txs = types.Transactions{setCodeTx} - data, blobHashes = dataAndHashesFromTxs(txs, &config, batcherAddr, logger) + data, blobHashes = dataAndHashesFromTxs(txs, &config, batcherAddr, logger, nil) require.Equal(t, 0, len(data)) require.Equal(t, 0, len(blobHashes)) } +func TestBlockWithFailedBlobTx(t *testing.T) { + // test setup + rng := rand.New(rand.NewSource(12345)) + privateKey := testutils.InsecureRandomKey(rng) + publicKey, _ := privateKey.Public().(*ecdsa.PublicKey) + batcherAddr := crypto.PubkeyToAddress(*publicKey) + batchInboxAddr := testutils.RandomAddress(rng) + logger := testlog.Logger(t, log.LvlInfo) + + chainId := new(big.Int).SetUint64(rng.Uint64()) + signer := types.NewCancunSigner(chainId) + config := DataSourceConfig{ + l1Signer: signer, + batchInboxAddress: batchInboxAddr, + } + + // create two valid blob batcher txs + var txs types.Transactions + for i := 0; i < 2; i++ { + blobHash := testutils.RandomHash(rng) + blobTxData := &types.BlobTx{ + Nonce: rng.Uint64(), + Gas: 2_000_000, + To: batchInboxAddr, + Data: testutils.RandomData(rng, rng.Intn(1000)), + BlobHashes: []common.Hash{blobHash}, + } + blobTx, _ := types.SignNewTx(privateKey, signer, blobTxData) + txs = append(txs, blobTx) + } + + // mark the first blob tx as failed + txSucceedMap := map[common.Hash]bool{txs[1].Hash(): true} + _, blobHashes := dataAndHashesFromTxs(txs, &config, batcherAddr, logger, txSucceedMap) + // check the returned blob index is 1 + require.True(t, len(blobHashes) == 1 && blobHashes[0].Index == 1) +} func TestFillBlobPointers(t *testing.T) { blob := eth.Blob{} rng := rand.New(rand.NewSource(1234)) diff --git a/op-node/rollup/derive/calldata_source.go b/op-node/rollup/derive/calldata_source.go index 0e8147261e93e..776428dce37b8 100644 --- a/op-node/rollup/derive/calldata_source.go +++ b/op-node/rollup/derive/calldata_source.go @@ -24,7 +24,7 @@ type CalldataSource struct { // Required to re-attempt fetching ref eth.L1BlockRef dsCfg DataSourceConfig - fetcher L1TransactionFetcher + fetcher L1Fetcher log log.Logger batcherAddr common.Address @@ -32,7 +32,7 @@ type CalldataSource struct { // NewCalldataSource creates a new calldata source. It suppresses errors in fetching the L1 block if they occur. // If there is an error, it will attempt to fetch the result on the next call to `Next`. -func NewCalldataSource(ctx context.Context, log log.Logger, dsCfg DataSourceConfig, fetcher L1TransactionFetcher, ref eth.L1BlockRef, batcherAddr common.Address) DataIter { +func NewCalldataSource(ctx context.Context, log log.Logger, dsCfg DataSourceConfig, fetcher L1Fetcher, ref eth.L1BlockRef, batcherAddr common.Address) DataIter { _, txs, err := fetcher.InfoAndTxsByHash(ctx, ref.Hash) if err != nil { return &CalldataSource{ @@ -44,6 +44,17 @@ func NewCalldataSource(ctx context.Context, log log.Logger, dsCfg DataSourceConf batcherAddr: batcherAddr, } } + txs, err = getTxSucceed(ctx, dsCfg.useInboxContract, fetcher, ref.Hash, txs) + if err != nil { + return &CalldataSource{ + open: false, + ref: ref, + dsCfg: dsCfg, + fetcher: fetcher, + log: log, + batcherAddr: batcherAddr, + } + } return &CalldataSource{ open: true, data: DataFromEVMTransactions(dsCfg, batcherAddr, txs, log.New("origin", ref)), @@ -56,6 +67,10 @@ func NewCalldataSource(ctx context.Context, log log.Logger, dsCfg DataSourceConf func (ds *CalldataSource) Next(ctx context.Context) (eth.Data, error) { if !ds.open { if _, txs, err := ds.fetcher.InfoAndTxsByHash(ctx, ds.ref.Hash); err == nil { + txs, err := getTxSucceed(ctx, ds.dsCfg.useInboxContract, ds.fetcher, ds.ref.Hash, txs) + if err != nil { + return nil, err + } ds.open = true ds.data = DataFromEVMTransactions(ds.dsCfg, ds.batcherAddr, txs, ds.log) } else if errors.Is(err, ethereum.NotFound) { diff --git a/op-node/rollup/derive/calldata_source_test.go b/op-node/rollup/derive/calldata_source_test.go index 01b2616cca3fa..31555996ddbe3 100644 --- a/op-node/rollup/derive/calldata_source_test.go +++ b/op-node/rollup/derive/calldata_source_test.go @@ -121,7 +121,7 @@ func TestDataFromEVMTransactions(t *testing.T) { } } - out := DataFromEVMTransactions(DataSourceConfig{cfg.L1Signer(), cfg.BatchInboxAddress, false}, batcherAddr, txs, testlog.Logger(t, log.LevelCrit)) + out := DataFromEVMTransactions(DataSourceConfig{cfg.L1Signer(), cfg.BatchInboxAddress, false, false}, batcherAddr, txs, testlog.Logger(t, log.LevelCrit)) require.ElementsMatch(t, expectedData, out) } diff --git a/op-node/rollup/derive/data_source.go b/op-node/rollup/derive/data_source.go index dfeda599501a1..a86e0de4b98ea 100644 --- a/op-node/rollup/derive/data_source.go +++ b/op-node/rollup/derive/data_source.go @@ -52,6 +52,7 @@ func NewDataSourceFactory(log log.Logger, cfg *rollup.Config, fetcher L1Fetcher, l1Signer: cfg.L1Signer(), batchInboxAddress: cfg.BatchInboxAddress, altDAEnabled: cfg.AltDAEnabled(), + useInboxContract: cfg.UseInboxContract(), } return &DataSourceFactory{ log: log, @@ -88,6 +89,7 @@ type DataSourceConfig struct { l1Signer types.Signer batchInboxAddress common.Address altDAEnabled bool + useInboxContract bool } // isValidBatchTx returns true if: diff --git a/op-node/rollup/types.go b/op-node/rollup/types.go index 316ba00c44c72..d8de47cf4c523 100644 --- a/op-node/rollup/types.go +++ b/op-node/rollup/types.go @@ -150,6 +150,8 @@ type Config struct { // If missing, it is loaded by the op-node from the embedded superchain config at startup. ChainOpConfig *params.OptimismConfig `json:"chain_op_config,omitempty"` + InboxContractConfig *InboxContractConfig `json:"inbox_contract_config,omitempty"` + // Optional Features // AltDAConfig. We are in the process of migrating to the AltDAConfig from these legacy top level values @@ -164,6 +166,15 @@ type Config struct { PectraBlobScheduleTime *uint64 `json:"pectra_blob_schedule_time,omitempty"` } +type InboxContractConfig struct { + UseInboxContract bool `json:"use_inbox_contract,omitempty"` +} + +// UseInboxContract returns whether inbox contract is enabled +func (cfg *Config) UseInboxContract() bool { + return cfg.InboxContractConfig != nil && cfg.InboxContractConfig.UseInboxContract +} + // ValidateL1Config checks L1 config variables for errors. func (cfg *Config) ValidateL1Config(ctx context.Context, client L1Client) error { // Validate the L1 Client Chain ID @@ -734,6 +745,7 @@ func (c *Config) Description(l2Chains map[string]string) string { c.forEachFork(func(name string, _ string, time *uint64) { banner += fmt.Sprintf(" - %v: %s\n", name, fmtForkTimeOrUnset(time)) }) + banner += fmt.Sprintf(" - Use inbox contract: %v\n", c.UseInboxContract()) // Report the protocol version banner += fmt.Sprintf("Node supports up to OP-Stack Protocol Version: %s\n", OPStackSupport) if c.AltDAConfig != nil { From ab6964d73b83c2c429ac3bdcb4e1658b628b3c94 Mon Sep 17 00:00:00 2001 From: blockchaindevsh Date: Tue, 15 Jul 2025 09:26:55 +0800 Subject: [PATCH 4/4] fix rollup config --- op-chain-ops/genesis/config.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/op-chain-ops/genesis/config.go b/op-chain-ops/genesis/config.go index d428d5bdfa61a..762e6f0c8f799 100644 --- a/op-chain-ops/genesis/config.go +++ b/op-chain-ops/genesis/config.go @@ -1059,9 +1059,11 @@ func (d *DeployConfig) RollupConfig(l1StartBlock *eth.BlockRef, l2GenesisBlockHa } chainOpConfig := ¶ms.OptimismConfig{ - EIP1559Elasticity: d.EIP1559Elasticity, - EIP1559Denominator: d.EIP1559Denominator, - EIP1559DenominatorCanyon: &d.EIP1559DenominatorCanyon, + EIP1559Elasticity: d.EIP1559Elasticity, + EIP1559Denominator: d.EIP1559Denominator, + EIP1559DenominatorCanyon: &d.EIP1559DenominatorCanyon, + L1BaseFeeScalarMultiplier: d.L1BaseFeeScalarMultiplier, + L1BlobBaseFeeScalarMultiplier: d.L1BlobBaseFeeScalarMultiplier, } var altDA *rollup.AltDAConfig