diff --git a/.gitmodules b/.gitmodules index 1591d79b2a8..94ed18462b5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -32,3 +32,9 @@ [submodule "packages/contracts-bedrock/lib/superchain-registry"] path = packages/contracts-bedrock/lib/superchain-registry url = https://github.com/ethereum-optimism/superchain-registry +[submodule "packages/contracts-bedrock/lib/espresso-tee-contracts"] + path = packages/contracts-bedrock/lib/espresso-tee-contracts + url = https://github.com/EspressoSystems/espresso-tee-contracts +[submodule "packages/contracts-bedrock/lib/openzeppelin-contracts-upgradeable-v5"] + path = packages/contracts-bedrock/lib/openzeppelin-contracts-upgradeable-v5 + url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable diff --git a/espresso/bindings/batch_authenticator.go b/espresso/bindings/batch_authenticator.go new file mode 100644 index 00000000000..3e72edfe49c --- /dev/null +++ b/espresso/bindings/batch_authenticator.go @@ -0,0 +1,2277 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package bindings + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// BatchAuthenticatorMetaData contains all meta data concerning the BatchAuthenticator contract. +var BatchAuthenticatorMetaData = &bind.MetaData{ + ABI: "[{\"type\":\"constructor\",\"inputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"acceptOwnership\",\"inputs\":[],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"activeIsEspresso\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"addGuardian\",\"inputs\":[{\"name\":\"guardian\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"authenticateBatchInfo\",\"inputs\":[{\"name\":\"_commitment\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"_signature\",\"type\":\"bytes\",\"internalType\":\"bytes\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"espressoBatcher\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"espressoBatcherAt\",\"inputs\":[{\"name\":\"_index\",\"type\":\"uint32\",\"internalType\":\"uint32\"}],\"outputs\":[{\"name\":\"batcher_\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"fromBlock_\",\"type\":\"uint64\",\"internalType\":\"uint64\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"espressoBatcherAtBlock\",\"inputs\":[{\"name\":\"_l1Block\",\"type\":\"uint64\",\"internalType\":\"uint64\"}],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"espressoBatcherHistoryLength\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"espressoTEEVerifier\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"contractIEspressoTEEVerifier\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"getGuardians\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address[]\",\"internalType\":\"address[]\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"guardianCount\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"initVersion\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint8\",\"internalType\":\"uint8\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"initialize\",\"inputs\":[{\"name\":\"_espressoTEEVerifier\",\"type\":\"address\",\"internalType\":\"contractIEspressoTEEVerifier\"},{\"name\":\"_espressoBatcher\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"_systemConfig\",\"type\":\"address\",\"internalType\":\"contractISystemConfig\"},{\"name\":\"_owner\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"_activeIsEspresso\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"isGuardian\",\"inputs\":[{\"name\":\"account\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"nitroValidator\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"owner\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"pendingOwner\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"proxyAdmin\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"contractIProxyAdmin\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"proxyAdminOwner\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"registerSigner\",\"inputs\":[{\"name\":\"_verificationData\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"_data\",\"type\":\"bytes\",\"internalType\":\"bytes\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"removeGuardian\",\"inputs\":[{\"name\":\"guardian\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"renounceOwnership\",\"inputs\":[],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"setActiveIsEspresso\",\"inputs\":[{\"name\":\"_desired\",\"type\":\"bool\",\"internalType\":\"bool\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"setEspressoBatcher\",\"inputs\":[{\"name\":\"_newEspressoBatcher\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"systemConfig\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"contractISystemConfig\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"transferOwnership\",\"inputs\":[{\"name\":\"newOwner\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"version\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"string\",\"internalType\":\"string\"}],\"stateMutability\":\"view\"},{\"type\":\"event\",\"name\":\"BatchInfoAuthenticated\",\"inputs\":[{\"name\":\"commitment\",\"type\":\"bytes32\",\"indexed\":false,\"internalType\":\"bytes32\"},{\"name\":\"caller\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"BatcherSwitched\",\"inputs\":[{\"name\":\"activeIsEspresso\",\"type\":\"bool\",\"indexed\":true,\"internalType\":\"bool\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"EspressoBatcherUpdated\",\"inputs\":[{\"name\":\"oldEspressoBatcher\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"newEspressoBatcher\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"fromBlock\",\"type\":\"uint64\",\"indexed\":true,\"internalType\":\"uint64\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"GuardianAdded\",\"inputs\":[{\"name\":\"guardian\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"GuardianRemoved\",\"inputs\":[{\"name\":\"guardian\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"Initialized\",\"inputs\":[{\"name\":\"version\",\"type\":\"uint64\",\"indexed\":false,\"internalType\":\"uint64\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"OwnershipTransferStarted\",\"inputs\":[{\"name\":\"previousOwner\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"newOwner\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"}],\"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\":\"SignerRegistrationInitiated\",\"inputs\":[{\"name\":\"caller\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"}],\"anonymous\":false},{\"type\":\"error\",\"name\":\"CheckpointUnorderedInsertion\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"InvalidAddress\",\"inputs\":[{\"name\":\"contract_\",\"type\":\"address\",\"internalType\":\"address\"}]},{\"type\":\"error\",\"name\":\"InvalidGuardianAddress\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"InvalidInitialization\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"InvalidSignature\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"NoChange\",\"inputs\":[{\"name\":\"batcher\",\"type\":\"address\",\"internalType\":\"address\"}]},{\"type\":\"error\",\"name\":\"NotGuardian\",\"inputs\":[{\"name\":\"caller\",\"type\":\"address\",\"internalType\":\"address\"}]},{\"type\":\"error\",\"name\":\"NotGuardianOrOwner\",\"inputs\":[{\"name\":\"caller\",\"type\":\"address\",\"internalType\":\"address\"}]},{\"type\":\"error\",\"name\":\"NotInitializing\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"OwnableInvalidOwner\",\"inputs\":[{\"name\":\"owner\",\"type\":\"address\",\"internalType\":\"address\"}]},{\"type\":\"error\",\"name\":\"OwnableUnauthorizedAccount\",\"inputs\":[{\"name\":\"account\",\"type\":\"address\",\"internalType\":\"address\"}]},{\"type\":\"error\",\"name\":\"OwnerCantBeGuardian\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ProxyAdminOwnedBase_NotProxyAdmin\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ProxyAdminOwnedBase_NotProxyAdminOwner\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ProxyAdminOwnedBase_NotResolvedDelegateProxy\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ProxyAdminOwnedBase_NotSharedProxyAdminOwner\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ProxyAdminOwnedBase_ProxyAdminNotFound\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ReinitializableBase_ZeroInitVersion\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"UnauthorizedEspressoBatcher\",\"inputs\":[{\"name\":\"sender\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"expected\",\"type\":\"address\",\"internalType\":\"address\"}]},{\"type\":\"error\",\"name\":\"UnauthorizedFallbackBatcher\",\"inputs\":[{\"name\":\"sender\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"expected\",\"type\":\"address\",\"internalType\":\"address\"}]}]", + Bin: "0x60a060405234801561000f575f5ffd5b50600160805261001d610022565b6100d4565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00805468010000000000000000900460ff16156100725760405163f92ee8a960e01b815260040160405180910390fd5b80546001600160401b03908116146100d15780546001600160401b0319166001600160401b0390811782556040519081527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50565b60805161250d6100f35f395f818161025e0152610e51015261250d5ff3fe608060405234801561000f575f5ffd5b50600436106101b0575f3560e01c806379ba5097116100f3578063e30c397811610093578063fa14fe6d1161006e578063fa14fe6d146103bd578063fc5b5fda146103dc578063fc619e41146103ef578063fd402af714610402575f5ffd5b8063e30c39781461037e578063eca919df14610386578063f2fde38b146103aa575f5ffd5b80638da5cb5b116100ce5780638da5cb5b14610348578063a526d83b14610350578063ba58e82a14610363578063dad544e014610376575f5ffd5b806379ba5097146103255780637d531a781461032d57806388da3bb714610340575f5ffd5b80633e47158c1161015e57806354fd4d501161013957806354fd4d50146102ae5780636c076871146102f7578063714041561461030a578063715018a61461031d575f5ffd5b80633e47158c146102885780634268ecaa1461029057806354387ad7146102a6575f5ffd5b80632ce532471161018e5780632ce532471461022257806333d7e2bd1461023757806338d38c9714610257575f5ffd5b80630665f04b146101b45780630c68ba21146101d25780631b076a4c146101f5575b5f5ffd5b6101bc61044a565b6040516101c99190611fda565b60405180910390f35b6101e56101e0366004612053565b61047a565b60405190151581526020016101c9565b6101fd6104d5565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020016101c9565b610235610230366004612053565b610563565b005b6001546101fd9073ffffffffffffffffffffffffffffffffffffffff1681565b60405160ff7f00000000000000000000000000000000000000000000000000000000000000001681526020016101c9565b6101fd610671565b610298610877565b6040519081526020016101c9565b610298610881565b6102ea6040518060400160405280600581526020017f312e322e3000000000000000000000000000000000000000000000000000000081525081565b6040516101c9919061206e565b6102356103053660046120ce565b6108ab565b610235610318366004612053565b6109e9565b610235610a68565b610235610a7b565b6101fd61033b3660046120e9565b610af3565b6101fd610b09565b6101fd610b14565b61023561035e366004612053565b610b1d565b610235610371366004612155565b610c91565b6101fd610d4b565b6101fd610d9c565b5f546101e59074010000000000000000000000000000000000000000900460ff1681565b6102356103b8366004612053565b610ddd565b5f546101fd9073ffffffffffffffffffffffffffffffffffffffff1681565b6102356103ea3660046121c1565b610e4f565b6102356103fd36600461222e565b6111bd565b610415610410366004612276565b611468565b6040805173ffffffffffffffffffffffffffffffffffffffff909316835267ffffffffffffffff9091166020830152016101c9565b60606104757f0f4ac8aae5a4fa6a3612928fcd8255b475ff86b500ae30bb272e61542cfc6f00611488565b905090565b5f6104cf827f0f4ac8aae5a4fa6a3612928fcd8255b475ff86b500ae30bb272e61542cfc6f005b9073ffffffffffffffffffffffffffffffffffffffff165f9081526001919091016020526040902054151590565b92915050565b5f5f5f9054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663d80a4c286040518163ffffffff1660e01b8152600401602060405180830381865afa15801561053f573d5f5f3e3d5ffd5b505050506040513d601f19601f820116820180604052508101906104759190612299565b61056b61149b565b5f610574610b09565b90508073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16036105f8576040517f81efeac100000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff831660048201526024015b60405180910390fd5b43610605600282856114f3565b50508067ffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fcf8f6f62babb05dd1d159c090ad8429ee0df72e16c82701004a5405f908cb0f560405160405180910390a4505050565b5f8061069b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b905073ffffffffffffffffffffffffffffffffffffffff8116156106be57919050565b6040518060400160405280601a81526020017f4f564d5f4c3143726f7373446f6d61696e4d657373656e67657200000000000081525051600261070191906122e1565b604080513060208201525f918101919091527f4f564d5f4c3143726f7373446f6d61696e4d657373656e676572000000000000919091179061075b906060015b604051602081830303815290604052805190602001205490565b14610792576040517f54e433cd00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b604080513060208201526001918101919091525f906107b390606001610741565b905073ffffffffffffffffffffffffffffffffffffffff811615610845578073ffffffffffffffffffffffffffffffffffffffff16638da5cb5b6040518163ffffffff1660e01b8152600401602060405180830381865afa15801561081a573d5f5f3e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061083e9190612299565b9250505090565b6040517f332144db00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f61047560025490565b5f6104757f0f4ac8aae5a4fa6a3612928fcd8255b475ff86b500ae30bb272e61542cfc6f0061150d565b6108d5337f0f4ac8aae5a4fa6a3612928fcd8255b475ff86b500ae30bb272e61542cfc6f006104a1565b15801561091557506108e5610b14565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614155b1561094e576040517fd53780c40000000000000000000000000000000000000000000000000000000081523360048201526024016105ef565b5f5460ff74010000000000000000000000000000000000000000909104161515811515146109e6575f80547fffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffff167401000000000000000000000000000000000000000083151590810291909117825560405190917fb957d7fc29e5974594db2f2e132076d52f42c0734eae05fd5ea080d1ba175ad391a25b50565b6109f161149b565b7f0f4ac8aae5a4fa6a3612928fcd8255b475ff86b500ae30bb272e61542cfc6f00610a1c8183611516565b610a24575050565b60405173ffffffffffffffffffffffffffffffffffffffff8316907fb8107d0c6b40be480ce3172ee66ba6d64b71f6b1685a851340036e6e2e3e3c52905f90a25050565b610a7061149b565b610a795f611537565b565b3380610a85610d9c565b73ffffffffffffffffffffffffffffffffffffffff1614610aea576040517f118cdaa700000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff821660048201526024016105ef565b6109e681611537565b5f6104cf600267ffffffffffffffff8416611587565b5f6104756002611653565b5f6104756116a0565b610b2561149b565b73ffffffffffffffffffffffffffffffffffffffff8116610b72576040517f1b08105400000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b610b7a610b14565b73ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff161480610be55750610bb6610d9c565b73ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff16145b15610c1c576040517f3af3c41c00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b7f0f4ac8aae5a4fa6a3612928fcd8255b475ff86b500ae30bb272e61542cfc6f00610c4781836116c8565b15610c8d5760405173ffffffffffffffffffffffffffffffffffffffff8316907f038596bb31e2e7d3d9f184d4c98b310103f6d7f5830e5eec32bffe6f1728f969905f90a25b5050565b5f80546040517fdac79fc800000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff9091169163dac79fc891610cee91889188918891889190600401612378565b5f604051808303815f87803b158015610d05575f5ffd5b505af1158015610d17573d5f5f3e3d5ffd5b50506040513392507f665b016a0ac50d1280744eaaff1cf21254d0fd30e4c3987d291913c32163416c91505f90a250505050565b5f610d54610671565b73ffffffffffffffffffffffffffffffffffffffff16638da5cb5b6040518163ffffffff1660e01b8152600401602060405180830381865afa15801561053f573d5f5f3e3d5ffd5b5f807f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c005b5473ffffffffffffffffffffffffffffffffffffffff1692915050565b610de561149b565b610e0f817f0f4ac8aae5a4fa6a3612928fcd8255b475ff86b500ae30bb272e61542cfc6f006104a1565b15610e46576040517f3af3c41c00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6109e6816116e9565b7f000000000000000000000000000000000000000000000000000000000000000060ff165f610e7c6117a0565b805490915068010000000000000000900460ff1680610ea95750805467ffffffffffffffff808416911610155b15610ee0576040517ff92ee8a900000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b80547fffffffffffffffffffffffffffffffffffffffffffffff0000000000000000001667ffffffffffffffff83161768010000000000000000178155610f256117c8565b610f2e84611849565b73ffffffffffffffffffffffffffffffffffffffff8616610f93576040517f8e4c8aa600000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff871660048201526024016105ef565b73ffffffffffffffffffffffffffffffffffffffff8516610ff8576040517f8e4c8aa600000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff861660048201526024016105ef565b73ffffffffffffffffffffffffffffffffffffffff871661105d576040517f8e4c8aa600000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff881660048201526024016105ef565b5f8054600180547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff8981169190911790915589167fffffffffffffffffffffff0000000000000000000000000000000000000000009091161774010000000000000000000000000000000000000000851515021790556002545f0361115157436110ff600282896114f3565b505060405167ffffffffffffffff82169073ffffffffffffffffffffffffffffffffffffffff8916905f907fcf8f6f62babb05dd1d159c090ad8429ee0df72e16c82701004a5405f908cb0f5908290a4505b80547fffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffff16815560405167ffffffffffffffff831681527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a150505050505050565b5f5474010000000000000000000000000000000000000000900460ff161561132c575f6111e8610b09565b90503373ffffffffffffffffffffffffffffffffffffffff821614611257576040517f8d1db98a00000000000000000000000000000000000000000000000000000000815233600482015273ffffffffffffffffffffffffffffffffffffffff821660248201526044016105ef565b5f80546040517fa81d9c5c00000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff9091169163a81d9c5c916112b191879187918a916004016123b8565b602060405180830381865afa1580156112cc573d5f5f3e3d5ffd5b505050506040513d601f19601f820116820180604052508101906112f091906123e9565b611326576040517f8baa579f00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5061142e565b600154604080517fe81b2c6d00000000000000000000000000000000000000000000000000000000815290515f9273ffffffffffffffffffffffffffffffffffffffff169163e81b2c6d9160048083019260209291908290030181865afa158015611399573d5f5f3e3d5ffd5b505050506040513d601f19601f820116820180604052508101906113bd9190612404565b90503373ffffffffffffffffffffffffffffffffffffffff82161461142c576040517f51f905ea00000000000000000000000000000000000000000000000000000000815233600482015273ffffffffffffffffffffffffffffffffffffffff821660248201526044016105ef565b505b60405183815233907f731978a77d438b0ea35a9034fb28d9cf9372e1649f18c213110adcfab65c5c5c9060200160405180910390a2505050565b5f808061147660028561185a565b60208101519051909590945092505050565b60605f611494836118e3565b9392505050565b336114a4610b14565b73ffffffffffffffffffffffffffffffffffffffff1614610a79576040517f118cdaa70000000000000000000000000000000000000000000000000000000081523360048201526024016105ef565b5f8061150085858561193c565b915091505b935093915050565b5f6104cf825490565b5f6114948373ffffffffffffffffffffffffffffffffffffffff8416611b41565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080547fffffffffffffffffffffffff0000000000000000000000000000000000000000168155610c8d82611c24565b81545f90818160058111156115e9575f6115a084611cb9565b6115aa908561241b565b5f888152602090209091508101546bffffffffffffffffffffffff90811690871610156115d9578091506115e7565b6115e481600161242e565b92505b505b5f6115f687878585611d9d565b905080156116465761161a8761160d60018461241b565b5f91825260209091200190565b546c01000000000000000000000000900473ffffffffffffffffffffffffffffffffffffffff16611648565b5f5b979650505050505050565b80545f9080156116985761166c8361160d60018461241b565b546c01000000000000000000000000900473ffffffffffffffffffffffffffffffffffffffff16611494565b5f9392505050565b5f807f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300610dc0565b5f6114948373ffffffffffffffffffffffffffffffffffffffff8416611e08565b6116f161149b565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff8316908117825561175a610b14565b73ffffffffffffffffffffffffffffffffffffffff167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a35050565b5f807ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a006104cf565b336117d1610671565b73ffffffffffffffffffffffffffffffffffffffff16141580156118125750336117f9610d4b565b73ffffffffffffffffffffffffffffffffffffffff1614155b15610a79576040517fc4050a2600000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b611851611e54565b6109e681611e92565b604080518082019091525f8082526020820152825f018263ffffffff168154811061188757611887612441565b5f918252602091829020604080518082019091529101546bffffffffffffffffffffffff811682526c01000000000000000000000000900473ffffffffffffffffffffffffffffffffffffffff16918101919091529392505050565b6060815f0180548060200260200160405190810160405280929190818152602001828054801561193057602002820191905f5260205f20905b81548152602001906001019080831161191c575b50505050509050919050565b82545f9081908015611acb575f6119588761160d60018561241b565b6040805180820190915290546bffffffffffffffffffffffff8082168084526c0100000000000000000000000090920473ffffffffffffffffffffffffffffffffffffffff16602084015291925090871610156119e1576040517f2520601d00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b80516bffffffffffffffffffffffff808816911603611a4f5784611a0a8861160d60018661241b565b805473ffffffffffffffffffffffffffffffffffffffff929092166c01000000000000000000000000026bffffffffffffffffffffffff909216919091179055611abb565b604080518082019091526bffffffffffffffffffffffff808816825273ffffffffffffffffffffffffffffffffffffffff80881660208085019182528b54600181018d555f8d815291909120945191519092166c01000000000000000000000000029216919091179101555b6020015192508391506115059050565b5050604080518082019091526bffffffffffffffffffffffff808516825273ffffffffffffffffffffffffffffffffffffffff80851660208085019182528854600181018a555f8a8152918220955192519093166c01000000000000000000000000029190931617920191909155905081611505565b5f8181526001830160205260408120548015611c1b575f611b6360018361241b565b85549091505f90611b769060019061241b565b9050808214611bd5575f865f018281548110611b9457611b94612441565b905f5260205f200154905080875f018481548110611bb457611bb4612441565b5f918252602080832090910192909255918252600188019052604090208390555b8554869080611be657611be661246e565b600190038181905f5260205f20015f90559055856001015f8681526020019081526020015f205f9055600193505050506104cf565b5f9150506104cf565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930080547fffffffffffffffffffffffff0000000000000000000000000000000000000000811673ffffffffffffffffffffffffffffffffffffffff848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b5f815f03611cc857505f919050565b5f6001611cd484611ea3565b901c6001901b90506001818481611ced57611ced61249b565b048201901c90506001818481611d0557611d0561249b565b048201901c90506001818481611d1d57611d1d61249b565b048201901c90506001818481611d3557611d3561249b565b048201901c90506001818481611d4d57611d4d61249b565b048201901c90506001818481611d6557611d6561249b565b048201901c90506001818481611d7d57611d7d61249b565b048201901c905061149481828581611d9757611d9761249b565b04611f36565b5f5b81831015611e00575f611db28484611f4b565b5f878152602090209091506bffffffffffffffffffffffff8616908201546bffffffffffffffffffffffff161115611dec57809250611dfa565b611df781600161242e565b93505b50611d9f565b509392505050565b5f818152600183016020526040812054611e4d57508154600181810184555f8481526020808220909301849055845484825282860190935260409020919091556104cf565b505f6104cf565b611e5c611f65565b610a79576040517fd7e6bcf800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b611e9a611e54565b6109e681611f83565b5f80608083901c15611eb757608092831c92015b604083901c15611ec957604092831c92015b602083901c15611edb57602092831c92015b601083901c15611eed57601092831c92015b600883901c15611eff57600892831c92015b600483901c15611f1157600492831c92015b600283901c15611f2357600292831c92015b600183901c156104cf5760010192915050565b5f818310611f445781611494565b5090919050565b5f611f5960028484186124c8565b6114949084841661242e565b5f611f6e6117a0565b5468010000000000000000900460ff16919050565b611f8b611e54565b73ffffffffffffffffffffffffffffffffffffffff8116610aea576040517f1e4fbdf70000000000000000000000000000000000000000000000000000000081525f60048201526024016105ef565b602080825282518282018190525f918401906040840190835b8181101561202757835173ffffffffffffffffffffffffffffffffffffffff16835260209384019390920191600101611ff3565b509095945050505050565b73ffffffffffffffffffffffffffffffffffffffff811681146109e6575f5ffd5b5f60208284031215612063575f5ffd5b813561149481612032565b602081525f82518060208401528060208501604085015e5f6040828501015260407fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f83011684010191505092915050565b80151581146109e6575f5ffd5b5f602082840312156120de575f5ffd5b8135611494816120c1565b5f602082840312156120f9575f5ffd5b813567ffffffffffffffff81168114611494575f5ffd5b5f5f83601f840112612120575f5ffd5b50813567ffffffffffffffff811115612137575f5ffd5b60208301915083602082850101111561214e575f5ffd5b9250929050565b5f5f5f5f60408587031215612168575f5ffd5b843567ffffffffffffffff81111561217e575f5ffd5b61218a87828801612110565b909550935050602085013567ffffffffffffffff8111156121a9575f5ffd5b6121b587828801612110565b95989497509550505050565b5f5f5f5f5f60a086880312156121d5575f5ffd5b85356121e081612032565b945060208601356121f081612032565b9350604086013561220081612032565b9250606086013561221081612032565b91506080860135612220816120c1565b809150509295509295909350565b5f5f5f60408486031215612240575f5ffd5b83359250602084013567ffffffffffffffff81111561225d575f5ffd5b61226986828701612110565b9497909650939450505050565b5f60208284031215612286575f5ffd5b813563ffffffff81168114611494575f5ffd5b5f602082840312156122a9575f5ffd5b815161149481612032565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b80820281158282048414176104cf576104cf6122b4565b81835281816020850137505f602082840101525f60207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f840116840101905092915050565b60018110612374577f4e487b71000000000000000000000000000000000000000000000000000000005f52602160045260245ffd5b9052565b606081525f61238b6060830187896122f8565b828103602084015261239e8186886122f8565b9150506123ae604083018461233f565b9695505050505050565b606081525f6123cb6060830186886122f8565b90508360208301526123e0604083018461233f565b95945050505050565b5f602082840312156123f9575f5ffd5b8151611494816120c1565b5f60208284031215612414575f5ffd5b5051919050565b818103818111156104cf576104cf6122b4565b808201808211156104cf576104cf6122b4565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603160045260245ffd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601260045260245ffd5b5f826124fb577f4e487b71000000000000000000000000000000000000000000000000000000005f52601260045260245ffd5b50049056fea164736f6c634300081c000a", +} + +// BatchAuthenticatorABI is the input ABI used to generate the binding from. +// Deprecated: Use BatchAuthenticatorMetaData.ABI instead. +var BatchAuthenticatorABI = BatchAuthenticatorMetaData.ABI + +// BatchAuthenticatorBin is the compiled bytecode used for deploying new contracts. +// Deprecated: Use BatchAuthenticatorMetaData.Bin instead. +var BatchAuthenticatorBin = BatchAuthenticatorMetaData.Bin + +// DeployBatchAuthenticator deploys a new Ethereum contract, binding an instance of BatchAuthenticator to it. +func DeployBatchAuthenticator(auth *bind.TransactOpts, backend bind.ContractBackend) (common.Address, *types.Transaction, *BatchAuthenticator, error) { + parsed, err := BatchAuthenticatorMetaData.GetAbi() + if err != nil { + return common.Address{}, nil, nil, err + } + if parsed == nil { + return common.Address{}, nil, nil, errors.New("GetABI returned nil") + } + + address, tx, contract, err := bind.DeployContract(auth, *parsed, common.FromHex(BatchAuthenticatorBin), backend) + if err != nil { + return common.Address{}, nil, nil, err + } + return address, tx, &BatchAuthenticator{BatchAuthenticatorCaller: BatchAuthenticatorCaller{contract: contract}, BatchAuthenticatorTransactor: BatchAuthenticatorTransactor{contract: contract}, BatchAuthenticatorFilterer: BatchAuthenticatorFilterer{contract: contract}}, nil +} + +// BatchAuthenticator is an auto generated Go binding around an Ethereum contract. +type BatchAuthenticator struct { + BatchAuthenticatorCaller // Read-only binding to the contract + BatchAuthenticatorTransactor // Write-only binding to the contract + BatchAuthenticatorFilterer // Log filterer for contract events +} + +// BatchAuthenticatorCaller is an auto generated read-only Go binding around an Ethereum contract. +type BatchAuthenticatorCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// BatchAuthenticatorTransactor is an auto generated write-only Go binding around an Ethereum contract. +type BatchAuthenticatorTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// BatchAuthenticatorFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type BatchAuthenticatorFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// BatchAuthenticatorSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type BatchAuthenticatorSession struct { + Contract *BatchAuthenticator // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// BatchAuthenticatorCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type BatchAuthenticatorCallerSession struct { + Contract *BatchAuthenticatorCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// BatchAuthenticatorTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type BatchAuthenticatorTransactorSession struct { + Contract *BatchAuthenticatorTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// BatchAuthenticatorRaw is an auto generated low-level Go binding around an Ethereum contract. +type BatchAuthenticatorRaw struct { + Contract *BatchAuthenticator // Generic contract binding to access the raw methods on +} + +// BatchAuthenticatorCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type BatchAuthenticatorCallerRaw struct { + Contract *BatchAuthenticatorCaller // Generic read-only contract binding to access the raw methods on +} + +// BatchAuthenticatorTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type BatchAuthenticatorTransactorRaw struct { + Contract *BatchAuthenticatorTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewBatchAuthenticator creates a new instance of BatchAuthenticator, bound to a specific deployed contract. +func NewBatchAuthenticator(address common.Address, backend bind.ContractBackend) (*BatchAuthenticator, error) { + contract, err := bindBatchAuthenticator(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &BatchAuthenticator{BatchAuthenticatorCaller: BatchAuthenticatorCaller{contract: contract}, BatchAuthenticatorTransactor: BatchAuthenticatorTransactor{contract: contract}, BatchAuthenticatorFilterer: BatchAuthenticatorFilterer{contract: contract}}, nil +} + +// NewBatchAuthenticatorCaller creates a new read-only instance of BatchAuthenticator, bound to a specific deployed contract. +func NewBatchAuthenticatorCaller(address common.Address, caller bind.ContractCaller) (*BatchAuthenticatorCaller, error) { + contract, err := bindBatchAuthenticator(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &BatchAuthenticatorCaller{contract: contract}, nil +} + +// NewBatchAuthenticatorTransactor creates a new write-only instance of BatchAuthenticator, bound to a specific deployed contract. +func NewBatchAuthenticatorTransactor(address common.Address, transactor bind.ContractTransactor) (*BatchAuthenticatorTransactor, error) { + contract, err := bindBatchAuthenticator(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &BatchAuthenticatorTransactor{contract: contract}, nil +} + +// NewBatchAuthenticatorFilterer creates a new log filterer instance of BatchAuthenticator, bound to a specific deployed contract. +func NewBatchAuthenticatorFilterer(address common.Address, filterer bind.ContractFilterer) (*BatchAuthenticatorFilterer, error) { + contract, err := bindBatchAuthenticator(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &BatchAuthenticatorFilterer{contract: contract}, nil +} + +// bindBatchAuthenticator binds a generic wrapper to an already deployed contract. +func bindBatchAuthenticator(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := BatchAuthenticatorMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_BatchAuthenticator *BatchAuthenticatorRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _BatchAuthenticator.Contract.BatchAuthenticatorCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_BatchAuthenticator *BatchAuthenticatorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _BatchAuthenticator.Contract.BatchAuthenticatorTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_BatchAuthenticator *BatchAuthenticatorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _BatchAuthenticator.Contract.BatchAuthenticatorTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_BatchAuthenticator *BatchAuthenticatorCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _BatchAuthenticator.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_BatchAuthenticator *BatchAuthenticatorTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _BatchAuthenticator.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_BatchAuthenticator *BatchAuthenticatorTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _BatchAuthenticator.Contract.contract.Transact(opts, method, params...) +} + +// ActiveIsEspresso is a free data retrieval call binding the contract method 0xeca919df. +// +// Solidity: function activeIsEspresso() view returns(bool) +func (_BatchAuthenticator *BatchAuthenticatorCaller) ActiveIsEspresso(opts *bind.CallOpts) (bool, error) { + var out []interface{} + err := _BatchAuthenticator.contract.Call(opts, &out, "activeIsEspresso") + + if err != nil { + return *new(bool), err + } + + out0 := *abi.ConvertType(out[0], new(bool)).(*bool) + + return out0, err + +} + +// ActiveIsEspresso is a free data retrieval call binding the contract method 0xeca919df. +// +// Solidity: function activeIsEspresso() view returns(bool) +func (_BatchAuthenticator *BatchAuthenticatorSession) ActiveIsEspresso() (bool, error) { + return _BatchAuthenticator.Contract.ActiveIsEspresso(&_BatchAuthenticator.CallOpts) +} + +// ActiveIsEspresso is a free data retrieval call binding the contract method 0xeca919df. +// +// Solidity: function activeIsEspresso() view returns(bool) +func (_BatchAuthenticator *BatchAuthenticatorCallerSession) ActiveIsEspresso() (bool, error) { + return _BatchAuthenticator.Contract.ActiveIsEspresso(&_BatchAuthenticator.CallOpts) +} + +// EspressoBatcher is a free data retrieval call binding the contract method 0x88da3bb7. +// +// Solidity: function espressoBatcher() view returns(address) +func (_BatchAuthenticator *BatchAuthenticatorCaller) EspressoBatcher(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _BatchAuthenticator.contract.Call(opts, &out, "espressoBatcher") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// EspressoBatcher is a free data retrieval call binding the contract method 0x88da3bb7. +// +// Solidity: function espressoBatcher() view returns(address) +func (_BatchAuthenticator *BatchAuthenticatorSession) EspressoBatcher() (common.Address, error) { + return _BatchAuthenticator.Contract.EspressoBatcher(&_BatchAuthenticator.CallOpts) +} + +// EspressoBatcher is a free data retrieval call binding the contract method 0x88da3bb7. +// +// Solidity: function espressoBatcher() view returns(address) +func (_BatchAuthenticator *BatchAuthenticatorCallerSession) EspressoBatcher() (common.Address, error) { + return _BatchAuthenticator.Contract.EspressoBatcher(&_BatchAuthenticator.CallOpts) +} + +// EspressoBatcherAt is a free data retrieval call binding the contract method 0xfd402af7. +// +// Solidity: function espressoBatcherAt(uint32 _index) view returns(address batcher_, uint64 fromBlock_) +func (_BatchAuthenticator *BatchAuthenticatorCaller) EspressoBatcherAt(opts *bind.CallOpts, _index uint32) (struct { + Batcher common.Address + FromBlock uint64 +}, error) { + var out []interface{} + err := _BatchAuthenticator.contract.Call(opts, &out, "espressoBatcherAt", _index) + + outstruct := new(struct { + Batcher common.Address + FromBlock uint64 + }) + if err != nil { + return *outstruct, err + } + + outstruct.Batcher = *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + outstruct.FromBlock = *abi.ConvertType(out[1], new(uint64)).(*uint64) + + return *outstruct, err + +} + +// EspressoBatcherAt is a free data retrieval call binding the contract method 0xfd402af7. +// +// Solidity: function espressoBatcherAt(uint32 _index) view returns(address batcher_, uint64 fromBlock_) +func (_BatchAuthenticator *BatchAuthenticatorSession) EspressoBatcherAt(_index uint32) (struct { + Batcher common.Address + FromBlock uint64 +}, error) { + return _BatchAuthenticator.Contract.EspressoBatcherAt(&_BatchAuthenticator.CallOpts, _index) +} + +// EspressoBatcherAt is a free data retrieval call binding the contract method 0xfd402af7. +// +// Solidity: function espressoBatcherAt(uint32 _index) view returns(address batcher_, uint64 fromBlock_) +func (_BatchAuthenticator *BatchAuthenticatorCallerSession) EspressoBatcherAt(_index uint32) (struct { + Batcher common.Address + FromBlock uint64 +}, error) { + return _BatchAuthenticator.Contract.EspressoBatcherAt(&_BatchAuthenticator.CallOpts, _index) +} + +// EspressoBatcherAtBlock is a free data retrieval call binding the contract method 0x7d531a78. +// +// Solidity: function espressoBatcherAtBlock(uint64 _l1Block) view returns(address) +func (_BatchAuthenticator *BatchAuthenticatorCaller) EspressoBatcherAtBlock(opts *bind.CallOpts, _l1Block uint64) (common.Address, error) { + var out []interface{} + err := _BatchAuthenticator.contract.Call(opts, &out, "espressoBatcherAtBlock", _l1Block) + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// EspressoBatcherAtBlock is a free data retrieval call binding the contract method 0x7d531a78. +// +// Solidity: function espressoBatcherAtBlock(uint64 _l1Block) view returns(address) +func (_BatchAuthenticator *BatchAuthenticatorSession) EspressoBatcherAtBlock(_l1Block uint64) (common.Address, error) { + return _BatchAuthenticator.Contract.EspressoBatcherAtBlock(&_BatchAuthenticator.CallOpts, _l1Block) +} + +// EspressoBatcherAtBlock is a free data retrieval call binding the contract method 0x7d531a78. +// +// Solidity: function espressoBatcherAtBlock(uint64 _l1Block) view returns(address) +func (_BatchAuthenticator *BatchAuthenticatorCallerSession) EspressoBatcherAtBlock(_l1Block uint64) (common.Address, error) { + return _BatchAuthenticator.Contract.EspressoBatcherAtBlock(&_BatchAuthenticator.CallOpts, _l1Block) +} + +// EspressoBatcherHistoryLength is a free data retrieval call binding the contract method 0x4268ecaa. +// +// Solidity: function espressoBatcherHistoryLength() view returns(uint256) +func (_BatchAuthenticator *BatchAuthenticatorCaller) EspressoBatcherHistoryLength(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _BatchAuthenticator.contract.Call(opts, &out, "espressoBatcherHistoryLength") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// EspressoBatcherHistoryLength is a free data retrieval call binding the contract method 0x4268ecaa. +// +// Solidity: function espressoBatcherHistoryLength() view returns(uint256) +func (_BatchAuthenticator *BatchAuthenticatorSession) EspressoBatcherHistoryLength() (*big.Int, error) { + return _BatchAuthenticator.Contract.EspressoBatcherHistoryLength(&_BatchAuthenticator.CallOpts) +} + +// EspressoBatcherHistoryLength is a free data retrieval call binding the contract method 0x4268ecaa. +// +// Solidity: function espressoBatcherHistoryLength() view returns(uint256) +func (_BatchAuthenticator *BatchAuthenticatorCallerSession) EspressoBatcherHistoryLength() (*big.Int, error) { + return _BatchAuthenticator.Contract.EspressoBatcherHistoryLength(&_BatchAuthenticator.CallOpts) +} + +// EspressoTEEVerifier is a free data retrieval call binding the contract method 0xfa14fe6d. +// +// Solidity: function espressoTEEVerifier() view returns(address) +func (_BatchAuthenticator *BatchAuthenticatorCaller) EspressoTEEVerifier(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _BatchAuthenticator.contract.Call(opts, &out, "espressoTEEVerifier") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// EspressoTEEVerifier is a free data retrieval call binding the contract method 0xfa14fe6d. +// +// Solidity: function espressoTEEVerifier() view returns(address) +func (_BatchAuthenticator *BatchAuthenticatorSession) EspressoTEEVerifier() (common.Address, error) { + return _BatchAuthenticator.Contract.EspressoTEEVerifier(&_BatchAuthenticator.CallOpts) +} + +// EspressoTEEVerifier is a free data retrieval call binding the contract method 0xfa14fe6d. +// +// Solidity: function espressoTEEVerifier() view returns(address) +func (_BatchAuthenticator *BatchAuthenticatorCallerSession) EspressoTEEVerifier() (common.Address, error) { + return _BatchAuthenticator.Contract.EspressoTEEVerifier(&_BatchAuthenticator.CallOpts) +} + +// GetGuardians is a free data retrieval call binding the contract method 0x0665f04b. +// +// Solidity: function getGuardians() view returns(address[]) +func (_BatchAuthenticator *BatchAuthenticatorCaller) GetGuardians(opts *bind.CallOpts) ([]common.Address, error) { + var out []interface{} + err := _BatchAuthenticator.contract.Call(opts, &out, "getGuardians") + + if err != nil { + return *new([]common.Address), err + } + + out0 := *abi.ConvertType(out[0], new([]common.Address)).(*[]common.Address) + + return out0, err + +} + +// GetGuardians is a free data retrieval call binding the contract method 0x0665f04b. +// +// Solidity: function getGuardians() view returns(address[]) +func (_BatchAuthenticator *BatchAuthenticatorSession) GetGuardians() ([]common.Address, error) { + return _BatchAuthenticator.Contract.GetGuardians(&_BatchAuthenticator.CallOpts) +} + +// GetGuardians is a free data retrieval call binding the contract method 0x0665f04b. +// +// Solidity: function getGuardians() view returns(address[]) +func (_BatchAuthenticator *BatchAuthenticatorCallerSession) GetGuardians() ([]common.Address, error) { + return _BatchAuthenticator.Contract.GetGuardians(&_BatchAuthenticator.CallOpts) +} + +// GuardianCount is a free data retrieval call binding the contract method 0x54387ad7. +// +// Solidity: function guardianCount() view returns(uint256) +func (_BatchAuthenticator *BatchAuthenticatorCaller) GuardianCount(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _BatchAuthenticator.contract.Call(opts, &out, "guardianCount") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// GuardianCount is a free data retrieval call binding the contract method 0x54387ad7. +// +// Solidity: function guardianCount() view returns(uint256) +func (_BatchAuthenticator *BatchAuthenticatorSession) GuardianCount() (*big.Int, error) { + return _BatchAuthenticator.Contract.GuardianCount(&_BatchAuthenticator.CallOpts) +} + +// GuardianCount is a free data retrieval call binding the contract method 0x54387ad7. +// +// Solidity: function guardianCount() view returns(uint256) +func (_BatchAuthenticator *BatchAuthenticatorCallerSession) GuardianCount() (*big.Int, error) { + return _BatchAuthenticator.Contract.GuardianCount(&_BatchAuthenticator.CallOpts) +} + +// InitVersion is a free data retrieval call binding the contract method 0x38d38c97. +// +// Solidity: function initVersion() view returns(uint8) +func (_BatchAuthenticator *BatchAuthenticatorCaller) InitVersion(opts *bind.CallOpts) (uint8, error) { + var out []interface{} + err := _BatchAuthenticator.contract.Call(opts, &out, "initVersion") + + if err != nil { + return *new(uint8), err + } + + out0 := *abi.ConvertType(out[0], new(uint8)).(*uint8) + + return out0, err + +} + +// InitVersion is a free data retrieval call binding the contract method 0x38d38c97. +// +// Solidity: function initVersion() view returns(uint8) +func (_BatchAuthenticator *BatchAuthenticatorSession) InitVersion() (uint8, error) { + return _BatchAuthenticator.Contract.InitVersion(&_BatchAuthenticator.CallOpts) +} + +// InitVersion is a free data retrieval call binding the contract method 0x38d38c97. +// +// Solidity: function initVersion() view returns(uint8) +func (_BatchAuthenticator *BatchAuthenticatorCallerSession) InitVersion() (uint8, error) { + return _BatchAuthenticator.Contract.InitVersion(&_BatchAuthenticator.CallOpts) +} + +// IsGuardian is a free data retrieval call binding the contract method 0x0c68ba21. +// +// Solidity: function isGuardian(address account) view returns(bool) +func (_BatchAuthenticator *BatchAuthenticatorCaller) IsGuardian(opts *bind.CallOpts, account common.Address) (bool, error) { + var out []interface{} + err := _BatchAuthenticator.contract.Call(opts, &out, "isGuardian", account) + + if err != nil { + return *new(bool), err + } + + out0 := *abi.ConvertType(out[0], new(bool)).(*bool) + + return out0, err + +} + +// IsGuardian is a free data retrieval call binding the contract method 0x0c68ba21. +// +// Solidity: function isGuardian(address account) view returns(bool) +func (_BatchAuthenticator *BatchAuthenticatorSession) IsGuardian(account common.Address) (bool, error) { + return _BatchAuthenticator.Contract.IsGuardian(&_BatchAuthenticator.CallOpts, account) +} + +// IsGuardian is a free data retrieval call binding the contract method 0x0c68ba21. +// +// Solidity: function isGuardian(address account) view returns(bool) +func (_BatchAuthenticator *BatchAuthenticatorCallerSession) IsGuardian(account common.Address) (bool, error) { + return _BatchAuthenticator.Contract.IsGuardian(&_BatchAuthenticator.CallOpts, account) +} + +// NitroValidator is a free data retrieval call binding the contract method 0x1b076a4c. +// +// Solidity: function nitroValidator() view returns(address) +func (_BatchAuthenticator *BatchAuthenticatorCaller) NitroValidator(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _BatchAuthenticator.contract.Call(opts, &out, "nitroValidator") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// NitroValidator is a free data retrieval call binding the contract method 0x1b076a4c. +// +// Solidity: function nitroValidator() view returns(address) +func (_BatchAuthenticator *BatchAuthenticatorSession) NitroValidator() (common.Address, error) { + return _BatchAuthenticator.Contract.NitroValidator(&_BatchAuthenticator.CallOpts) +} + +// NitroValidator is a free data retrieval call binding the contract method 0x1b076a4c. +// +// Solidity: function nitroValidator() view returns(address) +func (_BatchAuthenticator *BatchAuthenticatorCallerSession) NitroValidator() (common.Address, error) { + return _BatchAuthenticator.Contract.NitroValidator(&_BatchAuthenticator.CallOpts) +} + +// Owner is a free data retrieval call binding the contract method 0x8da5cb5b. +// +// Solidity: function owner() view returns(address) +func (_BatchAuthenticator *BatchAuthenticatorCaller) Owner(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _BatchAuthenticator.contract.Call(opts, &out, "owner") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// Owner is a free data retrieval call binding the contract method 0x8da5cb5b. +// +// Solidity: function owner() view returns(address) +func (_BatchAuthenticator *BatchAuthenticatorSession) Owner() (common.Address, error) { + return _BatchAuthenticator.Contract.Owner(&_BatchAuthenticator.CallOpts) +} + +// Owner is a free data retrieval call binding the contract method 0x8da5cb5b. +// +// Solidity: function owner() view returns(address) +func (_BatchAuthenticator *BatchAuthenticatorCallerSession) Owner() (common.Address, error) { + return _BatchAuthenticator.Contract.Owner(&_BatchAuthenticator.CallOpts) +} + +// PendingOwner is a free data retrieval call binding the contract method 0xe30c3978. +// +// Solidity: function pendingOwner() view returns(address) +func (_BatchAuthenticator *BatchAuthenticatorCaller) PendingOwner(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _BatchAuthenticator.contract.Call(opts, &out, "pendingOwner") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// PendingOwner is a free data retrieval call binding the contract method 0xe30c3978. +// +// Solidity: function pendingOwner() view returns(address) +func (_BatchAuthenticator *BatchAuthenticatorSession) PendingOwner() (common.Address, error) { + return _BatchAuthenticator.Contract.PendingOwner(&_BatchAuthenticator.CallOpts) +} + +// PendingOwner is a free data retrieval call binding the contract method 0xe30c3978. +// +// Solidity: function pendingOwner() view returns(address) +func (_BatchAuthenticator *BatchAuthenticatorCallerSession) PendingOwner() (common.Address, error) { + return _BatchAuthenticator.Contract.PendingOwner(&_BatchAuthenticator.CallOpts) +} + +// ProxyAdmin is a free data retrieval call binding the contract method 0x3e47158c. +// +// Solidity: function proxyAdmin() view returns(address) +func (_BatchAuthenticator *BatchAuthenticatorCaller) ProxyAdmin(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _BatchAuthenticator.contract.Call(opts, &out, "proxyAdmin") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// ProxyAdmin is a free data retrieval call binding the contract method 0x3e47158c. +// +// Solidity: function proxyAdmin() view returns(address) +func (_BatchAuthenticator *BatchAuthenticatorSession) ProxyAdmin() (common.Address, error) { + return _BatchAuthenticator.Contract.ProxyAdmin(&_BatchAuthenticator.CallOpts) +} + +// ProxyAdmin is a free data retrieval call binding the contract method 0x3e47158c. +// +// Solidity: function proxyAdmin() view returns(address) +func (_BatchAuthenticator *BatchAuthenticatorCallerSession) ProxyAdmin() (common.Address, error) { + return _BatchAuthenticator.Contract.ProxyAdmin(&_BatchAuthenticator.CallOpts) +} + +// ProxyAdminOwner is a free data retrieval call binding the contract method 0xdad544e0. +// +// Solidity: function proxyAdminOwner() view returns(address) +func (_BatchAuthenticator *BatchAuthenticatorCaller) ProxyAdminOwner(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _BatchAuthenticator.contract.Call(opts, &out, "proxyAdminOwner") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// ProxyAdminOwner is a free data retrieval call binding the contract method 0xdad544e0. +// +// Solidity: function proxyAdminOwner() view returns(address) +func (_BatchAuthenticator *BatchAuthenticatorSession) ProxyAdminOwner() (common.Address, error) { + return _BatchAuthenticator.Contract.ProxyAdminOwner(&_BatchAuthenticator.CallOpts) +} + +// ProxyAdminOwner is a free data retrieval call binding the contract method 0xdad544e0. +// +// Solidity: function proxyAdminOwner() view returns(address) +func (_BatchAuthenticator *BatchAuthenticatorCallerSession) ProxyAdminOwner() (common.Address, error) { + return _BatchAuthenticator.Contract.ProxyAdminOwner(&_BatchAuthenticator.CallOpts) +} + +// SystemConfig is a free data retrieval call binding the contract method 0x33d7e2bd. +// +// Solidity: function systemConfig() view returns(address) +func (_BatchAuthenticator *BatchAuthenticatorCaller) SystemConfig(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _BatchAuthenticator.contract.Call(opts, &out, "systemConfig") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// SystemConfig is a free data retrieval call binding the contract method 0x33d7e2bd. +// +// Solidity: function systemConfig() view returns(address) +func (_BatchAuthenticator *BatchAuthenticatorSession) SystemConfig() (common.Address, error) { + return _BatchAuthenticator.Contract.SystemConfig(&_BatchAuthenticator.CallOpts) +} + +// SystemConfig is a free data retrieval call binding the contract method 0x33d7e2bd. +// +// Solidity: function systemConfig() view returns(address) +func (_BatchAuthenticator *BatchAuthenticatorCallerSession) SystemConfig() (common.Address, error) { + return _BatchAuthenticator.Contract.SystemConfig(&_BatchAuthenticator.CallOpts) +} + +// Version is a free data retrieval call binding the contract method 0x54fd4d50. +// +// Solidity: function version() view returns(string) +func (_BatchAuthenticator *BatchAuthenticatorCaller) Version(opts *bind.CallOpts) (string, error) { + var out []interface{} + err := _BatchAuthenticator.contract.Call(opts, &out, "version") + + if err != nil { + return *new(string), err + } + + out0 := *abi.ConvertType(out[0], new(string)).(*string) + + return out0, err + +} + +// Version is a free data retrieval call binding the contract method 0x54fd4d50. +// +// Solidity: function version() view returns(string) +func (_BatchAuthenticator *BatchAuthenticatorSession) Version() (string, error) { + return _BatchAuthenticator.Contract.Version(&_BatchAuthenticator.CallOpts) +} + +// Version is a free data retrieval call binding the contract method 0x54fd4d50. +// +// Solidity: function version() view returns(string) +func (_BatchAuthenticator *BatchAuthenticatorCallerSession) Version() (string, error) { + return _BatchAuthenticator.Contract.Version(&_BatchAuthenticator.CallOpts) +} + +// AcceptOwnership is a paid mutator transaction binding the contract method 0x79ba5097. +// +// Solidity: function acceptOwnership() returns() +func (_BatchAuthenticator *BatchAuthenticatorTransactor) AcceptOwnership(opts *bind.TransactOpts) (*types.Transaction, error) { + return _BatchAuthenticator.contract.Transact(opts, "acceptOwnership") +} + +// AcceptOwnership is a paid mutator transaction binding the contract method 0x79ba5097. +// +// Solidity: function acceptOwnership() returns() +func (_BatchAuthenticator *BatchAuthenticatorSession) AcceptOwnership() (*types.Transaction, error) { + return _BatchAuthenticator.Contract.AcceptOwnership(&_BatchAuthenticator.TransactOpts) +} + +// AcceptOwnership is a paid mutator transaction binding the contract method 0x79ba5097. +// +// Solidity: function acceptOwnership() returns() +func (_BatchAuthenticator *BatchAuthenticatorTransactorSession) AcceptOwnership() (*types.Transaction, error) { + return _BatchAuthenticator.Contract.AcceptOwnership(&_BatchAuthenticator.TransactOpts) +} + +// AddGuardian is a paid mutator transaction binding the contract method 0xa526d83b. +// +// Solidity: function addGuardian(address guardian) returns() +func (_BatchAuthenticator *BatchAuthenticatorTransactor) AddGuardian(opts *bind.TransactOpts, guardian common.Address) (*types.Transaction, error) { + return _BatchAuthenticator.contract.Transact(opts, "addGuardian", guardian) +} + +// AddGuardian is a paid mutator transaction binding the contract method 0xa526d83b. +// +// Solidity: function addGuardian(address guardian) returns() +func (_BatchAuthenticator *BatchAuthenticatorSession) AddGuardian(guardian common.Address) (*types.Transaction, error) { + return _BatchAuthenticator.Contract.AddGuardian(&_BatchAuthenticator.TransactOpts, guardian) +} + +// AddGuardian is a paid mutator transaction binding the contract method 0xa526d83b. +// +// Solidity: function addGuardian(address guardian) returns() +func (_BatchAuthenticator *BatchAuthenticatorTransactorSession) AddGuardian(guardian common.Address) (*types.Transaction, error) { + return _BatchAuthenticator.Contract.AddGuardian(&_BatchAuthenticator.TransactOpts, guardian) +} + +// AuthenticateBatchInfo is a paid mutator transaction binding the contract method 0xfc619e41. +// +// Solidity: function authenticateBatchInfo(bytes32 _commitment, bytes _signature) returns() +func (_BatchAuthenticator *BatchAuthenticatorTransactor) AuthenticateBatchInfo(opts *bind.TransactOpts, _commitment [32]byte, _signature []byte) (*types.Transaction, error) { + return _BatchAuthenticator.contract.Transact(opts, "authenticateBatchInfo", _commitment, _signature) +} + +// AuthenticateBatchInfo is a paid mutator transaction binding the contract method 0xfc619e41. +// +// Solidity: function authenticateBatchInfo(bytes32 _commitment, bytes _signature) returns() +func (_BatchAuthenticator *BatchAuthenticatorSession) AuthenticateBatchInfo(_commitment [32]byte, _signature []byte) (*types.Transaction, error) { + return _BatchAuthenticator.Contract.AuthenticateBatchInfo(&_BatchAuthenticator.TransactOpts, _commitment, _signature) +} + +// AuthenticateBatchInfo is a paid mutator transaction binding the contract method 0xfc619e41. +// +// Solidity: function authenticateBatchInfo(bytes32 _commitment, bytes _signature) returns() +func (_BatchAuthenticator *BatchAuthenticatorTransactorSession) AuthenticateBatchInfo(_commitment [32]byte, _signature []byte) (*types.Transaction, error) { + return _BatchAuthenticator.Contract.AuthenticateBatchInfo(&_BatchAuthenticator.TransactOpts, _commitment, _signature) +} + +// Initialize is a paid mutator transaction binding the contract method 0xfc5b5fda. +// +// Solidity: function initialize(address _espressoTEEVerifier, address _espressoBatcher, address _systemConfig, address _owner, bool _activeIsEspresso) returns() +func (_BatchAuthenticator *BatchAuthenticatorTransactor) Initialize(opts *bind.TransactOpts, _espressoTEEVerifier common.Address, _espressoBatcher common.Address, _systemConfig common.Address, _owner common.Address, _activeIsEspresso bool) (*types.Transaction, error) { + return _BatchAuthenticator.contract.Transact(opts, "initialize", _espressoTEEVerifier, _espressoBatcher, _systemConfig, _owner, _activeIsEspresso) +} + +// Initialize is a paid mutator transaction binding the contract method 0xfc5b5fda. +// +// Solidity: function initialize(address _espressoTEEVerifier, address _espressoBatcher, address _systemConfig, address _owner, bool _activeIsEspresso) returns() +func (_BatchAuthenticator *BatchAuthenticatorSession) Initialize(_espressoTEEVerifier common.Address, _espressoBatcher common.Address, _systemConfig common.Address, _owner common.Address, _activeIsEspresso bool) (*types.Transaction, error) { + return _BatchAuthenticator.Contract.Initialize(&_BatchAuthenticator.TransactOpts, _espressoTEEVerifier, _espressoBatcher, _systemConfig, _owner, _activeIsEspresso) +} + +// Initialize is a paid mutator transaction binding the contract method 0xfc5b5fda. +// +// Solidity: function initialize(address _espressoTEEVerifier, address _espressoBatcher, address _systemConfig, address _owner, bool _activeIsEspresso) returns() +func (_BatchAuthenticator *BatchAuthenticatorTransactorSession) Initialize(_espressoTEEVerifier common.Address, _espressoBatcher common.Address, _systemConfig common.Address, _owner common.Address, _activeIsEspresso bool) (*types.Transaction, error) { + return _BatchAuthenticator.Contract.Initialize(&_BatchAuthenticator.TransactOpts, _espressoTEEVerifier, _espressoBatcher, _systemConfig, _owner, _activeIsEspresso) +} + +// RegisterSigner is a paid mutator transaction binding the contract method 0xba58e82a. +// +// Solidity: function registerSigner(bytes _verificationData, bytes _data) returns() +func (_BatchAuthenticator *BatchAuthenticatorTransactor) RegisterSigner(opts *bind.TransactOpts, _verificationData []byte, _data []byte) (*types.Transaction, error) { + return _BatchAuthenticator.contract.Transact(opts, "registerSigner", _verificationData, _data) +} + +// RegisterSigner is a paid mutator transaction binding the contract method 0xba58e82a. +// +// Solidity: function registerSigner(bytes _verificationData, bytes _data) returns() +func (_BatchAuthenticator *BatchAuthenticatorSession) RegisterSigner(_verificationData []byte, _data []byte) (*types.Transaction, error) { + return _BatchAuthenticator.Contract.RegisterSigner(&_BatchAuthenticator.TransactOpts, _verificationData, _data) +} + +// RegisterSigner is a paid mutator transaction binding the contract method 0xba58e82a. +// +// Solidity: function registerSigner(bytes _verificationData, bytes _data) returns() +func (_BatchAuthenticator *BatchAuthenticatorTransactorSession) RegisterSigner(_verificationData []byte, _data []byte) (*types.Transaction, error) { + return _BatchAuthenticator.Contract.RegisterSigner(&_BatchAuthenticator.TransactOpts, _verificationData, _data) +} + +// RemoveGuardian is a paid mutator transaction binding the contract method 0x71404156. +// +// Solidity: function removeGuardian(address guardian) returns() +func (_BatchAuthenticator *BatchAuthenticatorTransactor) RemoveGuardian(opts *bind.TransactOpts, guardian common.Address) (*types.Transaction, error) { + return _BatchAuthenticator.contract.Transact(opts, "removeGuardian", guardian) +} + +// RemoveGuardian is a paid mutator transaction binding the contract method 0x71404156. +// +// Solidity: function removeGuardian(address guardian) returns() +func (_BatchAuthenticator *BatchAuthenticatorSession) RemoveGuardian(guardian common.Address) (*types.Transaction, error) { + return _BatchAuthenticator.Contract.RemoveGuardian(&_BatchAuthenticator.TransactOpts, guardian) +} + +// RemoveGuardian is a paid mutator transaction binding the contract method 0x71404156. +// +// Solidity: function removeGuardian(address guardian) returns() +func (_BatchAuthenticator *BatchAuthenticatorTransactorSession) RemoveGuardian(guardian common.Address) (*types.Transaction, error) { + return _BatchAuthenticator.Contract.RemoveGuardian(&_BatchAuthenticator.TransactOpts, guardian) +} + +// RenounceOwnership is a paid mutator transaction binding the contract method 0x715018a6. +// +// Solidity: function renounceOwnership() returns() +func (_BatchAuthenticator *BatchAuthenticatorTransactor) RenounceOwnership(opts *bind.TransactOpts) (*types.Transaction, error) { + return _BatchAuthenticator.contract.Transact(opts, "renounceOwnership") +} + +// RenounceOwnership is a paid mutator transaction binding the contract method 0x715018a6. +// +// Solidity: function renounceOwnership() returns() +func (_BatchAuthenticator *BatchAuthenticatorSession) RenounceOwnership() (*types.Transaction, error) { + return _BatchAuthenticator.Contract.RenounceOwnership(&_BatchAuthenticator.TransactOpts) +} + +// RenounceOwnership is a paid mutator transaction binding the contract method 0x715018a6. +// +// Solidity: function renounceOwnership() returns() +func (_BatchAuthenticator *BatchAuthenticatorTransactorSession) RenounceOwnership() (*types.Transaction, error) { + return _BatchAuthenticator.Contract.RenounceOwnership(&_BatchAuthenticator.TransactOpts) +} + +// SetActiveIsEspresso is a paid mutator transaction binding the contract method 0x6c076871. +// +// Solidity: function setActiveIsEspresso(bool _desired) returns() +func (_BatchAuthenticator *BatchAuthenticatorTransactor) SetActiveIsEspresso(opts *bind.TransactOpts, _desired bool) (*types.Transaction, error) { + return _BatchAuthenticator.contract.Transact(opts, "setActiveIsEspresso", _desired) +} + +// SetActiveIsEspresso is a paid mutator transaction binding the contract method 0x6c076871. +// +// Solidity: function setActiveIsEspresso(bool _desired) returns() +func (_BatchAuthenticator *BatchAuthenticatorSession) SetActiveIsEspresso(_desired bool) (*types.Transaction, error) { + return _BatchAuthenticator.Contract.SetActiveIsEspresso(&_BatchAuthenticator.TransactOpts, _desired) +} + +// SetActiveIsEspresso is a paid mutator transaction binding the contract method 0x6c076871. +// +// Solidity: function setActiveIsEspresso(bool _desired) returns() +func (_BatchAuthenticator *BatchAuthenticatorTransactorSession) SetActiveIsEspresso(_desired bool) (*types.Transaction, error) { + return _BatchAuthenticator.Contract.SetActiveIsEspresso(&_BatchAuthenticator.TransactOpts, _desired) +} + +// SetEspressoBatcher is a paid mutator transaction binding the contract method 0x2ce53247. +// +// Solidity: function setEspressoBatcher(address _newEspressoBatcher) returns() +func (_BatchAuthenticator *BatchAuthenticatorTransactor) SetEspressoBatcher(opts *bind.TransactOpts, _newEspressoBatcher common.Address) (*types.Transaction, error) { + return _BatchAuthenticator.contract.Transact(opts, "setEspressoBatcher", _newEspressoBatcher) +} + +// SetEspressoBatcher is a paid mutator transaction binding the contract method 0x2ce53247. +// +// Solidity: function setEspressoBatcher(address _newEspressoBatcher) returns() +func (_BatchAuthenticator *BatchAuthenticatorSession) SetEspressoBatcher(_newEspressoBatcher common.Address) (*types.Transaction, error) { + return _BatchAuthenticator.Contract.SetEspressoBatcher(&_BatchAuthenticator.TransactOpts, _newEspressoBatcher) +} + +// SetEspressoBatcher is a paid mutator transaction binding the contract method 0x2ce53247. +// +// Solidity: function setEspressoBatcher(address _newEspressoBatcher) returns() +func (_BatchAuthenticator *BatchAuthenticatorTransactorSession) SetEspressoBatcher(_newEspressoBatcher common.Address) (*types.Transaction, error) { + return _BatchAuthenticator.Contract.SetEspressoBatcher(&_BatchAuthenticator.TransactOpts, _newEspressoBatcher) +} + +// TransferOwnership is a paid mutator transaction binding the contract method 0xf2fde38b. +// +// Solidity: function transferOwnership(address newOwner) returns() +func (_BatchAuthenticator *BatchAuthenticatorTransactor) TransferOwnership(opts *bind.TransactOpts, newOwner common.Address) (*types.Transaction, error) { + return _BatchAuthenticator.contract.Transact(opts, "transferOwnership", newOwner) +} + +// TransferOwnership is a paid mutator transaction binding the contract method 0xf2fde38b. +// +// Solidity: function transferOwnership(address newOwner) returns() +func (_BatchAuthenticator *BatchAuthenticatorSession) TransferOwnership(newOwner common.Address) (*types.Transaction, error) { + return _BatchAuthenticator.Contract.TransferOwnership(&_BatchAuthenticator.TransactOpts, newOwner) +} + +// TransferOwnership is a paid mutator transaction binding the contract method 0xf2fde38b. +// +// Solidity: function transferOwnership(address newOwner) returns() +func (_BatchAuthenticator *BatchAuthenticatorTransactorSession) TransferOwnership(newOwner common.Address) (*types.Transaction, error) { + return _BatchAuthenticator.Contract.TransferOwnership(&_BatchAuthenticator.TransactOpts, newOwner) +} + +// BatchAuthenticatorBatchInfoAuthenticatedIterator is returned from FilterBatchInfoAuthenticated and is used to iterate over the raw logs and unpacked data for BatchInfoAuthenticated events raised by the BatchAuthenticator contract. +type BatchAuthenticatorBatchInfoAuthenticatedIterator struct { + Event *BatchAuthenticatorBatchInfoAuthenticated // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *BatchAuthenticatorBatchInfoAuthenticatedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(BatchAuthenticatorBatchInfoAuthenticated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(BatchAuthenticatorBatchInfoAuthenticated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *BatchAuthenticatorBatchInfoAuthenticatedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *BatchAuthenticatorBatchInfoAuthenticatedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// BatchAuthenticatorBatchInfoAuthenticated represents a BatchInfoAuthenticated event raised by the BatchAuthenticator contract. +type BatchAuthenticatorBatchInfoAuthenticated struct { + Commitment [32]byte + Caller common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterBatchInfoAuthenticated is a free log retrieval operation binding the contract event 0x731978a77d438b0ea35a9034fb28d9cf9372e1649f18c213110adcfab65c5c5c. +// +// Solidity: event BatchInfoAuthenticated(bytes32 commitment, address indexed caller) +func (_BatchAuthenticator *BatchAuthenticatorFilterer) FilterBatchInfoAuthenticated(opts *bind.FilterOpts, caller []common.Address) (*BatchAuthenticatorBatchInfoAuthenticatedIterator, error) { + + var callerRule []interface{} + for _, callerItem := range caller { + callerRule = append(callerRule, callerItem) + } + + logs, sub, err := _BatchAuthenticator.contract.FilterLogs(opts, "BatchInfoAuthenticated", callerRule) + if err != nil { + return nil, err + } + return &BatchAuthenticatorBatchInfoAuthenticatedIterator{contract: _BatchAuthenticator.contract, event: "BatchInfoAuthenticated", logs: logs, sub: sub}, nil +} + +// WatchBatchInfoAuthenticated is a free log subscription operation binding the contract event 0x731978a77d438b0ea35a9034fb28d9cf9372e1649f18c213110adcfab65c5c5c. +// +// Solidity: event BatchInfoAuthenticated(bytes32 commitment, address indexed caller) +func (_BatchAuthenticator *BatchAuthenticatorFilterer) WatchBatchInfoAuthenticated(opts *bind.WatchOpts, sink chan<- *BatchAuthenticatorBatchInfoAuthenticated, caller []common.Address) (event.Subscription, error) { + + var callerRule []interface{} + for _, callerItem := range caller { + callerRule = append(callerRule, callerItem) + } + + logs, sub, err := _BatchAuthenticator.contract.WatchLogs(opts, "BatchInfoAuthenticated", callerRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(BatchAuthenticatorBatchInfoAuthenticated) + if err := _BatchAuthenticator.contract.UnpackLog(event, "BatchInfoAuthenticated", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseBatchInfoAuthenticated is a log parse operation binding the contract event 0x731978a77d438b0ea35a9034fb28d9cf9372e1649f18c213110adcfab65c5c5c. +// +// Solidity: event BatchInfoAuthenticated(bytes32 commitment, address indexed caller) +func (_BatchAuthenticator *BatchAuthenticatorFilterer) ParseBatchInfoAuthenticated(log types.Log) (*BatchAuthenticatorBatchInfoAuthenticated, error) { + event := new(BatchAuthenticatorBatchInfoAuthenticated) + if err := _BatchAuthenticator.contract.UnpackLog(event, "BatchInfoAuthenticated", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// BatchAuthenticatorBatcherSwitchedIterator is returned from FilterBatcherSwitched and is used to iterate over the raw logs and unpacked data for BatcherSwitched events raised by the BatchAuthenticator contract. +type BatchAuthenticatorBatcherSwitchedIterator struct { + Event *BatchAuthenticatorBatcherSwitched // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *BatchAuthenticatorBatcherSwitchedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(BatchAuthenticatorBatcherSwitched) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(BatchAuthenticatorBatcherSwitched) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *BatchAuthenticatorBatcherSwitchedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *BatchAuthenticatorBatcherSwitchedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// BatchAuthenticatorBatcherSwitched represents a BatcherSwitched event raised by the BatchAuthenticator contract. +type BatchAuthenticatorBatcherSwitched struct { + ActiveIsEspresso bool + Raw types.Log // Blockchain specific contextual infos +} + +// FilterBatcherSwitched is a free log retrieval operation binding the contract event 0xb957d7fc29e5974594db2f2e132076d52f42c0734eae05fd5ea080d1ba175ad3. +// +// Solidity: event BatcherSwitched(bool indexed activeIsEspresso) +func (_BatchAuthenticator *BatchAuthenticatorFilterer) FilterBatcherSwitched(opts *bind.FilterOpts, activeIsEspresso []bool) (*BatchAuthenticatorBatcherSwitchedIterator, error) { + + var activeIsEspressoRule []interface{} + for _, activeIsEspressoItem := range activeIsEspresso { + activeIsEspressoRule = append(activeIsEspressoRule, activeIsEspressoItem) + } + + logs, sub, err := _BatchAuthenticator.contract.FilterLogs(opts, "BatcherSwitched", activeIsEspressoRule) + if err != nil { + return nil, err + } + return &BatchAuthenticatorBatcherSwitchedIterator{contract: _BatchAuthenticator.contract, event: "BatcherSwitched", logs: logs, sub: sub}, nil +} + +// WatchBatcherSwitched is a free log subscription operation binding the contract event 0xb957d7fc29e5974594db2f2e132076d52f42c0734eae05fd5ea080d1ba175ad3. +// +// Solidity: event BatcherSwitched(bool indexed activeIsEspresso) +func (_BatchAuthenticator *BatchAuthenticatorFilterer) WatchBatcherSwitched(opts *bind.WatchOpts, sink chan<- *BatchAuthenticatorBatcherSwitched, activeIsEspresso []bool) (event.Subscription, error) { + + var activeIsEspressoRule []interface{} + for _, activeIsEspressoItem := range activeIsEspresso { + activeIsEspressoRule = append(activeIsEspressoRule, activeIsEspressoItem) + } + + logs, sub, err := _BatchAuthenticator.contract.WatchLogs(opts, "BatcherSwitched", activeIsEspressoRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(BatchAuthenticatorBatcherSwitched) + if err := _BatchAuthenticator.contract.UnpackLog(event, "BatcherSwitched", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseBatcherSwitched is a log parse operation binding the contract event 0xb957d7fc29e5974594db2f2e132076d52f42c0734eae05fd5ea080d1ba175ad3. +// +// Solidity: event BatcherSwitched(bool indexed activeIsEspresso) +func (_BatchAuthenticator *BatchAuthenticatorFilterer) ParseBatcherSwitched(log types.Log) (*BatchAuthenticatorBatcherSwitched, error) { + event := new(BatchAuthenticatorBatcherSwitched) + if err := _BatchAuthenticator.contract.UnpackLog(event, "BatcherSwitched", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// BatchAuthenticatorEspressoBatcherUpdatedIterator is returned from FilterEspressoBatcherUpdated and is used to iterate over the raw logs and unpacked data for EspressoBatcherUpdated events raised by the BatchAuthenticator contract. +type BatchAuthenticatorEspressoBatcherUpdatedIterator struct { + Event *BatchAuthenticatorEspressoBatcherUpdated // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *BatchAuthenticatorEspressoBatcherUpdatedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(BatchAuthenticatorEspressoBatcherUpdated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(BatchAuthenticatorEspressoBatcherUpdated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *BatchAuthenticatorEspressoBatcherUpdatedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *BatchAuthenticatorEspressoBatcherUpdatedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// BatchAuthenticatorEspressoBatcherUpdated represents a EspressoBatcherUpdated event raised by the BatchAuthenticator contract. +type BatchAuthenticatorEspressoBatcherUpdated struct { + OldEspressoBatcher common.Address + NewEspressoBatcher common.Address + FromBlock uint64 + Raw types.Log // Blockchain specific contextual infos +} + +// FilterEspressoBatcherUpdated is a free log retrieval operation binding the contract event 0xcf8f6f62babb05dd1d159c090ad8429ee0df72e16c82701004a5405f908cb0f5. +// +// Solidity: event EspressoBatcherUpdated(address indexed oldEspressoBatcher, address indexed newEspressoBatcher, uint64 indexed fromBlock) +func (_BatchAuthenticator *BatchAuthenticatorFilterer) FilterEspressoBatcherUpdated(opts *bind.FilterOpts, oldEspressoBatcher []common.Address, newEspressoBatcher []common.Address, fromBlock []uint64) (*BatchAuthenticatorEspressoBatcherUpdatedIterator, error) { + + var oldEspressoBatcherRule []interface{} + for _, oldEspressoBatcherItem := range oldEspressoBatcher { + oldEspressoBatcherRule = append(oldEspressoBatcherRule, oldEspressoBatcherItem) + } + var newEspressoBatcherRule []interface{} + for _, newEspressoBatcherItem := range newEspressoBatcher { + newEspressoBatcherRule = append(newEspressoBatcherRule, newEspressoBatcherItem) + } + var fromBlockRule []interface{} + for _, fromBlockItem := range fromBlock { + fromBlockRule = append(fromBlockRule, fromBlockItem) + } + + logs, sub, err := _BatchAuthenticator.contract.FilterLogs(opts, "EspressoBatcherUpdated", oldEspressoBatcherRule, newEspressoBatcherRule, fromBlockRule) + if err != nil { + return nil, err + } + return &BatchAuthenticatorEspressoBatcherUpdatedIterator{contract: _BatchAuthenticator.contract, event: "EspressoBatcherUpdated", logs: logs, sub: sub}, nil +} + +// WatchEspressoBatcherUpdated is a free log subscription operation binding the contract event 0xcf8f6f62babb05dd1d159c090ad8429ee0df72e16c82701004a5405f908cb0f5. +// +// Solidity: event EspressoBatcherUpdated(address indexed oldEspressoBatcher, address indexed newEspressoBatcher, uint64 indexed fromBlock) +func (_BatchAuthenticator *BatchAuthenticatorFilterer) WatchEspressoBatcherUpdated(opts *bind.WatchOpts, sink chan<- *BatchAuthenticatorEspressoBatcherUpdated, oldEspressoBatcher []common.Address, newEspressoBatcher []common.Address, fromBlock []uint64) (event.Subscription, error) { + + var oldEspressoBatcherRule []interface{} + for _, oldEspressoBatcherItem := range oldEspressoBatcher { + oldEspressoBatcherRule = append(oldEspressoBatcherRule, oldEspressoBatcherItem) + } + var newEspressoBatcherRule []interface{} + for _, newEspressoBatcherItem := range newEspressoBatcher { + newEspressoBatcherRule = append(newEspressoBatcherRule, newEspressoBatcherItem) + } + var fromBlockRule []interface{} + for _, fromBlockItem := range fromBlock { + fromBlockRule = append(fromBlockRule, fromBlockItem) + } + + logs, sub, err := _BatchAuthenticator.contract.WatchLogs(opts, "EspressoBatcherUpdated", oldEspressoBatcherRule, newEspressoBatcherRule, fromBlockRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(BatchAuthenticatorEspressoBatcherUpdated) + if err := _BatchAuthenticator.contract.UnpackLog(event, "EspressoBatcherUpdated", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseEspressoBatcherUpdated is a log parse operation binding the contract event 0xcf8f6f62babb05dd1d159c090ad8429ee0df72e16c82701004a5405f908cb0f5. +// +// Solidity: event EspressoBatcherUpdated(address indexed oldEspressoBatcher, address indexed newEspressoBatcher, uint64 indexed fromBlock) +func (_BatchAuthenticator *BatchAuthenticatorFilterer) ParseEspressoBatcherUpdated(log types.Log) (*BatchAuthenticatorEspressoBatcherUpdated, error) { + event := new(BatchAuthenticatorEspressoBatcherUpdated) + if err := _BatchAuthenticator.contract.UnpackLog(event, "EspressoBatcherUpdated", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// BatchAuthenticatorGuardianAddedIterator is returned from FilterGuardianAdded and is used to iterate over the raw logs and unpacked data for GuardianAdded events raised by the BatchAuthenticator contract. +type BatchAuthenticatorGuardianAddedIterator struct { + Event *BatchAuthenticatorGuardianAdded // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *BatchAuthenticatorGuardianAddedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(BatchAuthenticatorGuardianAdded) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(BatchAuthenticatorGuardianAdded) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *BatchAuthenticatorGuardianAddedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *BatchAuthenticatorGuardianAddedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// BatchAuthenticatorGuardianAdded represents a GuardianAdded event raised by the BatchAuthenticator contract. +type BatchAuthenticatorGuardianAdded struct { + Guardian common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterGuardianAdded is a free log retrieval operation binding the contract event 0x038596bb31e2e7d3d9f184d4c98b310103f6d7f5830e5eec32bffe6f1728f969. +// +// Solidity: event GuardianAdded(address indexed guardian) +func (_BatchAuthenticator *BatchAuthenticatorFilterer) FilterGuardianAdded(opts *bind.FilterOpts, guardian []common.Address) (*BatchAuthenticatorGuardianAddedIterator, error) { + + var guardianRule []interface{} + for _, guardianItem := range guardian { + guardianRule = append(guardianRule, guardianItem) + } + + logs, sub, err := _BatchAuthenticator.contract.FilterLogs(opts, "GuardianAdded", guardianRule) + if err != nil { + return nil, err + } + return &BatchAuthenticatorGuardianAddedIterator{contract: _BatchAuthenticator.contract, event: "GuardianAdded", logs: logs, sub: sub}, nil +} + +// WatchGuardianAdded is a free log subscription operation binding the contract event 0x038596bb31e2e7d3d9f184d4c98b310103f6d7f5830e5eec32bffe6f1728f969. +// +// Solidity: event GuardianAdded(address indexed guardian) +func (_BatchAuthenticator *BatchAuthenticatorFilterer) WatchGuardianAdded(opts *bind.WatchOpts, sink chan<- *BatchAuthenticatorGuardianAdded, guardian []common.Address) (event.Subscription, error) { + + var guardianRule []interface{} + for _, guardianItem := range guardian { + guardianRule = append(guardianRule, guardianItem) + } + + logs, sub, err := _BatchAuthenticator.contract.WatchLogs(opts, "GuardianAdded", guardianRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(BatchAuthenticatorGuardianAdded) + if err := _BatchAuthenticator.contract.UnpackLog(event, "GuardianAdded", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseGuardianAdded is a log parse operation binding the contract event 0x038596bb31e2e7d3d9f184d4c98b310103f6d7f5830e5eec32bffe6f1728f969. +// +// Solidity: event GuardianAdded(address indexed guardian) +func (_BatchAuthenticator *BatchAuthenticatorFilterer) ParseGuardianAdded(log types.Log) (*BatchAuthenticatorGuardianAdded, error) { + event := new(BatchAuthenticatorGuardianAdded) + if err := _BatchAuthenticator.contract.UnpackLog(event, "GuardianAdded", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// BatchAuthenticatorGuardianRemovedIterator is returned from FilterGuardianRemoved and is used to iterate over the raw logs and unpacked data for GuardianRemoved events raised by the BatchAuthenticator contract. +type BatchAuthenticatorGuardianRemovedIterator struct { + Event *BatchAuthenticatorGuardianRemoved // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *BatchAuthenticatorGuardianRemovedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(BatchAuthenticatorGuardianRemoved) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(BatchAuthenticatorGuardianRemoved) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *BatchAuthenticatorGuardianRemovedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *BatchAuthenticatorGuardianRemovedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// BatchAuthenticatorGuardianRemoved represents a GuardianRemoved event raised by the BatchAuthenticator contract. +type BatchAuthenticatorGuardianRemoved struct { + Guardian common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterGuardianRemoved is a free log retrieval operation binding the contract event 0xb8107d0c6b40be480ce3172ee66ba6d64b71f6b1685a851340036e6e2e3e3c52. +// +// Solidity: event GuardianRemoved(address indexed guardian) +func (_BatchAuthenticator *BatchAuthenticatorFilterer) FilterGuardianRemoved(opts *bind.FilterOpts, guardian []common.Address) (*BatchAuthenticatorGuardianRemovedIterator, error) { + + var guardianRule []interface{} + for _, guardianItem := range guardian { + guardianRule = append(guardianRule, guardianItem) + } + + logs, sub, err := _BatchAuthenticator.contract.FilterLogs(opts, "GuardianRemoved", guardianRule) + if err != nil { + return nil, err + } + return &BatchAuthenticatorGuardianRemovedIterator{contract: _BatchAuthenticator.contract, event: "GuardianRemoved", logs: logs, sub: sub}, nil +} + +// WatchGuardianRemoved is a free log subscription operation binding the contract event 0xb8107d0c6b40be480ce3172ee66ba6d64b71f6b1685a851340036e6e2e3e3c52. +// +// Solidity: event GuardianRemoved(address indexed guardian) +func (_BatchAuthenticator *BatchAuthenticatorFilterer) WatchGuardianRemoved(opts *bind.WatchOpts, sink chan<- *BatchAuthenticatorGuardianRemoved, guardian []common.Address) (event.Subscription, error) { + + var guardianRule []interface{} + for _, guardianItem := range guardian { + guardianRule = append(guardianRule, guardianItem) + } + + logs, sub, err := _BatchAuthenticator.contract.WatchLogs(opts, "GuardianRemoved", guardianRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(BatchAuthenticatorGuardianRemoved) + if err := _BatchAuthenticator.contract.UnpackLog(event, "GuardianRemoved", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseGuardianRemoved is a log parse operation binding the contract event 0xb8107d0c6b40be480ce3172ee66ba6d64b71f6b1685a851340036e6e2e3e3c52. +// +// Solidity: event GuardianRemoved(address indexed guardian) +func (_BatchAuthenticator *BatchAuthenticatorFilterer) ParseGuardianRemoved(log types.Log) (*BatchAuthenticatorGuardianRemoved, error) { + event := new(BatchAuthenticatorGuardianRemoved) + if err := _BatchAuthenticator.contract.UnpackLog(event, "GuardianRemoved", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// BatchAuthenticatorInitializedIterator is returned from FilterInitialized and is used to iterate over the raw logs and unpacked data for Initialized events raised by the BatchAuthenticator contract. +type BatchAuthenticatorInitializedIterator struct { + Event *BatchAuthenticatorInitialized // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *BatchAuthenticatorInitializedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(BatchAuthenticatorInitialized) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(BatchAuthenticatorInitialized) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *BatchAuthenticatorInitializedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *BatchAuthenticatorInitializedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// BatchAuthenticatorInitialized represents a Initialized event raised by the BatchAuthenticator contract. +type BatchAuthenticatorInitialized struct { + Version uint64 + Raw types.Log // Blockchain specific contextual infos +} + +// FilterInitialized is a free log retrieval operation binding the contract event 0xc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2. +// +// Solidity: event Initialized(uint64 version) +func (_BatchAuthenticator *BatchAuthenticatorFilterer) FilterInitialized(opts *bind.FilterOpts) (*BatchAuthenticatorInitializedIterator, error) { + + logs, sub, err := _BatchAuthenticator.contract.FilterLogs(opts, "Initialized") + if err != nil { + return nil, err + } + return &BatchAuthenticatorInitializedIterator{contract: _BatchAuthenticator.contract, event: "Initialized", logs: logs, sub: sub}, nil +} + +// WatchInitialized is a free log subscription operation binding the contract event 0xc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2. +// +// Solidity: event Initialized(uint64 version) +func (_BatchAuthenticator *BatchAuthenticatorFilterer) WatchInitialized(opts *bind.WatchOpts, sink chan<- *BatchAuthenticatorInitialized) (event.Subscription, error) { + + logs, sub, err := _BatchAuthenticator.contract.WatchLogs(opts, "Initialized") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(BatchAuthenticatorInitialized) + if err := _BatchAuthenticator.contract.UnpackLog(event, "Initialized", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseInitialized is a log parse operation binding the contract event 0xc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2. +// +// Solidity: event Initialized(uint64 version) +func (_BatchAuthenticator *BatchAuthenticatorFilterer) ParseInitialized(log types.Log) (*BatchAuthenticatorInitialized, error) { + event := new(BatchAuthenticatorInitialized) + if err := _BatchAuthenticator.contract.UnpackLog(event, "Initialized", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// BatchAuthenticatorOwnershipTransferStartedIterator is returned from FilterOwnershipTransferStarted and is used to iterate over the raw logs and unpacked data for OwnershipTransferStarted events raised by the BatchAuthenticator contract. +type BatchAuthenticatorOwnershipTransferStartedIterator struct { + Event *BatchAuthenticatorOwnershipTransferStarted // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *BatchAuthenticatorOwnershipTransferStartedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(BatchAuthenticatorOwnershipTransferStarted) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(BatchAuthenticatorOwnershipTransferStarted) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *BatchAuthenticatorOwnershipTransferStartedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *BatchAuthenticatorOwnershipTransferStartedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// BatchAuthenticatorOwnershipTransferStarted represents a OwnershipTransferStarted event raised by the BatchAuthenticator contract. +type BatchAuthenticatorOwnershipTransferStarted struct { + PreviousOwner common.Address + NewOwner common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterOwnershipTransferStarted is a free log retrieval operation binding the contract event 0x38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e22700. +// +// Solidity: event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner) +func (_BatchAuthenticator *BatchAuthenticatorFilterer) FilterOwnershipTransferStarted(opts *bind.FilterOpts, previousOwner []common.Address, newOwner []common.Address) (*BatchAuthenticatorOwnershipTransferStartedIterator, error) { + + var previousOwnerRule []interface{} + for _, previousOwnerItem := range previousOwner { + previousOwnerRule = append(previousOwnerRule, previousOwnerItem) + } + var newOwnerRule []interface{} + for _, newOwnerItem := range newOwner { + newOwnerRule = append(newOwnerRule, newOwnerItem) + } + + logs, sub, err := _BatchAuthenticator.contract.FilterLogs(opts, "OwnershipTransferStarted", previousOwnerRule, newOwnerRule) + if err != nil { + return nil, err + } + return &BatchAuthenticatorOwnershipTransferStartedIterator{contract: _BatchAuthenticator.contract, event: "OwnershipTransferStarted", logs: logs, sub: sub}, nil +} + +// WatchOwnershipTransferStarted is a free log subscription operation binding the contract event 0x38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e22700. +// +// Solidity: event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner) +func (_BatchAuthenticator *BatchAuthenticatorFilterer) WatchOwnershipTransferStarted(opts *bind.WatchOpts, sink chan<- *BatchAuthenticatorOwnershipTransferStarted, previousOwner []common.Address, newOwner []common.Address) (event.Subscription, error) { + + var previousOwnerRule []interface{} + for _, previousOwnerItem := range previousOwner { + previousOwnerRule = append(previousOwnerRule, previousOwnerItem) + } + var newOwnerRule []interface{} + for _, newOwnerItem := range newOwner { + newOwnerRule = append(newOwnerRule, newOwnerItem) + } + + logs, sub, err := _BatchAuthenticator.contract.WatchLogs(opts, "OwnershipTransferStarted", previousOwnerRule, newOwnerRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(BatchAuthenticatorOwnershipTransferStarted) + if err := _BatchAuthenticator.contract.UnpackLog(event, "OwnershipTransferStarted", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseOwnershipTransferStarted is a log parse operation binding the contract event 0x38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e22700. +// +// Solidity: event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner) +func (_BatchAuthenticator *BatchAuthenticatorFilterer) ParseOwnershipTransferStarted(log types.Log) (*BatchAuthenticatorOwnershipTransferStarted, error) { + event := new(BatchAuthenticatorOwnershipTransferStarted) + if err := _BatchAuthenticator.contract.UnpackLog(event, "OwnershipTransferStarted", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// BatchAuthenticatorOwnershipTransferredIterator is returned from FilterOwnershipTransferred and is used to iterate over the raw logs and unpacked data for OwnershipTransferred events raised by the BatchAuthenticator contract. +type BatchAuthenticatorOwnershipTransferredIterator struct { + Event *BatchAuthenticatorOwnershipTransferred // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *BatchAuthenticatorOwnershipTransferredIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(BatchAuthenticatorOwnershipTransferred) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(BatchAuthenticatorOwnershipTransferred) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *BatchAuthenticatorOwnershipTransferredIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *BatchAuthenticatorOwnershipTransferredIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// BatchAuthenticatorOwnershipTransferred represents a OwnershipTransferred event raised by the BatchAuthenticator contract. +type BatchAuthenticatorOwnershipTransferred struct { + PreviousOwner common.Address + NewOwner common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterOwnershipTransferred is a free log retrieval operation binding the contract event 0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0. +// +// Solidity: event OwnershipTransferred(address indexed previousOwner, address indexed newOwner) +func (_BatchAuthenticator *BatchAuthenticatorFilterer) FilterOwnershipTransferred(opts *bind.FilterOpts, previousOwner []common.Address, newOwner []common.Address) (*BatchAuthenticatorOwnershipTransferredIterator, error) { + + var previousOwnerRule []interface{} + for _, previousOwnerItem := range previousOwner { + previousOwnerRule = append(previousOwnerRule, previousOwnerItem) + } + var newOwnerRule []interface{} + for _, newOwnerItem := range newOwner { + newOwnerRule = append(newOwnerRule, newOwnerItem) + } + + logs, sub, err := _BatchAuthenticator.contract.FilterLogs(opts, "OwnershipTransferred", previousOwnerRule, newOwnerRule) + if err != nil { + return nil, err + } + return &BatchAuthenticatorOwnershipTransferredIterator{contract: _BatchAuthenticator.contract, event: "OwnershipTransferred", logs: logs, sub: sub}, nil +} + +// WatchOwnershipTransferred is a free log subscription operation binding the contract event 0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0. +// +// Solidity: event OwnershipTransferred(address indexed previousOwner, address indexed newOwner) +func (_BatchAuthenticator *BatchAuthenticatorFilterer) WatchOwnershipTransferred(opts *bind.WatchOpts, sink chan<- *BatchAuthenticatorOwnershipTransferred, previousOwner []common.Address, newOwner []common.Address) (event.Subscription, error) { + + var previousOwnerRule []interface{} + for _, previousOwnerItem := range previousOwner { + previousOwnerRule = append(previousOwnerRule, previousOwnerItem) + } + var newOwnerRule []interface{} + for _, newOwnerItem := range newOwner { + newOwnerRule = append(newOwnerRule, newOwnerItem) + } + + logs, sub, err := _BatchAuthenticator.contract.WatchLogs(opts, "OwnershipTransferred", previousOwnerRule, newOwnerRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(BatchAuthenticatorOwnershipTransferred) + if err := _BatchAuthenticator.contract.UnpackLog(event, "OwnershipTransferred", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseOwnershipTransferred is a log parse operation binding the contract event 0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0. +// +// Solidity: event OwnershipTransferred(address indexed previousOwner, address indexed newOwner) +func (_BatchAuthenticator *BatchAuthenticatorFilterer) ParseOwnershipTransferred(log types.Log) (*BatchAuthenticatorOwnershipTransferred, error) { + event := new(BatchAuthenticatorOwnershipTransferred) + if err := _BatchAuthenticator.contract.UnpackLog(event, "OwnershipTransferred", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// BatchAuthenticatorSignerRegistrationInitiatedIterator is returned from FilterSignerRegistrationInitiated and is used to iterate over the raw logs and unpacked data for SignerRegistrationInitiated events raised by the BatchAuthenticator contract. +type BatchAuthenticatorSignerRegistrationInitiatedIterator struct { + Event *BatchAuthenticatorSignerRegistrationInitiated // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *BatchAuthenticatorSignerRegistrationInitiatedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(BatchAuthenticatorSignerRegistrationInitiated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(BatchAuthenticatorSignerRegistrationInitiated) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *BatchAuthenticatorSignerRegistrationInitiatedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *BatchAuthenticatorSignerRegistrationInitiatedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// BatchAuthenticatorSignerRegistrationInitiated represents a SignerRegistrationInitiated event raised by the BatchAuthenticator contract. +type BatchAuthenticatorSignerRegistrationInitiated struct { + Caller common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterSignerRegistrationInitiated is a free log retrieval operation binding the contract event 0x665b016a0ac50d1280744eaaff1cf21254d0fd30e4c3987d291913c32163416c. +// +// Solidity: event SignerRegistrationInitiated(address indexed caller) +func (_BatchAuthenticator *BatchAuthenticatorFilterer) FilterSignerRegistrationInitiated(opts *bind.FilterOpts, caller []common.Address) (*BatchAuthenticatorSignerRegistrationInitiatedIterator, error) { + + var callerRule []interface{} + for _, callerItem := range caller { + callerRule = append(callerRule, callerItem) + } + + logs, sub, err := _BatchAuthenticator.contract.FilterLogs(opts, "SignerRegistrationInitiated", callerRule) + if err != nil { + return nil, err + } + return &BatchAuthenticatorSignerRegistrationInitiatedIterator{contract: _BatchAuthenticator.contract, event: "SignerRegistrationInitiated", logs: logs, sub: sub}, nil +} + +// WatchSignerRegistrationInitiated is a free log subscription operation binding the contract event 0x665b016a0ac50d1280744eaaff1cf21254d0fd30e4c3987d291913c32163416c. +// +// Solidity: event SignerRegistrationInitiated(address indexed caller) +func (_BatchAuthenticator *BatchAuthenticatorFilterer) WatchSignerRegistrationInitiated(opts *bind.WatchOpts, sink chan<- *BatchAuthenticatorSignerRegistrationInitiated, caller []common.Address) (event.Subscription, error) { + + var callerRule []interface{} + for _, callerItem := range caller { + callerRule = append(callerRule, callerItem) + } + + logs, sub, err := _BatchAuthenticator.contract.WatchLogs(opts, "SignerRegistrationInitiated", callerRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(BatchAuthenticatorSignerRegistrationInitiated) + if err := _BatchAuthenticator.contract.UnpackLog(event, "SignerRegistrationInitiated", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseSignerRegistrationInitiated is a log parse operation binding the contract event 0x665b016a0ac50d1280744eaaff1cf21254d0fd30e4c3987d291913c32163416c. +// +// Solidity: event SignerRegistrationInitiated(address indexed caller) +func (_BatchAuthenticator *BatchAuthenticatorFilterer) ParseSignerRegistrationInitiated(log types.Log) (*BatchAuthenticatorSignerRegistrationInitiated, error) { + event := new(BatchAuthenticatorSignerRegistrationInitiated) + if err := _BatchAuthenticator.contract.UnpackLog(event, "SignerRegistrationInitiated", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} diff --git a/espresso/cli.go b/espresso/cli.go new file mode 100644 index 00000000000..9d1772ecf53 --- /dev/null +++ b/espresso/cli.go @@ -0,0 +1,306 @@ +package espresso + +import ( + "crypto/ecdsa" + "fmt" + "strings" + "time" + + op "github.com/EspressoSystems/espresso-streamers/op" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/log" + + "github.com/urfave/cli/v2" + + espressoClient "github.com/EspressoSystems/espresso-network/sdks/go/client" + espressoLightClient "github.com/EspressoSystems/espresso-network/sdks/go/light-client" +) + +// espressoFlags returns the flag names for espresso +func espressoFlags(v string) string { + return "espresso." + v +} + +func espressoEnvs(envprefix, v string) []string { + return []string{envprefix + "_ESPRESSO_" + v} +} + +// Default values for batch submission receipt verification tuning. +// Defined here so that both the CLI flag defaults and the batcher logic +// can reference a single source of truth. +// +// Note: DefaultBatchAuthLookbackWindow lives in constants.go (mips64-clean +// build target shared with the derivation pipeline). +const ( + DefaultVerifyReceiptMaxBlocks uint64 = 5 + DefaultVerifyReceiptSafetyTimeout time.Duration = 5 * time.Minute + DefaultVerifyReceiptRetryDelay time.Duration = 100 * time.Millisecond + DefaultMaxInFlightRequestsToEspresso = 128 +) + +var ( + EnabledFlagName = espressoFlags("enabled") + PollIntervalFlagName = espressoFlags("poll-interval") + QueryServiceUrlsFlagName = espressoFlags("urls") + LightClientAddrFlagName = espressoFlags("light-client-addr") + L1UrlFlagName = espressoFlags("l1-url") + TestingBatcherPrivateKeyFlagName = espressoFlags("testing-batcher-private-key") + CaffeinationHeightEspresso = espressoFlags("origin-height-espresso") + CaffeinationHeightL2 = espressoFlags("origin-height-l2") + NamespaceFlagName = espressoFlags("namespace") + RollupL1UrlFlagName = espressoFlags("rollup-l1-url") + AttestationServiceFlagName = espressoFlags("espresso-attestation-service") + BatchAuthenticatorAddrFlagName = espressoFlags("batch-authenticator-addr") + VerifyReceiptMaxBlocksFlagName = espressoFlags("verify-receipt-max-blocks") + VerifyReceiptSafetyTimeoutFlagName = espressoFlags("verify-receipt-safety-timeout") + VerifyReceiptRetryDelayFlagName = espressoFlags("verify-receipt-retry-delay") +) + +func CLIFlags(envPrefix string, category string) []cli.Flag { + return []cli.Flag{ + &cli.BoolFlag{ + Name: EnabledFlagName, + Usage: "Enable Espresso mode", + Value: false, + EnvVars: espressoEnvs(envPrefix, "ENABLED"), + Category: category, + }, + &cli.DurationFlag{ + Name: PollIntervalFlagName, + Usage: "Polling interval for Espresso queries", + Value: 250 * time.Millisecond, + EnvVars: espressoEnvs(envPrefix, "POLL_INTERVAL"), + Category: category, + }, + &cli.StringSliceFlag{ + Name: QueryServiceUrlsFlagName, + Usage: "Comma-separated list of Espresso query service URLs", + EnvVars: espressoEnvs(envPrefix, "URLS"), + Category: category, + }, + &cli.StringFlag{ + Name: LightClientAddrFlagName, + Usage: "Address of the Espresso light client", + EnvVars: espressoEnvs(envPrefix, "LIGHT_CLIENT_ADDR"), + Category: category, + }, + &cli.StringFlag{ + Name: L1UrlFlagName, + Usage: "L1 RPC URL Espresso contracts are deployed on", + EnvVars: espressoEnvs(envPrefix, "L1_URL"), + Category: category, + }, + &cli.StringFlag{ + Name: TestingBatcherPrivateKeyFlagName, + Usage: "Pre-approved batcher ephemeral key (testing only)", + EnvVars: espressoEnvs(envPrefix, "TESTING_BATCHER_PRIVATE_KEY"), + Category: category, + }, + &cli.Uint64Flag{ + Name: CaffeinationHeightEspresso, + Usage: "Espresso transactions below this height will not be considered", + EnvVars: espressoEnvs(envPrefix, "ORIGIN_HEIGHT_ESPRESSO"), + Category: category, + }, + &cli.Uint64Flag{ + Name: CaffeinationHeightL2, + Usage: "L2 batch position at which the Espresso streamer starts emitting batches. " + + "Operational parameter for restarting batchers mid-chain. " + + "When zero, the streamer falls back to its internal default. " + + "Independent of the EspressoTime hardfork, which gates derivation semantics.", + Value: 0, + EnvVars: espressoEnvs(envPrefix, "ORIGIN_HEIGHT_L2"), + Category: category, + }, + &cli.Uint64Flag{ + Name: NamespaceFlagName, + Usage: "Namespace of Espresso transactions", + EnvVars: espressoEnvs(envPrefix, "NAMESPACE"), + Category: category, + }, + &cli.StringFlag{ + Name: RollupL1UrlFlagName, + Usage: "RPC URL of L1 backing the Rollup we're streaming for", + EnvVars: espressoEnvs(envPrefix, "ROLLUP_L1_URL"), + Category: category, + }, + &cli.StringFlag{ + Name: AttestationServiceFlagName, + Usage: "URL of the Espresso attestation service", + EnvVars: espressoEnvs(envPrefix, "ESPRESSO_ATTESTATION_SERVICE"), + Category: category, + }, + &cli.StringFlag{ + Name: BatchAuthenticatorAddrFlagName, + Usage: "Address of the Batch Authenticator contract", + EnvVars: espressoEnvs(envPrefix, "BATCH_AUTHENTICATOR_ADDR"), + Category: category, + }, + &cli.Uint64Flag{ + Name: VerifyReceiptMaxBlocksFlagName, + Usage: "Number of HotShot blocks to wait for a submitted transaction to become queryable before re-submitting", + Value: DefaultVerifyReceiptMaxBlocks, + EnvVars: espressoEnvs(envPrefix, "VERIFY_RECEIPT_MAX_BLOCKS"), + Category: category, + }, + &cli.DurationFlag{ + Name: VerifyReceiptSafetyTimeoutFlagName, + Usage: "Wall-clock backstop for receipt verification; re-submits the transaction if this duration is exceeded", + Value: DefaultVerifyReceiptSafetyTimeout, + EnvVars: espressoEnvs(envPrefix, "VERIFY_RECEIPT_SAFETY_TIMEOUT"), + Category: category, + }, + &cli.DurationFlag{ + Name: VerifyReceiptRetryDelayFlagName, + Usage: "Delay between receipt verification retries", + Value: DefaultVerifyReceiptRetryDelay, + EnvVars: espressoEnvs(envPrefix, "VERIFY_RECEIPT_RETRY_DELAY"), + Category: category, + }, + // Note: --espresso.fallback-auth-lead-time is registered by the + // fallback batcher in op-batcher/flags/flags.go; it is read by both + // the fallback and the TEE batcher paths. + } +} + +type CLIConfig struct { + Enabled bool + PollInterval time.Duration + QueryServiceURLs []string + LightClientAddr common.Address + BatchAuthenticatorAddr common.Address + L1URL string + RollupL1URL string + TestingBatcherPrivateKey *ecdsa.PrivateKey + Namespace uint64 + CaffeinationHeightEspresso uint64 + CaffeinationHeightL2 uint64 + EspressoAttestationService string + + // Batch submission receipt verification tuning + VerifyReceiptMaxBlocks uint64 + VerifyReceiptSafetyTimeout time.Duration + VerifyReceiptRetryDelay time.Duration + + // Non directly configurable option + allowEmptyAttestationService bool `json:"-"` +} + +// AllowEmptyAttestationService allows the attestation service URL to be +// empty. This is set explicitly from a public method, and isn't derivable +// from serialization or any other form other than this method. This allows +// this setting to be configured via the code, but not externally. +func (c *CLIConfig) AllowEmptyAttestationService() { + c.allowEmptyAttestationService = true +} + +func (c CLIConfig) Check() error { + if c.Enabled { + // Check required fields when Espresso is enabled + if len(c.QueryServiceURLs) == 0 { + return fmt.Errorf("query service URLs are required when Espresso is enabled") + } + if c.LightClientAddr == (common.Address{}) { + return fmt.Errorf("light client address is required when Espresso is enabled") + } + if c.L1URL == "" { + return fmt.Errorf("L1 URL is required when Espresso is enabled") + } + if c.RollupL1URL == "" { + return fmt.Errorf("rollup L1 URL is required when Espresso is enabled") + } + if c.Namespace == 0 { + return fmt.Errorf("namespace is required when Espresso is enabled") + } + if !c.allowEmptyAttestationService && c.EspressoAttestationService == "" { + return fmt.Errorf("attestation service URL is required when Espresso is enabled") + } + if c.VerifyReceiptMaxBlocks == 0 { + return fmt.Errorf("verify-receipt-max-blocks must be > 0") + } + if c.VerifyReceiptSafetyTimeout <= 0 { + return fmt.Errorf("verify-receipt-safety-timeout must be > 0") + } + if c.VerifyReceiptRetryDelay <= 0 { + return fmt.Errorf("verify-receipt-retry-delay must be > 0") + } + } + return nil +} + +func ReadCLIConfig(c *cli.Context) CLIConfig { + config := CLIConfig{ + Enabled: c.Bool(EnabledFlagName), + PollInterval: c.Duration(PollIntervalFlagName), + L1URL: c.String(L1UrlFlagName), + RollupL1URL: c.String(RollupL1UrlFlagName), + Namespace: c.Uint64(NamespaceFlagName), + CaffeinationHeightEspresso: c.Uint64(CaffeinationHeightEspresso), + CaffeinationHeightL2: c.Uint64(CaffeinationHeightL2), + EspressoAttestationService: c.String(AttestationServiceFlagName), + VerifyReceiptMaxBlocks: c.Uint64(VerifyReceiptMaxBlocksFlagName), + VerifyReceiptSafetyTimeout: c.Duration(VerifyReceiptSafetyTimeoutFlagName), + VerifyReceiptRetryDelay: c.Duration(VerifyReceiptRetryDelayFlagName), + } + + config.QueryServiceURLs = c.StringSlice(QueryServiceUrlsFlagName) + + addrStr := c.String(LightClientAddrFlagName) + config.LightClientAddr = common.HexToAddress(addrStr) + + batchAuthenticatorAddrStr := c.String(BatchAuthenticatorAddrFlagName) + config.BatchAuthenticatorAddr = common.HexToAddress(batchAuthenticatorAddrStr) + + pkStr := c.String(TestingBatcherPrivateKeyFlagName) + pkStr = strings.TrimPrefix(pkStr, "0x") + pk, err := crypto.HexToECDSA(pkStr) + if err == nil { + config.TestingBatcherPrivateKey = pk + } + + return config +} + +func BatchStreamerFromCLIConfig[B op.Batch]( + cfg CLIConfig, + log log.Logger, + unmarshalBatch func([]byte) (*B, error), +) (*op.BatchStreamer[B], error) { + if !cfg.Enabled { + return nil, fmt.Errorf("espresso is not enabled") + } + + l1Client, err := ethclient.Dial(cfg.L1URL) + if err != nil { + return nil, fmt.Errorf("failed to dial L1 RPC at %s: %w", cfg.L1URL, err) + } + + RollupL1Client, err := ethclient.Dial(cfg.RollupL1URL) + if err != nil { + return nil, fmt.Errorf("failed to dial Rollup L1 RPC at %s: %w", cfg.RollupL1URL, err) + } + + urlZero := cfg.QueryServiceURLs[0] + espressoClient := espressoClient.NewClient(urlZero) + + espressoLightClient, err := espressoLightClient.NewLightclientCaller(cfg.LightClientAddr, l1Client) + if err != nil { + return nil, fmt.Errorf("failed to create Espresso light client") + } + + return op.NewEspressoStreamer( + cfg.Namespace, + NewAdaptL1BlockRefClient(l1Client), + NewAdaptL1BlockRefClient(RollupL1Client), + espressoClient, + espressoLightClient, + log, + unmarshalBatch, + cfg.CaffeinationHeightEspresso, + cfg.CaffeinationHeightL2, + cfg.BatchAuthenticatorAddr, + false, + ) +} diff --git a/espresso/ethclient.go b/espresso/ethclient.go new file mode 100644 index 00000000000..38328ce88f8 --- /dev/null +++ b/espresso/ethclient.go @@ -0,0 +1,60 @@ +package espresso + +import ( + "context" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + + "github.com/ethereum-optimism/optimism/espresso/bindings" +) + +// AdaptL1BlockRefClient is a wrapper around eth.L1BlockRef that implements the espresso.L1Client interface +type AdaptL1BlockRefClient struct { + L1Client *ethclient.Client +} + +// NewAdaptL1BlockRefClient creates a new L1BlockRefClient +func NewAdaptL1BlockRefClient(L1Client *ethclient.Client) *AdaptL1BlockRefClient { + return &AdaptL1BlockRefClient{ + L1Client: L1Client, + } +} + +// HeaderHashByNumber implements the espresso.L1Client interface +func (c *AdaptL1BlockRefClient) HeaderHashByNumber(ctx context.Context, number *big.Int) (common.Hash, error) { + expectedL1BlockRef, err := c.L1Client.HeaderByNumber(ctx, number) + if err != nil { + return common.Hash{}, err + } + + return expectedL1BlockRef.Hash(), nil +} + +func (c *AdaptL1BlockRefClient) CodeAt(ctx context.Context, contract common.Address, blockNumber *big.Int) ([]byte, error) { + return c.L1Client.CodeAt(ctx, contract, blockNumber) +} + +func (c *AdaptL1BlockRefClient) CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { + return c.L1Client.CallContract(ctx, call, blockNumber) +} + +// FetchEspressoBatcherAddress reads the Espresso batcher address from the BatchAuthenticator +// contract on L1. This is used by the caff node to determine which address signed +// Espresso batches, since the Espresso batcher may use a different key than the +// SystemConfig batcher (fallback batcher). +func FetchEspressoBatcherAddress(ctx context.Context, l1Client *ethclient.Client, batchAuthenticatorAddr common.Address) (common.Address, error) { + caller, err := bindings.NewBatchAuthenticatorCaller(batchAuthenticatorAddr, l1Client) + if err != nil { + return common.Address{}, fmt.Errorf("failed to bind BatchAuthenticator at %s: %w", batchAuthenticatorAddr, err) + } + addr, err := caller.EspressoBatcher(&bind.CallOpts{Context: ctx}) + if err != nil { + return common.Address{}, fmt.Errorf("failed to call BatchAuthenticator.espressoBatcher(): %w", err) + } + return addr, nil +} diff --git a/espresso/interface.go b/espresso/interface.go new file mode 100644 index 00000000000..8445baf08c6 --- /dev/null +++ b/espresso/interface.go @@ -0,0 +1,77 @@ +package espresso + +import ( + "context" + + op "github.com/EspressoSystems/espresso-streamers/op" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum/go-ethereum/common" +) + +// EspressoStreamer defines the interface for the Espresso streamer. +type EspressoStreamer[B op.Batch] interface { + // Update will update the `EspressoStreamer“ by attempting to ensure that + // the next call to the `Next` method will return a `Batch`. + // + // It attempts to ensure the existence of a next batch, provided no errors + // occur when communicating with HotShot, by processing Blocks retrieved + // from `HotShot` in discreet batches. If each processing of a batch of + // blocks will not yield a new `Batch`, then it will continue to process + // the next batch of blocks from HotShot until it runs out of blocks to + // process. + // + // NOTE: this method is best effort. It is unable to guarantee that the + // next call to `Next` will return a batch. However, the only things + // that will prevent the next call to `Next` from returning a batch is if + // there are no more HotShot blocks to process currently, or if an error + // occurs when communicating with HotShot. + Update(ctx context.Context) error + + // Refresh updates the local references of the EspressoStreamer to the + // specified values. + // + // These values can be used to help determine whether the Streamer needs + // to be reset or not. + // + // NOTE: This will only automatically reset the Streamer if the + // `safeBatchNumber` moves backwards. + Refresh(ctx context.Context, finalizedL1 eth.L1BlockRef, safeBatchNumber uint64, safeL1Origin eth.BlockID) error + + // RefreshSafeL1Origin updates the safe L1 origin for the streamer. This is + // used to help the streamer determine if it needs to be reset or not based + // on the safe L1 origin moving backwards. + // + // NOTE: This will only automatically reset the Streamer if the + // `safeL1Origin` moves backwards. + RefreshSafeL1Origin(safeL1Origin eth.BlockID) + + // Reset will reset the Streamer to the last known good safe state. + // This generally means resetting to the last know good safe batch + // position, but in the case of consuming blocks from Espresso, it will + // also reset the starting Espresso block position to the last known + // good safe block position there as well. + Reset() + + // UnmarshalBatch is a convenience method that allows the caller to + // attempt to unmarshal a batch from the provided byte slice. + UnmarshalBatch(b []byte) (*B, error) + + // HasNext checks to see if there are any batches left to read in the + // streamer. + HasNext(ctx context.Context) bool + + // Next attempts to return the next batch from the streamer. If there + // are no batches left to read, at the moment of the call, it will return + // nil. + Next(ctx context.Context) *B + + // Peek attempts to return the next batch from the streamer without advancing the streamer's position. + // If there are no batches left to read, at the moment of the call, it will return nil. + Peek(ctx context.Context) *B + + // SetProperHead drains stale/wrong-fork entries from the buffer front, + // positioning it at the correct fork for the next Peek call. Should be called + // when Peek returns a batch whose parentHash doesn't match the current chain tip. + // No-ops if headBatch's block number doesn't match the expected next batch position. + SetProperHead(parentHash common.Hash) +} diff --git a/go.mod b/go.mod index a51b969a148..a98c2518a8d 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,8 @@ toolchain go1.24.13 require ( github.com/BurntSushi/toml v1.5.0 + github.com/EspressoSystems/espresso-network/sdks/go v0.3.5-0.20260410134522-1a819609a513 + github.com/EspressoSystems/espresso-streamers v1.1.0 github.com/Masterminds/semver/v3 v3.3.1 github.com/andybalholm/brotli v1.1.0 github.com/base/go-bip39 v1.1.0 @@ -21,7 +23,7 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.4-0.20251001155152-4eb15ccedf7e github.com/ethereum-optimism/superchain-registry/validation v0.0.0-20260115192958-fb86a23cd30e - github.com/ethereum/go-ethereum v1.17.0 + github.com/ethereum/go-ethereum v1.17.1 github.com/fsnotify/fsnotify v1.9.0 github.com/golang/snappy v1.0.0 github.com/google/go-cmp v0.7.0 @@ -32,6 +34,8 @@ require ( github.com/hashicorp/raft v1.7.3 github.com/hashicorp/raft-boltdb/v2 v2.3.1 github.com/hashicorp/raft-wal v0.4.2 + github.com/hf/nitrite v0.0.0-20241225144000-c2d5d3c4f303 + github.com/hf/nsm v0.0.0-20220930140112-cd181bd646b9 github.com/holiman/uint256 v1.3.2 github.com/ipfs/go-datastore v0.6.0 github.com/ipfs/go-ds-leveldb v0.5.0 @@ -69,7 +73,10 @@ require ( gopkg.in/yaml.v3 v3.0.1 ) -require github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect +require ( + github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect + github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f // indirect +) require ( github.com/benbjohnson/immutable v0.4.0 // indirect @@ -77,10 +84,12 @@ require ( github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect github.com/coreos/pkg v0.0.0-20220810130054-c7d1c02cb6cf // indirect github.com/fatih/color v1.18.0 // indirect + github.com/fxamacker/cbor/v2 v2.2.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/grafana/pyroscope-go v1.2.7 // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/x448/float16 v0.8.4 // indirect golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 // indirect ) @@ -113,7 +122,7 @@ require ( github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect github.com/containerd/cgroups v1.1.0 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect github.com/dchest/siphash v1.2.3 // indirect diff --git a/go.sum b/go.sum index e3f1c2994ab..6e5dca219ba 100644 --- a/go.sum +++ b/go.sum @@ -30,6 +30,10 @@ github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3 github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/zstd v1.5.6-0.20230824185856-869dae002e5e h1:ZIWapoIRN1VqT8GR8jAwb1Ie9GyehWjVcGh32Y2MznE= github.com/DataDog/zstd v1.5.6-0.20230824185856-869dae002e5e/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/EspressoSystems/espresso-network/sdks/go v0.3.5-0.20260410134522-1a819609a513 h1:4KqlmnDg8pPPj1hCtiLnN0Fh9ltp+PPKOft9I06zQOA= +github.com/EspressoSystems/espresso-network/sdks/go v0.3.5-0.20260410134522-1a819609a513/go.mod h1:kaxR08mJb5Mijy7a2RhWCIWOevFI4PcXwDkzoEbsVTk= +github.com/EspressoSystems/espresso-streamers v1.1.0 h1:+35Y9I3BCMyVIyFX6DNO6tq8ZKeuyOAve4DrgIhaY2s= +github.com/EspressoSystems/espresso-streamers v1.1.0/go.mod h1:eDGUEvlyxNey9gdpQhKaswAOp5ZWHPVbhNtWfHkpJvU= github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= @@ -171,8 +175,8 @@ github.com/coreos/pkg v0.0.0-20220810130054-c7d1c02cb6cf h1:GOPo6vn/vTN+3IwZBvXX github.com/coreos/pkg v0.0.0-20220810130054-c7d1c02cb6cf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= -github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/crate-crypto/go-eth-kzg v1.5.0 h1:FYRiJMJG2iv+2Dy3fi14SVGjcPteZ5HAAUe4YWlJygc= github.com/crate-crypto/go-eth-kzg v1.5.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= github.com/crate-crypto/go-kzg-4844 v1.1.0 h1:EN/u9k2TF6OWSHrCCDBBU6GLNMq88OspHHlMnHfoyU4= @@ -256,6 +260,8 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4 github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.2.0 h1:6eXqdDDe588rSYAi1HfZKbx6YYQO4mxQ9eC6xYpU/JQ= +github.com/fxamacker/cbor/v2 v2.2.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 h1:f6D9Hr8xV8uYKlyuj8XIruxlh9WjVjdh1gIicAS7ays= github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= github.com/getkin/kin-openapi v0.53.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= @@ -419,6 +425,10 @@ github.com/hashicorp/raft-boltdb/v2 v2.3.1 h1:ackhdCNPKblmOhjEU9+4lHSJYFkJd6Jqyv github.com/hashicorp/raft-boltdb/v2 v2.3.1/go.mod h1:n4S+g43dXF1tqDT+yzcXHhXM6y7MrlUd3TTwGRcUvQE= github.com/hashicorp/raft-wal v0.4.2 h1:DV1jgqEumNfdNpOaZ9mL1Gu7Mz59epFtiE6CoqnHrlY= github.com/hashicorp/raft-wal v0.4.2/go.mod h1:S92ainH+6fRuWk6BtZKJ8EgcGgNTKx48Hk5dhOOY1DM= +github.com/hf/nitrite v0.0.0-20241225144000-c2d5d3c4f303 h1:XBSq4rXFUgD8ic6Mr7dBwJN/47yg87XpZQhiknfr4Cg= +github.com/hf/nitrite v0.0.0-20241225144000-c2d5d3c4f303/go.mod h1:ycRhVmo6wegyEl6WN+zXOHUTJvB0J2tiuH88q/McTK8= +github.com/hf/nsm v0.0.0-20220930140112-cd181bd646b9 h1:pU32bJGmZwF4WXb9Yaz0T8vHDtIPVxqDOdmYdwTQPqw= +github.com/hf/nsm v0.0.0-20220930140112-cd181bd646b9/go.mod h1:MJsac5D0fKcNWfriUERtln6segcGfD6Nu0V5uGBbPf8= github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db h1:IZUYC/xb3giYwBLMnr8d0TGTzPKFGNTCGgGLoyeX330= github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db/go.mod h1:xTEYN9KCHxuYHs+NmrmzFcnvHMzLLNiGFafCb1n3Mfg= github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= @@ -823,6 +833,8 @@ github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go. github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= +github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f h1:1R9KdKjCNSd7F8iGTxIpoID9prlYH8nuNYKt0XvweHA= +github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f/go.mod h1:vQhwQ4meQEDfahT5kd61wLAF5AAeh5ZPLVI4JJ/tYo8= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= @@ -881,6 +893,8 @@ github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMI github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/wlynxg/anet v0.0.4 h1:0de1OFQxnNqAu+x2FAKKCVIrnfGKQbs7FQz++tB0+Uw= github.com/wlynxg/anet v0.0.4/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= @@ -956,6 +970,7 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -1127,6 +1142,7 @@ golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105210202-9ed45478a130/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= diff --git a/op-batcher/batcher/config.go b/op-batcher/batcher/config.go index c6530742453..9089332dc70 100644 --- a/op-batcher/batcher/config.go +++ b/op-batcher/batcher/config.go @@ -8,6 +8,7 @@ import ( "github.com/ethereum/go-ethereum/params" "github.com/urfave/cli/v2" + "github.com/ethereum-optimism/optimism/espresso" altda "github.com/ethereum-optimism/optimism/op-alt-da" "github.com/ethereum-optimism/optimism/op-batcher/compressor" "github.com/ethereum-optimism/optimism/op-batcher/config" @@ -152,6 +153,15 @@ type CLIConfig struct { PprofConfig oppprof.CLIConfig RPC oprpc.CLIConfig AltDA altda.CLIConfig + + // FallbackAuthLeadTime is the lead time for the fallback batcher's + // authentication gate. See BatcherConfig.FallbackAuthLeadTime in + // service.go and isFallbackAuthRequired in espresso_active.go. + FallbackAuthLeadTime time.Duration + + // Espresso groups all TEE-batcher-specific CLI flags. See + // espresso/cli.go for the flag definitions. + Espresso espresso.CLIConfig } func (c *CLIConfig) Check() error { @@ -248,6 +258,8 @@ func NewConfig(ctx *cli.Context) *CLIConfig { PprofConfig: oppprof.ReadCLIConfig(ctx), RPC: oprpc.ReadCLIConfig(ctx), AltDA: altda.ReadCLIConfig(ctx), + FallbackAuthLeadTime: ctx.Duration(flags.FallbackAuthLeadTimeFlag.Name), + Espresso: espresso.ReadCLIConfig(ctx), ThrottleConfig: ThrottleConfig{ AdditionalEndpoints: ctx.StringSlice(flags.AdditionalThrottlingEndpointsFlag.Name), TxSizeLowerLimit: ctx.Uint64(flags.ThrottleTxSizeLowerLimitFlag.Name), diff --git a/op-batcher/batcher/driver.go b/op-batcher/batcher/driver.go index 6924440757e..178156f28f3 100644 --- a/op-batcher/batcher/driver.go +++ b/op-batcher/batcher/driver.go @@ -12,6 +12,7 @@ import ( "golang.org/x/sync/errgroup" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core" @@ -20,6 +21,7 @@ import ( "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rpc" + "github.com/ethereum-optimism/optimism/espresso" altda "github.com/ethereum-optimism/optimism/op-alt-da" "github.com/ethereum-optimism/optimism/op-batcher/batcher/throttler" config "github.com/ethereum-optimism/optimism/op-batcher/config" @@ -28,6 +30,7 @@ import ( "github.com/ethereum-optimism/optimism/op-node/rollup/derive" "github.com/ethereum-optimism/optimism/op-service/dial" "github.com/ethereum-optimism/optimism/op-service/eth" + oplog "github.com/ethereum-optimism/optimism/op-service/log" "github.com/ethereum-optimism/optimism/op-service/txmgr" ) @@ -73,6 +76,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) + bind.ContractBackend } type L2Client interface { @@ -100,6 +104,11 @@ type DriverSetup struct { ChannelConfig ChannelConfigProvider AltDA AltDAClient ChannelOutFactory ChannelOutFactory + + // Espresso groups all TEE-batcher-specific runtime state plumbed from + // BatcherService. Defined in espresso_driver.go to keep the upstream + // Optimism field block compact. + Espresso EspressoDriverSetup } // BatchSubmitter encapsulates a service responsible for submitting L2 tx @@ -124,6 +133,21 @@ type BatchSubmitter struct { throttleController *throttler.ThrottleController publishSignal chan pubInfo + + // authGroup serializes in-flight BatchAuthenticator submissions issued by + // the fallback batcher's authentication path and by the TEE batcher's + // authentication path so the publishing loop can drain them on shutdown. + // Bounded to fallbackAuthGroupLimit; see espresso_driver.go. + authGroup errgroup.Group + + espressoSubmitter *espressoTransactionSubmitter + espressoStreamer espresso.EspressoStreamer[derive.EspressoBatch] + + teeVerifierAddress common.Address + + // degradedLog throttles repeated warnings from tick-driven loops so the + // log debouncer doesn't see the same message every poll interval. + degradedLog *oplog.RepeatStateLogger } // NewBatchSubmitter initializes the BatchSubmitter driver from a preconfigured DriverSetup @@ -136,6 +160,7 @@ func NewBatchSubmitter(setup DriverSetup) *BatchSubmitter { batcher := &BatchSubmitter{ DriverSetup: setup, channelMgr: state, + degradedLog: oplog.NewRepeatStateLogger(), } err := batcher.SetThrottleController(setup.Config.ThrottleParams.ControllerType, setup.Config.ThrottleParams.PIDConfig) @@ -143,6 +168,9 @@ func NewBatchSubmitter(setup DriverSetup) *BatchSubmitter { panic(err) } + batcher.initAuthGroup() + batcher.setupEspressoStreamer() + return batcher } @@ -190,10 +218,16 @@ func (l *BatchSubmitter) StartBatchSubmitting() error { l.Log.Warn("Throttling loop is DISABLED due to 0 throttle-threshold. This should not be disabled in prod.") } - l.wg.Add(3) - go l.receiptsLoop(l.wg, receiptsCh) // ranges over receiptsCh channel - go l.publishingLoop(l.killCtx, l.wg, receiptsCh, publishSignal) // ranges over publishSignal, spawns routines which send on receiptsCh. Closes receiptsCh when done. - go l.blockLoadingLoop(l.shutdownCtx, l.wg, unsafeBytesUpdated, publishSignal) // sends on unsafeBytesUpdated (if throttling enabled), and publishSignal. Closes them both when done + if l.Config.Espresso.Enabled { + if err := l.startEspressoLoops(receiptsCh, publishSignal); err != nil { + return err + } + } else { + l.wg.Add(3) + go l.receiptsLoop(l.wg, receiptsCh) // ranges over receiptsCh channel + go l.publishingLoop(l.killCtx, l.wg, receiptsCh, publishSignal) // ranges over publishSignal, spawns routines which send on receiptsCh. Closes receiptsCh when done. + go l.blockLoadingLoop(l.shutdownCtx, l.wg, unsafeBytesUpdated, publishSignal) // sends on unsafeBytesUpdated (if throttling enabled), and publishSignal. Closes them both when done + } l.Log.Info("Batch Submitter started") return nil @@ -516,6 +550,12 @@ func (l *BatchSubmitter) publishingLoop(ctx context.Context, wg *sync.WaitGroup, } } + // Wait for all in-flight fallback-auth submissions to complete to prevent + // new transactions being queued. No-op when the rollup is not configured + // with a BatchAuthenticator or when the EspressoTime hardfork has not + // activated. + l.waitForAuthGroup() + // We _must_ wait for all senders on receiptsCh to finish before we can close it. if err := txQueue.Wait(); err != nil { if !errors.Is(err, context.Canceled) { @@ -829,6 +869,7 @@ func (l *BatchSubmitter) clearState(ctx context.Context) { l.channelMgrMutex.Lock() defer l.channelMgrMutex.Unlock() l.channelMgr.Clear(l1SafeOrigin) + l.resetEspressoStreamer() return true } } @@ -1035,6 +1076,13 @@ func (l *BatchSubmitter) sendTx(txdata txData, isCancel bool, candidate *txmgr.T candidate.GasLimit = floorDataGas } + // Route through the fallback-auth path when a BatchAuthenticator is + // configured and the EspressoTime hardfork is active. Falls through to + // the upstream queue.Send path otherwise. + if l.dispatchAuthenticatedSendTx(txdata, isCancel, candidate, queue, receiptsCh) { + return + } + queue.Send(txRef{id: txdata.ID(), isCancel: isCancel, isBlob: txdata.daType == DaTypeBlob, daType: txdata.daType, size: txdata.Len()}, *candidate, receiptsCh) } diff --git a/op-batcher/batcher/driver_test.go b/op-batcher/batcher/driver_test.go index 5ae3ade33fe..6c50feac001 100644 --- a/op-batcher/batcher/driver_test.go +++ b/op-batcher/batcher/driver_test.go @@ -14,6 +14,8 @@ import ( "testing" "time" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + altda "github.com/ethereum-optimism/optimism/op-alt-da" "github.com/ethereum-optimism/optimism/op-batcher/compressor" "github.com/ethereum-optimism/optimism/op-batcher/config" @@ -481,6 +483,10 @@ func TestBatchSubmitter_CriticalError(t *testing.T) { // fakeL1Client is just a dummy struct. All fault injection is done via the fakeTxMgr (which doesn't interact with this fakeL1Client). type fakeL1Client struct { + // Embed bind.ContractBackend so the type satisfies the L1Client interface + // (which requires it for the BatchAuthenticator binding used by the + // fallback batcher). AltDA tests never exercise these methods. + bind.ContractBackend } func (f *fakeL1Client) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) { diff --git a/op-batcher/batcher/espresso.go b/op-batcher/batcher/espresso.go new file mode 100644 index 00000000000..5825626bf11 --- /dev/null +++ b/op-batcher/batcher/espresso.go @@ -0,0 +1,1485 @@ +package batcher + +import ( + "bytes" + "context" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "math/big" + "net/http" + "strings" + "sync" + "sync/atomic" + "time" + + espressoClient "github.com/EspressoSystems/espresso-network/sdks/go/client" + tagged_base64 "github.com/EspressoSystems/espresso-network/sdks/go/tagged-base64" + espressoCommon "github.com/EspressoSystems/espresso-network/sdks/go/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/signer/core/apitypes" + + "github.com/ethereum-optimism/optimism/espresso" + "github.com/ethereum-optimism/optimism/espresso/bindings" + "github.com/ethereum-optimism/optimism/op-node/rollup/derive" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/txmgr" +) + +// EspressoOnchainProof is the proof structure returned by the attestation service for onchain verification. +type EspressoOnchainProof struct { + Proof json.RawMessage `json:"proof,omitempty"` + Data json.RawMessage `json:"data,omitempty"` + RawProof struct { + Journal string `json:"journal"` + } `json:"raw_proof"` + OnchainProof string `json:"onchain_proof"` +} + +// espressoSubmitTransactionJob is a struct that holds the state required to +// submit a transaction to Espresso. +// It contains the transaction to be submitted itself, and a number to +// track the total number of attempts to submit this transaction to Espresso. +type espressoSubmitTransactionJob struct { + attempts int + transaction *espressoCommon.Transaction +} + +// espressoSubmitTransactionJobResponse is a struct that holds the +// response from the Espresso client after submitting a transaction. +// It contains the job that was submitted, the hash of the transaction +// that was submitted (if successful), and any error that occurred during the +// submission (if unsuccessful). +type espressoSubmitTransactionJobResponse struct { + job espressoSubmitTransactionJob + hash *espressoCommon.TaggedBase64 + err error +} + +// espressoTransactionJobAttempt is a struct that holds the job and +// response channel for a transaction submission job. +// +// This is the unit of work that is submitted to the worker to process +// for transaction submissions. +type espressoTransactionJobAttempt struct { + job espressoSubmitTransactionJob + resp chan espressoSubmitTransactionJobResponse +} + +// espressoVerifyReceiptJob is a struct that holds the state required to +// verify a receipt for a transaction that was submitted to Espresso. +// It contains the transaction that was submitted, the hash of the +// transaction, and the number of attempts to verify the receipt. +type espressoVerifyReceiptJob struct { + attempts int + startHeight uint64 // HotShot block height when verification began (set on first attempt) + startTime time.Time // wall-clock time when verification began (safety backstop) + transaction espressoSubmitTransactionJob + hash *espressoCommon.TaggedBase64 +} + +// espressoVerifyReceiptJobResponse is a struct that holds the +// response from the Espresso client after verifying a receipt. +// It contains the job that was submitted, and any error that occurred +// during the verification (if unsuccessful). +type espressoVerifyReceiptJobResponse struct { + job espressoVerifyReceiptJob + err error + currentHeight uint64 // latest known HotShot block height at time of verification attempt +} + +// espressoVerifyReceiptJobAttempt is a struct that holds the job and +// response channel for a receipt verification job. +// +// This is the unit of work that is submitted to the worker to process +// for receipt verifications. +type espressoVerifyReceiptJobAttempt struct { + job espressoVerifyReceiptJob + resp chan espressoVerifyReceiptJobResponse +} + +// espressoTransactionSubmitter is a struct that holds the state that governs +// the worker queue processing details for submitting transactions to Espresso +// without spawning arbitrarily many goroutines. +type espressoTransactionSubmitter struct { + ctx context.Context + wg *sync.WaitGroup + submitJobQueue chan espressoSubmitTransactionJob + submitRespQueue chan espressoSubmitTransactionJobResponse + submitWorkerQueue chan chan espressoTransactionJobAttempt + verifyReceiptJobQueue chan espressoVerifyReceiptJob + verifyReceiptRespQueue chan espressoVerifyReceiptJobResponse + verifyReceiptWorkerQueue chan chan espressoVerifyReceiptJobAttempt + espresso espressoClient.EspressoClient + latestBlockHeight atomic.Uint64 // shared HotShot block height, updated by trackBlockHeight + verifyReceiptMaxBlocks uint64 + verifyReceiptSafetyTimeout time.Duration + verifyReceiptRetryDelay time.Duration + numInFlightJobs atomic.Int64 + numMaxInFlightJobs int +} + +// EspressoTransactionSubmitterConfig is a configuration struct for the +// EspressoTransactionSubmitter. It contains the configurable details for +// creating the EspressoTransactionSubmitter. +type EspressoTransactionSubmitterConfig struct { + Ctx context.Context + EspressoClient espressoClient.EspressoClient + Wg *sync.WaitGroup + SubmitJobQueueCapacity int + SubmitResponseQueueCapacity int + VerifyReceiptJobQueueCapacity int + VerifyReceiptResponseQueueCapacity int + VerifyReceiptMaxBlocks uint64 + VerifyReceiptSafetyTimeout time.Duration + VerifyReceiptRetryDelay time.Duration + MaxInFlightJobs int +} + +// EspressoTransactionSubmitterOption is a function that can be used to +// configure the EspressoTransactionSubmitterConfig. +type EspressoTransactionSubmitterOption func(*EspressoTransactionSubmitterConfig) + +// WithContext is an option that can be used to set the Espresso client +// for the EspressoTransactionSubmitterConfig. +func WithContext(ctx context.Context) EspressoTransactionSubmitterOption { + return func(config *EspressoTransactionSubmitterConfig) { + config.Ctx = ctx + } +} + +// WithEspressoClient is an option that can be used to set the Espresso client +// for the EspressoTransactionSubmitterConfig. +func WithEspressoClient(client espressoClient.EspressoClient) EspressoTransactionSubmitterOption { + return func(config *EspressoTransactionSubmitterConfig) { + config.EspressoClient = client + } +} + +// WithWaitGroup is an option that can be used to set the wait group +// for the EspressoTransactionSubmitterConfig. +func WithWaitGroup(wg *sync.WaitGroup) EspressoTransactionSubmitterOption { + return func(config *EspressoTransactionSubmitterConfig) { + config.Wg = wg + } +} + +// WithVerifyReceiptMaxBlocks sets the number of HotShot blocks to wait for a +// submitted transaction to become queryable before re-submitting. +func WithVerifyReceiptMaxBlocks(n uint64) EspressoTransactionSubmitterOption { + return func(config *EspressoTransactionSubmitterConfig) { + config.VerifyReceiptMaxBlocks = n + } +} + +// WithVerifyReceiptSafetyTimeout sets the wall-clock backstop for receipt +// verification. If the block height tracker is stale or broken, re-submission +// is triggered after this duration. +func WithVerifyReceiptSafetyTimeout(d time.Duration) EspressoTransactionSubmitterOption { + return func(config *EspressoTransactionSubmitterConfig) { + config.VerifyReceiptSafetyTimeout = d + } +} + +// WithVerifyReceiptRetryDelay sets the delay between receipt verification retries. +func WithVerifyReceiptRetryDelay(d time.Duration) EspressoTransactionSubmitterOption { + return func(config *EspressoTransactionSubmitterConfig) { + config.VerifyReceiptRetryDelay = d + } +} + +// WithMaxInFlightJobs sets the maximum number of inflight requests to +// have at once. Once at capacity all new submission attempts will +// automatically fail. +func WithMaxInFlightJobs(n int) EspressoTransactionSubmitterOption { + return func(config *EspressoTransactionSubmitterConfig) { + config.MaxInFlightJobs = n + } +} + +// NewEspressoTransactionSubmitter creates a new EspressoTransactionSubmitter +// throttle with the given context and espresso client. It will create a new +// transaction submitter with some default options, and apply those options to +// the configuration. +// +// The resulting instance should reflect the given configuration. +// After returning, the caller should call SpawnWorkers to start the workers, +// and Start to start the job scheduling and response handling portions of the +// transaction submitter. After that, the user should be able to submit +// transactions to the submitter via the SubmitTransaction method. +func NewEspressoTransactionSubmitter(options ...EspressoTransactionSubmitterOption) *espressoTransactionSubmitter { + config := EspressoTransactionSubmitterConfig{ + Ctx: context.Background(), + Wg: new(sync.WaitGroup), + SubmitJobQueueCapacity: espresso.DefaultMaxInFlightRequestsToEspresso, + SubmitResponseQueueCapacity: 10, + VerifyReceiptJobQueueCapacity: espresso.DefaultMaxInFlightRequestsToEspresso, + VerifyReceiptResponseQueueCapacity: 10, + VerifyReceiptMaxBlocks: espresso.DefaultVerifyReceiptMaxBlocks, + VerifyReceiptSafetyTimeout: espresso.DefaultVerifyReceiptSafetyTimeout, + VerifyReceiptRetryDelay: espresso.DefaultVerifyReceiptRetryDelay, + MaxInFlightJobs: espresso.DefaultMaxInFlightRequestsToEspresso, + } + + for _, option := range options { + option(&config) + } + + if config.EspressoClient == nil { + panic("Espresso client is required") + } + + return &espressoTransactionSubmitter{ + ctx: config.Ctx, + wg: config.Wg, + submitJobQueue: make(chan espressoSubmitTransactionJob, config.SubmitJobQueueCapacity), + submitRespQueue: make(chan espressoSubmitTransactionJobResponse, config.SubmitResponseQueueCapacity), + submitWorkerQueue: make(chan chan espressoTransactionJobAttempt), + verifyReceiptJobQueue: make(chan espressoVerifyReceiptJob, config.VerifyReceiptJobQueueCapacity), + verifyReceiptRespQueue: make(chan espressoVerifyReceiptJobResponse, config.VerifyReceiptResponseQueueCapacity), + verifyReceiptWorkerQueue: make(chan chan espressoVerifyReceiptJobAttempt), + espresso: config.EspressoClient, + verifyReceiptMaxBlocks: config.VerifyReceiptMaxBlocks, + verifyReceiptSafetyTimeout: config.VerifyReceiptSafetyTimeout, + verifyReceiptRetryDelay: config.VerifyReceiptRetryDelay, + numInFlightJobs: atomic.Int64{}, + numMaxInFlightJobs: config.MaxInFlightJobs, + } +} + +// ErrTooManyInFlightRequests is an error that is returned when there are +// too many requests in flight at once right now. +type ErrTooManyInFlightRequests struct { + NumInFlightRequests int + MaxInFlightRequests int +} + +// Error implements error +func (e ErrTooManyInFlightRequests) Error() string { + return fmt.Sprintf("too many requests in flight to espresso, in flight requests: %d, maximum allowed: %d", e.NumInFlightRequests, e.MaxInFlightRequests) +} + +// ErrSubmitToEspressoChannelFull is an error that is returned when the channel +// to submit a transaction to the espresso job channel is full. +// +// This ultimately means that the channel buffer size of the job channel is +// smaller than the max number of in flight requests allowed. +type ErrSubmitToEspressoChannelFull struct { + Capacity int + Len int +} + +// Error implements error +func (e ErrSubmitToEspressoChannelFull) Error() string { + return fmt.Sprintf("submit transaction to espresso job channel is full, len: %d, capacity: %d", e.Len, e.Capacity) +} + +// SubmitTransaction will submit a transaction to the Job queue. +// +// NOTE: There is a limit on the maximum number of inflight requests we allow +// at once. If we're over or at capacity, we'll return an error indicating so. +// If we have capacity available, we'll attempt to submit to the channel, and +// if we're unable to, we'll return an error. This will **NOT** block. +func (s *espressoTransactionSubmitter) SubmitTransaction(job *espressoCommon.Transaction) error { + // Check to see if we're over capacity, and if we are, then immediately + // return an error. + if numInFlightRequests, numMaxInFlightRequests := s.numInFlightJobs.Load(), int64(s.numMaxInFlightJobs); numInFlightRequests >= numMaxInFlightRequests { + return ErrTooManyInFlightRequests{ + NumInFlightRequests: int(numInFlightRequests), + MaxInFlightRequests: int(numMaxInFlightRequests), + } + } + + // Construct the job submission + jobSubmission := espressoSubmitTransactionJob{ + transaction: job, + } + + select { + default: + return ErrSubmitToEspressoChannelFull{ + Len: len(s.submitJobQueue), + Capacity: cap(s.submitJobQueue), + } + case s.submitJobQueue <- jobSubmission: + // increment in flight requests + s.numInFlightJobs.Add(1) + return nil + } +} + +// Evaluation result for a job. +type JobEvaluation int + +const ( + // Continue handling the current job. + Handle JobEvaluation = iota + // Retry the submission. + RetrySubmission + // Retry the verification. + RetryVerification + // Skip the current job and proceed to the next one. + Skip +) + +// Evaluate the submission job. +// +// # Returns +// +// * If there is no error: Handle. +// +// * If there is a permanent issue that won't be fixed by a retry: Skip. +// +// * Otherwise: RetrySubmission. +func evaluateSubmission(jobResp espressoSubmitTransactionJobResponse) JobEvaluation { + err := jobResp.err + + // If there's no error, continue handling the submission. + if err == nil { + return Handle + } + + if errors.Is(err, espressoClient.ErrPermanent) { + return Skip + } + + if !errors.Is(err, espressoClient.ErrEphemeral) { + // Log the warning for a potentially missed error handling, but still retry it. + log.Warn("error not explicitly marked as retryable or not", "err", err) + } + + // Otherwise, retry the submission. + return RetrySubmission +} + +// handleTransactionSubmitJobResponse is a function that is meant to be run in a +// goroutine. +// +// It handles the responses from the submit transaction jobs. It will +// determine if the transaction was successfully submitted to Espresso, and +// if not, it will retry the transaction. If the transaction was successfully +// submitted, it will then submit a job to the verify receipt job queue to +// verify the receipt of the transaction. +func (s *espressoTransactionSubmitter) handleTransactionSubmitJobResponse() { + ticker := time.NewTicker(10 * time.Minute) + defer ticker.Stop() + + for { + var jobResp espressoSubmitTransactionJobResponse + var ok bool + + select { + case <-s.ctx.Done(): + return + case <-ticker.C: + log.Debug("Espresso transaction submitter queue status", + "submitJobQueue", len(s.submitJobQueue), + "submitRespQueue", len(s.submitRespQueue), + "verifyReceiptJobQueue", len(s.verifyReceiptJobQueue), + "verifyReceiptRespQueue", len(s.verifyReceiptRespQueue)) + continue + case jobResp, ok = <-s.submitRespQueue: + if !ok { + // Our channel is closed, and we are done + return + } + } + + switch evaluation := evaluateSubmission(jobResp); evaluation { + case Skip: + s.numInFlightJobs.Add(-1) + continue + case RetrySubmission: + s.submitJobQueue <- jobResp.job + continue + } + + verifyJob := espressoVerifyReceiptJob{ + startTime: time.Now(), + transaction: jobResp.job, + hash: jobResp.hash, + } + + select { + case <-s.ctx.Done(): + return + // Move to verifying the receipt + case s.verifyReceiptJobQueue <- verifyJob: + } + } +} + +// Default values for receipt verification tuning are defined as exported +// constants in the espresso package (espresso.DefaultVerifyReceipt*) so that +// the CLI flag defaults and this batcher logic share a single source of truth. + +// evaluateVerification evaluates the verification job response. +// +// # Returns +// +// * If there is no error: Handle. +// +// * If there is a permanent issue that won't be fixed by a retry: Skip. +// +// * If enough HotShot blocks have passed since verification started: RetrySubmission. +// +// * If the wall-clock safety timeout is exceeded: RetrySubmission. +// +// * Otherwise: RetryVerification. +func (s *espressoTransactionSubmitter) evaluateVerification(jobResp espressoVerifyReceiptJobResponse) JobEvaluation { + err := jobResp.err + + // If there's no error, continue handling the verification. + if err == nil { + return Handle + } + + if errors.Is(err, espressoClient.ErrPermanent) { + return Skip + } + + if !errors.Is(err, espressoClient.ErrEphemeral) { + // Log the warning for a potentially missed error handling, but still retry it. + log.Warn("error not explicitly marked as retryable or not", "err", err) + } + + // Block-count-based timeout: re-submit if enough HotShot blocks have + // passed since verification started. The startHeight guard handles the + // edge case where the height tracker hasn't fetched its first value yet. + if jobResp.job.startHeight > 0 && jobResp.currentHeight >= jobResp.job.startHeight+s.verifyReceiptMaxBlocks { + log.Info("Verification timed out by block count, re-submitting", + "startHeight", jobResp.job.startHeight, + "currentHeight", jobResp.currentHeight, + "maxBlocks", s.verifyReceiptMaxBlocks) + return RetrySubmission + } + + // Wall-clock safety backstop in case the block height tracker is stale + // or broken (e.g., query service returning old data). + if elapsed := time.Since(jobResp.job.startTime); elapsed > s.verifyReceiptSafetyTimeout { + log.Warn("Verification timed out by safety timeout, re-submitting", + "elapsed", elapsed, + "safetyTimeout", s.verifyReceiptSafetyTimeout) + return RetrySubmission + } + + // Otherwise, retry the verification. + return RetryVerification +} + +// handleVerifyReceiptJobResponse is a function that is meant to be run in a +// goroutine. +// +// This function handles responses from the verify receipt job queue. It will +// check the results for any errors, and if there are any errors that are +// applicable to retry, it will requeue the job for another attempt. +// If the the job is successful, no further processing is needed and it is +// considered complete. +// If the job has taken too long to verify, then it will re-submit the job +// back to the submit transaction queue for another attempt. +// +// NOTE: This function currently will loop forever if the transaction is +// never going to be available. +func (s *espressoTransactionSubmitter) handleVerifyReceiptJobResponse() { + for { + var jobResp espressoVerifyReceiptJobResponse + var ok bool + + select { + case <-s.ctx.Done(): + return + case jobResp, ok = <-s.verifyReceiptRespQueue: + if !ok { + // Our channel is closed, and we are done + return + } + } + + switch evaluation := s.evaluateVerification(jobResp); evaluation { + case Skip: + // decrement in flight jobs on skip, since we're done with this job + s.numInFlightJobs.Add(-1) + continue + case RetrySubmission: + s.submitJobQueue <- jobResp.job.transaction + continue + case RetryVerification: + s.verifyReceiptJobQueue <- jobResp.job + continue + } + + s.numInFlightJobs.Add(-1) + // We're done with this job and transaction, we have successfully + // confirmed that the transaction was submitted to Espresso + commitment := jobResp.job.transaction.transaction.Commit() + hash, _ := tagged_base64.New("TX", commitment[:]) + log.Info("Transaction confirmed on Espresso", "hash", hash.String()) + } +} + +// scheduleSubmitTransactionJobs is a function that is meant to be run in a +// goroutine. +// +// It handles the scheduling of submit transaction jobs so that the submit +// transaction workers can process them. +func (s *espressoTransactionSubmitter) scheduleSubmitTransactionJobs() { + for { + var ok bool + + // Get a worker from the worker queue + var worker chan espressoTransactionJobAttempt + select { + case <-s.ctx.Done(): + return + + case worker, ok = <-s.submitWorkerQueue: + if !ok { + // Our channel is closed, and we are done + return + } + } + + // Get a job from the job queue + var job espressoSubmitTransactionJob + select { + case <-s.ctx.Done(): + return + case job, ok = <-s.submitJobQueue: + if !ok { + // Our channel is closed, and we are done + return + } + } + + // Submit the job to the worker + select { + case <-s.ctx.Done(): + return + + case worker <- espressoTransactionJobAttempt{job: job, resp: s.submitRespQueue}: + } + } +} + +// scheduleVerifyReceiptJobs is a function that is meant to be run in a +// goroutine. +// +// It handles the scheduling of verify receipt jobs so that the verify receipt +// workers can process them. +func (s *espressoTransactionSubmitter) scheduleVerifyReceiptsJobs() { + for { + var ok bool + + // Get a worker from the worker queue + var worker chan espressoVerifyReceiptJobAttempt + select { + case <-s.ctx.Done(): + return + + case worker, ok = <-s.verifyReceiptWorkerQueue: + if !ok { + // Our channel is closed, and we are done + return + } + } + + // Get a job from the job queue + var job espressoVerifyReceiptJob + select { + case <-s.ctx.Done(): + return + case job, ok = <-s.verifyReceiptJobQueue: + if !ok { + // Our channel is closed, and we are done + return + } + } + + // Submit the job to the worker + select { + case <-s.ctx.Done(): + return + + case worker <- espressoVerifyReceiptJobAttempt{job: job, resp: s.verifyReceiptRespQueue}: + } + } +} + +// espressoSubmitTransactionWorker is a function that is meant to be run as a +// goroutine. It will create a channel for it's job queue, and submit those to +// the worker queue in order to wait for work. It will then take that job and +// attempt to submit the transaction contained within to espresso using the +// given espresso client. It will submit the response back to the channel +// contained within the job attempt it received. +// +// It's lifetime is governed by the context passed to it, and it will stop +// processing when that context is cancelled. +// +// NOTE: If the context is cancelled after a job has been received, but before +// it is able to submit the transaction, or report about it's result, the job +// may be lost. +func espressoSubmitTransactionWorker( + ctx context.Context, + wg *sync.WaitGroup, + cli espressoClient.EspressoClient, + workerQueue chan<- chan espressoTransactionJobAttempt, +) { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + defer wg.Done() + ch := make(chan espressoTransactionJobAttempt) + defer close(ch) + + for { + var ok bool + select { + case <-ctx.Done(): + return + + // Queue our job queue, asking for work + case workerQueue <- ch: + } + + // Wait for a job to run + var jobAttempt espressoTransactionJobAttempt + select { + case <-ctx.Done(): + return + case jobAttempt, ok = <-ch: + if !ok { + // Our channel is closed, and we are done + return + } + } + + // Submit the transaction to Espresso + hash, err := cli.SubmitTransaction(ctx, *jobAttempt.job.transaction) + if err == nil { + log.Info("Submitted transaction to Espresso", "hash", hash) + } + + jobAttempt.job.attempts++ + resp := espressoSubmitTransactionJobResponse{ + job: jobAttempt.job, + hash: hash, + err: err, + } + + select { + case <-ctx.Done(): + return + + // Send the response back via the channel in the job attempt struct + case jobAttempt.resp <- resp: + } + } +} + +// espressoVerifyTransactionWorker is a function that is meant to be run as a +// goroutine. It will create a channel for it's job queue, and submit those to +// the worker queue in order to wait for work. It will then take that job and +// attempt to verify the transaction contained within to espresso using the +// given espresso client. It will submit the response back to the channel +// contained within the job attempt it received. +func espressoVerifyTransactionWorker( + ctx context.Context, + wg *sync.WaitGroup, + cli espressoClient.EspressoClient, + workerQueue chan<- chan espressoVerifyReceiptJobAttempt, + latestHeight *atomic.Uint64, + retryDelay time.Duration, +) { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + defer wg.Done() + ch := make(chan espressoVerifyReceiptJobAttempt) + defer close(ch) + + for { + var ok bool + select { + case <-ctx.Done(): + return + + // Queue our job queue, asking for work + case workerQueue <- ch: + } + + // Wait for a job to run + var jobAttempt espressoVerifyReceiptJobAttempt + select { + case <-ctx.Done(): + return + case jobAttempt, ok = <-ch: + if !ok { + // Our channel is closed, and we are done + return + } + } + + // On the first attempt, snapshot the current block height so we + // can measure how many blocks pass during verification. + if jobAttempt.job.attempts == 0 { + jobAttempt.job.startHeight = latestHeight.Load() + } + + if jobAttempt.job.attempts > 0 { + // We have already attempted this job, so we will wait a bit + // NOTE: this prevents this worker from being able to process + // other jobs while we wait for this delay. + time.Sleep(retryDelay) + } + + _, err := cli.FetchTransactionByHash(ctx, jobAttempt.job.hash) + + jobAttempt.job.attempts++ + resp := espressoVerifyReceiptJobResponse{ + job: jobAttempt.job, + err: err, + currentHeight: latestHeight.Load(), + } + + select { + case <-ctx.Done(): + return + + case jobAttempt.resp <- resp: + } + } +} + +// SpawnWorkers spawns the given number of workers to process the +// submit transaction jobs and verify receipt jobs. +func (s *espressoTransactionSubmitter) SpawnWorkers(numSubmitTransactionWorkers, numVerifyReceiptWorkers int) { + workersCtx := s.ctx + + for i := 0; i < numSubmitTransactionWorkers; i++ { + s.wg.Add(1) + go espressoSubmitTransactionWorker(workersCtx, s.wg, s.espresso, s.submitWorkerQueue) + } + + for i := 0; i < numVerifyReceiptWorkers; i++ { + s.wg.Add(1) + go espressoVerifyTransactionWorker(workersCtx, s.wg, s.espresso, s.verifyReceiptWorkerQueue, &s.latestBlockHeight, s.verifyReceiptRetryDelay) + } +} + +// trackBlockHeight periodically polls FetchLatestBlockHeight and stores +// the result in s.latestBlockHeight for verify jobs to compare against. +// This avoids redundant height queries from individual verify workers. +func (s *espressoTransactionSubmitter) trackBlockHeight() { + for { + height, err := s.espresso.FetchLatestBlockHeight(s.ctx) + if err == nil { + s.latestBlockHeight.Store(height) + } else if s.ctx.Err() == nil { + log.Debug("failed to fetch latest block height for verification tracking", "err", err) + } + + // Wait for the next interval or until context is done. + select { + case <-time.After(s.verifyReceiptRetryDelay): + case <-s.ctx.Done(): + return + } + } +} + +func (s *espressoTransactionSubmitter) Start() { + // Block height tracker for verify receipt timeout + go s.trackBlockHeight() + + // Submit Transaction Jobs + go s.scheduleSubmitTransactionJobs() + go s.handleTransactionSubmitJobResponse() + + // Verify Receipt Jobs + go s.scheduleVerifyReceiptsJobs() + go s.handleVerifyReceiptJobResponse() +} + +// Converts a block to an EspressoBatch and starts a goroutine that publishes it to Espresso +// Returns error only if batch conversion fails, otherwise it is infallible, as the goroutine +// will retry publishing until successful. +func (l *BatchSubmitter) queueBlockToEspresso(ctx context.Context, block *types.Block) error { + espressoBatch, err := derive.BlockToEspressoBatch(l.RollupConfig, block) + if err != nil { + l.Log.Warn("Failed to derive batch from block", "err", err) + return fmt.Errorf("failed to derive batch from block: %w", err) + } + + transaction, err := espressoBatch.ToEspressoTransaction(ctx, l.RollupConfig.L2ChainID.Uint64(), l.Espresso.ChainSigner) + if err != nil { + l.Log.Warn("Failed to create Espresso transaction from a batch", "err", err) + return fmt.Errorf("failed to create Espresso transaction from a batch: %w", err) + } + + commitment := transaction.Commit() + hash, _ := tagged_base64.New("TX", commitment[:]) + l.Log.Info("Created Espresso transaction from batch", "hash", hash, "batchNr", espressoBatch.BatchHeader.Number.Uint64()) + + if err := l.espressoSubmitter.SubmitTransaction(transaction); err != nil { + return fmt.Errorf("failed to submit job to espresso: %w", err) + } + + return nil +} + +func (l *BatchSubmitter) espressoSyncAndRefresh(ctx context.Context, newSyncStatus *eth.SyncStatus) { + err := l.EspressoStreamer().Refresh(ctx, newSyncStatus.FinalizedL1, newSyncStatus.SafeL2.Number, newSyncStatus.FinalizedL2.L1Origin) + if err != nil { + l.degradedLog.Warn(l.Log, "espressoStreamerRefreshErr", "Failed to refresh Espresso streamer", "err", err) + } else { + l.degradedLog.Clear(l.Log, "espressoStreamerRefreshErr", "Espresso streamer refresh recovered") + } + + l.channelMgrMutex.Lock() + defer l.channelMgrMutex.Unlock() + syncActions, outOfSync := computeSyncActions(*newSyncStatus, l.prevCurrentL1, l.channelMgr.blocks, l.channelMgr.channelQueue, l.Log) + if outOfSync { + l.degradedLog.Warn(l.Log, "sequencerOutOfSync", "Sequencer is out of sync, retrying next tick.") + return + } + l.degradedLog.Clear(l.Log, "sequencerOutOfSync", "Sequencer back in sync") + l.prevCurrentL1 = newSyncStatus.CurrentL1 + if syncActions.clearState != nil { + l.channelMgr.Clear(*syncActions.clearState) + l.EspressoStreamer().Reset() + } else { + l.channelMgr.PruneSafeBlocks(syncActions.blocksToPrune) + l.channelMgr.PruneChannels(syncActions.channelsToPrune) + } +} + +// peekNextBatch returns the next batch from the streamer, performing a fork check +// against an expected parent hash. +// +// The expected parent is tip when tip is set. When tip is zero (channel manager was +// just cleared), we fall back to safeL2.Hash if the batch is at exactly safeL2+1 — +// the one position where we can set tip to the known safe head. Otherwise we accept the batch as-is. +func (l *BatchSubmitter) peekNextBatch(ctx context.Context, syncStatus *eth.SyncStatus) *derive.EspressoBatch { + l.channelMgrMutex.Lock() + tip := l.channelMgr.tip + l.channelMgrMutex.Unlock() + + batch := l.EspressoStreamer().Peek(ctx) + if batch == nil { + return nil + } + + // Check if we can set the tip if not set + if tip == (common.Hash{}) && (*batch).Number() == syncStatus.SafeL2.Number+1 { + l.Log.Info( + "setting tip to safe l2 hash", + "batchNr", (*batch).Number(), + "batchParent", (*batch).Header().ParentHash.Hex(), + "tip", tip, + ) + tip = syncStatus.SafeL2.Hash + } + + if tip == (common.Hash{}) { + l.Log.Warn( + "tip is not set, taking available batch", + "blockParentHash", (*batch).Header().ParentHash.Hex(), + "blockHash", (*batch).Header().Hash().Hex(), + ) + return batch + } + + if (*batch).Header().ParentHash != tip { + l.Log.Warn( + "head batch fork mismatch, seeking to proper head", + "batchNr", (*batch).Number(), + "batchParent", (*batch).Header().ParentHash, + "tip", tip, + ) + l.EspressoStreamer().SetProperHead(tip) + return nil + } + + return batch +} + +// Periodically refreshes the sync status and polls Espresso streamer for new batches +func (l *BatchSubmitter) espressoBatchLoadingLoop(ctx context.Context, wg *sync.WaitGroup, publishSignal chan pubInfo) { + l.Log.Info("Starting EspressoBatchLoadingLoop", "polling interval", l.Config.Espresso.PollInterval) + + defer wg.Done() + ticker := time.NewTicker(l.Config.Espresso.PollInterval) + defer ticker.Stop() + defer close(publishSignal) + + for { + select { + case <-ticker.C: + newSyncStatus, err := l.getSyncStatus(ctx) + if err != nil { + l.degradedLog.Warn(l.Log, "syncStatusErr/espressoBatchLoading", "failed to refresh sync status", "err", err) + continue + } + l.degradedLog.Clear(l.Log, "syncStatusErr/espressoBatchLoading", "sync status fetch recovered") + + l.espressoSyncAndRefresh(ctx, newSyncStatus) + + err = l.EspressoStreamer().Update(ctx) + + var batch *derive.EspressoBatch + + for { + + batch = l.peekNextBatch(ctx, newSyncStatus) + + if batch == nil { + break + } + + // This should happen ONLY if the batch is malformed. ToBlock has to guarantee no + // transient errors. + block, err := batch.ToBlock(l.RollupConfig) + if err != nil { + l.Log.Error("failed to convert singular batch to block", "err", err) + l.EspressoStreamer().Next(ctx) + continue + } + + l.Log.Info( + "Received block from Espresso", + "blockNr", block.NumberU64(), + "blockHash", block.Hash(), + "parentHash", block.ParentHash(), + ) + + l.channelMgrMutex.Lock() + err = l.channelMgr.AddL2Block(block) + l.channelMgrMutex.Unlock() + + if err != nil { + l.Log.Error("failed to add L2 block to channel manager", "err", err) + l.clearState(ctx) + l.EspressoStreamer().Reset() + break + } + + l.EspressoStreamer().Next(ctx) + l.Log.Info("Added L2 block to channel manager", "blockNr", block.NumberU64()) + } + + l.tryPublishSignal(publishSignal, pubInfo{}) + + // A failure in the streamer Update can happen after the buffer has been partially filled + if err != nil { + l.degradedLog.Warn(l.Log, "espressoStreamerUpdateErr", "failed to update Espresso streamer", "err", err) + continue + } + l.degradedLog.Clear(l.Log, "espressoStreamerUpdateErr", "Espresso streamer update recovered") + + case <-ctx.Done(): + l.Log.Info("espressoBatchLoadingLoop returning") + return + } + } +} + +type BlockLoader struct { + queuedBlocks []eth.L2BlockRef + prevSyncStatus *eth.SyncStatus + batcher *BatchSubmitter +} + +func (l *BlockLoader) reset(ctx context.Context) { + l.prevSyncStatus = nil + l.queuedBlocks = nil + l.batcher.clearState(ctx) +} + +func (l *BlockLoader) EnqueueBlocks(ctx context.Context, blocksToQueue inclusiveBlockRange) { + l.batcher.Log.Debug("Loading and queueing blocks", "range", blocksToQueue) + for i := blocksToQueue.start; i <= blocksToQueue.end; i++ { + block, err := l.batcher.fetchBlock(ctx, i) + if err != nil { + l.batcher.degradedLog.Warn(l.batcher.Log, "fetchBlockErr", "Failed to fetch block", "err", err) + break + } + l.batcher.degradedLog.Clear(l.batcher.Log, "fetchBlockErr", "Block fetching recovered") + + for _, txn := range block.Transactions() { + l.batcher.Log.Debug("tx hash before submitting to Espresso", "hash", txn.Hash().String()) + } + + if len(l.queuedBlocks) > 0 && block.ParentHash() != l.queuedBlocks[len(l.queuedBlocks)-1].Hash { + l.batcher.Log.Warn("Found L2 reorg", "block_number", i) + l.reset(ctx) + break + } + + blockRef, err := derive.L2BlockToBlockRef(l.batcher.RollupConfig, block) + if err != nil { + // NOTE: if we fail to convert an L2Block to a BlockRef, it's + // unlikely that breaking here, and waiting for resubmission would + // actually ever result in it succeeding. + // For now, we add a log, but this may be a Fatal unrecoverable + // error if it ever occurs. + l.batcher.Log.Warn("failed to convert block to block reference", "err", err) + break + } + + err = l.batcher.queueBlockToEspresso(ctx, block) + if err != nil { + l.batcher.Log.Debug("queue block to espresso failed", "err", err) + break + } + + l.queuedBlocks = append(l.queuedBlocks, blockRef) + } +} + +type EnqueueBlockAction uint + +const ( + ActionEnqueue = iota + ActionRetry + ActionReset +) + +// This function is an analogue of `computeSyncActions` for Espresso batcher mode +// +// It computes the next block range to enqueue to Espresso based on new newSyncStatus and +// does a number of checks to ensure consistency of the chain. +// +// If reorg is detected, empty range and ActionReset is returned. +// If there isn't enough information or no blocks to load yet, empty range and ActionRetry is returned. +func (l *BlockLoader) nextBlockRange(newSyncStatus *eth.SyncStatus) (inclusiveBlockRange, EnqueueBlockAction) { + if newSyncStatus.HeadL1 == (eth.L1BlockRef{}) { + // empty sync status + return inclusiveBlockRange{}, ActionRetry + } + + if l.prevSyncStatus == nil { + l.prevSyncStatus = newSyncStatus + } + + if newSyncStatus.CurrentL1.Number < l.prevSyncStatus.CurrentL1.Number { + // sequencer restarted and hasn't caught up yet + l.batcher.degradedLog.Warn(l.batcher.Log, "sequencerCurrentL1Reversed", "sequencer currentL1 reversed", "new currentL1", newSyncStatus.CurrentL1.Number, "previous currentL1", l.prevSyncStatus.CurrentL1.Number) + return inclusiveBlockRange{}, ActionRetry + } + l.batcher.degradedLog.Clear(l.batcher.Log, "sequencerCurrentL1Reversed", "sequencer currentL1 caught up") + + safeL2 := newSyncStatus.SafeL2 + + // State empty, just enqueue all unsafe blocks + if len(l.queuedBlocks) == 0 { + return inclusiveBlockRange{safeL2.Number + 1, newSyncStatus.UnsafeL2.Number}, ActionEnqueue + } + + lastQueuedBlock := l.queuedBlocks[len(l.queuedBlocks)-1] + firstQueuedBlock := l.queuedBlocks[0] + nextSafeBlockNum := safeL2.Number + 1 + + if lastQueuedBlock.Number >= newSyncStatus.UnsafeL2.Number { + // nothing to enqueue, unsafe block number is not higher than safe + return inclusiveBlockRange{}, ActionRetry + } + + if lastQueuedBlock.Number < safeL2.Number { + // derivation pipeline is somehow ahead of us, reset + return inclusiveBlockRange{}, ActionReset + } + + if nextSafeBlockNum < firstQueuedBlock.Number { + l.batcher.Log.Warn("next safe block is below oldest block in state") + return inclusiveBlockRange{}, ActionReset + } + + numBlocksToEnqueue := nextSafeBlockNum - firstQueuedBlock.Number + + if numBlocksToEnqueue > uint64(len(l.queuedBlocks)) { + l.batcher.Log.Warn("safe head above newest block in state, resetting loader") + return inclusiveBlockRange{}, ActionReset + } + + if numBlocksToEnqueue > 0 && l.queuedBlocks[numBlocksToEnqueue-1].Hash != safeL2.Hash { + l.batcher.Log.Warn("safe chain reorg, resetting loader") + return inclusiveBlockRange{}, ActionReset + } + + if safeL2.Number > firstQueuedBlock.Number { + numFinalizedBlocksInQueue := safeL2.Number - firstQueuedBlock.Number + l.batcher.Log.Warn( + "Removing finalized blocks from queued", + "numFinalizedBlocksInQueue", numFinalizedBlocksInQueue, + "safeL2", safeL2, + "firstQueuedBlock", firstQueuedBlock) + l.queuedBlocks = l.queuedBlocks[numFinalizedBlocksInQueue:] + } + + return inclusiveBlockRange{lastQueuedBlock.Number + 1, newSyncStatus.UnsafeL2.Number}, ActionEnqueue +} + +// numBlocks is a convenience method for inclusiveBlockRange that can be +// utilized to quickly determine how many blocks are being referenced within +// the range. +func (i inclusiveBlockRange) numBlocks() uint64 { + return 1 + i.end - i.start +} + +// LARGE_BLOCK_GAP_THRESHOLD is a threshold for the number of blocks that we +// consider to be a "large gap" when queueing blocks to Espresso. We're +// interested in being alerted when we're falling behind. +const LARGE_BLOCK_GAP_THRESHOLD = 30 * 60 + +// blockLoadingLoop +// - polls the sequencer, +// - queues unsafe blocks from the sequencer to Espresso +func (l *BatchSubmitter) espressoBatchQueueingLoop(ctx context.Context, wg *sync.WaitGroup) { + ticker := time.NewTicker(l.Config.PollInterval) + defer ticker.Stop() + defer wg.Done() + + loader := BlockLoader{ + batcher: l, + } + + // * + // * BEFORE we start: + // * - scan batchInbox from batchInbox.lastBackfilled + // * - enqueue all batches from batchInbox that are _by fallback batcher_ to Espresso + // * - wait for espresso queue to clear + // * - set lastBackfilled to block height of the last of such batches + // * + + for { + select { + case <-ticker.C: + newSyncStatus, err := l.getSyncStatus(ctx) + if err != nil { + l.degradedLog.Warn(l.Log, "syncStatusErr/espressoBatchQueueing", "Couldn't get sync status", "error", err) + continue + } + l.degradedLog.Clear(l.Log, "syncStatusErr/espressoBatchQueueing", "sync status fetch recovered") + + blocksToQueue, action := loader.nextBlockRange(newSyncStatus) + + // We add a check here to add visibility to us that we've exceeded + // a threshold. + if numBlocks := blocksToQueue.numBlocks(); numBlocks >= LARGE_BLOCK_GAP_THRESHOLD { + l.Log.Warn("Large gap of blocks to enqueue to Espresso detected", "numBlocks", numBlocks, "blocksToQueue", blocksToQueue) + } + + if action == ActionEnqueue { + numEnqueuedBlocksBefore := len(loader.queuedBlocks) + loader.EnqueueBlocks(ctx, blocksToQueue) + numEnqueuedBlocksAfter := len(loader.queuedBlocks) + + // This is a check to help us determine whether we're able to + // push through all of the blocks we've attempted to or not. + if (numEnqueuedBlocksAfter - numEnqueuedBlocksBefore) < int(blocksToQueue.numBlocks()) { + // If we're in this conditional, it means we weren't able + // submit all of the blocks to Espresso that we were + // attempting to. + // + // TODO: We should probably throttle a bit. + } + } else if action == ActionReset { + loader.reset(ctx) + } + + case <-ctx.Done(): + l.Log.Info("blockLoadingLoop returning") + return + } + } +} + +func (l *BatchSubmitter) fetchBlock(ctx context.Context, blockNumber uint64) (*types.Block, error) { + l2Client, err := l.EndpointProvider.EthClient(ctx) + if err != nil { + return nil, fmt.Errorf("getting L2 client: %w", err) + } + + cCtx, cancel := context.WithTimeout(ctx, l.Config.NetworkTimeout) + defer cancel() + + block, err := l2Client.BlockByNumber(cCtx, new(big.Int).SetUint64(blockNumber)) + if err != nil { + return nil, fmt.Errorf("getting L2 block: %w", err) + } + + return block, nil +} + +// resolveTEEVerifierAddress queries the BatchAuthenticator contract to get the +// EspressoTEEVerifier address. +func (l *BatchSubmitter) resolveTEEVerifierAddress() error { + if l.RollupConfig.BatchAuthenticatorAddress == (common.Address{}) { + // If batcher authenticator address is nil, we will keep teeVerifierAddress to nil as well + return nil + } + auth, err := bindings.NewBatchAuthenticatorCaller(l.RollupConfig.BatchAuthenticatorAddress, l.L1Client) + if err != nil { + return fmt.Errorf("failed to create BatchAuthenticator caller: %w", err) + } + addr, err := auth.EspressoTEEVerifier(nil) + if err != nil { + return fmt.Errorf("failed to query EspressoTEEVerifier address: %w", err) + } + l.teeVerifierAddress = addr + l.Log.Info("Resolved TEE verifier address", "address", addr.Hex()) + return nil +} + +func (l *BatchSubmitter) registerBatcher(ctx context.Context) error { + if len(l.Espresso.Attestation) == 0 { + l.Log.Warn("Attestation is empty, skipping registration") + return nil + } + + if l.Config.Espresso.AttestationService == "" { + l.Log.Warn("EspressoAttestationServices is not set, skipping registration") + return nil + } + + l.Log.Info("Batch authenticator address", "value", l.RollupConfig.BatchAuthenticatorAddress) + code, err := l.L1Client.CodeAt(ctx, l.RollupConfig.BatchAuthenticatorAddress, nil) + if err != nil { + return fmt.Errorf("failed to check code at contract address: %w", err) + } + if len(code) == 0 { + return fmt.Errorf("no contract deployed at this address %w", err) + } + + abi, err := bindings.BatchAuthenticatorMetaData.GetAbi() + if err != nil { + return fmt.Errorf("failed to get Batch Authenticator ABI: %w", err) + } + + onchainProof, err := l.GenerateZKProof(ctx, l.Espresso.Attestation) + if err != nil { + l.Log.Error("failed to generate zk proof from nitro attestation", "err", err) + return fmt.Errorf("failed to generate zk proof from nitro attestation: %w", err) + } + + journalBytes, err := hex.DecodeString(stripHexPrefix(onchainProof.RawProof.Journal)) + if err != nil { + l.Log.Error("failed to decode journal hex string", "err", err) + return fmt.Errorf("failed to decode journal hex string: %w", err) + } + onchainProofBytes, err := hex.DecodeString(stripHexPrefix(onchainProof.OnchainProof)) + if err != nil { + l.Log.Error("failed to decode onchain proof hex string", "err", err) + return fmt.Errorf("failed to decode onchain proof hex string: %w", err) + } + log.Info("successfully generated zk proof from nitro attestation") + + txData, err := abi.Pack("registerSigner", journalBytes, onchainProofBytes) + if err != nil { + return fmt.Errorf("failed to create registerSigner transaction: %w", err) + } + + candidate := txmgr.TxCandidate{ + TxData: txData, + To: &l.RollupConfig.BatchAuthenticatorAddress, + } + + l.Log.Info("Registering batcher with the BatchAuthenticator contract") + _, err = l.Txmgr.Send(ctx, candidate) + if err != nil { + return fmt.Errorf("failed to send registerBatcher transaction: %w", err) + } + + l.Log.Info("Registered batcher with the BatchAuthenticator contract") + + return nil +} + +func (l *BatchSubmitter) GenerateZKProof(ctx context.Context, attestationBytes []byte) (*EspressoOnchainProof, error) { + attestationServiceURL := strings.TrimSuffix(l.Config.Espresso.AttestationService, "/") + url := attestationServiceURL + "/generate_proof" + request, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(attestationBytes)) + if err != nil { + return nil, err + } + + request.Header.Set("Content-Type", "application/octet-stream") + client := http.Client{ + Timeout: 5 * time.Minute, + } + res, err := client.Do(request) + if err != nil { + return nil, err + } + defer (func() { + _ = res.Body.Close() + })() + + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("received non-200 response: %d", res.StatusCode) + } + + responseData, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + + var zkProof EspressoOnchainProof + err = json.Unmarshal(responseData, &zkProof) + if err != nil { + return nil, err + } + + return &zkProof, nil +} + +// sendTxWithEspresso 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) sendTxWithEspresso(txdata txData, isCancel bool, candidate *txmgr.TxCandidate, queue TxSender[txRef], receiptsCh chan txmgr.TxReceipt[txRef]) { + transactionReference := txRef{id: txdata.ID(), isCancel: isCancel, isBlob: txdata.daType == DaTypeBlob} + l.Log.Debug("Sending Espresso-enabled L1 transaction", "txRef", transactionReference) + + commitment, err := computeCommitment(candidate) + if err != nil { + receiptsCh <- txmgr.TxReceipt[txRef]{ + ID: transactionReference, + Err: err, + } + return + } + l.Log.Debug("Computed batch commitment", "txRef", transactionReference, "commitment", hexutil.Encode(commitment[:])) + + signature, err := l.signEIP712Commitment(commitment) + if err != nil { + receiptsCh <- txmgr.TxReceipt[txRef]{ + ID: transactionReference, + Err: fmt.Errorf("failed to sign transaction: %w", err), + } + return + } + + l.Log.Debug("Signed transaction", "txRef", transactionReference, "commitment", hexutil.Encode(commitment[:]), "sig", hexutil.Encode(signature)) + + batchAuthenticatorAbi, err := bindings.BatchAuthenticatorMetaData.GetAbi() + if err != nil { + receiptsCh <- txmgr.TxReceipt[txRef]{ + ID: transactionReference, + Err: fmt.Errorf("failed to get batch authenticator ABI: %w", err), + } + return + } + + authenticateBatchCalldata, err := batchAuthenticatorAbi.Pack("authenticateBatchInfo", commitment, signature) + if err != nil { + receiptsCh <- txmgr.TxReceipt[txRef]{ + ID: transactionReference, + Err: fmt.Errorf("failed to pack authenticateBatch calldata: %w", err), + } + return + } + + verifyCandidate := txmgr.TxCandidate{ + TxData: authenticateBatchCalldata, + To: &l.RollupConfig.BatchAuthenticatorAddress, + } + + l.Log.Debug( + "Sending authenticateBatch transaction", + "txRef", transactionReference, + "commitment", hexutil.Encode(commitment[:]), + "sig", hexutil.Encode(signature), + "address", l.RollupConfig.BatchAuthenticatorAddress.String(), + ) + verificationReceipt, err := l.Txmgr.Send(l.killCtx, verifyCandidate) + if err != nil { + l.Log.Error("Failed to send authenticateBatch transaction", "txRef", transactionReference, "err", err) + receiptsCh <- txmgr.TxReceipt[txRef]{ + ID: transactionReference, + Err: fmt.Errorf("failed to send authenticateBatch transaction: %w", err), + } + return + } + + receipt, err := l.Txmgr.Send(l.killCtx, *candidate) + if err != nil { + l.Log.Error("Failed to send batch inbox transaction", "txRef", transactionReference, "err", err) + receiptsCh <- txmgr.TxReceipt[txRef]{ + ID: transactionReference, + Err: fmt.Errorf("failed to send batch inbox transaction: %w", err), + } + return + } + + distance := new(big.Int).Sub(receipt.BlockNumber, verificationReceipt.BlockNumber) + lookbackWindow := new(big.Int).SetUint64(derive.BatchAuthLookbackWindow) + if distance.Sign() < 0 || distance.Cmp(lookbackWindow) >= 0 { + l.Log.Error("authenticateBatch transaction too far from batch inbox transaction", "txRef", transactionReference, "distance", distance) + receiptsCh <- txmgr.TxReceipt[txRef]{ + ID: transactionReference, + Err: fmt.Errorf("authenticateBatch transaction too far from batch inbox transaction: %s", distance), + } + return + } + + receiptsCh <- txmgr.TxReceipt[txRef]{ + ID: transactionReference, + Receipt: receipt, + Err: nil, + } +} + +// signEIP712Commitment creates an EIP-712 signature for the given commitment using the batcher's private key. +func (l *BatchSubmitter) signEIP712Commitment(commitment [32]byte) ([]byte, error) { + typedData := apitypes.TypedData{ + Types: apitypes.Types{ + "EIP712Domain": []apitypes.Type{ + {Name: "name", Type: "string"}, + {Name: "version", Type: "string"}, + {Name: "chainId", Type: "uint256"}, + {Name: "verifyingContract", Type: "address"}, + }, + "EspressoTEEVerifier": []apitypes.Type{ + {Name: "commitment", Type: "bytes32"}, + }, + }, + PrimaryType: "EspressoTEEVerifier", + Domain: apitypes.TypedDataDomain{ + Name: "EspressoTEEVerifier", + Version: "1", + ChainId: (*math.HexOrDecimal256)(l.RollupConfig.L1ChainID), + VerifyingContract: l.teeVerifierAddress.String(), + }, + Message: map[string]interface{}{ + "commitment": commitment, + }, + } + // Calculate the hash using go-ethereum's EIP-712 implementation + hash, _, err := apitypes.TypedDataAndHash(typedData) + if err != nil { + return nil, fmt.Errorf("failed to calculate EIP-712 hash: %w", err) + } + + signature, err := crypto.Sign(hash, l.Config.Espresso.BatcherPrivateKey) + if err != nil { + return nil, fmt.Errorf("failed to sign EIP-712 hash: %w", err) + } + + // Normalize the recovery ID (v) from 0/1 to 27/28 for Solidity's ECDSA.recover + // See: https://github.com/ethereum/go-ethereum/issues/19751#issuecomment-504900739 + if signature[64] < 27 { + signature[64] += 27 + } + return signature, nil +} + +func stripHexPrefix(hexStr string) string { + if len(hexStr) >= 2 && hexStr[:2] == "0x" { + return hexStr[2:] + } + return hexStr +} diff --git a/op-batcher/batcher/espresso_active.go b/op-batcher/batcher/espresso_active.go new file mode 100644 index 00000000000..150e0ae19ad --- /dev/null +++ b/op-batcher/batcher/espresso_active.go @@ -0,0 +1,102 @@ +package batcher + +import ( + "context" + "fmt" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + + "github.com/ethereum-optimism/optimism/espresso/bindings" +) + +// isBatcherActive checks if the current batcher is the active one by querying +// the BatchAuthenticator contract. Returns true if this batcher instance should +// be publishing batches, false if it should stay idle. +// +// The active batcher is determined by the contract's activeIsEspresso flag: +// - If activeIsEspresso is true, the Espresso batcher address is active +// - If activeIsEspresso is false, the fallback batcher address is active +// +// This method compares the batcher's own role (Config.Espresso.Enabled) +// against the contract's activeIsEspresso flag. +func (l *BatchSubmitter) isBatcherActive(ctx context.Context) (bool, error) { + // Check if contract code exists at the address + code, err := l.L1Client.CodeAt(ctx, l.RollupConfig.BatchAuthenticatorAddress, nil) + if err != nil { + return false, fmt.Errorf("failed to check code at BatchAuthenticator address: %w", err) + } + if len(code) == 0 { + return false, fmt.Errorf("no contract code at BatchAuthenticator address %s", l.RollupConfig.BatchAuthenticatorAddress.Hex()) + } + + batchAuthenticator, err := bindings.NewBatchAuthenticator(l.RollupConfig.BatchAuthenticatorAddress, l.L1Client) + if err != nil { + return false, fmt.Errorf("failed to create BatchAuthenticator binding: %w", err) + } + + cCtx, cancel := context.WithTimeout(ctx, l.Config.NetworkTimeout) + defer cancel() + + callOpts := &bind.CallOpts{Context: cCtx} + + activeIsEspresso, err := batchAuthenticator.ActiveIsEspresso(callOpts) + if err != nil { + return false, fmt.Errorf("failed to check activeIsEspresso: %w", err) + } + + batcherAddr := l.Txmgr.From() + + isActive := (activeIsEspresso && l.Config.Espresso.Enabled) || + (!activeIsEspresso && !l.Config.Espresso.Enabled) + + if !isActive { + l.Log.Info("Batcher is not the active batcher, skipping publish", + "batcherAddr", batcherAddr, + "activeIsEspresso", activeIsEspresso, + "EspressoEnabled", l.Config.Espresso.Enabled, + ) + } + + return isActive, nil +} + +// hasBatchAuthenticator returns true if the rollup config has a non-zero +// BatchAuthenticatorAddress, indicating that the BatchAuthenticator-based +// authentication path is in use. +func (l *BatchSubmitter) hasBatchAuthenticator() bool { + return l.RollupConfig.BatchAuthenticatorAddress != (common.Address{}) +} + +// isFallbackAuthRequired reports whether the fallback (non-TEE) batcher must +// route its batch txs through BatchAuthenticator.authenticateBatchInfo before +// posting to the BatchInbox. +// +// This decision must align with the verifier's per-L1-block fork gate +// (DataSourceConfig.isEspressoEnforcement, which evaluates the hardfork +// activation predicate against the *containing* L1 block's timestamp). Since +// the tx is not yet mined at decision time, its eventual containing block +// has a strictly greater timestamp than the L1 tip the batcher observes: +// +// l1Tip.Time (batcher's view) < l1OriginTime (block containing the tx) +// +// Without compensation, in the window [forkTime − maxL1InclusionDelay, forkTime) +// the batcher would skip authenticateBatchInfo while the verifier — once the +// tx lands in a post-fork block — would require the resulting +// BatchInfoAuthenticated event, silently dropping the batch. +// +// To prevent this, we add Config.FallbackAuthLeadTime to the L1 tip's +// timestamp before evaluating the fork predicate. This makes the batcher +// start authenticating slightly before the verifier requires it. The reverse +// asymmetry (authenticated tx lands pre-fork) is harmless: pre-fork the +// verifier uses sender-based authorization and the auth event is just an +// unrelated L1 tx that does not affect derivation. +func (l *BatchSubmitter) isFallbackAuthRequired(ctx context.Context) (bool, error) { + tip, err := l.l1Tip(ctx) + if err != nil { + return false, fmt.Errorf("failed to fetch L1 tip for fallback-auth gate: %w", err) + } + leadSec := uint64(l.Config.FallbackAuthLeadTime / time.Second) + return l.RollupConfig.IsEspresso(tip.Time + leadSec), nil +} diff --git a/op-batcher/batcher/espresso_driver.go b/op-batcher/batcher/espresso_driver.go new file mode 100644 index 00000000000..20865bc2da2 --- /dev/null +++ b/op-batcher/batcher/espresso_driver.go @@ -0,0 +1,211 @@ +package batcher + +import ( + "context" + "errors" + "fmt" + "math/big" + + espressoClient "github.com/EspressoSystems/espresso-network/sdks/go/client" + espressoLightClient "github.com/EspressoSystems/espresso-network/sdks/go/light-client" + op "github.com/EspressoSystems/espresso-streamers/op" + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + + "github.com/ethereum-optimism/optimism/espresso" + "github.com/ethereum-optimism/optimism/op-node/rollup/derive" + opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto" + "github.com/ethereum-optimism/optimism/op-service/txmgr" +) + +// EspressoDriverSetup groups all TEE-batcher-specific runtime state plumbed +// from BatcherService into DriverSetup. Defined here to keep the upstream +// Optimism DriverSetup field block compact (see driver.go). +// +// All fields are nil/zero when --espresso.enabled is false except for the +// fallback batcher's ChainSigner/SequencerAddress, which are always populated +// by applyEspressoDriverSetup. +type EspressoDriverSetup struct { + Client *espressoClient.MultipleNodesClient + LightClient *espressoLightClient.LightclientCaller + ChainSigner opcrypto.ChainSigner + SequencerAddress common.Address + Attestation []byte +} + +// batcherL1Adapter wraps the batcher's L1Client to implement espresso.L1Client +// (HeaderHashByNumber + bind.ContractCaller). +type batcherL1Adapter struct { + L1Client L1Client +} + +func (a *batcherL1Adapter) HeaderHashByNumber(ctx context.Context, number *big.Int) (common.Hash, error) { + h, err := a.L1Client.HeaderByNumber(ctx, number) + if err != nil { + return common.Hash{}, err + } + return h.Hash(), nil +} + +func (a *batcherL1Adapter) CodeAt(ctx context.Context, contract common.Address, blockNumber *big.Int) ([]byte, error) { + return a.L1Client.CodeAt(ctx, contract, blockNumber) +} + +func (a *batcherL1Adapter) CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { + return a.L1Client.CallContract(ctx, call, blockNumber) +} + +// authGroup is bounded to this many concurrent BatchInbox transactions +// simultaneously waiting on a BatchAuthenticator transaction (both for the +// fallback and the TEE path). +const authGroupLimit = 128 + +// initAuthGroup applies the concurrency limit. Called from NewBatchSubmitter. +func (l *BatchSubmitter) initAuthGroup() { + l.authGroup.SetLimit(authGroupLimit) +} + +// waitForAuthGroup blocks until all in-flight authentication submissions +// (fallback or TEE) have completed. Called from publishingLoop's tail; blocks +// until killCtx is cancelled if any auth retries are still in flight. +func (l *BatchSubmitter) waitForAuthGroup() { + if err := l.authGroup.Wait(); err != nil { + if !errors.Is(err, context.Canceled) { + l.Log.Error("error waiting for transaction authentication requests to complete", "err", err) + } + } +} + +// EspressoStreamer returns the Espresso batch streamer for use by the service and tests. +func (l *BatchSubmitter) EspressoStreamer() espresso.EspressoStreamer[derive.EspressoBatch] { + return l.espressoStreamer +} + +// setupEspressoStreamer constructs the Espresso streamer (and its buffered +// wrapper) for a freshly-built BatchSubmitter. Called from NewBatchSubmitter +// only when --espresso.enabled is set; no-op otherwise. Panics on streamer +// construction failure to mirror the existing NewBatchSubmitter behavior. +func (l *BatchSubmitter) setupEspressoStreamer() { + if !l.Config.Espresso.Enabled { + return + } + l1Adapter := &batcherL1Adapter{L1Client: l.L1Client} + // Convert typed nil pointer to untyped nil interface to avoid typed-nil interface panic + // in confirmEspressoBlockHeight when EspressoLightClient is not configured. + var lightClientIface op.LightClientCallerInterface + if l.Espresso.LightClient != nil { + lightClientIface = l.Espresso.LightClient + } + unbufferedStreamer, err := op.NewEspressoStreamer( + l.RollupConfig.L2ChainID.Uint64(), + l1Adapter, + l1Adapter, + l.Espresso.Client, + lightClientIface, + l.Log, + derive.CreateEspressoBatchUnmarshaler(), + l.Config.Espresso.CaffeinationHeightEspresso, + l.Config.Espresso.CaffeinationHeightL2, + l.RollupConfig.BatchAuthenticatorAddress, + false, + ) + if err != nil { + panic(fmt.Sprintf("failed to create Espresso streamer: %v", err)) + } + l.espressoStreamer = op.NewBufferedEspressoStreamer(unbufferedStreamer) + l.Log.Info("Streamer started", "streamer", l.espressoStreamer) +} + +// startEspressoLoops registers the batcher with the BatchAuthenticator +// contract, resolves the TEE verifier address, spawns the Espresso transaction +// submitter, and starts the four Espresso-specific batcher goroutines (in +// addition to the upstream receiptsLoop and publishingLoop). Replaces the +// upstream three-goroutine pattern when --espresso.enabled is set. +func (l *BatchSubmitter) startEspressoLoops(receiptsCh chan txmgr.TxReceipt[txRef], publishSignal chan pubInfo) error { + if err := l.registerBatcher(l.killCtx); err != nil { + return fmt.Errorf("could not register with BatchAuthenticator contract: %w", err) + } + + // Resolve the TEE verifier address from the BatchAuthenticator contract. + if err := l.resolveTEEVerifierAddress(); err != nil { + return fmt.Errorf("could not resolve TEE verifier address: %w", err) + } + + l.espressoSubmitter = NewEspressoTransactionSubmitter( + WithContext(l.shutdownCtx), + WithWaitGroup(l.wg), + WithEspressoClient(l.Espresso.Client), + WithVerifyReceiptMaxBlocks(l.Config.Espresso.VerifyReceiptMaxBlocks), + WithVerifyReceiptSafetyTimeout(l.Config.Espresso.VerifyReceiptSafetyTimeout), + WithVerifyReceiptRetryDelay(l.Config.Espresso.VerifyReceiptRetryDelay), + ) + l.espressoSubmitter.SpawnWorkers(4, 4) + l.espressoSubmitter.Start() + + l.wg.Add(4) + go l.receiptsLoop(l.wg, receiptsCh) // ranges over receiptsCh channel + go l.espressoBatchQueueingLoop(l.shutdownCtx, l.wg) + go l.espressoBatchLoadingLoop(l.shutdownCtx, l.wg, publishSignal) + go l.publishingLoop(l.killCtx, l.wg, receiptsCh, publishSignal) // ranges over publishSignal, spawns routines which send on receiptsCh. Closes receiptsCh when done. + return nil +} + +// resetEspressoStreamer resets the Espresso streamer when --espresso.enabled +// is set; no-op otherwise. Called from clearState alongside the upstream +// channel-manager reset so the streamer's view of "next batch" matches the +// freshly-cleared channel state. +func (l *BatchSubmitter) resetEspressoStreamer() { + if l.Config.Espresso.Enabled { + l.EspressoStreamer().Reset() + } +} + +// dispatchAuthenticatedSendTx routes sendTx through the Espresso (TEE) auth +// path or the fallback-batcher post-fork auth path, returning true when the tx +// has been handed off to authGroup. Returns false to mean "fall through to +// the upstream queue.Send path" — pre-fork fallback batcher and any cancel tx. +// +// The TEE batcher (Config.Espresso.Enabled == true) always authenticates. The +// fallback batcher consults isFallbackAuthRequired to gate authentication +// behind the EspressoTime hardfork: pre-fork the verifier accepts plain +// sender-authenticated batches, and the BatchAuthenticator contract is +// irrelevant; calling authenticateBatchInfo pre-fork would also revert against +// the default activeIsEspresso=true contract state. +func (l *BatchSubmitter) dispatchAuthenticatedSendTx(txdata txData, isCancel bool, candidate *txmgr.TxCandidate, queue TxSender[txRef], receiptsCh chan txmgr.TxReceipt[txRef]) bool { + if isCancel { + return false + } + // Espresso batcher: authenticate via BatchAuthenticator. + if l.Config.Espresso.Enabled { + l.authGroup.Go( + func() error { + l.sendTxWithEspresso(txdata, isCancel, candidate, queue, receiptsCh) + return nil + }, + ) + return true + } + // Fallback batcher: authenticate via BatchAuthenticator only after the + // EspressoTime hardfork has activated. + if !l.hasBatchAuthenticator() { + return false + } + fallbackAuthRequired, err := l.isFallbackAuthRequired(l.killCtx) + if err != nil { + receiptsCh <- txmgr.TxReceipt[txRef]{ + ID: txRef{id: txdata.ID(), isCancel: isCancel, isBlob: txdata.daType == DaTypeBlob, daType: txdata.daType, size: txdata.Len()}, + Err: fmt.Errorf("failed to evaluate fallback-auth gate: %w", err), + } + return true + } + if fallbackAuthRequired { + l.authGroup.Go( + func() error { + l.sendTxWithFallbackAuth(txdata, isCancel, candidate, queue, receiptsCh) + return nil + }, + ) + return true + } + return false +} diff --git a/op-batcher/batcher/espresso_helpers_test.go b/op-batcher/batcher/espresso_helpers_test.go new file mode 100644 index 00000000000..31118a3f80f --- /dev/null +++ b/op-batcher/batcher/espresso_helpers_test.go @@ -0,0 +1,263 @@ +package batcher_test + +import ( + "context" + "encoding/binary" + "encoding/json" + "errors" + "sync" + + espressoClient "github.com/EspressoSystems/espresso-network/sdks/go/client" + tagged_base64 "github.com/EspressoSystems/espresso-network/sdks/go/tagged-base64" + "github.com/EspressoSystems/espresso-network/sdks/go/types" + common "github.com/EspressoSystems/espresso-network/sdks/go/types/common" +) + +// ErrNotImplemented is a sentinel error used to indicate that a method +// was not implemented. +var ErrNotImplemented = errors.New("not implemented") + +// AlwaysFailingEspressoClient is a mock implementation of the EspressoClient +// interface that always returns an error for every method call. This is +// useful for testing error handling in the batcher without relying on a +// real Espresso client. +type AlwaysFailingEspressoClient struct{} + +// Compile time check to ensure adherence to EspressoClient interface +var _ espressoClient.EspressoClient = (*AlwaysFailingEspressoClient)(nil) + +func (*AlwaysFailingEspressoClient) FetchLatestBlockHeight(ctx context.Context) (uint64, error) { + return 0, ErrNotImplemented +} + +func (*AlwaysFailingEspressoClient) FetchHeaderByHeight(ctx context.Context, height uint64) (types.HeaderImpl, error) { + return types.HeaderImpl{}, ErrNotImplemented +} + +func (*AlwaysFailingEspressoClient) FetchRawHeaderByHeight(ctx context.Context, height uint64) (json.RawMessage, error) { + return json.RawMessage{}, ErrNotImplemented +} + +func (*AlwaysFailingEspressoClient) FetchHeadersByRange(ctx context.Context, from uint64, until uint64) ([]types.HeaderImpl, error) { + return []types.HeaderImpl{}, ErrNotImplemented +} + +func (*AlwaysFailingEspressoClient) FetchTransactionsInBlock(ctx context.Context, blockHeight uint64, namespace uint64) (espressoClient.TransactionsInBlock, error) { + return espressoClient.TransactionsInBlock{}, ErrNotImplemented +} + +func (*AlwaysFailingEspressoClient) FetchTransactionByHash(ctx context.Context, hash *types.TaggedBase64) (types.TransactionQueryData, error) { + return types.TransactionQueryData{}, ErrNotImplemented +} + +func (*AlwaysFailingEspressoClient) FetchVidCommonByHeight(ctx context.Context, blockHeight uint64) (types.VidCommon, error) { + return types.VidCommon{}, ErrNotImplemented +} + +func (*AlwaysFailingEspressoClient) FetchExplorerTransactionByHash(ctx context.Context, hash *types.TaggedBase64) (types.ExplorerTransactionQueryData, error) { + return types.ExplorerTransactionQueryData{}, ErrNotImplemented +} + +func (*AlwaysFailingEspressoClient) StreamPayloads(ctx context.Context, height uint64) (espressoClient.Stream[types.PayloadQueryData], error) { + return nil, ErrNotImplemented +} + +func (*AlwaysFailingEspressoClient) StreamTransactions(ctx context.Context, height uint64) (espressoClient.Stream[types.TransactionQueryData], error) { + return nil, ErrNotImplemented +} + +func (*AlwaysFailingEspressoClient) StreamTransactionsInNamespace(ctx context.Context, height uint64, namespace uint64) (espressoClient.Stream[types.TransactionQueryData], error) { + return nil, ErrNotImplemented +} + +func (*AlwaysFailingEspressoClient) FetchNamespaceTransactionsInRange(ctx context.Context, fromHeight uint64, toHeight uint64, namespace uint64) ([]types.NamespaceTransactionsRangeData, error) { + return []types.NamespaceTransactionsRangeData{}, ErrNotImplemented +} + +func (*AlwaysFailingEspressoClient) SubmitTransaction(ctx context.Context, tx common.Transaction) (*common.TaggedBase64, error) { + return nil, ErrNotImplemented +} + +// EspressoClientSwappableImplementation is as implementation of EspressoClient +// that is just a proxy. +// +// This allows it to be created and swapped easily as needed for testing. +type EspressoClientSwappableImplementation struct { + sync.RWMutex + espClient espressoClient.EspressoClient +} + +// Compile time check to ensure adherence to EspressoClient interface +var _ espressoClient.EspressoClient = (*EspressoClientSwappableImplementation)(nil) + +func (c *EspressoClientSwappableImplementation) SetEspressoClient(client espressoClient.EspressoClient) { + c.Lock() + defer c.Unlock() + + c.espClient = client +} + +func (c *EspressoClientSwappableImplementation) FetchLatestBlockHeight(ctx context.Context) (uint64, error) { + c.RLock() + defer c.RUnlock() + + return c.espClient.FetchLatestBlockHeight(ctx) +} + +func (c *EspressoClientSwappableImplementation) FetchHeaderByHeight(ctx context.Context, height uint64) (types.HeaderImpl, error) { + c.RLock() + defer c.RUnlock() + + return c.espClient.FetchHeaderByHeight(ctx, height) +} + +func (c *EspressoClientSwappableImplementation) FetchRawHeaderByHeight(ctx context.Context, height uint64) (json.RawMessage, error) { + c.RLock() + defer c.RUnlock() + + return c.espClient.FetchRawHeaderByHeight(ctx, height) +} + +func (c *EspressoClientSwappableImplementation) FetchHeadersByRange(ctx context.Context, from uint64, until uint64) ([]types.HeaderImpl, error) { + c.RLock() + defer c.RUnlock() + + return c.espClient.FetchHeadersByRange(ctx, from, until) +} + +func (c *EspressoClientSwappableImplementation) FetchTransactionsInBlock(ctx context.Context, blockHeight uint64, namespace uint64) (espressoClient.TransactionsInBlock, error) { + c.RLock() + defer c.RUnlock() + + return c.espClient.FetchTransactionsInBlock(ctx, blockHeight, namespace) +} + +func (c *EspressoClientSwappableImplementation) FetchTransactionByHash(ctx context.Context, hash *types.TaggedBase64) (types.TransactionQueryData, error) { + c.RLock() + defer c.RUnlock() + + return c.espClient.FetchTransactionByHash(ctx, hash) +} + +func (c *EspressoClientSwappableImplementation) FetchVidCommonByHeight(ctx context.Context, blockHeight uint64) (types.VidCommon, error) { + c.RLock() + defer c.RUnlock() + + return c.espClient.FetchVidCommonByHeight(ctx, blockHeight) +} + +func (c *EspressoClientSwappableImplementation) FetchExplorerTransactionByHash(ctx context.Context, hash *types.TaggedBase64) (types.ExplorerTransactionQueryData, error) { + c.RLock() + defer c.RUnlock() + + return c.espClient.FetchExplorerTransactionByHash(ctx, hash) +} + +func (c *EspressoClientSwappableImplementation) StreamPayloads(ctx context.Context, height uint64) (espressoClient.Stream[types.PayloadQueryData], error) { + c.RLock() + defer c.RUnlock() + + return c.espClient.StreamPayloads(ctx, height) +} + +func (c *EspressoClientSwappableImplementation) StreamTransactions(ctx context.Context, height uint64) (espressoClient.Stream[types.TransactionQueryData], error) { + c.RLock() + defer c.RUnlock() + + return c.espClient.StreamTransactions(ctx, height) +} + +func (c *EspressoClientSwappableImplementation) StreamTransactionsInNamespace(ctx context.Context, height uint64, namespace uint64) (espressoClient.Stream[types.TransactionQueryData], error) { + c.RLock() + defer c.RUnlock() + + return c.espClient.StreamTransactionsInNamespace(ctx, height, namespace) +} + +func (c *EspressoClientSwappableImplementation) FetchNamespaceTransactionsInRange(ctx context.Context, fromHeight uint64, toHeight uint64, namespace uint64) ([]types.NamespaceTransactionsRangeData, error) { + c.RLock() + defer c.RUnlock() + + return c.espClient.FetchNamespaceTransactionsInRange(ctx, fromHeight, toHeight, namespace) +} + +func (c *EspressoClientSwappableImplementation) SubmitTransaction(ctx context.Context, tx common.Transaction) (*common.TaggedBase64, error) { + c.RLock() + defer c.RUnlock() + + return c.espClient.SubmitTransaction(ctx, tx) +} + +// FakeSubmissionSucceedingEspressoClient is a mock implementation of the +// EspressoClient for the explicit purposes of submitting transactions, and +// seeing their response. +type FakeSubmissionSucceedingEspressoClient struct { + sync.RWMutex + espressoClient.EspressoClient + txns map[string]common.Transaction +} + +// Compile time check to ensure adherence to EspressoClient interface +var _ espressoClient.EspressoClient = (*FakeSubmissionSucceedingEspressoClient)(nil) + +var ( + ErrNotInitialized = errors.New("not initialized") + ErrHashCannotBeNil = errors.New("hash cannot be nil") + ErrTransactionNotFound = errors.New("transaction not found") +) + +func (c *FakeSubmissionSucceedingEspressoClient) Init() { + c.Lock() + defer c.Unlock() + c.txns = make(map[string]common.Transaction) +} + +// FetchTransactionByHash simulates fetching a transaction by its hash. it +// looks up a transaction for the given hash, and returns the transaction +// if it is found. +func (c *FakeSubmissionSucceedingEspressoClient) FetchTransactionByHash(ctx context.Context, hash *types.TaggedBase64) (types.TransactionQueryData, error) { + c.RLock() + defer c.RUnlock() + if c.txns == nil { + return types.TransactionQueryData{}, ErrNotInitialized + } + + if hash == nil { + return types.TransactionQueryData{}, ErrHashCannotBeNil + } + + txn, found := c.txns[hash.String()] + if !found { + return types.TransactionQueryData{}, ErrTransactionNotFound + } + + // Just to simulate some processing on the transaction + height := binary.LittleEndian.Uint64(txn.Payload) + + return types.TransactionQueryData{ + Transaction: txn, + Hash: hash, + Index: 0, + Proof: json.RawMessage{}, + BlockHash: nil, + BlockHeight: height, + }, nil +} + +// SubmitTransaction simulates a successful transaction submission and stores +// it for future retrieval +func (c *FakeSubmissionSucceedingEspressoClient) SubmitTransaction(ctx context.Context, tx common.Transaction) (*common.TaggedBase64, error) { + c.Lock() + defer c.Unlock() + if c.txns == nil { + return nil, ErrNotInitialized + } + + hash, err := tagged_base64.New("TX", tx.Payload) + if err != nil { + return nil, err + } + + c.txns[hash.String()] = tx + return hash, nil +} diff --git a/op-batcher/batcher/espresso_service.go b/op-batcher/batcher/espresso_service.go new file mode 100644 index 00000000000..4217abd8af9 --- /dev/null +++ b/op-batcher/batcher/espresso_service.go @@ -0,0 +1,191 @@ +package batcher + +import ( + "crypto/ecdsa" + "fmt" + "time" + + espressoClient "github.com/EspressoSystems/espresso-network/sdks/go/client" + espressoLightClient "github.com/EspressoSystems/espresso-network/sdks/go/light-client" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" + "github.com/hf/nitrite" + + "github.com/ethereum-optimism/optimism/espresso" + "github.com/ethereum-optimism/optimism/op-batcher/enclave" + "github.com/ethereum-optimism/optimism/op-node/rollup/derive" + opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto" +) + +// EspressoBatcherConfig groups all Espresso-specific configuration the +// batcher consumes at runtime. It is embedded as a single field on +// BatcherConfig (see service.go) to keep the upstream Optimism +// BatcherConfig field block compact and minimize cherry-pick churn. +// +// Fields are populated from CLIConfig.Espresso (and a few RollupConfig +// fallbacks) by initEspresso below; ephemeral key material is generated +// by initKeyPair (TEE) or copied from CLIConfig.Espresso.TestingBatcherPrivateKey +// (devnet/test). +type EspressoBatcherConfig struct { + Enabled bool + PollInterval time.Duration + AttestationService string + CaffeinationHeightEspresso uint64 + // CaffeinationHeightL2 is the L2 batch position at which the Espresso + // streamer should start emitting batches. Operational parameter for + // restarting batchers mid-chain (e.g. after a fallback batcher event). + // When zero, the driver falls back to + // RollupConfig.EspressoOriginBatchPos(). + CaffeinationHeightL2 uint64 + + // Receipt verification tuning for the Espresso transaction submitter. + VerifyReceiptMaxBlocks uint64 + VerifyReceiptSafetyTimeout time.Duration + VerifyReceiptRetryDelay time.Duration + + // BatcherPublicKey/BatcherPrivateKey is the batcher's identity for the + // Espresso authentication path. In TEE deployments the private key is + // generated inside the enclave (initKeyPair) and the public key is + // attested to via Nitro Enclave PCR0; outside TEE (devnet/test), the + // configured TestingBatcherPrivateKey overrides them in initEspresso. + BatcherPublicKey *ecdsa.PublicKey + BatcherPrivateKey *ecdsa.PrivateKey +} + +// EspressoStreamer returns the Espresso batch streamer driven by this batcher. +func (bs *BatcherService) EspressoStreamer() espresso.EspressoStreamer[derive.EspressoBatch] { + return bs.driver.espressoStreamer +} + +// initChainSigner asserts that the configured TxManager implements the +// ChainSigner interface and stores the embedded ChainSigner on the service. +// Espresso uses ChainSigner to sign batch authentication payloads sent to the +// BatchAuthenticator contract; the cast is required by every Espresso path. +func (bs *BatcherService) initChainSigner() error { + cast, castOk := bs.TxManager.(opcrypto.ChainSigner) + if !castOk { + return fmt.Errorf("tx manager does not implement ChainSigner") + } + bs.ChainSigner = cast + return nil +} + +// applyEspressoDriverSetup writes the Espresso-specific fields onto a +// DriverSetup populated with upstream-Optimism fields. Kept separate from the +// main initDriver struct literal so that the upstream block stays in upstream +// shape — minimizing cherry-pick churn when upstream renames or reorders +// fields. +func (bs *BatcherService) applyEspressoDriverSetup(ds *DriverSetup) { + ds.Espresso.SequencerAddress = bs.TxManager.From() + ds.Espresso.ChainSigner = bs.ChainSigner + ds.Espresso.Client = bs.EspressoClient + ds.Espresso.LightClient = bs.EspressoLightClient + ds.Espresso.Attestation = bs.Attestation +} + +// initKeyPair generates an ephemeral ECDSA key pair for the batcher's +// Espresso authentication path. In TEE deployments this key is attested +// to via Nitro Enclave PCR0; outside TEE (devnet/test), the configured +// TestingBatcherPrivateKey overrides this key in initEspresso. +func (bs *BatcherService) initKeyPair() error { + key, err := crypto.GenerateKey() + if err != nil { + return fmt.Errorf("failed to generate key pair for batcher: %w", err) + } + bs.Espresso.BatcherPrivateKey = key + bs.Espresso.BatcherPublicKey = &key.PublicKey + return nil +} + +// initEspresso configures the Espresso TEE-batcher integration on the +// BatcherService. When --espresso.enabled is false this is a no-op (the +// fallback batcher gets its own FallbackAuthLeadTime knob from +// BatcherConfig). When enabled, it wires up the Espresso query-service +// client, light client, ephemeral key pair, and Nitro Enclave attestation +// (if running in TEE). +func (bs *BatcherService) initEspresso(cfg *CLIConfig) error { + if !cfg.Espresso.Enabled { + return nil + } + + if cfg.Espresso.RollupL1URL == "" { + cfg.Espresso.RollupL1URL = cfg.L1EthRpc + } + + if cfg.Espresso.RollupL1URL != cfg.L1EthRpc { + log.Warn("Espresso Rollup L1 URL differs from batcher's L1EthRpc") + } + + if cfg.Espresso.L1URL == "" { + log.Warn("Espresso L1 URL not provided, using batcher's L1EthRpc") + cfg.Espresso.L1URL = cfg.L1EthRpc + } + if cfg.Espresso.Namespace == 0 { + log.Info("Using L2 chain ID as namespace by default") + cfg.Espresso.Namespace = bs.RollupConfig.L2ChainID.Uint64() + } + if cfg.Espresso.BatchAuthenticatorAddr == (common.Address{}) { + cfg.Espresso.BatchAuthenticatorAddr = bs.RollupConfig.BatchAuthenticatorAddress + } + + if err := cfg.Espresso.Check(); err != nil { + return fmt.Errorf("invalid Espresso config: %w", err) + } + + bs.Espresso.Enabled = true + bs.Espresso.PollInterval = cfg.Espresso.PollInterval + bs.Espresso.AttestationService = cfg.Espresso.EspressoAttestationService + bs.Espresso.CaffeinationHeightEspresso = cfg.Espresso.CaffeinationHeightEspresso + bs.Espresso.CaffeinationHeightL2 = cfg.Espresso.CaffeinationHeightL2 + bs.Espresso.VerifyReceiptMaxBlocks = cfg.Espresso.VerifyReceiptMaxBlocks + bs.Espresso.VerifyReceiptSafetyTimeout = cfg.Espresso.VerifyReceiptSafetyTimeout + bs.Espresso.VerifyReceiptRetryDelay = cfg.Espresso.VerifyReceiptRetryDelay + + client, err := espressoClient.NewMultipleNodesClient(cfg.Espresso.QueryServiceURLs) + if err != nil { + return fmt.Errorf("failed to create Espresso client: %w", err) + } + bs.EspressoClient = client + + lightClient, err := espressoLightClient.NewLightclientCaller(cfg.Espresso.LightClientAddr, bs.L1Client) + if err != nil { + return fmt.Errorf("failed to create Espresso light client: %w", err) + } + bs.EspressoLightClient = lightClient + + if err := bs.initKeyPair(); err != nil { + return fmt.Errorf("failed to create key pair for batcher: %w", err) + } + + // try to generate attestationBytes on public key when start batcher + attestationBytes, err := enclave.AttestationWithPublicKey(bs.Espresso.BatcherPublicKey) + if err != nil { + bs.Log.Info("Not running in enclave, skipping attestation", "info", err) + + // Replace ephemeral keys with configured keys, as in devnet they'll be pre-approved for batching + privateKey := cfg.Espresso.TestingBatcherPrivateKey + if privateKey == nil { + return fmt.Errorf("when not running in enclave, testing batcher private key should be set") + } + + publicKey := privateKey.Public() + publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) + if !ok { + return fmt.Errorf("error casting public key to ECDSA") + } + + bs.Espresso.BatcherPrivateKey = privateKey + bs.Espresso.BatcherPublicKey = publicKeyECDSA + } else { + // output length of attestation + bs.Log.Info("Successfully got attestation. Attestation length", "length", len(attestationBytes)) + _, err := nitrite.Verify(attestationBytes, nitrite.VerifyOptions{}) + if err != nil { + return fmt.Errorf("Couldn't verify attestation: %w", err) + } + bs.Attestation = attestationBytes + } + + return nil +} diff --git a/op-batcher/batcher/espresso_transaction_submitter_test.go b/op-batcher/batcher/espresso_transaction_submitter_test.go new file mode 100644 index 00000000000..6c736bdbe3b --- /dev/null +++ b/op-batcher/batcher/espresso_transaction_submitter_test.go @@ -0,0 +1,177 @@ +package batcher_test + +import ( + "context" + "encoding/binary" + "testing" + "time" + + espressoCommon "github.com/EspressoSystems/espresso-network/sdks/go/types" + "github.com/ethereum-optimism/optimism/op-batcher/batcher" + "github.com/stretchr/testify/require" +) + +const NUMBER_OF_TRANSACTIONS_TO_SUBMIT = 1024 * 4 + +// TestEspressoTransactionSubmitterDeadlock is a test that is meant to test +// the underlying conditions in the espressoTransactionSubmitter that may +// result in a deadlock. +// +// The basic idea is to intentionally trigger the circumstances that can +// trigger the deadlock to occur in the espressoTransactionSubmitter. This +// test with this description **SHOULD** remain as a regression test. +// +// The specific suspected criteria for triggering a deadlock in the +// implementation of the espressoTransactionSubmitter as of 2026-04-23 is +// as follows: +// +// - Assume that the Espresso Service is not ever returning successfully +// - Assume we are receiving a consistent stream of of new blocks coming in +// +// If we have both of these criteria, it should be possible to fill up the +// underlying channels of `espressoTransactionSubmitter` for both Submission +// jobs, and verification jobs. +// +// The way we *should* be able to detect the deadlock is if a call to +// SubmitTransaction blocks. +// +// NOTE: After this has been fixed, it is unlikely that this will fail in the +// future. This will primarily be due to `SubmitTransaction` being modified +// to longer explicitly block. +func TestEspressoTransactionSubmitterDeadlock(t *testing.T) { + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + submitter := batcher.NewEspressoTransactionSubmitter( + batcher.WithEspressoClient(new(AlwaysFailingEspressoClient)), + batcher.WithContext(ctx), + ) + + submitter.SpawnWorkers(4, 4) + submitter.Start() + + for i := 0; i < NUMBER_OF_TRANSACTIONS_TO_SUBMIT; i++ { + txn := espressoCommon.Transaction{ + Namespace: 1, + Payload: make([]byte, 8), + } + binary.LittleEndian.PutUint64(txn.Payload, uint64(i)) + + submitCtx, submitCancel := context.WithCancel(context.Background()) + go (func(cancel context.CancelFunc, txn espressoCommon.Transaction) { + submitter.SubmitTransaction(&txn) + cancel() + })(submitCancel, txn) + + timeoutCtx, timeoutCancel := context.WithTimeout(context.Background(), 200*time.Millisecond) + + select { + case <-submitCtx.Done(): + // Things progressed without issue. + timeoutCancel() + submitCancel() + continue + case <-timeoutCtx.Done(): + // The call to SubmitTransaction did not return within the expected + // time frame. + + timeoutCancel() + submitCancel() + t.Fatalf("SubmitTransaction has blocked for longer than 200 milliseconds, on transaction %d, for error: %s\n", i, timeoutCtx.Err()) + } + } + + // If we've gotten here, we have passed without deadlocking. +} + +// TestEspressoTransactionSubmitterProgress is a test that is meant to test +// and verify the modified behavior of the espressoTransactionSubmitter after +// the behavior change to explicitly address the potential for the previous +// deadlock condition. +// +// The resolution is to target and limit the number of active inflight requests +// pending to be submitted and verified on Espresso at a given time. We target +// this behavior explicitly by being able to configure a maximum number of +// pending or "inflight" requests that are being waited on. By setting this +// limit, and being able to track things going through the pipeline, we can +// ensure that we continue to make progress, and put back pressure on the +// submitter themselves, so that they don't keep trying to submit new +// transactions if we are unable to effectively handle them at our current +// capacity. +// +// This test ensures that this workflow can be processed by first starting with +// a failing EspressoClient, and once we hit the threshold of having too +// many in flight requests, we swap the Client over to a succeeding client, +// and ensure that we're able to get through our pending backlog, and all new +// transactions to submit without stalling. +func TestEspressoTransactionSubmitterProgress(t *testing.T) { + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + failingClient := new(AlwaysFailingEspressoClient) + succeedingClient := new(FakeSubmissionSucceedingEspressoClient) + succeedingClient.Init() + succeedingClient.EspressoClient = failingClient + espClient := new(EspressoClientSwappableImplementation) + espClient.SetEspressoClient(failingClient) + submitter := batcher.NewEspressoTransactionSubmitter( + batcher.WithEspressoClient(espClient), + batcher.WithContext(ctx), + ) + + submitter.SpawnWorkers(4, 4) + submitter.Start() + + i := 0 + for ; i < NUMBER_OF_TRANSACTIONS_TO_SUBMIT; i++ { + txn := espressoCommon.Transaction{ + Namespace: 1, + Payload: make([]byte, 8), + } + binary.LittleEndian.PutUint64(txn.Payload, uint64(i)) + err := submitter.SubmitTransaction(&txn) + + if err == nil { + continue + } + + // We've triggered the initial condition, and we're no longer + if _, ok := err.(batcher.ErrTooManyInFlightRequests); ok { + // able to submit new transactions to the queue, as we've filled + // the in flight capacity. + // NOTE: we decrement `i` here, as we didn't successfully submit it. + i-- + break + } + + require.NoError(t, err, "unexpected error encountered while submit transaction has been called") + } + + // Now we trigger the EspressoClient to start working again. + // This will effectively simulate that the external network has + // no recovered. + + espClient.SetEspressoClient(succeedingClient) + + // We need to wait a little bit to give the submitter time to process + // some of its backlog. + + time.Sleep(10 * time.Millisecond) + + for ; i < NUMBER_OF_TRANSACTIONS_TO_SUBMIT; i++ { + txn := espressoCommon.Transaction{ + Namespace: 1, + Payload: make([]byte, 8), + } + binary.LittleEndian.PutUint64(txn.Payload, uint64(i)) + err := submitter.SubmitTransaction(&txn) + + if _, ok := err.(batcher.ErrTooManyInFlightRequests); ok { + // Slow down a bit, and decrement `i` so we try it again. + i-- + time.Sleep(10 * time.Millisecond) + continue + } + + // No further errors should occur + require.NoError(t, err, "unexpected error encountered while submit transaction has been called") + } +} diff --git a/op-batcher/batcher/fallback_auth.go b/op-batcher/batcher/fallback_auth.go new file mode 100644 index 00000000000..c27c2ade436 --- /dev/null +++ b/op-batcher/batcher/fallback_auth.go @@ -0,0 +1,122 @@ +package batcher + +import ( + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/ethereum-optimism/optimism/espresso/bindings" + "github.com/ethereum-optimism/optimism/op-node/rollup/derive" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/txmgr" +) + +// computeCommitment computes the batch commitment hash from a transaction candidate. +// For calldata transactions, it returns keccak256(calldata). +// For blob transactions, it returns keccak256(concat(blobVersionedHashes)). +func computeCommitment(candidate *txmgr.TxCandidate) ([32]byte, error) { + if len(candidate.Blobs) == 0 { + return crypto.Keccak256Hash(candidate.TxData), nil + } + + concatenatedBlobHashes := make([]byte, 0) + for _, blob := range candidate.Blobs { + blobCommitment, err := blob.ComputeKZGCommitment() + if err != nil { + return [32]byte{}, fmt.Errorf("failed to compute KZG commitment for blob: %w", err) + } + blobHash := eth.KZGToVersionedHash(blobCommitment) + concatenatedBlobHashes = append(concatenatedBlobHashes, blobHash.Bytes()...) + } + return crypto.Keccak256Hash(concatenatedBlobHashes), nil +} + +// sendTxWithFallbackAuth authenticates a batch transaction via the BatchAuthenticator contract +// using the fallback batcher's sender identity (msg.sender check on-chain), then sends the +// batch data to the BatchInbox address. +// +// The contract's fallback path checks msg.sender against systemConfig.batcherHash(), so no +// separate signature is needed — the L1 transaction is already signed by the TxManager's key. +func (l *BatchSubmitter) sendTxWithFallbackAuth(txdata txData, isCancel bool, candidate *txmgr.TxCandidate, queue TxSender[txRef], receiptsCh chan txmgr.TxReceipt[txRef]) { + transactionReference := txRef{id: txdata.ID(), isCancel: isCancel, isBlob: txdata.daType == DaTypeBlob, daType: txdata.daType, size: txdata.Len()} + l.Log.Debug("Sending fallback-authenticated L1 transaction", "txRef", transactionReference) + + commitment, err := computeCommitment(candidate) + if err != nil { + receiptsCh <- txmgr.TxReceipt[txRef]{ + ID: transactionReference, + Err: fmt.Errorf("failed to compute commitment: %w", err), + } + return + } + l.Log.Debug("Computed fallback batch commitment", "txRef", transactionReference, "commitment", hexutil.Encode(commitment[:])) + + batchAuthenticatorAbi, err := bindings.BatchAuthenticatorMetaData.GetAbi() + if err != nil { + receiptsCh <- txmgr.TxReceipt[txRef]{ + ID: transactionReference, + Err: fmt.Errorf("failed to get batch authenticator ABI: %w", err), + } + return + } + + // Pass an empty signature — the contract checks msg.sender for the fallback path. + authenticateBatchCalldata, err := batchAuthenticatorAbi.Pack("authenticateBatchInfo", commitment, []byte{}) + if err != nil { + receiptsCh <- txmgr.TxReceipt[txRef]{ + ID: transactionReference, + Err: fmt.Errorf("failed to pack authenticateBatchInfo calldata: %w", err), + } + return + } + + verifyCandidate := txmgr.TxCandidate{ + TxData: authenticateBatchCalldata, + To: &l.RollupConfig.BatchAuthenticatorAddress, + } + + l.Log.Debug( + "Sending fallback authenticateBatchInfo transaction", + "txRef", transactionReference, + "commitment", hexutil.Encode(commitment[:]), + "address", l.RollupConfig.BatchAuthenticatorAddress.String(), + ) + verificationReceipt, err := l.Txmgr.Send(l.killCtx, verifyCandidate) + if err != nil { + l.Log.Error("Failed to send fallback authenticateBatchInfo transaction", "txRef", transactionReference, "err", err) + receiptsCh <- txmgr.TxReceipt[txRef]{ + ID: transactionReference, + Err: fmt.Errorf("failed to send fallback authenticateBatchInfo transaction: %w", err), + } + return + } + + receipt, err := l.Txmgr.Send(l.killCtx, *candidate) + if err != nil { + l.Log.Error("Failed to send batch inbox transaction", "txRef", transactionReference, "err", err) + receiptsCh <- txmgr.TxReceipt[txRef]{ + ID: transactionReference, + Err: fmt.Errorf("failed to send batch inbox transaction: %w", err), + } + return + } + + distance := new(big.Int).Sub(receipt.BlockNumber, verificationReceipt.BlockNumber) + lookbackWindow := new(big.Int).SetUint64(derive.BatchAuthLookbackWindow) + if distance.Sign() < 0 || distance.Cmp(lookbackWindow) >= 0 { + l.Log.Error("authenticateBatchInfo transaction too far from batch inbox transaction", "txRef", transactionReference, "distance", distance) + receiptsCh <- txmgr.TxReceipt[txRef]{ + ID: transactionReference, + Err: fmt.Errorf("authenticateBatchInfo transaction too far from batch inbox transaction: %s", distance), + } + return + } + + receiptsCh <- txmgr.TxReceipt[txRef]{ + ID: transactionReference, + Receipt: receipt, + Err: nil, + } +} diff --git a/op-batcher/batcher/service.go b/op-batcher/batcher/service.go index 02759ca5156..7db4d6866e3 100644 --- a/op-batcher/batcher/service.go +++ b/op-batcher/batcher/service.go @@ -8,6 +8,8 @@ import ( "sync/atomic" "time" + espressoClient "github.com/EspressoSystems/espresso-network/sdks/go/client" + espressoLightClient "github.com/EspressoSystems/espresso-network/sdks/go/light-client" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/log" @@ -20,6 +22,7 @@ import ( "github.com/ethereum-optimism/optimism/op-node/params" "github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-service/cliapp" + opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto" "github.com/ethereum-optimism/optimism/op-service/dial" "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/httputil" @@ -50,6 +53,19 @@ type BatcherConfig struct { // For throttling DA. See CLIConfig in config.go for details on these parameters. ThrottleParams config.ThrottleParams + + // FallbackAuthLeadTime is consulted by the fallback batcher's + // authentication gate to advance the switch to authenticated batches + // relative to the on-chain EspressoTime hardfork. It absorbs the + // worst-case L1 inclusion delay between batcher decision time (L1 tip) + // and verifier evaluation time (containing L1 block). See + // isFallbackAuthRequired in espresso_active.go for details. + FallbackAuthLeadTime time.Duration + + // Espresso groups all TEE-batcher-specific configuration. Defined in + // espresso_service.go to keep the upstream Optimism field block compact. + // Zero-valued when --espresso.enabled=false. + Espresso EspressoBatcherConfig } // BatcherService represents a full batch-submitter instance and its resources, @@ -80,6 +96,14 @@ type BatcherService struct { stopped atomic.Bool NotSubmittingOnStart bool + + // Espresso runtime state. Defined in espresso_service.go to keep the + // upstream Optimism field block compact. EspressoClient and + // EspressoLightClient are nil when --espresso.enabled=false. + EspressoClient *espressoClient.MultipleNodesClient + EspressoLightClient *espressoLightClient.LightclientCaller + opcrypto.ChainSigner + Attestation []byte } type DriverSetupOption func(setup *DriverSetup) @@ -109,6 +133,7 @@ func (bs *BatcherService) initFromCLIConfig(ctx context.Context, closeApp contex bs.NetworkTimeout = cfg.TxMgrConfig.NetworkTimeout bs.CheckRecentTxsDepth = cfg.CheckRecentTxsDepth bs.WaitNodeSync = cfg.WaitNodeSync + bs.FallbackAuthLeadTime = cfg.FallbackAuthLeadTime bs.ThrottleParams = config.ThrottleParams{ LowerThreshold: cfg.ThrottleConfig.LowerThreshold, @@ -186,6 +211,9 @@ func (bs *BatcherService) initFromCLIConfig(ctx context.Context, closeApp contex if err := bs.initPProf(cfg); err != nil { return fmt.Errorf("failed to init profiling: %w", err) } + if err := bs.initEspresso(cfg); err != nil { + return fmt.Errorf("failed to init Espresso: %w", err) + } bs.initDriver(opts...) if err := bs.initRPCServer(cfg); err != nil { return fmt.Errorf("failed to start RPC server: %w", err) @@ -358,6 +386,9 @@ func (bs *BatcherService) initTxManager(_ context.Context, cfg *CLIConfig) error return err } bs.TxManager = txManager + if err := bs.initChainSigner(); err != nil { + return err + } return nil } @@ -410,6 +441,7 @@ func (bs *BatcherService) initDriver(opts ...DriverSetupOption) { ChannelConfig: bs.ChannelConfig, AltDA: bs.AltDA, } + bs.applyEspressoDriverSetup(&ds) for _, opt := range opts { opt(&ds) } diff --git a/op-batcher/enclave/attestation.go b/op-batcher/enclave/attestation.go new file mode 100644 index 00000000000..26c4adf7205 --- /dev/null +++ b/op-batcher/enclave/attestation.go @@ -0,0 +1,47 @@ +package enclave + +import ( + "crypto/ecdsa" + "errors" + + "github.com/ethereum/go-ethereum/crypto" + "github.com/hf/nsm" + "github.com/hf/nsm/request" +) + +func attest(nonce, userData, publicKey []byte) ([]byte, error) { + sess, err := nsm.OpenDefaultSession() + if err != nil { + return nil, err + } + defer sess.Close() + + res, err := sess.Send(&request.Attestation{ + Nonce: nonce, + UserData: userData, + PublicKey: publicKey, + }) + if err != nil { + return nil, err + } + + if res.Error != "" { + return nil, errors.New(string(res.Error)) + } + + if res.Attestation == nil || res.Attestation.Document == nil { + return nil, errors.New("NSM device did not return an attestation") + } + + return res.Attestation.Document, nil +} + +func AttestationWithPublicKey(publicKey *ecdsa.PublicKey) ([]byte, error) { + // Use empty slices for nonce and publicKey when they're not needed + nonce := make([]byte, 0) + txData := make([]byte, 0) + publicKeyBytes := crypto.FromECDSAPub(publicKey) + + // Call the existing attest function with txData as userData + return attest(nonce, txData, publicKeyBytes) +} diff --git a/op-batcher/flags/flags.go b/op-batcher/flags/flags.go index 0d894fad2e5..9de791d94d3 100644 --- a/op-batcher/flags/flags.go +++ b/op-batcher/flags/flags.go @@ -8,6 +8,7 @@ import ( "github.com/urfave/cli/v2" + "github.com/ethereum-optimism/optimism/espresso" altda "github.com/ethereum-optimism/optimism/op-alt-da" "github.com/ethereum-optimism/optimism/op-batcher/compressor" "github.com/ethereum-optimism/optimism/op-node/rollup/derive" @@ -160,6 +161,18 @@ var ( Value: false, EnvVars: prefixEnvVars("WAIT_NODE_SYNC"), } + FallbackAuthLeadTimeFlag = &cli.DurationFlag{ + Name: "espresso.fallback-auth-lead-time", + Usage: "Lead time for the fallback batcher's Espresso authentication gate. " + + "How far ahead of the on-chain EspressoTime the fallback batcher " + + "starts routing batch txs through BatchAuthenticator.authenticateBatchInfo. " + + "This absorbs worst-case L1 inclusion delay between the batcher's decision " + + "(based on L1 tip time) and the verifier's gate (based on the containing " + + "L1 block's time). Has no effect outside the boundary window around the " + + "EspressoTime hardfork.", + Value: 5 * time.Minute, + EnvVars: prefixEnvVars("ESPRESSO_FALLBACK_AUTH_LEAD_TIME"), + } // Legacy Flags SequencerHDPathFlag = txmgr.SequencerHDPathFlag @@ -189,6 +202,7 @@ var optionalFlags = []cli.Flag{ DataAvailabilityTypeFlag, ActiveSequencerCheckDurationFlag, CompressionAlgoFlag, + FallbackAuthLeadTimeFlag, } func init() { @@ -199,6 +213,7 @@ func init() { optionalFlags = append(optionalFlags, oppprof.CLIFlags(EnvVarPrefix)...) optionalFlags = append(optionalFlags, txmgr.CLIFlagsWithBTO(EnvVarPrefix)...) optionalFlags = append(optionalFlags, altda.CLIFlags(EnvVarPrefix, "")...) + optionalFlags = append(optionalFlags, espresso.CLIFlags(EnvVarPrefix, "")...) Flags = append(requiredFlags, optionalFlags...) } diff --git a/op-chain-ops/script/cheatcodes_external.go b/op-chain-ops/script/cheatcodes_external.go index 063f63ee3a5..d2714473e1b 100644 --- a/op-chain-ops/script/cheatcodes_external.go +++ b/op-chain-ops/script/cheatcodes_external.go @@ -50,6 +50,11 @@ func (c *CheatCodesPrecompile) getArtifact(input string) (*foundry.Artifact, err name = parts[0] contract = parts[1] } + // Foundry accepts fully-qualified names of the form "path/to/File.sol:Contract" + // (e.g. "src/universal/Proxy.sol:Proxy", used to disambiguate from a shadowing + // library artifact). The artifacts FS is keyed by the source-file basename, so + // reduce any directory-qualified path to its basename before lookup. + name = path.Base(name) return c.h.af.ReadArtifact(name, contract) } diff --git a/op-chain-ops/script/script_test.go b/op-chain-ops/script/script_test.go index 0cff33dd603..607b78abf2f 100644 --- a/op-chain-ops/script/script_test.go +++ b/op-chain-ops/script/script_test.go @@ -67,6 +67,31 @@ func TestScript(t *testing.T) { require.NoError(t, h.cheatcodes.Precompile.DumpState("noop")) } +func TestGetCodeArtifactResolution(t *testing.T) { + logger := testlog.Logger(t, log.LevelInfo) + af := foundry.OpenArtifactsDir("./testdata/test-artifacts") + h := NewHost(logger, af, nil, DefaultContext) + require.NoError(t, h.EnableCheats()) + + // The artifacts FS is keyed by the source-file basename. Foundry's getCode + // cheatcode accepts a "File.sol:Contract" identifier and a directory-qualified + // "path/to/File.sol:Contract" identifier. Both must resolve to the same + // artifact in the Go host. + want, err := af.ReadArtifact("ScriptExample.s.sol", "FooBar") + require.NoError(t, err) + + for _, input := range []string{ + "ScriptExample.s.sol:FooBar", + "some/nested/dir/ScriptExample.s.sol:FooBar", + } { + t.Run(input, func(t *testing.T) { + got, err := h.cheatcodes.Precompile.GetCode(input) + require.NoError(t, err) + require.Equal(t, []byte(want.Bytecode.Object), got) + }) + } +} + func mustEncodeStringCalldata(t *testing.T, method, input string) []byte { packer, err := abi.JSON(strings.NewReader(fmt.Sprintf(`[{"type":"function","name":"%s","inputs":[{"type":"string","name":"input"}]}]`, method))) require.NoError(t, err) diff --git a/op-node/rollup/derive/altda_data_source_test.go b/op-node/rollup/derive/altda_data_source_test.go index fdf088ab23b..d8fd4cb5fa7 100644 --- a/op-node/rollup/derive/altda_data_source_test.go +++ b/op-node/rollup/derive/altda_data_source_test.go @@ -2,6 +2,7 @@ package derive import ( "context" + "errors" "io" "math/big" "math/rand" @@ -118,12 +119,11 @@ func TestAltDADataSource(t *testing.T) { } l1Refs = append(l1Refs, ref) logger.Info("new l1 block", "ref", ref) - // called for each l1 block to sync challenges - l1F.ExpectFetchReceipts(ref.Hash, nil, types.Receipts{}, nil) // pick a random number of commitments to include in the l1 block c := rng.Intn(4) var txs []*types.Transaction + var receipts types.Receipts for j := 0; j < c; j++ { // mock input commitments in l1 transactions @@ -147,9 +147,17 @@ func TestAltDADataSource(t *testing.T) { }) require.NoError(t, err) + receipt := types.Receipt{ + TxHash: tx.Hash(), + Status: types.ReceiptStatusSuccessful, + } + txs = append(txs, tx) + receipts = append(receipts, &receipt) } + l1F.SetFetchReceipts(ref.Hash, testutils.RandomBlockInfo(rng), receipts, nil) + logger.Info("included commitments", "count", c) l1F.ExpectInfoAndTxsByHash(ref.Hash, testutils.RandomBlockInfo(rng), txs, nil) // called once per derivation @@ -221,12 +229,11 @@ func TestAltDADataSource(t *testing.T) { } l1Refs = append(l1Refs, ref) logger.Info("new l1 block", "ref", ref) - // called for each l1 block to sync challenges - l1F.ExpectFetchReceipts(ref.Hash, nil, types.Receipts{}, nil) // pick a random number of commitments to include in the l1 block c := rng.Intn(4) var txs []*types.Transaction + var receipts []*types.Receipt for j := 0; j < c; j++ { // mock input commitments in l1 transactions @@ -249,11 +256,18 @@ func TestAltDADataSource(t *testing.T) { }) require.NoError(t, err) + receipt := &types.Receipt{ + TxHash: tx.Hash(), + Status: types.ReceiptStatusSuccessful, + } + txs = append(txs, tx) + receipts = append(receipts, receipt) } logger.Info("included commitments", "count", c) l1F.ExpectInfoAndTxsByHash(ref.Hash, testutils.RandomBlockInfo(rng), txs, nil) + l1F.SetFetchReceipts(ref.Hash, testutils.RandomBlockInfo(rng), receipts, nil) } // create a new data source for each block @@ -352,7 +366,6 @@ func TestAltDADataSourceStall(t *testing.T) { ParentHash: parent.Hash, Time: parent.Time + l1Time, } - l1F.ExpectFetchReceipts(ref.Hash, nil, types.Receipts{}, nil) // mock input commitments in l1 transactions input := testutils.RandomData(rng, 2000) comm, _ := storage.SetInput(ctx, input) @@ -370,7 +383,9 @@ func TestAltDADataSourceStall(t *testing.T) { require.NoError(t, err) txs := []*types.Transaction{tx} + receipts := types.Receipts{&types.Receipt{TxHash: tx.Hash(), Status: types.ReceiptStatusSuccessful}} + l1F.SetFetchReceipts(ref.Hash, nil, receipts, nil) l1F.ExpectInfoAndTxsByHash(ref.Hash, testutils.RandomBlockInfo(rng), txs, nil) // delete the input from the DA provider so it returns not found @@ -475,7 +490,6 @@ func TestAltDADataSourceInvalidData(t *testing.T) { ParentHash: parent.Hash, Time: parent.Time + l1Time, } - l1F.ExpectFetchReceipts(ref.Hash, nil, types.Receipts{}, nil) // mock input commitments in l1 transactions with an oversized input input := testutils.RandomData(rng, altda.MaxInputSize+1) comm, _ := storage.SetInput(ctx, input) @@ -522,7 +536,12 @@ func TestAltDADataSourceInvalidData(t *testing.T) { require.NoError(t, err) txs := []*types.Transaction{tx1, tx2, tx3} + receipts := types.Receipts{ + &types.Receipt{TxHash: tx1.Hash(), Status: types.ReceiptStatusSuccessful}, + &types.Receipt{TxHash: tx2.Hash(), Status: types.ReceiptStatusSuccessful}, + &types.Receipt{TxHash: tx3.Hash(), Status: types.ReceiptStatusSuccessful}} + l1F.SetFetchReceipts(ref.Hash, nil, receipts, nil) l1F.ExpectInfoAndTxsByHash(ref.Hash, testutils.RandomBlockInfo(rng), txs, nil) src, err := factory.OpenData(ctx, ref, batcherAddr) @@ -543,3 +562,118 @@ func TestAltDADataSourceInvalidData(t *testing.T) { l1F.AssertExpectations(t) } + +// TestAltDADataSourceL1FetcherErrors tests that the pipeline handles intermittent errors in +// L1Source correctly. +func TestAltDADataSourceL1FetcherErrors(t *testing.T) { + logger := testlog.Logger(t, log.LevelDebug) + ctx := context.Background() + + rng := rand.New(rand.NewSource(1234)) + + l1F := &testutils.MockL1Source{} + + storage := altda.NewMockDAClient(logger) + + pcfg := altda.Config{ + ChallengeWindow: 90, ResolveWindow: 90, + } + + da := altda.NewAltDAWithStorage(logger, pcfg, storage, &altda.NoopMetrics{}) + + // Create rollup genesis and config + l1Time := uint64(2) + refA := testutils.RandomBlockRef(rng) + refA.Number = 1 + l1Refs := []eth.L1BlockRef{refA} + refA0 := eth.L2BlockRef{ + Hash: testutils.RandomHash(rng), + Number: 0, + ParentHash: common.Hash{}, + Time: refA.Time, + L1Origin: refA.ID(), + SequenceNumber: 0, + } + batcherPriv := testutils.RandomKey() + batcherAddr := crypto.PubkeyToAddress(batcherPriv.PublicKey) + batcherInbox := common.Address{42} + cfg := &rollup.Config{ + Genesis: rollup.Genesis{ + L1: refA.ID(), + L2: refA0.ID(), + L2Time: refA0.Time, + }, + L1ChainID: big.NewInt(1), + BlockTime: 1, + SeqWindowSize: 20, + BatchInboxAddress: batcherInbox, + AltDAConfig: &rollup.AltDAConfig{ + DAChallengeWindow: pcfg.ChallengeWindow, + DAResolveWindow: pcfg.ResolveWindow, + CommitmentType: altda.KeccakCommitmentString, + }, + } + + signer := cfg.L1Signer() + + factory := NewDataSourceFactory(logger, cfg, l1F, nil, da) + + parent := l1Refs[0] + // create a new mock l1 ref + ref := eth.L1BlockRef{ + Hash: testutils.RandomHash(rng), + Number: parent.Number + 1, + ParentHash: parent.Hash, + Time: parent.Time + l1Time, + } + // mock input to include in l1 transaction + input := testutils.RandomData(rng, 200) + comm, _ := storage.SetInput(ctx, input) + + tx, err := types.SignNewTx(batcherPriv, signer, &types.DynamicFeeTx{ + ChainID: signer.ChainID(), + Nonce: 0, + GasTipCap: big.NewInt(2 * params.GWei), + GasFeeCap: big.NewInt(30 * params.GWei), + Gas: 100_000, + To: &batcherInbox, + Value: big.NewInt(int64(0)), + Data: comm.TxData(), + }) + require.NoError(t, err) + + txs := []*types.Transaction{tx} + + // First attempt: InfoAndTxsByHash fails, so CalldataSource opens in closed state. + // Note: the mock panics on nil interface type-assert, so we pass a dummy BlockInfo even for error cases. + l1F.ExpectInfoAndTxsByHash(ref.Hash, testutils.RandomBlockInfo(rng), nil, errors.New("Intermittent error")) + + src, err := factory.OpenData(ctx, ref, batcherAddr) + // Data source should still be opened correctly (error is deferred) + require.NoError(t, err) + + // On Next(), AltDA calls AdvanceL1Origin which fetches receipts for challenge events, + // then the inner CalldataSource retries InfoAndTxsByHash. + + // Second attempt: AdvanceL1Origin needs receipts, then InfoAndTxsByHash fails again. + l1F.ExpectFetchReceipts(ref.Hash, nil, types.Receipts{}, nil) + l1F.ExpectInfoAndTxsByHash(ref.Hash, testutils.RandomBlockInfo(rng), nil, errors.New("Intermittent error")) + + // Should fail because InfoAndTxsByHash still returns error + _, err = src.Next(ctx) + require.Error(t, err) + + // Third attempt: InfoAndTxsByHash succeeds, data is returned. + // AltDA AdvanceL1Origin is a no-op since the origin was already advanced. + l1F.ExpectInfoAndTxsByHash(ref.Hash, testutils.RandomBlockInfo(rng), txs, nil) + + // regular input is passed through + data, err := src.Next(ctx) + require.NoError(t, err) + require.Equal(t, hexutil.Bytes(input), data) + + _, err = src.Next(ctx) + require.ErrorIs(t, err, io.EOF) + + l1F.AssertExpectations(t) +} diff --git a/op-node/rollup/derive/batch_authenticator.go b/op-node/rollup/derive/batch_authenticator.go new file mode 100644 index 00000000000..1fd554e6b6e --- /dev/null +++ b/op-node/rollup/derive/batch_authenticator.go @@ -0,0 +1,183 @@ +package derive + +import ( + "context" + "fmt" + + lru "github.com/hashicorp/golang-lru/v2" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" + + "github.com/ethereum-optimism/optimism/op-service/eth" +) + +var ( + // BatchInfoAuthenticatedABI is the event signature for + // BatchInfoAuthenticated(bytes32 commitment, address indexed caller). + // The commitment is an unindexed (data) argument; only caller is indexed. + BatchInfoAuthenticatedABI = "BatchInfoAuthenticated(bytes32,address)" + BatchInfoAuthenticatedABIHash = crypto.Keccak256Hash([]byte(BatchInfoAuthenticatedABI)) +) + +// BatchAuthCaches holds the LRU caches used by CollectAuthenticatedBatches. +// Keyed by block hash so they are naturally reorg-safe: after a reorg the +// parent-hash traversal follows a different chain and stale entries are +// never hit. Thread-safe via lru.Cache's internal mutex. +type BatchAuthCaches struct { + // AuthCache maps L1 block hash to the set of authenticated batch + // commitments found in that block's receipts, where each commitment maps to + // the caller (the address that emitted the auth event). + AuthCache *lru.Cache[common.Hash, map[common.Hash]common.Address] + // RefCache maps L1 block hash to its L1BlockRef, avoiding redundant + // L1BlockRefByHash RPC calls during lookback window traversal. + RefCache *lru.Cache[common.Hash, eth.L1BlockRef] +} + +// NewBatchAuthCaches creates caches sized for the BatchAuthLookbackWindow. +func NewBatchAuthCaches() *BatchAuthCaches { + // BatchAuthLookbackWindow past blocks + 1 current block + 1 LRU overhead. + // lru.New only errors on size <= 0. + size := int(BatchAuthLookbackWindow) + 2 + authCache, _ := lru.New[common.Hash, map[common.Hash]common.Address](size) + refCache, _ := lru.New[common.Hash, eth.L1BlockRef](size) + return &BatchAuthCaches{AuthCache: authCache, RefCache: refCache} +} + +// ComputeCalldataBatchHash computes keccak256(calldata), matching the BatchAuthenticator +// contract's calldata batch validation path. +func ComputeCalldataBatchHash(data []byte) common.Hash { + return crypto.Keccak256Hash(data) +} + +// ComputeBlobBatchHash computes keccak256(concat(blobHashes)), matching the BatchAuthenticator +// contract's blob batch validation path. +func ComputeBlobBatchHash(blobHashes []common.Hash) common.Hash { + concatenated := make([]byte, 32*len(blobHashes)) + for i, h := range blobHashes { + copy(concatenated[i*32:(i+1)*32], h[:]) + } + return crypto.Keccak256Hash(concatenated) +} + +// collectAuthEventsFromReceipts extracts all authenticated batch commitments from +// the given receipts, mapping each commitment to the caller that emitted the +// BatchInfoAuthenticated event (the indexed Topics[1]). The caller is later +// matched against the batch transaction's L1 sender, so a batch is only accepted +// if the same address both authenticated and submitted it. +func collectAuthEventsFromReceipts(receipts types.Receipts, authenticatorAddr common.Address) map[common.Hash]common.Address { + result := make(map[common.Hash]common.Address) + for _, receipt := range receipts { + if receipt.Status != types.ReceiptStatusSuccessful { + continue + } + for _, lg := range receipt.Logs { + if lg.Address != authenticatorAddr { + continue + } + if len(lg.Topics) >= 2 && lg.Topics[0] == BatchInfoAuthenticatedABIHash && len(lg.Data) >= 32 { + commitment := common.BytesToHash(lg.Data[:32]) + caller := common.BytesToAddress(lg.Topics[1][:]) + result[commitment] = caller + } + } + } + return result +} + +// CollectAuthenticatedBatches scans L1 receipts in the range +// [ref.Number - lookbackWindow, ref.Number] and returns a map from each batch +// commitment hash that was authenticated via a BatchInfoAuthenticated event to +// the caller that emitted it (the event's indexed `caller`). Callers use this to +// require that a batch transaction's L1 sender matches the address that +// authenticated the batch. +// +// This is called once per L1 block by the data source, and the returned set is checked +// against each candidate batch transaction. This avoids rescanning the lookback window +// for every individual batch transaction. +// +// The scan walks newest block to oldest; when the same commitment is authenticated +// in more than one block, the newest event's caller is retained. +// +// Results are cached per block hash in the provided BatchAuthCaches. For consecutive +// L1 blocks the lookback windows overlap by ~99 blocks, so only one new block's +// receipts need to be fetched on each call. The cache is keyed by block hash (not +// number) so it is naturally reorg-safe. +// +// Using event scanning (rather than L1 contract state reads) keeps the derivation +// pipeline compatible with the op-program fault proof environment, which can only +// access L1 block headers, transactions, receipts, and blobs. +func CollectAuthenticatedBatches( + ctx context.Context, + fetcher L1Fetcher, + ref eth.L1BlockRef, + authenticatorAddr common.Address, + caches *BatchAuthCaches, + logger log.Logger, +) (map[common.Hash]common.Address, error) { + cache := caches.AuthCache + refCache := caches.RefCache + + // Cache the starting block ref so future calls that traverse through this + // block (as part of their lookback window) can resolve it without an RPC call. + refCache.Add(ref.Hash, ref) + + // Traversal is newest-block-first, so a commitment already in the map was + // seen in a newer block; mergeNewest keeps that newer caller (see doc above). + allAuthenticated := make(map[common.Hash]common.Address) + mergeNewest := func(src map[common.Hash]common.Address) { + for commitment, caller := range src { + if _, seen := allAuthenticated[commitment]; !seen { + allAuthenticated[commitment] = caller + } + } + } + + currentBlock := ref + receiptCacheHits := 0 + refCacheHits := 0 + + for { + // Check receipt cache first + if cached, ok := cache.Get(currentBlock.Hash); ok { + mergeNewest(cached) + receiptCacheHits++ + } else { + // Cache miss: fetch receipts, extract events, cache the result + _, receipts, err := fetcher.FetchReceipts(ctx, currentBlock.Hash) + if err != nil { + return nil, NewTemporaryError(fmt.Errorf("batch auth: failed to fetch receipts for block %d: %w", currentBlock.Number, err)) + } + events := collectAuthEventsFromReceipts(receipts, authenticatorAddr) + cache.Add(currentBlock.Hash, events) + mergeNewest(events) + } + + if currentBlock.Number == 0 || ref.Number-currentBlock.Number >= BatchAuthLookbackWindow { + break + } + + // Resolve parent block ref, using the cache to avoid redundant RPC calls. + // Consecutive L1 blocks share ~99 blocks in their lookback windows, so + // after the first full traversal almost every parent lookup is a cache hit. + parentHash := currentBlock.ParentHash + if cachedRef, ok := refCache.Get(parentHash); ok { + currentBlock = cachedRef + refCacheHits++ + } else { + parentRef, err := fetcher.L1BlockRefByHash(ctx, parentHash) + if err != nil { + return nil, NewTemporaryError(fmt.Errorf("batch auth: failed to fetch L1 block ref %s: %w", parentHash.String(), err)) + } + refCache.Add(parentHash, parentRef) + currentBlock = parentRef + } + } + + logger.Debug("collected authenticated batches from lookback window", + "count", len(allAuthenticated), "fromBlock", currentBlock.Number, "toBlock", ref.Number, + "receiptCacheHits", receiptCacheHits, "refCacheHits", refCacheHits) + return allAuthenticated, nil +} diff --git a/op-node/rollup/derive/blob_data_source.go b/op-node/rollup/derive/blob_data_source.go index d4ec55fa1b2..539b432f948 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, @@ -86,7 +86,10 @@ func (ds *BlobDataSource) open(ctx context.Context) ([]blobOrCalldata, error) { return nil, NewTemporaryError(fmt.Errorf("failed to open blob data source: %w", err)) } - data, hashes := dataAndHashesFromTxs(txs, &ds.dsCfg, ds.batcherAddr, ds.log) + data, hashes, err := dataAndHashesFromTxs(ctx, txs, &ds.dsCfg, ds.batcherAddr, ds.fetcher, ds.ref, ds.log) + if err != nil { + return nil, err + } if len(hashes) == 0 { // there are no blobs to fetch so we can return immediately @@ -115,14 +118,52 @@ 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, []common.Hash) { +// +// Pre-Espresso (the L1 origin time of `ref` is < *EspressoTime, or unset), +// this runs upstream Optimism semantics: filter by batch inbox + sender == +// batcher. +// +// Post-Espresso, it collects all authenticated batch hashes from a lookback +// window once and rejects any batch whose commitment hash is not in the +// authenticated set. For blob transactions, the batch hash is computed from +// the concatenated blob versioned hashes. +func dataAndHashesFromTxs(ctx context.Context, txs types.Transactions, config *DataSourceConfig, batcherAddr common.Address, fetcher L1Fetcher, ref eth.L1BlockRef, logger log.Logger) ([]blobOrCalldata, []common.Hash, error) { + // Only collect authenticated batch commitments when the Espresso fork is + // active at the L1 origin time of the block we're scanning. Pre-fork, the + // upstream sender-based authorization path is used and authenticatedHashes + // is unused. + var authenticatedHashes map[common.Hash]common.Address + if config.rollupCfg.IsEspresso(ref.Time) { + var err error + authenticatedHashes, err = CollectAuthenticatedBatches( + ctx, fetcher, ref, config.rollupCfg.BatchAuthenticatorAddress, config.batchAuthCaches, logger, + ) + if err != nil { + return nil, nil, err + } + } + data := []blobOrCalldata{} var hashes []common.Hash for _, tx := range txs { - // skip any non-batcher transactions - if !isValidBatchTx(tx, config.l1Signer, config.batchInboxAddress, batcherAddr, logger) { + // skip any non-batcher transactions (wrong type or wrong To address) + if !isValidBatchTx(tx, config.batchInboxAddress, logger) { continue } + + // Compute batch hash depending on tx type + var batchHash common.Hash + if tx.Type() == types.BlobTxType { + batchHash = ComputeBlobBatchHash(tx.BlobHashes()) + } else { + batchHash = ComputeCalldataBatchHash(tx.Data()) + } + + // Check authorization (sender-based pre-fork; event-based post-fork). + if !isBatchTxAuthorized(tx, *config, batcherAddr, batchHash, authenticatedHashes, ref.Time, logger) { + continue + } + // handle non-blob batcher transactions by extracting their calldata if tx.Type() != types.BlobTxType { calldata := eth.Data(tx.Data()) @@ -138,7 +179,7 @@ func dataAndHashesFromTxs(txs types.Transactions, config *DataSourceConfig, batc data = append(data, blobOrCalldata{nil, nil}) // will fill in blob pointers after we download them below } } - return data, hashes + return data, hashes, nil } // fillBlobPointers goes back through the data array and fills in the pointers to the fetched blob diff --git a/op-node/rollup/derive/blob_data_source_test.go b/op-node/rollup/derive/blob_data_source_test.go index a20205544c4..2c07af0fb9d 100644 --- a/op-node/rollup/derive/blob_data_source_test.go +++ b/op-node/rollup/derive/blob_data_source_test.go @@ -1,7 +1,9 @@ package derive import ( + "context" "crypto/ecdsa" + "io" "math/big" "math/rand" "testing" @@ -9,12 +11,16 @@ import ( "github.com/stretchr/testify/require" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/testlog" "github.com/ethereum-optimism/optimism/op-service/testutils" + "github.com/ethereum-optimism/optimism/op-service/txmgr" "github.com/ethereum/go-ethereum/log" ) @@ -34,6 +40,9 @@ func TestDataAndHashesFromTxs(t *testing.T) { batchInboxAddress: batchInboxAddr, } + ctx := context.Background() + ref := eth.L1BlockRef{Number: 1} + // create a valid non-blob batcher transaction and make sure it's picked up txData := &types.LegacyTx{ Nonce: rng.Uint64(), @@ -45,7 +54,9 @@ func TestDataAndHashesFromTxs(t *testing.T) { } calldataTx, _ := types.SignNewTx(privateKey, signer, txData) txs := types.Transactions{calldataTx} - data, blobHashes := dataAndHashesFromTxs(txs, &config, batcherAddr, logger) + // Legacy mode: no L1Fetcher needed (sender check is local) + data, blobHashes, err := dataAndHashesFromTxs(ctx, txs, &config, batcherAddr, nil, ref, logger) + require.NoError(t, err) require.Equal(t, 1, len(data)) require.Equal(t, 0, len(blobHashes)) @@ -60,14 +71,16 @@ func TestDataAndHashesFromTxs(t *testing.T) { } blobTx, _ := types.SignNewTx(privateKey, signer, blobTxData) txs = types.Transactions{blobTx} - data, blobHashes = dataAndHashesFromTxs(txs, &config, batcherAddr, logger) + data, blobHashes, err = dataAndHashesFromTxs(ctx, txs, &config, batcherAddr, nil, ref, logger) + require.NoError(t, err) 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, err = dataAndHashesFromTxs(ctx, txs, &config, batcherAddr, nil, ref, logger) + require.NoError(t, err) require.Equal(t, 2, len(data)) require.Equal(t, 1, len(blobHashes)) require.NotNil(t, data[1].calldata) @@ -75,7 +88,8 @@ 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, err = dataAndHashesFromTxs(ctx, txs, &config, batcherAddr, nil, ref, logger) + require.NoError(t, err) require.Equal(t, 0, len(data)) require.Equal(t, 0, len(blobHashes)) @@ -84,7 +98,8 @@ 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, err = dataAndHashesFromTxs(ctx, txs, &config, batcherAddr, nil, ref, logger) + require.NoError(t, err) require.Equal(t, 0, len(data)) require.Equal(t, 0, len(blobHashes)) @@ -98,7 +113,8 @@ 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, err = dataAndHashesFromTxs(ctx, txs, &config, batcherAddr, nil, ref, logger) + require.NoError(t, err) require.Equal(t, 0, len(data)) require.Equal(t, 0, len(blobHashes)) } @@ -152,3 +168,110 @@ func TestFillBlobPointers(t *testing.T) { require.Equal(t, calldataLen, calldataCount) } } + +// TestBlobDataSourceL1FetcherErrors tests that BlobDataSource handles intermittent errors in +// L1Source correctly. +func TestBlobDataSourceL1FetcherErrors(t *testing.T) { + logger := testlog.Logger(t, log.LevelDebug) + ctx := context.Background() + + rng := rand.New(rand.NewSource(1234)) + + l1F := &testutils.MockL1Source{} + blobF := &testutils.MockBlobsFetcher{} + + // Create rollup genesis and config + l1Time := uint64(2) + refA := testutils.RandomBlockRef(rng) + refA.Number = 1 + l1Refs := []eth.L1BlockRef{refA} + refA0 := eth.L2BlockRef{ + Hash: testutils.RandomHash(rng), + Number: 0, + ParentHash: common.Hash{}, + Time: refA.Time, + L1Origin: refA.ID(), + SequenceNumber: 0, + } + batcherPriv := testutils.RandomKey() + batcherAddr := crypto.PubkeyToAddress(batcherPriv.PublicKey) + batcherInbox := common.Address{42} + cfg := &rollup.Config{ + Genesis: rollup.Genesis{ + L1: refA.ID(), + L2: refA0.ID(), + L2Time: refA0.Time, + }, + L1ChainID: big.NewInt(1), + BlockTime: 1, + SeqWindowSize: 20, + BatchInboxAddress: batcherInbox, + EcotoneTime: new(uint64), + } + + signer := cfg.L1Signer() + + factory := NewDataSourceFactory(logger, cfg, l1F, blobF, nil) + + parent := l1Refs[0] + // create a new mock l1 ref + ref := eth.L1BlockRef{ + Hash: testutils.RandomHash(rng), + Number: parent.Number + 1, + ParentHash: parent.Hash, + Time: parent.Time + l1Time, + } + + input := testutils.RandomData(rng, 200) + tx, err := types.SignNewTx(batcherPriv, signer, &types.DynamicFeeTx{ + ChainID: signer.ChainID(), + Nonce: 0, + GasTipCap: big.NewInt(2 * params.GWei), + GasFeeCap: big.NewInt(30 * params.GWei), + Gas: 100_000, + To: &batcherInbox, + Value: big.NewInt(int64(0)), + Data: input, + }) + require.NoError(t, err) + + blobInput := testutils.RandomData(rng, 1024) + blob := new(eth.Blob) + err = blob.FromData(blobInput) + require.NoError(t, err) + _, blobHashes, err := txmgr.MakeSidecar([]*eth.Blob{blob}, false) + require.NoError(t, err) + blobTxData := &types.BlobTx{ + Nonce: rng.Uint64(), + Gas: 2_000_000, + To: batcherInbox, + Data: testutils.RandomData(rng, rng.Intn(1000)), + BlobHashes: blobHashes, + } + blobTx, _ := types.SignNewTx(batcherPriv, signer, blobTxData) + + txs := []*types.Transaction{tx, blobTx} + + // Open with valid txs — should succeed and fetch blobs + l1F.ExpectInfoAndTxsByHash(ref.Hash, testutils.RandomBlockInfo(rng), txs, nil) + blobF.ExpectOnGetBlobsByHash(ctx, ref.Time, []common.Hash{blobHashes[0]}, []*eth.Blob{(*eth.Blob)(blob)}, nil) + + src, err := factory.OpenData(ctx, ref, batcherAddr) + require.IsType(t, &BlobDataSource{}, src, src) + require.NoError(t, err) + + // calldata input is passed through + data, err := src.Next(ctx) + require.NoError(t, err) + require.Equal(t, hexutil.Bytes(input), data) + + // blob input is passed through + data, err = src.Next(ctx) + require.NoError(t, err) + require.Equal(t, hexutil.Bytes(blobInput), data) + + _, err = src.Next(ctx) + require.ErrorIs(t, err, io.EOF) + + l1F.AssertExpectations(t) +} diff --git a/op-node/rollup/derive/calldata_source.go b/op-node/rollup/derive/calldata_source.go index 0e8147261e9..2803301378e 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,21 +32,27 @@ 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 { + closedSource := &CalldataSource{ + open: false, + ref: ref, + dsCfg: dsCfg, + fetcher: fetcher, + log: log, + batcherAddr: batcherAddr, + } + _, txs, err := fetcher.InfoAndTxsByHash(ctx, ref.Hash) if err != nil { - return &CalldataSource{ - open: false, - ref: ref, - dsCfg: dsCfg, - fetcher: fetcher, - log: log, - batcherAddr: batcherAddr, - } + return closedSource + } + data, err := DataFromEVMTransactions(ctx, dsCfg, batcherAddr, txs, fetcher, ref, log.New("origin", ref)) + if err != nil { + return closedSource } return &CalldataSource{ open: true, - data: DataFromEVMTransactions(dsCfg, batcherAddr, txs, log.New("origin", ref)), + data: data, } } @@ -55,14 +61,17 @@ func NewCalldataSource(ctx context.Context, log log.Logger, dsCfg DataSourceConf // otherwise it returns a temporary error if fetching the block returns an error. 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 { - ds.open = true - ds.data = DataFromEVMTransactions(ds.dsCfg, ds.batcherAddr, txs, ds.log) - } else if errors.Is(err, ethereum.NotFound) { + _, txs, err := ds.fetcher.InfoAndTxsByHash(ctx, ds.ref.Hash) + if errors.Is(err, ethereum.NotFound) { return nil, NewResetError(fmt.Errorf("failed to open calldata source: %w", err)) - } else { + } else if err != nil { return nil, NewTemporaryError(fmt.Errorf("failed to open calldata source: %w", err)) } + ds.data, err = DataFromEVMTransactions(ctx, ds.dsCfg, ds.batcherAddr, txs, ds.fetcher, ds.ref, ds.log) + if err != nil { + return nil, err + } + ds.open = true } if len(ds.data) == 0 { return nil, io.EOF @@ -76,12 +85,42 @@ func (ds *CalldataSource) Next(ctx context.Context) (eth.Data, error) { // DataFromEVMTransactions filters all of the transactions and returns the calldata from transactions // that are sent to the batch inbox address from the batch sender address. // This will return an empty array if no valid transactions are found. -func DataFromEVMTransactions(dsCfg DataSourceConfig, batcherAddr common.Address, txs types.Transactions, log log.Logger) []eth.Data { +// +// Pre-Espresso (the L1 origin time of `ref` is < *EspressoTime, or unset), +// this runs upstream Optimism semantics: filter by batch inbox + sender == +// batcher. +// +// Post-Espresso, it collects all authenticated batch hashes from a lookback +// window once and rejects any batch whose commitment hash is not in the +// authenticated set. +func DataFromEVMTransactions(ctx context.Context, dsCfg DataSourceConfig, batcherAddr common.Address, txs types.Transactions, fetcher L1Fetcher, ref eth.L1BlockRef, log log.Logger) ([]eth.Data, error) { + // Only collect authenticated batch commitments when the Espresso fork is + // active at the L1 origin time of the block we're scanning. Pre-fork, the + // upstream sender-based authorization path inside isBatchTxAuthorized is used + // and the authenticatedHashes map is unused. + var authenticatedHashes map[common.Hash]common.Address + if dsCfg.rollupCfg.IsEspresso(ref.Time) { + var err error + authenticatedHashes, err = CollectAuthenticatedBatches( + ctx, fetcher, ref, dsCfg.rollupCfg.BatchAuthenticatorAddress, dsCfg.batchAuthCaches, log, + ) + if err != nil { + return nil, err + } + } + out := []eth.Data{} for _, tx := range txs { - if isValidBatchTx(tx, dsCfg.l1Signer, dsCfg.batchInboxAddress, batcherAddr, log) { - out = append(out, tx.Data()) + if !isValidBatchTx(tx, dsCfg.batchInboxAddress, log) { + continue } + + batchHash := ComputeCalldataBatchHash(tx.Data()) + if !isBatchTxAuthorized(tx, dsCfg, batcherAddr, batchHash, authenticatedHashes, ref.Time, log) { + continue + } + + out = append(out, tx.Data()) } - return out + return out, nil } diff --git a/op-node/rollup/derive/calldata_source_test.go b/op-node/rollup/derive/calldata_source_test.go index 01b2616cca3..4d6a854155e 100644 --- a/op-node/rollup/derive/calldata_source_test.go +++ b/op-node/rollup/derive/calldata_source_test.go @@ -1,6 +1,7 @@ package derive import ( + "context" "crypto/ecdsa" "math/big" "math/rand" @@ -115,14 +116,24 @@ func TestDataFromEVMTransactions(t *testing.T) { var expectedData []eth.Data var txs []*types.Transaction for i, tx := range tc.txs { - txs = append(txs, tx.Create(t, signer, rng)) + transaction := tx.Create(t, signer, rng) + txs = append(txs, transaction) + if tx.good { expectedData = append(expectedData, txs[i].Data()) } } - out := DataFromEVMTransactions(DataSourceConfig{cfg.L1Signer(), cfg.BatchInboxAddress, false}, batcherAddr, txs, testlog.Logger(t, log.LevelCrit)) + // Legacy mode (no batch authenticator, Espresso inactive) — uses sender-based auth + dsCfg := DataSourceConfig{ + l1Signer: cfg.L1Signer(), + batchInboxAddress: cfg.BatchInboxAddress, + rollupCfg: cfg, + } + ref := eth.L1BlockRef{Number: 1} + // In legacy mode, no L1Fetcher calls are needed for auth (sender check is local) + out, err := DataFromEVMTransactions(context.Background(), dsCfg, batcherAddr, txs, nil, ref, testlog.Logger(t, log.LevelCrit)) + require.NoError(t, err) require.ElementsMatch(t, expectedData, out) } - } diff --git a/op-node/rollup/derive/data_source.go b/op-node/rollup/derive/data_source.go index d90cfb42ff5..3d659250379 100644 --- a/op-node/rollup/derive/data_source.go +++ b/op-node/rollup/derive/data_source.go @@ -48,10 +48,16 @@ type DataSourceFactory struct { } func NewDataSourceFactory(log log.Logger, cfg *rollup.Config, fetcher L1Fetcher, blobsFetcher L1BlobsFetcher, altDAFetcher AltDAInputFetcher) *DataSourceFactory { + var caches *BatchAuthCaches + if cfg.EspressoTime != nil { + caches = NewBatchAuthCaches() + } config := DataSourceConfig{ l1Signer: cfg.L1Signer(), batchInboxAddress: cfg.BatchInboxAddress, altDAEnabled: cfg.AltDAEnabled(), + rollupCfg: cfg, + batchAuthCaches: caches, } return &DataSourceFactory{ log: log, @@ -64,6 +70,13 @@ func NewDataSourceFactory(log log.Logger, cfg *rollup.Config, fetcher L1Fetcher, } // OpenData returns the appropriate data source for the L1 block `ref`. +// +// The Espresso gate is evaluated against the L1 origin time (ref.Time), +// mirroring the upstream pattern used for ecotoneTime: the data-source layer +// is per-L1-block, so it gates on L1 time. The fork timestamp itself is +// conceptually an L2 timestamp but the per-L1-block decision is stable as +// long as L1 origin time and L2 block time are within MaxSequencerDrift of +// each other (always true on a healthy chain). func (ds *DataSourceFactory) OpenData(ctx context.Context, ref eth.L1BlockRef, batcherAddr common.Address) (DataIter, error) { // Creates a data iterator from blob or calldata source so we can forward it to the altDA source // if enabled as it still requires an L1 data source for fetching input commmitments. @@ -88,13 +101,22 @@ type DataSourceConfig struct { l1Signer types.Signer batchInboxAddress common.Address altDAEnabled bool + // rollupCfg provides Espresso-specific configuration (EspressoTime, + // BatchAuthenticatorAddress) consulted when post-Espresso event-based + // batch authentication is active. + rollupCfg *rollup.Config + // batchAuthCaches holds the LRU caches for batch authentication lookback + // window traversal. Nil when Espresso is not configured. + batchAuthCaches *BatchAuthCaches } -// isValidBatchTx returns true if: +// isValidBatchTx checks basic transaction validity for batch submission: // 1. the transaction type is any of Legacy, ACL, DynamicFee, Blob, or Deposit (for L3s). -// 2. the transaction has a To() address that matches the batch inbox address, and -// 3. the transaction has a valid signature from the batcher address -func isValidBatchTx(tx *types.Transaction, l1Signer types.Signer, batchInboxAddr, batcherAddr common.Address, logger log.Logger) bool { +// 2. the transaction has a To() address that matches the batch inbox address +// +// It does NOT check authentication (sender or event-based) — that is handled separately +// by isBatchTxAuthorized. +func isValidBatchTx(tx *types.Transaction, batchInboxAddr common.Address, logger log.Logger) bool { // For now, we want to disallow the SetCodeTx type or any future types. if tx.Type() > types.BlobTxType && tx.Type() != types.DepositTxType { return false @@ -104,14 +126,76 @@ func isValidBatchTx(tx *types.Transaction, l1Signer types.Signer, batchInboxAddr if to == nil || *to != batchInboxAddr { return false } - seqDataSubmitter, err := l1Signer.Sender(tx) // optimization: only derive sender if To is correct + + return true +} + +// isAuthorizedBatchSender performs upstream-style sender-based authorization: it +// recovers the L1 sender of the transaction and checks it matches the configured +// batcher address. This is the pre-Espresso authorization path. +func isAuthorizedBatchSender(tx *types.Transaction, l1Signer types.Signer, batcherAddr common.Address, logger log.Logger) bool { + sender, err := l1Signer.Sender(tx) if err != nil { logger.Warn("tx in inbox with invalid signature", "hash", tx.Hash(), "err", err) return false } - // some random L1 user might have sent a transaction to our batch inbox, ignore them - if seqDataSubmitter != batcherAddr { - logger.Warn("tx in inbox with unauthorized submitter", "addr", seqDataSubmitter, "hash", tx.Hash(), "err", err) + if sender != batcherAddr { + logger.Warn("tx in inbox with unauthorized submitter", "addr", sender, "hash", tx.Hash()) + return false + } + return true +} + +// isBatchTxAuthorized determines whether a batch transaction is authorized for inclusion. +// +// The fork gate is evaluated against the L1 origin time of the enclosing L1 +// block (passed as l1OriginTime), mirroring the data-source layer's ecotoneTime +// treatment. +// +// Pre-Espresso (l1OriginTime < *EspressoTime, or unset): +// +// upstream behavior — the L1 sender of the transaction must match the configured +// batcher address. The authenticatedHashes map is unused. +// +// Post-Espresso: +// +// the batch's commitment hash must appear in authenticatedHashes (i.e. a +// BatchInfoAuthenticated event was emitted for this commitment within the +// derivation pipeline's lookback window) AND the L1 sender of the batch +// transaction must equal the caller that emitted that event. This binds each +// batch to the address that authenticated it, so a batch authenticated by one +// batcher cannot be submitted by another. Sender-based-only authorization is +// rejected. +func isBatchTxAuthorized( + tx *types.Transaction, + dsCfg DataSourceConfig, + batcherAddr common.Address, + batchHash common.Hash, + authenticatedHashes map[common.Hash]common.Address, + l1OriginTime uint64, + logger log.Logger, +) bool { + if !dsCfg.rollupCfg.IsEspresso(l1OriginTime) { + // Pre-fork: upstream sender-based authorization. + return isAuthorizedBatchSender(tx, dsCfg.l1Signer, batcherAddr, logger) + } + // Post-fork: the commitment must have been authenticated within the lookback window. + authCaller, ok := authenticatedHashes[batchHash] + if !ok { + logger.Warn("batch not authenticated", + "txHash", tx.Hash(), "batchHash", batchHash) + return false + } + // The batch tx must be submitted by the same address that authenticated it. + sender, err := dsCfg.l1Signer.Sender(tx) + if err != nil { + logger.Warn("authenticated batch tx with invalid signature", + "txHash", tx.Hash(), "batchHash", batchHash, "err", err) + return false + } + if sender != authCaller { + logger.Warn("authenticated batch submitted by a different sender than the authenticating caller", + "txHash", tx.Hash(), "batchHash", batchHash, "sender", sender, "authCaller", authCaller) return false } return true diff --git a/op-node/rollup/derive/espresso_batch.go b/op-node/rollup/derive/espresso_batch.go new file mode 100644 index 00000000000..fc22780bfd5 --- /dev/null +++ b/op-node/rollup/derive/espresso_batch.go @@ -0,0 +1,138 @@ +package derive + +import ( + "bytes" + "context" + "fmt" + + espressoCommon "github.com/EspressoSystems/espresso-network/sdks/go/types" + "github.com/ethereum-optimism/optimism/op-node/rollup" + opCrypto "github.com/ethereum-optimism/optimism/op-service/crypto" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/rlp" +) + +// A SingularBatch with block number attached to restore ordering +// when fetching from Espresso +type EspressoBatch struct { + BatchHeader *types.Header + Batch SingularBatch + L1InfoDeposit *types.Transaction + SignerAddress common.Address +} + +func (b EspressoBatch) Number() uint64 { + return b.BatchHeader.Number.Uint64() +} + +func (b EspressoBatch) L1Origin() eth.BlockID { + return b.Batch.Epoch() +} + +func (b EspressoBatch) Header() *types.Header { + return b.BatchHeader +} + +func (b EspressoBatch) Hash() common.Hash { + hash := crypto.Keccak256Hash(b.BatchHeader.Hash().Bytes(), b.L1InfoDeposit.Hash().Bytes()) + return hash +} + +func (b EspressoBatch) Signer() common.Address { + return b.SignerAddress +} + +func (b *EspressoBatch) ToEspressoTransaction(ctx context.Context, namespace uint64, signer opCrypto.ChainSigner) (*espressoCommon.Transaction, error) { + buf := new(bytes.Buffer) + err := rlp.Encode(buf, *b) + if err != nil { + return nil, fmt.Errorf("failed to encode batch: %w", err) + } + + batcherSignature, err := signer.Sign(ctx, crypto.Keccak256(buf.Bytes())) + + if err != nil { + return nil, fmt.Errorf("failed to create batcher signature: %w", err) + } + + payload := append(batcherSignature, buf.Bytes()...) + + return &espressoCommon.Transaction{Namespace: namespace, Payload: payload}, nil + +} + +func BlockToEspressoBatch(rollupCfg *rollup.Config, block *types.Block) (*EspressoBatch, error) { + if len(block.Transactions()) == 0 { + return nil, fmt.Errorf("Block doesn't contain any transactions") + } + + l1InfoDeposit := block.Transactions()[0] + if !l1InfoDeposit.IsDepositTx() { + return nil, fmt.Errorf("First transaction is not L1 info deposit") + } + + batch, _, err := BlockToSingularBatch(rollupCfg, block) + if err != nil { + return nil, err + } + + return &EspressoBatch{ + BatchHeader: block.Header(), + Batch: *batch, + L1InfoDeposit: l1InfoDeposit, + }, nil +} + +// CreateEspressoBatchUnmarshaler returns a function that can be used to +// unmarshal an Espresso transaction into an EspressoBatch. +// The signer address is recovered from the signature and stored on the batch +// for later verification in CheckBatch (two-phase verification). +func CreateEspressoBatchUnmarshaler() func(data []byte) (*EspressoBatch, error) { + return func(data []byte) (*EspressoBatch, error) { + return UnmarshalEspressoTransaction(data) + } +} + +func UnmarshalEspressoTransaction(data []byte) (*EspressoBatch, error) { + if len(data) < crypto.SignatureLength { + return nil, fmt.Errorf("transaction data too short: %d bytes, need at least %d", len(data), crypto.SignatureLength) + } + signatureData, batchData := data[:crypto.SignatureLength], data[crypto.SignatureLength:] + batchHash := crypto.Keccak256(batchData) + + signerKey, err := crypto.SigToPub(batchHash, signatureData) + if err != nil { + return nil, err + } + signer := crypto.PubkeyToAddress(*signerKey) + + var batch EspressoBatch + if err := rlp.DecodeBytes(batchData, &batch); err != nil { + return nil, err + } + batch.SignerAddress = signer + + return &batch, nil +} + +// NOTE: This function MUST guarantee no transient errors. It is allowed to fail only on +// invalid batches or in case of misconfiguration of the batcher, in which case it should fail +// for all batches. +func (b *EspressoBatch) ToBlock(rollupCfg *rollup.Config) (*types.Block, error) { + // Re-insert the deposit transaction + txs := []*types.Transaction{b.L1InfoDeposit} + for i, opaqueTx := range b.Batch.Transactions { + var tx types.Transaction + err := tx.UnmarshalBinary(opaqueTx) + if err != nil { + return nil, fmt.Errorf("could not decode tx %d: %w", i, err) + } + txs = append(txs, &tx) + } + return types.NewBlockWithHeader(b.BatchHeader).WithBody(types.Body{ + Transactions: txs, + }), nil +} diff --git a/op-node/rollup/derive/espresso_batch_authenticator_test.go b/op-node/rollup/derive/espresso_batch_authenticator_test.go new file mode 100644 index 00000000000..05691585917 --- /dev/null +++ b/op-node/rollup/derive/espresso_batch_authenticator_test.go @@ -0,0 +1,308 @@ +package derive + +import ( + "context" + "math/rand" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" + "github.com/stretchr/testify/require" + + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/testlog" + "github.com/ethereum-optimism/optimism/op-service/testutils" +) + +// batchAuthLog builds a BatchInfoAuthenticated log as emitted by the +// BatchAuthenticator contract at address authenticatorAddr: Topics[0] is the +// event signature hash, Topics[1] is the indexed caller (the address that +// emitted the event), and the commitment is the first 32 bytes of the data. +func batchAuthLog(authenticatorAddr, caller common.Address, commitment common.Hash) *types.Log { + return &types.Log{ + Address: authenticatorAddr, + Topics: []common.Hash{ + BatchInfoAuthenticatedABIHash, + common.BytesToHash(caller.Bytes()), + }, + Data: commitment.Bytes(), + } +} + +func TestComputeCalldataBatchHash(t *testing.T) { + data := []byte("hello world") + hash := ComputeCalldataBatchHash(data) + expected := crypto.Keccak256Hash(data) + require.Equal(t, expected, hash) +} + +func TestComputeCalldataBatchHashEmpty(t *testing.T) { + hash := ComputeCalldataBatchHash([]byte{}) + expected := crypto.Keccak256Hash([]byte{}) + require.Equal(t, expected, hash) +} + +func TestComputeBlobBatchHash(t *testing.T) { + h1 := common.HexToHash("0x0100000000000000000000000000000000000000000000000000000000000001") + h2 := common.HexToHash("0x0100000000000000000000000000000000000000000000000000000000000002") + + hash := ComputeBlobBatchHash([]common.Hash{h1, h2}) + + // Manually compute expected: keccak256(h1 ++ h2) + concatenated := make([]byte, 64) + copy(concatenated[0:32], h1[:]) + copy(concatenated[32:64], h2[:]) + expected := crypto.Keccak256Hash(concatenated) + require.Equal(t, expected, hash) +} + +func TestComputeBlobBatchHashSingle(t *testing.T) { + h := common.HexToHash("0xabcdef") + hash := ComputeBlobBatchHash([]common.Hash{h}) + expected := crypto.Keccak256Hash(h[:]) + require.Equal(t, expected, hash) +} + +// buildL1Chain creates a chain of L1BlockRef values with proper parent-hash linkage. +// The chain goes from block number `start` to `end` (inclusive). +// Returns a slice indexed by block number (relative to start), and the full map by number. +func buildL1Chain(rng *rand.Rand, start, end uint64) map[uint64]eth.L1BlockRef { + chain := make(map[uint64]eth.L1BlockRef) + for num := start; num <= end; num++ { + ref := eth.L1BlockRef{ + Number: num, + Hash: testutils.RandomHash(rng), + } + if num > start { + ref.ParentHash = chain[num-1].Hash + } + chain[num] = ref + } + return chain +} + +func TestCollectAuthenticatedBatches(t *testing.T) { + logger := testlog.Logger(t, log.LevelDebug) + ctx := context.Background() + rng := rand.New(rand.NewSource(1234)) + caches := NewBatchAuthCaches() + + authenticatorAddr := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678") + caller := common.HexToAddress("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + batchHash := crypto.Keccak256Hash([]byte("test batch data")) + + // Build a matching receipt + matchingReceipts := types.Receipts{ + { + Status: types.ReceiptStatusSuccessful, + Logs: []*types.Log{batchAuthLog(authenticatorAddr, caller, batchHash)}, + }, + } + emptyReceipts := types.Receipts{} + + // expectChainTraversal sets up mock expectations for a backward parent-hash + // traversal from chain[end] down to chain[start]. For each block it expects + // FetchReceipts (by hash), and for all blocks except the first (end) it + // expects L1BlockRefByHash to resolve the parent hash. + // receiptsByBlock allows overriding receipts for specific block numbers. + expectChainTraversal := func(l1F *testutils.MockL1Source, chain map[uint64]eth.L1BlockRef, start, end uint64, receiptsByBlock map[uint64]types.Receipts) { + for num := end; num >= start; num-- { + ref := chain[num] + receipts := emptyReceipts + if r, ok := receiptsByBlock[num]; ok { + receipts = r + } + l1F.ExpectFetchReceipts(ref.Hash, nil, receipts, nil) + // L1BlockRefByHash is called for every block except the first one (ref itself) + if num > start { + l1F.ExpectL1BlockRefByHash(chain[num-1].Hash, chain[num-1], nil) + } + if num == 0 { + break // avoid underflow + } + } + } + + t.Run("found in same block", func(t *testing.T) { + l1F := &testutils.MockL1Source{} + chain := buildL1Chain(rng, 100, 200) + ref := chain[200] + + // Auth event is in block 200 (same block as ref). Traversal goes 200 -> 100. + expectChainTraversal(l1F, chain, 100, 200, map[uint64]types.Receipts{ + 200: matchingReceipts, + }) + + result, err := CollectAuthenticatedBatches(ctx, l1F, ref, authenticatorAddr, caches, logger) + require.NoError(t, err) + require.Equal(t, caller, result[batchHash]) + require.Len(t, result, 1) + l1F.AssertExpectations(t) + }) + + t.Run("found in earliest block of window", func(t *testing.T) { + l1F := &testutils.MockL1Source{} + chain := buildL1Chain(rng, 100, 200) + ref := chain[200] + + // Auth event is in block 100 (last block of the lookback window). + expectChainTraversal(l1F, chain, 100, 200, map[uint64]types.Receipts{ + 100: matchingReceipts, + }) + + result, err := CollectAuthenticatedBatches(ctx, l1F, ref, authenticatorAddr, caches, logger) + require.NoError(t, err) + require.Equal(t, caller, result[batchHash]) + require.Len(t, result, 1) + l1F.AssertExpectations(t) + }) + + t.Run("not found", func(t *testing.T) { + l1F := &testutils.MockL1Source{} + chain := buildL1Chain(rng, 100, 200) + ref := chain[200] + + // No auth event in any block in the window + expectChainTraversal(l1F, chain, 100, 200, nil) + + result, err := CollectAuthenticatedBatches(ctx, l1F, ref, authenticatorAddr, caches, logger) + require.NoError(t, err) + require.Len(t, result, 0) + l1F.AssertExpectations(t) + }) + + t.Run("low block number - window clamps to 0", func(t *testing.T) { + l1F := &testutils.MockL1Source{} + chain := buildL1Chain(rng, 0, 10) + ref := chain[10] + + // Window should clamp to [0, 10]. Auth event is in block 10. + expectChainTraversal(l1F, chain, 0, 10, map[uint64]types.Receipts{ + 10: matchingReceipts, + }) + + result, err := CollectAuthenticatedBatches(ctx, l1F, ref, authenticatorAddr, caches, logger) + require.NoError(t, err) + require.Equal(t, caller, result[batchHash]) + require.Len(t, result, 1) + l1F.AssertExpectations(t) + }) + + t.Run("multiple hashes collected", func(t *testing.T) { + l1F := &testutils.MockL1Source{} + chain := buildL1Chain(rng, 0, 10) + ref := chain[10] + + batchHash2 := crypto.Keccak256Hash([]byte("second batch")) + caller2 := common.HexToAddress("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") + multiReceipts := types.Receipts{ + { + Status: types.ReceiptStatusSuccessful, + Logs: []*types.Log{ + batchAuthLog(authenticatorAddr, caller, batchHash), + batchAuthLog(authenticatorAddr, caller2, batchHash2), + }, + }, + } + + // Both auth events are in block 10 + expectChainTraversal(l1F, chain, 0, 10, map[uint64]types.Receipts{ + 10: multiReceipts, + }) + + result, err := CollectAuthenticatedBatches(ctx, l1F, ref, authenticatorAddr, caches, logger) + require.NoError(t, err) + require.Len(t, result, 2) + require.Equal(t, caller, result[batchHash]) + require.Equal(t, caller2, result[batchHash2]) + l1F.AssertExpectations(t) + }) + + t.Run("newest caller wins when commitment authenticated twice", func(t *testing.T) { + l1F := &testutils.MockL1Source{} + chain := buildL1Chain(rng, 100, 200) + ref := chain[200] + + caller2 := common.HexToAddress("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") + // Older block 100 authenticates batchHash with caller2; newer block 200 + // authenticates the same batchHash with caller. The newest (block 200) + // caller must win. + olderReceipts := types.Receipts{ + { + Status: types.ReceiptStatusSuccessful, + Logs: []*types.Log{batchAuthLog(authenticatorAddr, caller2, batchHash)}, + }, + } + expectChainTraversal(l1F, chain, 100, 200, map[uint64]types.Receipts{ + 200: matchingReceipts, // caller + 100: olderReceipts, // caller2 + }) + + result, err := CollectAuthenticatedBatches(ctx, l1F, ref, authenticatorAddr, caches, logger) + require.NoError(t, err) + require.Len(t, result, 1) + require.Equal(t, caller, result[batchHash]) + l1F.AssertExpectations(t) + }) +} + +// TestCollectAuthenticatedBatchesBlockRefCache verifies that the block ref LRU cache +// eliminates redundant L1BlockRefByHash RPC calls when processing consecutive L1 blocks. +// On the first call (block N), all ~100 L1BlockRefByHash calls are made. On the second +// call (block N+1), the overlapping window means ~99 block refs are already cached, +// so only 1 new L1BlockRefByHash call is needed. +func TestCollectAuthenticatedBatchesBlockRefCache(t *testing.T) { + logger := testlog.Logger(t, log.LevelDebug) + ctx := context.Background() + rng := rand.New(rand.NewSource(5678)) + caches := NewBatchAuthCaches() + + authenticatorAddr := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678") + emptyReceipts := types.Receipts{} + + // Build a chain long enough for two consecutive lookback windows: + // Block 200's window is [100, 200], block 201's window is [101, 201]. + chain := buildL1Chain(rng, 100, 201) + + // --- First call: block 200, window [100, 200] --- + // Expects all 101 FetchReceipts calls and 100 L1BlockRefByHash calls (full traversal). + l1F := &testutils.MockL1Source{} + for num := uint64(200); num >= 100; num-- { + ref := chain[num] + l1F.ExpectFetchReceipts(ref.Hash, nil, emptyReceipts, nil) + if num > 100 { + l1F.ExpectL1BlockRefByHash(chain[num-1].Hash, chain[num-1], nil) + } + } + + result, err := CollectAuthenticatedBatches(ctx, l1F, chain[200], authenticatorAddr, caches, logger) + require.NoError(t, err) + require.Len(t, result, 0) + l1F.AssertExpectations(t) + + // --- Second call: block 201, window [101, 201] --- + // Both receipt and block ref caches are warm for blocks [100, 200]. + // Only block 201 needs FetchReceipts (new block, not in receipt cache). + // Only block 200 needs L1BlockRefByHash resolution — but it was cached as the + // `ref` of the previous call (we cache ref.Hash -> ref at the top of the function). + // So NO L1BlockRefByHash calls should be needed at all. + l1F2 := &testutils.MockL1Source{} + // Only block 201's receipts are uncached + l1F2.ExpectFetchReceipts(chain[201].Hash, nil, emptyReceipts, nil) + // All block refs in [101, 200] are cached from the first call, and block 200 + // was cached as the ref argument. No L1BlockRefByHash calls expected. + + result2, err := CollectAuthenticatedBatches(ctx, l1F2, chain[201], authenticatorAddr, caches, logger) + require.NoError(t, err) + require.Len(t, result2, 0) + l1F2.AssertExpectations(t) +} + +func TestBatchInfoAuthenticatedABIHash(t *testing.T) { + // Verify the ABI hash matches what Solidity would compute for + // BatchInfoAuthenticated(bytes32 commitment, address indexed caller). + expected := crypto.Keccak256Hash([]byte("BatchInfoAuthenticated(bytes32,address)")) + require.Equal(t, expected, BatchInfoAuthenticatedABIHash) +} diff --git a/op-node/rollup/derive/espresso_batch_test.go b/op-node/rollup/derive/espresso_batch_test.go new file mode 100644 index 00000000000..05ef9c2f9d3 --- /dev/null +++ b/op-node/rollup/derive/espresso_batch_test.go @@ -0,0 +1,237 @@ +package derive_test + +import ( + "bytes" + "math/big" + "math/rand" + "slices" + "testing" + "time" + + "github.com/ethereum-optimism/optimism/op-node/rollup" + derive "github.com/ethereum-optimism/optimism/op-node/rollup/derive" + dtest "github.com/ethereum-optimism/optimism/op-node/rollup/derive/test" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum/go-ethereum/common" + gethTypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/require" +) + +var defaultTestRollUpConfig = &rollup.Config{ + Genesis: rollup.Genesis{L2: eth.BlockID{Number: 0}}, + L2ChainID: big.NewInt(1234), +} + +// compareHash is a helper function that compares two hashes. +func compareHash(a, b common.Hash) int { + if c := bytes.Compare(a[:], b[:]); c != 0 { + return c + } + return 0 +} + +// compareTransaction is a helper function that compares two transactions +// by only inspecting their hashes. +func compareTransaction(a, b *gethTypes.Transaction) int { + return compareHash(a.Hash(), b.Hash()) +} + +// compareHeader is a helper function that compares two headers +// by only inspecting their hashes. +func compareHeader(a, b *gethTypes.Header) int { + return compareHash(a.Hash(), b.Hash()) +} + +// compareWithdrawl is a helper function that compares two withdrawals +// by checking that their slice members compare equivalently. +func compareWithdrawl(a, b *gethTypes.Withdrawal) int { + if c := a.Index - b.Index; c != 0 { + return int(c) + } + + if c := a.Validator - b.Validator; c != 0 { + return int(c) + } + + if c := a.Address.Cmp(b.Address); c != 0 { + return c + } + + if c := a.Amount - b.Amount; c != 0 { + return int(c) + } + + return 0 +} + +// compareBody is a helper function that compares two bodies +// by checking that their slice members compare equivalently. +func compareBody(a, b *gethTypes.Body) int { + if c := slices.CompareFunc(a.Transactions, b.Transactions, compareTransaction); c != 0 { + return c + } + + if c := slices.CompareFunc(a.Uncles, b.Uncles, compareHeader); c != 0 { + return c + } + + if c := slices.CompareFunc(a.Withdrawals, b.Withdrawals, compareWithdrawl); c != 0 { + return c + } + + return 0 +} + +// TestUnmarshalEspressoTransactionTooShort verifies that UnmarshalEspressoTransaction +// returns an error (rather than panicking) when the input is shorter than a signature. +func TestUnmarshalEspressoTransactionTooShort(t *testing.T) { + cases := [][]byte{ + nil, + {}, + make([]byte, crypto.SignatureLength-1), + } + for _, data := range cases { + _, err := derive.UnmarshalEspressoTransaction(data) + require.Error(t, err, "expected error for %d-byte input", len(data)) + } +} + +// TestEspressoBatchConversion tests the conversion of a block to an Espresso +// Batch, and ensures that the recovery of the original Block is possible with +// the contents of the Espresso Batch. +func TestEspressoBatchConversion(t *testing.T) { + rng := rand.New(rand.NewSource(4982432)) + ti := time.Now() + + originalBlock := dtest.RandomL2BlockWithChainIdAndTime(rng, rng.Intn(32), defaultTestRollUpConfig.L2ChainID, ti) + + espressoBatch, err := derive.BlockToEspressoBatch(defaultTestRollUpConfig, originalBlock) + if have, want := err, error(nil); have != want { + t.Fatalf("failed to convert block to batch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + decodedBlock, err := espressoBatch.ToBlock(defaultTestRollUpConfig) + if have, want := err, error(nil); have != want { + t.Fatalf("failed to decode batch back to block:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + // Let's perform a sanity check on the decoded block to ensure that all of + // the fields match the original block. + + if have, want := decodedBlock.BaseFee(), originalBlock.BaseFee(); have.Cmp(want) != 0 { + t.Errorf("decoded block base fee mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.BeaconRoot(), originalBlock.BeaconRoot(); have != want { + t.Errorf("decoded block beacon root mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.BlobGasUsed(), originalBlock.BlobGasUsed(); have != want { + t.Errorf("decoded block blob gas used mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.Bloom(), originalBlock.Bloom(); have != want { + t.Errorf("decoded block bloom mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.Body(), originalBlock.Body(); compareBody(have, want) != 0 { + t.Errorf("decoded block body mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.Coinbase(), originalBlock.Coinbase(); have != want { + t.Errorf("decoded block coinbase mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.Difficulty(), originalBlock.Difficulty(); have.Cmp(want) != 0 { + t.Errorf("decoded block difficulty mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.ExcessBlobGas(), originalBlock.ExcessBlobGas(); have != want { + t.Errorf("decoded block excess blob gas mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.ExecutionWitness(), originalBlock.ExecutionWitness(); have != want { + t.Errorf("decoded block execution witness mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.Extra(), originalBlock.Extra(); !bytes.Equal(have, want) { + t.Errorf("decoded block extra mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.GasLimit(), originalBlock.GasLimit(); have != want { + t.Errorf("decoded block gas limit mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.GasUsed(), originalBlock.GasUsed(); have != want { + t.Errorf("decoded block gas used mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.Hash(), originalBlock.Hash(); have != want { + t.Errorf("decoded block hash mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.Header(), originalBlock.Header(); compareHeader(have, want) != 0 { + t.Errorf("decoded block header mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.MixDigest(), originalBlock.MixDigest(); have != want { + t.Errorf("decoded block mix digest mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.Nonce(), originalBlock.Nonce(); have != want { + t.Errorf("decoded block nonce mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.Number(), originalBlock.Number(); have.Cmp(want) != 0 { + t.Errorf("decoded block number mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.NumberU64(), originalBlock.NumberU64(); have != want { + t.Errorf("decoded block number u64 mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.ParentHash(), originalBlock.ParentHash(); have != want { + t.Errorf("decoded block parent hash mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.ReceiptHash(), originalBlock.ReceiptHash(); have != want { + t.Errorf("decoded block receipt hash mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.RequestsHash(), originalBlock.RequestsHash(); have != want { + t.Errorf("decoded block requests hash mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.Root(), originalBlock.Root(); have != want { + t.Errorf("decoded block root mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.Size(), originalBlock.Size(); have != want { + t.Errorf("decoded block size mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.Time(), originalBlock.Time(); have != want { + t.Errorf("decoded block time mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.Transactions(), originalBlock.Transactions(); slices.CompareFunc(have, want, compareTransaction) != 0 { + t.Errorf("decoded block transactions mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.TxHash(), originalBlock.TxHash(); have != want { + t.Errorf("decoded block tx hash mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.UncleHash(), originalBlock.UncleHash(); have != want { + t.Errorf("decoded block uncle hash mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.Withdrawals(), originalBlock.Withdrawals(); slices.CompareFunc(have, want, compareWithdrawl) != 0 { + t.Errorf("decoded block withdrawals mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } + + if have, want := decodedBlock.WithdrawalsRoot(), originalBlock.WithdrawalsRoot(); have != want { + t.Errorf("decoded block withdrawals root mismatch:\nhave:\n\t\"%v\"\nwant:\n\t\"%v\"\n", have, want) + } +} diff --git a/op-node/rollup/derive/espresso_blob_data_source_test.go b/op-node/rollup/derive/espresso_blob_data_source_test.go new file mode 100644 index 00000000000..fb96ed62005 --- /dev/null +++ b/op-node/rollup/derive/espresso_blob_data_source_test.go @@ -0,0 +1,318 @@ +package derive + +import ( + "context" + "crypto/ecdsa" + "math/big" + "math/rand" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" + + "github.com/ethereum-optimism/optimism/op-node/rollup" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/testlog" + "github.com/ethereum-optimism/optimism/op-service/testutils" + "github.com/ethereum/go-ethereum/log" +) + +// TestDataAndHashesFromTxsEventAuth tests event-based batch authentication for both +// calldata and blob transactions in the blob data source path. +// +// Event-based authentication is only active post-Espresso; the fixture +// activates the fork at L1 origin time 0 (genesis) so all test refs satisfy +// ref.Time >= *EspressoTime. +func TestDataAndHashesFromTxsEventAuth(t *testing.T) { + rng := rand.New(rand.NewSource(9999)) + privateKey := testutils.InsecureRandomKey(rng) + altKey := testutils.InsecureRandomKey(rng) + batcherAddr := crypto.PubkeyToAddress(*privateKey.Public().(*ecdsa.PublicKey)) + altAddr := crypto.PubkeyToAddress(*altKey.Public().(*ecdsa.PublicKey)) + batchInboxAddr := testutils.RandomAddress(rng) + authenticatorAddr := testutils.RandomAddress(rng) + logger := testlog.Logger(t, log.LvlInfo) + + chainId := new(big.Int).SetUint64(rng.Uint64()) + signer := types.NewPragueSigner(chainId) + espressoTime := uint64(0) + config := DataSourceConfig{ + l1Signer: signer, + batchInboxAddress: batchInboxAddr, + rollupCfg: &rollup.Config{ + EspressoTime: &espressoTime, + BatchAuthenticatorAddress: authenticatorAddr, + }, + batchAuthCaches: NewBatchAuthCaches(), + } + + ctx := context.Background() + + t.Run("authenticated calldata tx accepted", func(t *testing.T) { + l1F := &testutils.MockL1Source{} + txData := &types.LegacyTx{ + Nonce: rng.Uint64(), + GasPrice: new(big.Int).SetUint64(rng.Uint64()), + Gas: 2_000_000, + To: &batchInboxAddr, + Value: big.NewInt(10), + Data: testutils.RandomData(rng, 200), + } + calldataTx, _ := types.SignNewTx(privateKey, signer, txData) + + ref := eth.L1BlockRef{Number: 1, Hash: testutils.RandomHash(rng)} + batchHash := ComputeCalldataBatchHash(calldataTx.Data()) + ref = mockAuthEvents(l1F, rng, ref, authenticatorAddr, batcherAddr, []common.Hash{batchHash}) + + data, blobHashes, err := dataAndHashesFromTxs(ctx, types.Transactions{calldataTx}, &config, batcherAddr, l1F, ref, logger) + require.NoError(t, err) + require.Equal(t, 1, len(data)) + require.Equal(t, 0, len(blobHashes)) + require.NotNil(t, data[0].calldata) + require.Equal(t, eth.Data(calldataTx.Data()), *data[0].calldata) + l1F.AssertExpectations(t) + }) + + t.Run("authenticated blob tx accepted", func(t *testing.T) { + l1F := &testutils.MockL1Source{} + blobHash := testutils.RandomHash(rng) + blobTxData := &types.BlobTx{ + Nonce: rng.Uint64(), + Gas: 2_000_000, + To: batchInboxAddr, + Data: testutils.RandomData(rng, 100), + BlobHashes: []common.Hash{blobHash}, + } + blobTx, _ := types.SignNewTx(privateKey, signer, blobTxData) + + ref := eth.L1BlockRef{Number: 1, Hash: testutils.RandomHash(rng)} + batchHash := ComputeBlobBatchHash([]common.Hash{blobHash}) + ref = mockAuthEvents(l1F, rng, ref, authenticatorAddr, batcherAddr, []common.Hash{batchHash}) + + data, blobHashes, err := dataAndHashesFromTxs(ctx, types.Transactions{blobTx}, &config, batcherAddr, l1F, ref, logger) + require.NoError(t, err) + require.Equal(t, 1, len(data)) + require.Equal(t, 1, len(blobHashes)) + require.Equal(t, blobHash, blobHashes[0]) // the authenticated blob's hash, not just any + require.Nil(t, data[0].calldata) // blob placeholder + require.Nil(t, data[0].blob) // blob placeholder + l1F.AssertExpectations(t) + }) + + t.Run("unknown sender rejected without auth event", func(t *testing.T) { + l1F := &testutils.MockL1Source{} + txData := &types.LegacyTx{ + Nonce: rng.Uint64(), + GasPrice: new(big.Int).SetUint64(rng.Uint64()), + Gas: 2_000_000, + To: &batchInboxAddr, + Value: big.NewInt(10), + Data: testutils.RandomData(rng, 200), + } + // Signed by an unknown key (not batcherAddr), no auth event — should be rejected + calldataTx, _ := types.SignNewTx(altKey, signer, txData) + + ref := eth.L1BlockRef{Number: 1, Hash: testutils.RandomHash(rng)} + ref = mockAuthEvents(l1F, rng, ref, authenticatorAddr, batcherAddr, nil) // no auth events + + data, blobHashes, err := dataAndHashesFromTxs(ctx, types.Transactions{calldataTx}, &config, batcherAddr, l1F, ref, logger) + require.NoError(t, err) + require.Equal(t, 0, len(data), "unknown sender tx without auth event should be rejected") + require.Equal(t, 0, len(blobHashes)) + l1F.AssertExpectations(t) + }) + + t.Run("fallback batcher without auth event rejected", func(t *testing.T) { + l1F := &testutils.MockL1Source{} + txData := &types.LegacyTx{ + Nonce: rng.Uint64(), + GasPrice: new(big.Int).SetUint64(rng.Uint64()), + Gas: 2_000_000, + To: &batchInboxAddr, + Value: big.NewInt(10), + Data: testutils.RandomData(rng, 200), + } + // Signed by batcher key (SystemConfig batcherAddr), no auth event — should be rejected + // because all batchers now require event-based authentication + calldataTx, _ := types.SignNewTx(privateKey, signer, txData) + + ref := eth.L1BlockRef{Number: 1, Hash: testutils.RandomHash(rng)} + ref = mockAuthEvents(l1F, rng, ref, authenticatorAddr, batcherAddr, nil) // no auth events + + data, blobHashes, err := dataAndHashesFromTxs(ctx, types.Transactions{calldataTx}, &config, batcherAddr, l1F, ref, logger) + require.NoError(t, err) + require.Equal(t, 0, len(data), "fallback batcher without auth event should be rejected") + require.Equal(t, 0, len(blobHashes)) + l1F.AssertExpectations(t) + }) + + t.Run("non-batcher sender accepted when it matches the auth caller", func(t *testing.T) { + l1F := &testutils.MockL1Source{} + txData := &types.LegacyTx{ + Nonce: rng.Uint64(), + GasPrice: new(big.Int).SetUint64(rng.Uint64()), + Gas: 2_000_000, + To: &batchInboxAddr, + Value: big.NewInt(10), + Data: testutils.RandomData(rng, 200), + } + // Signed by alt key (not the SystemConfig batcher), and the auth event was + // emitted by that same alt address — should be accepted. + calldataTx, _ := types.SignNewTx(altKey, signer, txData) + + ref := eth.L1BlockRef{Number: 1, Hash: testutils.RandomHash(rng)} + batchHash := ComputeCalldataBatchHash(calldataTx.Data()) + ref = mockAuthEvents(l1F, rng, ref, authenticatorAddr, altAddr, []common.Hash{batchHash}) + + data, blobHashes, err := dataAndHashesFromTxs(ctx, types.Transactions{calldataTx}, &config, batcherAddr, l1F, ref, logger) + require.NoError(t, err) + require.Equal(t, 1, len(data)) + require.Equal(t, 0, len(blobHashes)) + require.NotNil(t, data[0].calldata) + require.Equal(t, eth.Data(calldataTx.Data()), *data[0].calldata) // the authenticated tx, not just any + l1F.AssertExpectations(t) + }) + + t.Run("authenticated tx rejected when sender differs from auth caller", func(t *testing.T) { + l1F := &testutils.MockL1Source{} + txData := &types.LegacyTx{ + Nonce: rng.Uint64(), + GasPrice: new(big.Int).SetUint64(rng.Uint64()), + Gas: 2_000_000, + To: &batchInboxAddr, + Value: big.NewInt(10), + Data: testutils.RandomData(rng, 200), + } + // Signed by alt key, but the commitment was authenticated by batcherAddr. + // The submitter must match the auth caller — should be rejected. + calldataTx, _ := types.SignNewTx(altKey, signer, txData) + + ref := eth.L1BlockRef{Number: 1, Hash: testutils.RandomHash(rng)} + batchHash := ComputeCalldataBatchHash(calldataTx.Data()) + ref = mockAuthEvents(l1F, rng, ref, authenticatorAddr, batcherAddr, []common.Hash{batchHash}) + + data, blobHashes, err := dataAndHashesFromTxs(ctx, types.Transactions{calldataTx}, &config, batcherAddr, l1F, ref, logger) + require.NoError(t, err) + require.Equal(t, 0, len(data), "batch authenticated by a different address than the submitter must be rejected") + require.Equal(t, 0, len(blobHashes)) + l1F.AssertExpectations(t) + }) +} + +// TestDataAndHashesFromTxsForkBoundary exercises the Espresso fork gate flipping in the +// blob data source path (dataAndHashesFromTxs) across a single fixed DataSourceConfig. +// +// This is the path a chain with Ecotone active actually runs: OpenData always selects the +// blob source, and calldata (type-2) batches flow through its non-blob branch. Pre-Espresso +// (L1 origin time < EspressoTime) must use upstream sender-based authorization with no event +// scanning; at and after activation it must switch to event-based authentication. The gate is +// implemented separately here from the calldata source, so this mirrors +// TestDataFromEVMTransactionsForkBoundary to pin both copies. +func TestDataAndHashesFromTxsForkBoundary(t *testing.T) { + rng := rand.New(rand.NewSource(7777)) + privateKey := testutils.InsecureRandomKey(rng) + altKey := testutils.InsecureRandomKey(rng) + batcherAddr := crypto.PubkeyToAddress(*privateKey.Public().(*ecdsa.PublicKey)) + batchInboxAddr := testutils.RandomAddress(rng) + authenticatorAddr := testutils.RandomAddress(rng) + logger := testlog.Logger(t, log.LvlInfo) + + chainId := new(big.Int).SetUint64(rng.Uint64()) + signer := types.NewPragueSigner(chainId) + + // Fork activates at L1 origin time 1000. A single config is reused across all + // sub-tests; only ref.Time changes to cross the boundary. + espressoTime := uint64(1000) + config := DataSourceConfig{ + l1Signer: signer, + batchInboxAddress: batchInboxAddr, + rollupCfg: &rollup.Config{ + EspressoTime: &espressoTime, + BatchAuthenticatorAddress: authenticatorAddr, + }, + batchAuthCaches: NewBatchAuthCaches(), + } + + ctx := context.Background() + + // newCalldataBatchTx builds a type-2 calldata batch tx to the inbox (the tx shape an + // Ecotone-active, calldata-batching chain submits through the blob source). + newCalldataBatchTx := func(t *testing.T, author *ecdsa.PrivateKey, data []byte) *types.Transaction { + t.Helper() + tx, err := types.SignNewTx(author, signer, &types.DynamicFeeTx{ + ChainID: chainId, Nonce: rng.Uint64(), Gas: 2_000_000, + GasTipCap: big.NewInt(2 * params.GWei), GasFeeCap: big.NewInt(30 * params.GWei), + To: &batchInboxAddr, Data: data, + }) + require.NoError(t, err) + return tx + } + + t.Run("pre-fork: batcher accepted via sender auth, no event scan", func(t *testing.T) { + // The empty mock asserts pre-fork derivation performs zero L1 receipt scanning: + // any FetchReceipts/L1BlockRefByHash call would be an unexpected call and panic. + l1F := &testutils.MockL1Source{} + txData := testutils.RandomData(rng, 200) + tx := newCalldataBatchTx(t, privateKey, txData) + + ref := eth.L1BlockRef{Number: 1, Time: espressoTime - 1, Hash: testutils.RandomHash(rng)} + data, hashes, err := dataAndHashesFromTxs(ctx, types.Transactions{tx}, &config, batcherAddr, l1F, ref, logger) + require.NoError(t, err) + require.Equal(t, 1, len(data), "pre-fork batcher tx should be accepted via sender-based auth") + require.Equal(t, 0, len(hashes)) + require.NotNil(t, data[0].calldata) + require.Equal(t, eth.Data(txData), *data[0].calldata) + l1F.AssertExpectations(t) + }) + + t.Run("pre-fork: non-batcher sender rejected", func(t *testing.T) { + l1F := &testutils.MockL1Source{} + tx := newCalldataBatchTx(t, altKey, testutils.RandomData(rng, 200)) + + ref := eth.L1BlockRef{Number: 1, Time: espressoTime - 1, Hash: testutils.RandomHash(rng)} + data, hashes, err := dataAndHashesFromTxs(ctx, types.Transactions{tx}, &config, batcherAddr, l1F, ref, logger) + require.NoError(t, err) + require.Equal(t, 0, len(data), "pre-fork tx from a non-batcher sender should be rejected") + require.Equal(t, 0, len(hashes)) + l1F.AssertExpectations(t) + }) + + t.Run("activation block: same batcher tx rejected without auth event", func(t *testing.T) { + // At the exact activation time (ref.Time == EspressoTime) the event-based path is + // active, so a sender-only batcher tx is no longer sufficient. + l1F := &testutils.MockL1Source{} + txData := testutils.RandomData(rng, 200) + tx := newCalldataBatchTx(t, privateKey, txData) + + ref := eth.L1BlockRef{Number: 1, Time: espressoTime, Hash: testutils.RandomHash(rng)} + ref = mockAuthEvents(l1F, rng, ref, authenticatorAddr, batcherAddr, nil) + + data, hashes, err := dataAndHashesFromTxs(ctx, types.Transactions{tx}, &config, batcherAddr, l1F, ref, logger) + require.NoError(t, err) + require.Equal(t, 0, len(data), "post-fork batcher tx without an auth event must be rejected") + require.Equal(t, 0, len(hashes)) + l1F.AssertExpectations(t) + }) + + t.Run("activation block: same batcher tx accepted with auth event", func(t *testing.T) { + l1F := &testutils.MockL1Source{} + txData := testutils.RandomData(rng, 200) + tx := newCalldataBatchTx(t, privateKey, txData) + + ref := eth.L1BlockRef{Number: 1, Time: espressoTime, Hash: testutils.RandomHash(rng)} + batchHash := ComputeCalldataBatchHash(tx.Data()) + ref = mockAuthEvents(l1F, rng, ref, authenticatorAddr, batcherAddr, []common.Hash{batchHash}) + + data, hashes, err := dataAndHashesFromTxs(ctx, types.Transactions{tx}, &config, batcherAddr, l1F, ref, logger) + require.NoError(t, err) + require.Equal(t, 1, len(data), "post-fork batcher tx with a matching auth event must be accepted") + require.Equal(t, 0, len(hashes)) + require.NotNil(t, data[0].calldata) + require.Equal(t, eth.Data(txData), *data[0].calldata) + l1F.AssertExpectations(t) + }) +} diff --git a/op-node/rollup/derive/espresso_calldata_source_test.go b/op-node/rollup/derive/espresso_calldata_source_test.go new file mode 100644 index 00000000000..b1d314d3aae --- /dev/null +++ b/op-node/rollup/derive/espresso_calldata_source_test.go @@ -0,0 +1,447 @@ +package derive + +import ( + "context" + "crypto/ecdsa" + "math/big" + "math/rand" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" + + "github.com/ethereum-optimism/optimism/op-node/rollup" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/testlog" + "github.com/ethereum-optimism/optimism/op-service/testutils" +) + +// mockAuthEvents sets up L1 mock expectations for CollectAuthenticatedBatches to find auth events +// for the given batch hashes at the given ref's block number. Auth events for batch hashes in +// `authenticated` are placed in the ref block's receipts; all other blocks in the lookback +// window have empty receipts. +// +// CollectAuthenticatedBatches traverses backward from ref via parent hashes, so this helper +// builds a chain of L1BlockRef values with proper parent-hash linkage, sets up FetchReceipts +// for each block, and L1BlockRefByHash for each parent. +// +// The auth events are emitted with `caller` as the indexed caller, which the +// pipeline matches against the batch transaction's L1 sender. Tests pass the +// expected batcher address here. +// +// Returns the updated ref with its ParentHash properly set to the chain. Callers must use +// the returned ref when calling functions that invoke CollectAuthenticatedBatches. +func mockAuthEvents(l1F *testutils.MockL1Source, rng *rand.Rand, ref eth.L1BlockRef, authenticatorAddr, caller common.Address, authenticated []common.Hash) eth.L1BlockRef { + startBlock := ref.Number + if startBlock > BatchAuthLookbackWindow { + startBlock = ref.Number - BatchAuthLookbackWindow + } else { + startBlock = 0 + } + windowSize := ref.Number - startBlock + 1 + + // Build the auth receipts for the ref block. The commitment is the unindexed + // data argument; only the caller is indexed (Topics[1]). + var authLogs []*types.Log + for _, bh := range authenticated { + authLogs = append(authLogs, &types.Log{ + Address: authenticatorAddr, + Topics: []common.Hash{ + BatchInfoAuthenticatedABIHash, + common.BytesToHash(caller.Bytes()), + }, + Data: bh.Bytes(), + }) + } + authReceipts := types.Receipts{} + if len(authLogs) > 0 { + authReceipts = types.Receipts{{Status: types.ReceiptStatusSuccessful, Logs: authLogs}} + } + + // Build parent-hash-linked chain from startBlock to ref.Number. + // chain[i] corresponds to block number startBlock + i. + chain := make([]eth.L1BlockRef, windowSize) + for i := uint64(0); i < windowSize; i++ { + blockNum := startBlock + i + if blockNum == ref.Number { + chain[i] = ref + } else { + chain[i] = eth.L1BlockRef{Number: blockNum, Hash: testutils.RandomHash(rng)} + } + if i > 0 { + chain[i].ParentHash = chain[i-1].Hash + } + } + + // Update the ref at the end of the chain with the correct ParentHash + updatedRef := chain[windowSize-1] + + // Set up expectations for backward traversal: ref -> ref-1 -> ... -> startBlock + for i := int(windowSize) - 1; i >= 0; i-- { + blockRef := chain[i] + if blockRef.Number == ref.Number { + l1F.ExpectFetchReceipts(blockRef.Hash, nil, authReceipts, nil) + } else { + l1F.ExpectFetchReceipts(blockRef.Hash, nil, types.Receipts{}, nil) + } + // L1BlockRefByHash is called for every parent except when we've reached the end of the window + if i > 0 { + l1F.ExpectL1BlockRefByHash(chain[i-1].Hash, chain[i-1], nil) + } + } + + return updatedRef +} + +// TestDataFromEVMTransactionsEventAuth tests event-based batch authentication +// where a BatchInfoAuthenticated event in the lookback window authorizes a batch. +// +// Event-based authentication is only active post-Espresso; the fixture +// activates the fork at L1 origin time 0 (genesis) so all test refs satisfy +// ref.Time >= *EspressoTime. +func TestDataFromEVMTransactionsEventAuth(t *testing.T) { + rng := rand.New(rand.NewSource(42)) + batcherPriv := testutils.RandomKey() + altAuthor := testutils.RandomKey() + batchInboxAddr := testutils.RandomAddress(rng) + authenticatorAddr := testutils.RandomAddress(rng) + batcherAddr := crypto.PubkeyToAddress(batcherPriv.PublicKey) + altAuthorAddr := crypto.PubkeyToAddress(altAuthor.PublicKey) + signer := types.NewCancunSigner(big.NewInt(100)) + + espressoTime := uint64(0) + dsCfg := DataSourceConfig{ + l1Signer: signer, + batchInboxAddress: batchInboxAddr, + rollupCfg: &rollup.Config{ + EspressoTime: &espressoTime, + BatchAuthenticatorAddress: authenticatorAddr, + }, + batchAuthCaches: NewBatchAuthCaches(), + } + + ctx := context.Background() + logger := testlog.Logger(t, log.LevelDebug) + + t.Run("authenticated tx accepted", func(t *testing.T) { + l1F := &testutils.MockL1Source{} + txData := testutils.RandomData(rng, 100) + tx, err := types.SignNewTx(batcherPriv, signer, &types.DynamicFeeTx{ + ChainID: big.NewInt(100), Nonce: 0, Gas: 100_000, + GasTipCap: big.NewInt(2 * params.GWei), GasFeeCap: big.NewInt(30 * params.GWei), + To: &batchInboxAddr, Data: txData, + }) + require.NoError(t, err) + + // Use block number 1 so lookback window is [0, 1] — only 2 blocks to mock + ref := eth.L1BlockRef{Number: 1, Hash: testutils.RandomHash(rng)} + batchHash := ComputeCalldataBatchHash(txData) + ref = mockAuthEvents(l1F, rng, ref, authenticatorAddr, batcherAddr, []common.Hash{batchHash}) + + out, err := DataFromEVMTransactions(ctx, dsCfg, batcherAddr, types.Transactions{tx}, l1F, ref, logger) + require.NoError(t, err) + require.Len(t, out, 1) + require.Equal(t, eth.Data(txData), out[0]) + l1F.AssertExpectations(t) + }) + + t.Run("unauthenticated tx from unknown sender rejected", func(t *testing.T) { + l1F := &testutils.MockL1Source{} + txData := testutils.RandomData(rng, 100) + tx, err := types.SignNewTx(altAuthor, signer, &types.DynamicFeeTx{ + ChainID: big.NewInt(100), Nonce: 0, Gas: 100_000, + GasTipCap: big.NewInt(2 * params.GWei), GasFeeCap: big.NewInt(30 * params.GWei), + To: &batchInboxAddr, Data: txData, + }) + require.NoError(t, err) + + ref := eth.L1BlockRef{Number: 1, Hash: testutils.RandomHash(rng)} + // No auth events — empty authenticated list + ref = mockAuthEvents(l1F, rng, ref, authenticatorAddr, batcherAddr, nil) + + out, err := DataFromEVMTransactions(ctx, dsCfg, batcherAddr, types.Transactions{tx}, l1F, ref, logger) + require.NoError(t, err) + require.Len(t, out, 0) + l1F.AssertExpectations(t) + }) + + t.Run("fallback batcher without auth event rejected", func(t *testing.T) { + // The fallback batcher now also authenticates via BatchAuthenticator events. + // Without an auth event, even the SystemConfig batcher address is rejected. + l1F := &testutils.MockL1Source{} + txData := testutils.RandomData(rng, 100) + tx, err := types.SignNewTx(batcherPriv, signer, &types.DynamicFeeTx{ + ChainID: big.NewInt(100), Nonce: 0, Gas: 100_000, + GasTipCap: big.NewInt(2 * params.GWei), GasFeeCap: big.NewInt(30 * params.GWei), + To: &batchInboxAddr, Data: txData, + }) + require.NoError(t, err) + + ref := eth.L1BlockRef{Number: 1, Hash: testutils.RandomHash(rng)} + ref = mockAuthEvents(l1F, rng, ref, authenticatorAddr, batcherAddr, nil) + + out, err := DataFromEVMTransactions(ctx, dsCfg, batcherAddr, types.Transactions{tx}, l1F, ref, logger) + require.NoError(t, err) + require.Len(t, out, 0, "fallback batcher without auth event should be rejected") + l1F.AssertExpectations(t) + }) + + t.Run("wrong inbox address rejected without auth check", func(t *testing.T) { + // Tx to wrong address should be filtered by isValidBatchTx. + // CollectAuthenticatedBatches still runs (it's a block-level operation), + // but no tx passes the inbox address check. + l1F := &testutils.MockL1Source{} + wrongAddr := testutils.RandomAddress(rng) + txData := testutils.RandomData(rng, 100) + tx, err := types.SignNewTx(batcherPriv, signer, &types.DynamicFeeTx{ + ChainID: big.NewInt(100), Nonce: 0, Gas: 100_000, + GasTipCap: big.NewInt(2 * params.GWei), GasFeeCap: big.NewInt(30 * params.GWei), + To: &wrongAddr, Data: txData, + }) + require.NoError(t, err) + + ref := eth.L1BlockRef{Number: 1, Hash: testutils.RandomHash(rng)} + // Mock the lookback window scan (returns no authenticated hashes) + ref = mockAuthEvents(l1F, rng, ref, authenticatorAddr, batcherAddr, nil) + + out, err := DataFromEVMTransactions(ctx, dsCfg, batcherAddr, types.Transactions{tx}, l1F, ref, logger) + require.NoError(t, err) + require.Len(t, out, 0) + l1F.AssertExpectations(t) + }) + + t.Run("mixed: only event-authenticated txs accepted", func(t *testing.T) { + l1F := &testutils.MockL1Source{} + // tx1: has auth event — should be accepted + txData1 := testutils.RandomData(rng, 100) + tx1, err := types.SignNewTx(batcherPriv, signer, &types.DynamicFeeTx{ + ChainID: big.NewInt(100), Nonce: 0, Gas: 100_000, + GasTipCap: big.NewInt(2 * params.GWei), GasFeeCap: big.NewInt(30 * params.GWei), + To: &batchInboxAddr, Data: txData1, + }) + require.NoError(t, err) + + // tx2: no auth event — should be rejected even though sender is batcherAddr + txData2 := testutils.RandomData(rng, 100) + tx2, err := types.SignNewTx(batcherPriv, signer, &types.DynamicFeeTx{ + ChainID: big.NewInt(100), Nonce: 1, Gas: 100_000, + GasTipCap: big.NewInt(2 * params.GWei), GasFeeCap: big.NewInt(30 * params.GWei), + To: &batchInboxAddr, Data: txData2, + }) + require.NoError(t, err) + + // tx3: unknown sender without auth event — should be rejected + txData3 := testutils.RandomData(rng, 100) + tx3, err := types.SignNewTx(altAuthor, signer, &types.DynamicFeeTx{ + ChainID: big.NewInt(100), Nonce: 2, Gas: 100_000, + GasTipCap: big.NewInt(2 * params.GWei), GasFeeCap: big.NewInt(30 * params.GWei), + To: &batchInboxAddr, Data: txData3, + }) + require.NoError(t, err) + + ref := eth.L1BlockRef{Number: 1, Hash: testutils.RandomHash(rng)} + batchHash1 := ComputeCalldataBatchHash(txData1) + // Only tx1 has an auth event (caller = batcherAddr, matching tx1's sender). + // tx2 and tx3 do not — both should be rejected. + ref = mockAuthEvents(l1F, rng, ref, authenticatorAddr, batcherAddr, []common.Hash{batchHash1}) + + out, err := DataFromEVMTransactions(ctx, dsCfg, batcherAddr, types.Transactions{tx1, tx2, tx3}, l1F, ref, logger) + require.NoError(t, err) + require.Len(t, out, 1, "only event-authenticated tx should pass") + require.Equal(t, eth.Data(txData1), out[0]) + l1F.AssertExpectations(t) + }) + + t.Run("auth event accepts a non-batcher sender that matches its caller", func(t *testing.T) { + // Event-based mode does not require the SystemConfig batcher: any sender is + // accepted as long as it matches the caller that emitted the auth event. + // Here altAuthor both submits the batch and is the auth event caller. + l1F := &testutils.MockL1Source{} + txData := testutils.RandomData(rng, 100) + tx, err := types.SignNewTx(altAuthor, signer, &types.DynamicFeeTx{ + ChainID: big.NewInt(100), Nonce: 0, Gas: 100_000, + GasTipCap: big.NewInt(2 * params.GWei), GasFeeCap: big.NewInt(30 * params.GWei), + To: &batchInboxAddr, Data: txData, + }) + require.NoError(t, err) + + ref := eth.L1BlockRef{Number: 1, Hash: testutils.RandomHash(rng)} + batchHash := ComputeCalldataBatchHash(txData) + ref = mockAuthEvents(l1F, rng, ref, authenticatorAddr, altAuthorAddr, []common.Hash{batchHash}) + + out, err := DataFromEVMTransactions(ctx, dsCfg, batcherAddr, types.Transactions{tx}, l1F, ref, logger) + require.NoError(t, err) + require.Len(t, out, 1) + require.Equal(t, eth.Data(txData), out[0]) + l1F.AssertExpectations(t) + }) + + t.Run("authenticated batch from a different sender than the caller is rejected", func(t *testing.T) { + // The batch commitment is authenticated, but by batcherAddr; the batch tx is + // submitted by altAuthor. The sender must match the auth event caller, so the + // batch is rejected even though the commitment was authenticated. + l1F := &testutils.MockL1Source{} + txData := testutils.RandomData(rng, 100) + tx, err := types.SignNewTx(altAuthor, signer, &types.DynamicFeeTx{ + ChainID: big.NewInt(100), Nonce: 0, Gas: 100_000, + GasTipCap: big.NewInt(2 * params.GWei), GasFeeCap: big.NewInt(30 * params.GWei), + To: &batchInboxAddr, Data: txData, + }) + require.NoError(t, err) + + ref := eth.L1BlockRef{Number: 1, Hash: testutils.RandomHash(rng)} + batchHash := ComputeCalldataBatchHash(txData) + ref = mockAuthEvents(l1F, rng, ref, authenticatorAddr, batcherAddr, []common.Hash{batchHash}) + + out, err := DataFromEVMTransactions(ctx, dsCfg, batcherAddr, types.Transactions{tx}, l1F, ref, logger) + require.NoError(t, err) + require.Len(t, out, 0, "batch authenticated by a different address than the submitter must be rejected") + l1F.AssertExpectations(t) + }) + + t.Run("multiple authenticated txs each accepted for their own commitment", func(t *testing.T) { + // Two distinct batches, each authenticated by its own commitment event from the + // batcher. Both must be accepted, in order, each mapped to its own data — verifying + // every tx is matched against its own commitment, not just "some" authenticated entry. + l1F := &testutils.MockL1Source{} + txDataA := testutils.RandomData(rng, 100) + txA, err := types.SignNewTx(batcherPriv, signer, &types.DynamicFeeTx{ + ChainID: big.NewInt(100), Nonce: 0, Gas: 100_000, + GasTipCap: big.NewInt(2 * params.GWei), GasFeeCap: big.NewInt(30 * params.GWei), + To: &batchInboxAddr, Data: txDataA, + }) + require.NoError(t, err) + txDataB := testutils.RandomData(rng, 100) + txB, err := types.SignNewTx(batcherPriv, signer, &types.DynamicFeeTx{ + ChainID: big.NewInt(100), Nonce: 1, Gas: 100_000, + GasTipCap: big.NewInt(2 * params.GWei), GasFeeCap: big.NewInt(30 * params.GWei), + To: &batchInboxAddr, Data: txDataB, + }) + require.NoError(t, err) + + ref := eth.L1BlockRef{Number: 1, Hash: testutils.RandomHash(rng)} + ref = mockAuthEvents(l1F, rng, ref, authenticatorAddr, batcherAddr, + []common.Hash{ComputeCalldataBatchHash(txDataA), ComputeCalldataBatchHash(txDataB)}) + + out, err := DataFromEVMTransactions(ctx, dsCfg, batcherAddr, types.Transactions{txA, txB}, l1F, ref, logger) + require.NoError(t, err) + require.Len(t, out, 2) + require.Equal(t, eth.Data(txDataA), out[0], "first tx must map to its own data") + require.Equal(t, eth.Data(txDataB), out[1], "second tx must map to its own data") + l1F.AssertExpectations(t) + }) +} + +// TestDataFromEVMTransactionsForkBoundary exercises the Espresso fork gate flipping +// across a single fixed DataSourceConfig. Pre-Espresso (L1 origin time < EspressoTime) +// must use upstream sender-based authorization with no event scanning at all; at and +// after activation (L1 origin time >= EspressoTime) it must switch to event-based +// authentication. +// +// This pins the gate boundary — the IsEspresso(ref.Time) check (`timestamp >= +// *EspressoTime`) consulted in DataFromEVMTransactions and isBatchTxAuthorized. The same +// batcher transaction is accepted pre-fork without any auth event, but rejected at the +// activation block unless a BatchInfoAuthenticated event authorizes it. A regression in +// the boundary is caught in both directions: a pre-fork block would start scanning +// receipts (unexpected mock calls panic), and the activation block would otherwise accept +// an unauthenticated batch. +func TestDataFromEVMTransactionsForkBoundary(t *testing.T) { + rng := rand.New(rand.NewSource(99)) + batcherPriv := testutils.RandomKey() + altAuthor := testutils.RandomKey() + batchInboxAddr := testutils.RandomAddress(rng) + authenticatorAddr := testutils.RandomAddress(rng) + batcherAddr := crypto.PubkeyToAddress(batcherPriv.PublicKey) + signer := types.NewCancunSigner(big.NewInt(100)) + + // Fork activates at L1 origin time 1000. A single config is reused across all + // sub-tests; only ref.Time changes to cross the boundary. + espressoTime := uint64(1000) + dsCfg := DataSourceConfig{ + l1Signer: signer, + batchInboxAddress: batchInboxAddr, + rollupCfg: &rollup.Config{ + EspressoTime: &espressoTime, + BatchAuthenticatorAddress: authenticatorAddr, + }, + batchAuthCaches: NewBatchAuthCaches(), + } + + ctx := context.Background() + logger := testlog.Logger(t, log.LevelDebug) + + newBatchTx := func(t *testing.T, author *ecdsa.PrivateKey, data []byte) *types.Transaction { + t.Helper() + tx, err := types.SignNewTx(author, signer, &types.DynamicFeeTx{ + ChainID: big.NewInt(100), Nonce: 0, Gas: 100_000, + GasTipCap: big.NewInt(2 * params.GWei), GasFeeCap: big.NewInt(30 * params.GWei), + To: &batchInboxAddr, Data: data, + }) + require.NoError(t, err) + return tx + } + + t.Run("pre-fork: batcher accepted via sender auth, no event scan", func(t *testing.T) { + // The empty mock asserts pre-fork derivation performs zero L1 receipt scanning: + // any FetchReceipts/L1BlockRefByHash call would be an unexpected call and panic. + l1F := &testutils.MockL1Source{} + txData := testutils.RandomData(rng, 100) + tx := newBatchTx(t, batcherPriv, txData) + + ref := eth.L1BlockRef{Number: 1, Time: espressoTime - 1, Hash: testutils.RandomHash(rng)} + out, err := DataFromEVMTransactions(ctx, dsCfg, batcherAddr, types.Transactions{tx}, l1F, ref, logger) + require.NoError(t, err) + require.Len(t, out, 1, "pre-fork batcher tx should be accepted via sender-based auth") + require.Equal(t, eth.Data(txData), out[0]) + l1F.AssertExpectations(t) + }) + + t.Run("pre-fork: non-batcher sender rejected", func(t *testing.T) { + l1F := &testutils.MockL1Source{} + txData := testutils.RandomData(rng, 100) + tx := newBatchTx(t, altAuthor, txData) + + ref := eth.L1BlockRef{Number: 1, Time: espressoTime - 1, Hash: testutils.RandomHash(rng)} + out, err := DataFromEVMTransactions(ctx, dsCfg, batcherAddr, types.Transactions{tx}, l1F, ref, logger) + require.NoError(t, err) + require.Len(t, out, 0, "pre-fork tx from a non-batcher sender should be rejected") + l1F.AssertExpectations(t) + }) + + t.Run("activation block: same batcher tx rejected without auth event", func(t *testing.T) { + // At the exact activation time (ref.Time == EspressoTime) the event-based path is + // active, so a sender-only batcher tx is no longer sufficient. + l1F := &testutils.MockL1Source{} + txData := testutils.RandomData(rng, 100) + tx := newBatchTx(t, batcherPriv, txData) + + ref := eth.L1BlockRef{Number: 1, Time: espressoTime, Hash: testutils.RandomHash(rng)} + ref = mockAuthEvents(l1F, rng, ref, authenticatorAddr, batcherAddr, nil) + + out, err := DataFromEVMTransactions(ctx, dsCfg, batcherAddr, types.Transactions{tx}, l1F, ref, logger) + require.NoError(t, err) + require.Len(t, out, 0, "post-fork batcher tx without an auth event must be rejected") + l1F.AssertExpectations(t) + }) + + t.Run("activation block: same batcher tx accepted with auth event", func(t *testing.T) { + l1F := &testutils.MockL1Source{} + txData := testutils.RandomData(rng, 100) + tx := newBatchTx(t, batcherPriv, txData) + + ref := eth.L1BlockRef{Number: 1, Time: espressoTime, Hash: testutils.RandomHash(rng)} + batchHash := ComputeCalldataBatchHash(txData) + ref = mockAuthEvents(l1F, rng, ref, authenticatorAddr, batcherAddr, []common.Hash{batchHash}) + + out, err := DataFromEVMTransactions(ctx, dsCfg, batcherAddr, types.Transactions{tx}, l1F, ref, logger) + require.NoError(t, err) + require.Len(t, out, 1, "post-fork batcher tx with a matching auth event must be accepted") + require.Equal(t, eth.Data(txData), out[0]) + l1F.AssertExpectations(t) + }) +} diff --git a/op-node/rollup/derive/params.go b/op-node/rollup/derive/params.go index c4511d95088..010e84fe46f 100644 --- a/op-node/rollup/derive/params.go +++ b/op-node/rollup/derive/params.go @@ -21,6 +21,16 @@ func frameSize(frame Frame) uint64 { // or transaction per block allowed in a span batch. const MaxSpanBatchElementCount = 10_000_000 +// BatchAuthLookbackWindow is the number of L1 blocks before a batch submission to +// scan for a BatchInfoAuthenticated event. The authentication transaction must land +// in this window (or in the same block as the batch submission) for the batch to be +// considered valid post-Espresso. +// +// At ~12s per L1 block, 100 blocks ≈ 20 minutes. This gives the batcher time to land +// the batch data transaction on L1 after the authentication transaction, even under +// L1 congestion or batcher restarts. +const BatchAuthLookbackWindow uint64 = 100 + // DuplicateErr is returned when a newly read frame is already known var DuplicateErr = errors.New("duplicate frame") diff --git a/op-node/rollup/espresso_types.go b/op-node/rollup/espresso_types.go new file mode 100644 index 00000000000..45ecc8b5b98 --- /dev/null +++ b/op-node/rollup/espresso_types.go @@ -0,0 +1,13 @@ +package rollup + +// IsEspresso returns true if the Espresso upgrade is active at or past the +// given timestamp. EspressoTime is conceptually an L2-timestamp fork activation +// time, but the derivation pipeline calls this with the L1 origin time of the +// enclosing L1 block (mirroring upstream's ecotoneTime treatment), so the fork +// is effectively gated per L2 epoch. When active, the derivation pipeline runs +// all Espresso-specific semantics (event-based batch authentication via the +// BatchAuthenticator contract). When inactive, the pipeline behaves exactly as +// upstream Optimism. +func (c *Config) IsEspresso(timestamp uint64) bool { + return c != nil && c.EspressoTime != nil && timestamp >= *c.EspressoTime +} diff --git a/op-node/rollup/types.go b/op-node/rollup/types.go index 23b2e1cdb38..a128bb52c47 100644 --- a/op-node/rollup/types.go +++ b/op-node/rollup/types.go @@ -38,6 +38,8 @@ var ( ErrChainIDsSame = errors.New("L1 and L2 chain IDs must be different") ErrL1ChainIDNotPositive = errors.New("L1 chain ID must be non-zero and positive") ErrL2ChainIDNotPositive = errors.New("L2 chain ID must be non-zero and positive") + + ErrMissingBatchAuthenticatorAddress = errors.New("missing batch authenticator address when Espresso is enabled") ) type Genesis struct { @@ -166,6 +168,22 @@ type Config struct { // This feature (de)activates by L1 origin timestamp, to keep a consistent L1 block info per L2 // epoch. PectraBlobScheduleTime *uint64 `json:"pectra_blob_schedule_time,omitempty"` + + // EspressoTime sets the activation time of the Espresso upgrade. + // Pre-fork, the derivation pipeline behaves exactly as upstream Optimism: batches are + // accepted based on the L1 transaction sender matching the SystemConfig batcher address. + // Post-fork, batches must be authenticated via BatchInfoAuthenticated events emitted by + // the BatchAuthenticator contract; sender-based authorization is rejected. + // EspressoTime is conceptually an L2-timestamp fork activation time, but the + // derivation pipeline gates on it by comparing against the L1 origin time of the + // enclosing L1 block (the L2 epoch's L1 origin), mirroring upstream's ecotoneTime + // treatment, to keep a consistent batch-authorization decision per L2 epoch. + // Active if EspressoTime != nil && the block's L1 origin time >= *EspressoTime. + EspressoTime *uint64 `json:"espresso_time,omitempty"` + + // BatchAuthenticatorAddress is the L1 address of the BatchAuthenticator contract whose + // BatchInfoAuthenticated(bytes32,address) events the derivation pipeline scans post-Espresso. + BatchAuthenticatorAddress common.Address `json:"batch_authenticator_address,omitempty,omitzero"` } // ValidateL1Config checks L1 config variables for errors. @@ -367,6 +385,12 @@ func (cfg *Config) Check() error { return err } + // When Espresso is enabled, batches must be authenticated via BatchInfoAuthenticated events + // emitted by the BatchAuthenticator contract, so a non-zero authenticator address is required. + if cfg.EspressoTime != nil && cfg.BatchAuthenticatorAddress == (common.Address{}) { + return ErrMissingBatchAuthenticatorAddress + } + return nil } @@ -869,6 +893,10 @@ func (c *Config) forEachFork(callback func(name string, logName string, time *ui callback("Jovian", "jovian_time", c.JovianTime) callback("Karst", "karst_time", c.KarstTime) callback("Interop", "interop_time", c.InteropTime) + if c.EspressoTime != nil { + // only report if config is set + callback("Espresso", "espresso_time", c.EspressoTime) + } } func (c *Config) ParseRollupConfig(in io.Reader) error { diff --git a/op-node/rollup/types_test.go b/op-node/rollup/types_test.go index be5262eddfc..207b0215d2d 100644 --- a/op-node/rollup/types_test.go +++ b/op-node/rollup/types_test.go @@ -572,6 +572,24 @@ func TestConfig_Check(t *testing.T) { modifier: func(cfg *Config) { cfg.L2ChainID = big.NewInt(0) }, expectedErr: ErrL2ChainIDNotPositive, }, + { + name: "EspressoEnabledWithoutBatchAuthenticatorAddress", + modifier: func(cfg *Config) { + espressoTime := uint64(1) + cfg.EspressoTime = &espressoTime + cfg.BatchAuthenticatorAddress = common.Address{} + }, + expectedErr: ErrMissingBatchAuthenticatorAddress, + }, + { + name: "EspressoEnabledWithBatchAuthenticatorAddress", + modifier: func(cfg *Config) { + espressoTime := uint64(1) + cfg.EspressoTime = &espressoTime + cfg.BatchAuthenticatorAddress = common.Address{0x01} + }, + expectedErr: nil, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { diff --git a/op-service/crypto/espresso.go b/op-service/crypto/espresso.go new file mode 100644 index 00000000000..b61e4b6fff1 --- /dev/null +++ b/op-service/crypto/espresso.go @@ -0,0 +1,168 @@ +package crypto + +import ( + "bytes" + "context" + "crypto/ecdsa" + "errors" + "fmt" + "math/big" + "strings" + + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" + + hdwallet "github.com/ethereum-optimism/go-ethereum-hdwallet" + opsigner "github.com/ethereum-optimism/optimism/op-service/signer" +) + +// ChainSignerFactory creates a SignerFn that is bound to a specific ChainID +type ChainSignerFactory func(chainID *big.Int, from common.Address) ChainSigner + +// ChainSigner is a generic interface for signing transactions or arbitrary data. +type ChainSigner interface { + + // SignTransaction signs a transaction with the given address. + SignTransaction(ctx context.Context, addr common.Address, tx *types.Transaction) (*types.Transaction, error) + + // Sign signs the hash of arbitrary data. + Sign(ctx context.Context, hash []byte) ([]byte, error) +} + +// clientSigner is a ChainSigner that utilizes a remote signer to perform +// Sign and SignTransaction +type clientSigner struct { + signerClient *opsigner.SignerClient + fromAddress common.Address + chainID *big.Int +} + +// Sign implements Signer. +func (c *clientSigner) Sign(ctx context.Context, data []byte) ([]byte, error) { + return c.signerClient.Sign(ctx, c.fromAddress, data) +} + +// VerifySignature verifies that the signature was produced by the expected address. +// data is the original message (e.g., txdata.CallData()) and signature is the result +// from eth_sign. +func Verify(data []byte, signature []byte, expected common.Address) error { + + pubKey, err := crypto.SigToPub(data, signature) + if err != nil { + return fmt.Errorf("failed to recover public key: %w", err) + } + + // Convert the ecdsa.PublicKey to an Address + address := crypto.PubkeyToAddress(*pubKey) + + // Ensure that the derived address matches the expected address. + if !bytes.Equal(address.Bytes(), expected.Bytes()) { + return fmt.Errorf("address mismatch: got %s, expected %s", address.Hex(), expected.Hex()) + } + + return nil +} + +// SignTransaction implements Signer. +func (c *clientSigner) SignTransaction(ctx context.Context, address common.Address, tx *types.Transaction) (*types.Transaction, error) { + if !bytes.Equal(address[:], c.fromAddress[:]) { + return nil, fmt.Errorf("attempting to sign for %s, expected %s: ", address, c.fromAddress) + } + return c.signerClient.SignTransaction(ctx, c.chainID, address, tx) +} + +var _ ChainSigner = &clientSigner{} + +// privateKeySigner is a ChainSigner that delegates to the stored +// functions for performing Sign and SignTransaction. In general these stored +// functions are expected to have access to a private key that is not +// explicitly stored within the structure itself. +type privateKeySigner struct { + chainID *big.Int + st bind.SignerFn + fromAddress common.Address + s func(common.Address, []byte) ([]byte, error) +} + +// Sign implements Signer. +func (p *privateKeySigner) Sign(ctx context.Context, hash []byte) ([]byte, error) { + return p.s(p.fromAddress, hash) +} + +// SignTransaction implements Signer. +func (p *privateKeySigner) SignTransaction(ctx context.Context, addr common.Address, tx *types.Transaction) (*types.Transaction, error) { + return p.st(addr, tx) +} + +var _ ChainSigner = &privateKeySigner{} + +// ChainSignerFactoryFromConfig considers three ways that signers are created & then creates single factory from those config options. +// It can either take a remote signer (via opsigner.CLIConfig) or it can be provided either a mnemonic + derivation path or a private key. +// It prefers the remote signer, then the mnemonic or private key (only one of which can be provided). +func ChainSignerFactoryFromConfig(l log.Logger, privateKey, mnemonic, hdPath string, signerConfig opsigner.CLIConfig) (ChainSignerFactory, common.Address, error) { + var signer ChainSignerFactory + var fromAddress common.Address + if signerConfig.Enabled() { + signerClient, err := opsigner.NewSignerClientFromConfig(l, signerConfig) + if err != nil { + l.Error("Unable to create Signer Client", "error", err) + return nil, common.Address{}, fmt.Errorf("failed to create the signer client: %w", err) + } + fromAddress = common.HexToAddress(signerConfig.Address) + signer = func(chainID *big.Int, _ common.Address) ChainSigner { + return &clientSigner{ + signerClient: signerClient, + fromAddress: fromAddress, + chainID: chainID, + } + } + } else { + var privKey *ecdsa.PrivateKey + var err error + + if privateKey != "" && mnemonic != "" { + return nil, common.Address{}, errors.New("cannot specify both a private key and a mnemonic") + } + if privateKey == "" { + // Parse l2output wallet private key and L2OO contract address. + wallet, err := hdwallet.NewFromMnemonic(mnemonic) + if err != nil { + return nil, common.Address{}, fmt.Errorf("failed to parse mnemonic: %w", err) + } + + privKey, err = wallet.PrivateKey(accounts.Account{ + URL: accounts.URL{ + Path: hdPath, + }, + }) + if err != nil { + return nil, common.Address{}, fmt.Errorf("failed to create a wallet: %w", err) + } + } else { + privKey, err = crypto.HexToECDSA(strings.TrimPrefix(privateKey, "0x")) + if err != nil { + return nil, common.Address{}, fmt.Errorf("failed to parse the private key: %w", err) + } + } + // we force the curve to Geth's instance, because Geth does an equality check in the nocgo version: + // https://github.com/ethereum/go-ethereum/blob/723b1e36ad6a9e998f06f74cc8b11d51635c6402/crypto/signature_nocgo.go#L82 + privKey.PublicKey.Curve = crypto.S256() + fromAddress = crypto.PubkeyToAddress(privKey.PublicKey) + signer = func(chainID *big.Int, from common.Address) ChainSigner { + s := PrivateKeySignerFn(privKey, chainID) + return &privateKeySigner{ + chainID: chainID, + st: s, + s: func(addr common.Address, hash []byte) ([]byte, error) { + return crypto.Sign(hash, privKey) + }, + } + } + } + + return signer, fromAddress, nil +} diff --git a/op-service/crypto/espresso_test.go b/op-service/crypto/espresso_test.go new file mode 100644 index 00000000000..8d42df96f8b --- /dev/null +++ b/op-service/crypto/espresso_test.go @@ -0,0 +1,134 @@ +package crypto + +import ( + "testing" + + "context" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + + "github.com/ethereum-optimism/optimism/op-service/signer" + "github.com/ethereum-optimism/optimism/op-service/testlog" + + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" +) + +// should be run with CGO_ENABLED=0 + +func TestVerify(t *testing.T) { + // happy path + batcherSignature := []byte{ + 109, 206, 105, 108, 152, 110, 156, 111, 239, 153, 224, 182, 140, 49, 105, 120, + 153, 163, 162, 47, 119, 34, 68, 128, 118, 33, 143, 79, 101, 212, 75, 161, + 124, 77, 236, 159, 70, 167, 95, 51, 92, 127, 236, 253, 4, 211, 222, 117, + 54, 27, 214, 232, 135, 87, 33, 77, 16, 155, 164, 116, 220, 116, 31, 208, 1, + } + sequencerBatchesByte := []byte{ + 166, 136, 91, 55, 49, 112, 45, 166, + 46, 142, 74, 143, 88, 74, 196, 106, + 127, 104, 34, 244, 226, 186, 80, 251, + 169, 2, 246, 123, 21, 136, 210, 59, + } + + expected := common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8") + err := Verify(sequencerBatchesByte, batcherSignature, expected) + require.NoError(t, err) + + // wrong length batcher signature + wrongLengthBatcherSignature := []byte{ + 1, + } + err = Verify(sequencerBatchesByte, wrongLengthBatcherSignature, expected) + // check it returns an correct error: address mismatch + require.Error(t, err) + require.Contains(t, err.Error(), "failed to recover public key: invalid signature length") + + // wrong batcher signature + wrongBatcherSignature := []byte{ + 1, 1, 1, 1, 152, 110, 156, 111, 239, 153, 224, 182, 140, 49, 105, 120, + 153, 163, 162, 47, 119, 34, 68, 128, 118, 33, 143, 79, 101, 212, 75, 161, + 124, 77, 236, 159, 70, 167, 95, 51, 92, 127, 236, 253, 4, 211, 222, 117, + 54, 27, 214, 232, 135, 87, 33, 77, 16, 155, 164, 116, 220, 116, 31, 208, 1, + } + err = Verify(sequencerBatchesByte, wrongBatcherSignature, expected) + // check it returns an correct error: address mismatch + require.Error(t, err) + require.Contains(t, err.Error(), "address mismatch") + + // wrong expected address + wrongExpected := common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C9") + err = Verify(sequencerBatchesByte, batcherSignature, wrongExpected) + require.Error(t, err) + require.Contains(t, err.Error(), "address mismatch") + +} + +func TestChainSignerFactoryFromMnemonic(t *testing.T) { + mnemonic := "test test test test test test test test test test test junk" + hdPath := "m/44'/60'/0'/0/1" + testChainSignerSignTransaction(t, "", mnemonic, hdPath, signer.CLIConfig{}) + testChainSignerSign(t, "", mnemonic, hdPath, signer.CLIConfig{}) +} + +func TestChainSignerFactoryFromKey(t *testing.T) { + priv := "59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" + testChainSignerSignTransaction(t, priv, "", "", signer.CLIConfig{}) + testChainSignerSign(t, priv, "", "", signer.CLIConfig{}) +} + +func testChainSignerSignTransaction(t *testing.T, priv, mnemonic, hdPath string, cfg signer.CLIConfig) { + logger := testlog.Logger(t, log.LevelDebug) + + factoryFn, addr, err := ChainSignerFactoryFromConfig(logger, priv, mnemonic, hdPath, cfg) + require.NoError(t, err) + expectedAddr := common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8") + require.Equal(t, expectedAddr, addr) + chainID := big.NewInt(10) + chainSigner := factoryFn(chainID, addr) // for chain ID 10 + tx := types.NewTx(&types.DynamicFeeTx{ + ChainID: chainID, + Nonce: 0, + GasTipCap: big.NewInt(1), + GasFeeCap: big.NewInt(1), + Gas: 21000, + To: nil, + Value: big.NewInt(0), + Data: []byte("test"), + }) + signedTx, err := chainSigner.SignTransaction(context.Background(), addr, tx) + require.NoError(t, err) + gethSigner := types.LatestSignerForChainID(chainID) + sender, err := gethSigner.Sender(signedTx) + require.NoError(t, err) + require.Equal(t, expectedAddr, sender) +} + +func testChainSignerSign(t *testing.T, priv, mnemonic, hdPath string, cfg signer.CLIConfig) { + logger := testlog.Logger(t, log.LevelDebug) + + factoryFn, addr, err := ChainSignerFactoryFromConfig(logger, priv, mnemonic, hdPath, cfg) + require.NoError(t, err) + expectedAddr := common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8") + require.Equal(t, expectedAddr, addr) + chainID := big.NewInt(10) + chainSigner := factoryFn(chainID, addr) // for chain ID 10 + + payload := []byte{0x01, 0x02, 0x03, 0x04} + hash := crypto.Keccak256(payload) + signed, err := chainSigner.Sign(context.Background(), hash) + require.NoError(t, err) + + // Recover the public key from the signature and hash + pubKey, err := crypto.SigToPub(hash, signed) + require.NoError(t, err) + + // Convert the ecdsa.PublicKey to an Address + address := crypto.PubkeyToAddress(*pubKey) + + // Ensure that the derived address matches the expected address. + require.Equal(t, expectedAddr, address) +} diff --git a/op-service/log/repeat_state.go b/op-service/log/repeat_state.go new file mode 100644 index 00000000000..2147a05fec6 --- /dev/null +++ b/op-service/log/repeat_state.go @@ -0,0 +1,91 @@ +package log + +import ( + "sync" + "time" + + "github.com/ethereum/go-ethereum/log" +) + +// RepeatStateReminderInterval is how often a long-running degraded state +// re-emits a Warn so operators don't lose visibility while the state persists. +const RepeatStateReminderInterval = 5 * time.Minute + +// RepeatStateLogger collapses warnings tied to a "degraded state" into a single +// log on entry, periodic reminders while the state persists, and a recovery +// log on exit. This avoids flooding the log debouncer when a tick-driven loop +// fires the same warning every poll interval. +// +// State is keyed by a free-form string supplied by the caller; entries with +// different keys are independent. Safe for concurrent use. +// +// Unlike DebouncingHandler (which operates at the slog.Handler level on a +// short time window), RepeatStateLogger is caller-driven: the caller signals +// state recovery explicitly via Clear, which lets it emit a recovery log a +// handler-level facility can't produce. +type RepeatStateLogger struct { + mu sync.Mutex + states map[string]*repeatStateEntry + clock func() time.Time +} + +type repeatStateEntry struct { + firstSeen time.Time + lastLogged time.Time + occurrences int +} + +func NewRepeatStateLogger() *RepeatStateLogger { + return &RepeatStateLogger{ + states: make(map[string]*repeatStateEntry), + clock: time.Now, + } +} + +// Warn reports an observation of a degraded state. The first observation since +// the most recent Clear (or first ever for the key) emits at warn level. +// Subsequent observations within RepeatStateReminderInterval are silently +// counted; once the interval has elapsed a single reminder warn is emitted +// with the cumulative occurrence count and the total duration since the state +// became active. +func (r *RepeatStateLogger) Warn(l log.Logger, key, msg string, ctx ...any) { + now := r.clock() + r.mu.Lock() + e, active := r.states[key] + if !active { + r.states[key] = &repeatStateEntry{firstSeen: now, lastLogged: now, occurrences: 1} + r.mu.Unlock() + l.Warn(msg, ctx...) + return + } + e.occurrences++ + if now.Sub(e.lastLogged) < RepeatStateReminderInterval { + r.mu.Unlock() + return + } + occurrences := e.occurrences + duration := now.Sub(e.firstSeen).Round(time.Second) + e.lastLogged = now + r.mu.Unlock() + + l.Warn(msg, append([]any{"occurrences", occurrences, "duration", duration}, ctx...)...) +} + +// Clear marks the named state as resolved. If the state was active a single +// info-level recovery log is emitted summarising the duration and total +// occurrences. Calling Clear when the state is not active is a no-op, so it is +// safe to call on every successful tick of the loop. +func (r *RepeatStateLogger) Clear(l log.Logger, key, recoveryMsg string, ctx ...any) { + r.mu.Lock() + e, active := r.states[key] + delete(r.states, key) + r.mu.Unlock() + if !active { + return + } + args := append([]any{ + "duration", r.clock().Sub(e.firstSeen).Round(time.Second), + "occurrences", e.occurrences, + }, ctx...) + l.Info(recoveryMsg, args...) +} diff --git a/op-service/log/repeat_state_test.go b/op-service/log/repeat_state_test.go new file mode 100644 index 00000000000..1d74dc2a7be --- /dev/null +++ b/op-service/log/repeat_state_test.go @@ -0,0 +1,234 @@ +package log + +import ( + "context" + "log/slog" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/log" +) + +// safeTestRecorder is a thread-safe slog.Handler that captures records for +// assertions in concurrent tests. +type safeTestRecorder struct { + mu sync.Mutex + records []slog.Record +} + +func (r *safeTestRecorder) Enabled(context.Context, slog.Level) bool { return true } + +func (r *safeTestRecorder) Handle(_ context.Context, rec slog.Record) error { + r.mu.Lock() + defer r.mu.Unlock() + r.records = append(r.records, rec) + return nil +} + +func (r *safeTestRecorder) WithAttrs([]slog.Attr) slog.Handler { return r } +func (r *safeTestRecorder) WithGroup(string) slog.Handler { return r } + +func (r *safeTestRecorder) GetRecords() []slog.Record { + r.mu.Lock() + defer r.mu.Unlock() + result := make([]slog.Record, len(r.records)) + copy(result, r.records) + return result +} + +func (r *safeTestRecorder) Len() int { + r.mu.Lock() + defer r.mu.Unlock() + return len(r.records) +} + +// fakeClock returns the time stored at *t, allowing the test to advance time +// deterministically without sleeping. +func fakeClock(now *time.Time) func() time.Time { + return func() time.Time { return *now } +} + +// matchingRecords returns records whose level and message match the given +// level and msg. Pass an empty msg to match any message. +func matchingRecords(records []slog.Record, level slog.Level, msg string) []slog.Record { + var out []slog.Record + for _, r := range records { + if r.Level != level { + continue + } + if msg != "" && r.Message != msg { + continue + } + out = append(out, r) + } + return out +} + +// attrValue extracts the value of the named attribute, or nil if absent. +func attrValue(r slog.Record, key string) any { + var v any + r.Attrs(func(a slog.Attr) bool { + if a.Key == key { + v = a.Value.Any() + return false + } + return true + }) + return v +} + +func newCapturingLogger() (log.Logger, *safeTestRecorder) { + rec := new(safeTestRecorder) + return log.NewLogger(rec), rec +} + +func TestRepeatStateLogger_FirstWarnEmitsThenSuppresses(t *testing.T) { + lgr, rec := newCapturingLogger() + now := time.Unix(0, 0) + r := NewRepeatStateLogger() + r.clock = fakeClock(&now) + + r.Warn(lgr, "k1", "degraded", "err", "boom") + now = now.Add(1 * time.Second) + r.Warn(lgr, "k1", "degraded", "err", "boom") + now = now.Add(1 * time.Second) + r.Warn(lgr, "k1", "degraded", "err", "boom") + + warns := matchingRecords(rec.GetRecords(), slog.LevelWarn, "degraded") + require.Len(t, warns, 1, "only the first observation should emit a log") + + require.Equal(t, "boom", attrValue(warns[0], "err")) + require.Nil(t, attrValue(warns[0], "occurrences"), "first emission should not carry an occurrences count") +} + +func TestRepeatStateLogger_ReminderAfterInterval(t *testing.T) { + lgr, rec := newCapturingLogger() + now := time.Unix(0, 0) + r := NewRepeatStateLogger() + r.clock = fakeClock(&now) + + // Initial emission. + r.Warn(lgr, "k1", "degraded") + + // 9 silent observations, every 30s. Cumulative 4m30s, still under the 5m threshold. + for i := 0; i < 9; i++ { + now = now.Add(30 * time.Second) + r.Warn(lgr, "k1", "degraded") + } + warns := matchingRecords(rec.GetRecords(), slog.LevelWarn, "degraded") + require.Len(t, warns, 1, "no reminder before the interval has elapsed") + + // Cross the threshold. + now = now.Add(31 * time.Second) + r.Warn(lgr, "k1", "degraded") + + warns = matchingRecords(rec.GetRecords(), slog.LevelWarn, "degraded") + require.Len(t, warns, 2, "reminder should fire once the interval has elapsed") + + // Reminder reports cumulative occurrences: 1 initial + 9 silent + 1 reminder = 11. + require.EqualValues(t, int64(11), attrValue(warns[1], "occurrences")) + // Duration since firstSeen is 9*30s + 31s = 5m1s, rounded to nearest second. + require.Equal(t, 5*time.Minute+1*time.Second, attrValue(warns[1], "duration")) +} + +func TestRepeatStateLogger_KeysAreIndependent(t *testing.T) { + lgr, rec := newCapturingLogger() + now := time.Unix(0, 0) + r := NewRepeatStateLogger() + r.clock = fakeClock(&now) + + r.Warn(lgr, "k1", "first state") + r.Warn(lgr, "k2", "second state") + // Both keys are now active. Repeats for either should be suppressed. + r.Warn(lgr, "k1", "first state") + r.Warn(lgr, "k2", "second state") + + require.Len(t, matchingRecords(rec.GetRecords(), slog.LevelWarn, "first state"), 1) + require.Len(t, matchingRecords(rec.GetRecords(), slog.LevelWarn, "second state"), 1) + + // Clearing one key must not affect the other. + r.Clear(lgr, "k1", "first recovered") + r.Warn(lgr, "k2", "second state") // still suppressed + require.Len(t, matchingRecords(rec.GetRecords(), slog.LevelWarn, "second state"), 1) +} + +func TestRepeatStateLogger_ClearEmitsRecoveryWhenActive(t *testing.T) { + lgr, rec := newCapturingLogger() + now := time.Unix(0, 0) + r := NewRepeatStateLogger() + r.clock = fakeClock(&now) + + r.Warn(lgr, "k1", "degraded") + now = now.Add(2 * time.Second) + r.Warn(lgr, "k1", "degraded") // suppressed, but increments totalOccurrences + now = now.Add(3 * time.Second) + r.Clear(lgr, "k1", "recovered", "extra", "ctx") + + infos := matchingRecords(rec.GetRecords(), slog.LevelInfo, "recovered") + require.Len(t, infos, 1) + require.Equal(t, 5*time.Second, attrValue(infos[0], "duration")) + require.EqualValues(t, int64(2), attrValue(infos[0], "occurrences")) + require.Equal(t, "ctx", attrValue(infos[0], "extra")) +} + +func TestRepeatStateLogger_ClearWhenInactiveIsNoop(t *testing.T) { + lgr, rec := newCapturingLogger() + now := time.Unix(0, 0) + r := NewRepeatStateLogger() + r.clock = fakeClock(&now) + + r.Clear(lgr, "k1", "recovered") + + require.Empty(t, matchingRecords(rec.GetRecords(), slog.LevelInfo, "recovered")) +} + +func TestRepeatStateLogger_FreshAfterClear(t *testing.T) { + lgr, rec := newCapturingLogger() + now := time.Unix(0, 0) + r := NewRepeatStateLogger() + r.clock = fakeClock(&now) + + r.Warn(lgr, "k1", "degraded") + r.Warn(lgr, "k1", "degraded") // suppressed + r.Clear(lgr, "k1", "recovered") + + // After Clear, the next Warn should emit again as a fresh first observation. + r.Warn(lgr, "k1", "degraded") + + warns := matchingRecords(rec.GetRecords(), slog.LevelWarn, "degraded") + require.Len(t, warns, 2, "first Warn after Clear should emit") + require.Nil(t, attrValue(warns[1], "occurrences"), "fresh emission should not carry an occurrences count") +} + +func TestRepeatStateLogger_ConcurrentCallersDoNotRace(t *testing.T) { + lgr, _ := newCapturingLogger() + r := NewRepeatStateLogger() + + const goroutines = 32 + const callsPerG = 200 + + var wg sync.WaitGroup + wg.Add(goroutines) + var clears atomic.Int64 + for g := 0; g < goroutines; g++ { + go func(id int) { + defer wg.Done() + key := "k" + string(rune('a'+(id%4))) + for i := 0; i < callsPerG; i++ { + r.Warn(lgr, key, "degraded", "g", id) + if i%50 == 0 { + r.Clear(lgr, key, "recovered") + clears.Add(1) + } + } + }(g) + } + wg.Wait() + // No assertion on counts — this test exists to flush out races under -race + // and to assert the logger does not panic under concurrent Warn/Clear. + require.Greater(t, clears.Load(), int64(0)) +} diff --git a/op-service/signer/espresso.go b/op-service/signer/espresso.go new file mode 100644 index 00000000000..f5895de6473 --- /dev/null +++ b/op-service/signer/espresso.go @@ -0,0 +1,18 @@ +package signer + +import ( + "context" + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +// Sign represents the interface for signing things via eth_sign. +func (s *SignerClient) Sign(ctx context.Context, address common.Address, data []byte) ([]byte, error) { + var result hexutil.Bytes + if err := s.client.CallContext(ctx, &result, "eth_sign", address, data); err != nil { + return nil, fmt.Errorf("eth_sign failed: %w", err) + } + return result, nil +} diff --git a/op-service/testutils/mock_eth_client.go b/op-service/testutils/mock_eth_client.go index 9975293866c..28ae160c144 100644 --- a/op-service/testutils/mock_eth_client.go +++ b/op-service/testutils/mock_eth_client.go @@ -148,6 +148,15 @@ func (m *MockEthClient) ExpectFetchReceipts(hash common.Hash, info eth.BlockInfo m.Mock.On("FetchReceipts", hash).Once().Return(&info, receipts, err) } +// SetFetchReceipts is like ExpectFetchReceipts, but registers an unbounded +// expectation (matches any number of FetchReceipts calls with the given hash, +// rather than exactly one). Useful when the derivation pipeline scans the same +// L1 block multiple times — for example when collecting BatchInfoAuthenticated +// events for batch authorization. +func (m *MockEthClient) SetFetchReceipts(hash common.Hash, info eth.BlockInfo, receipts types.Receipts, err error) { + m.Mock.On("FetchReceipts", hash).Return(&info, receipts, err) +} + func (m *MockEthClient) GetProof(ctx context.Context, address common.Address, storage []common.Hash, blockTag string) (*eth.AccountResult, error) { out := m.Mock.Called(address, storage, blockTag) return out.Get(0).(*eth.AccountResult), out.Error(1) diff --git a/packages/contracts-bedrock/foundry.toml b/packages/contracts-bedrock/foundry.toml index 5d6ebd0fe07..dfe0ff35dd9 100644 --- a/packages/contracts-bedrock/foundry.toml +++ b/packages/contracts-bedrock/foundry.toml @@ -7,7 +7,12 @@ src = 'src' out = 'forge-artifacts' script = 'scripts' +# espresso: explicitly set libs to prevent Forge from searching espresso-tee-contracts/lib +# nested subdirectories, which causes ambiguous import errors for openzeppelin-contracts. +libs = ['lib'] build_info_path = 'artifacts/build-info' +snapshots = 'notarealpath' # workaround for foundry#9477 +allow_internal_expect_revert = true # workaround described in https://github.com/PaulRBerg/prb-math/issues/248 optimizer = true optimizer_runs = 999999 @@ -20,6 +25,7 @@ optimizer_runs = 999999 additional_compiler_profiles = [ { name = "dispute", optimizer_runs = 5000 }, { name = "validator", optimizer_runs = 200 }, + { name = "via-ir", via_ir = true }, ] compilation_restrictions = [ { paths = "src/dispute/FaultDisputeGame.sol", optimizer_runs = 5000 }, @@ -42,18 +48,35 @@ ast = true evm_version = 'cancun' remappings = [ + # Espresso-tee-contracts context-specific remappings (must come before general @openzeppelin remappings) + 'lib/espresso-tee-contracts/:@espresso-tee/=lib/espresso-tee-contracts/src/', + 'lib/espresso-tee-contracts/:@openzeppelin/contracts/=lib/openzeppelin-contracts-v5/contracts', + # espresso: OZ upgradeable v5 is at a top-level lib (not nested under espresso-tee-contracts/lib/) + # to avoid ambiguous import errors when Forge adds lib/espresso-tee-contracts/ as an include path. + 'lib/espresso-tee-contracts/:@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable-v5/contracts', + 'lib/espresso-tee-contracts/:aws-nitro-enclave-attestation/=lib/espresso-tee-contracts/lib/aws-nitro-enclave-attestation/contracts/src/', + 'lib/espresso-tee-contracts/:solady/=lib/solady/src', + # Context remappings for OZ upgradeable v5: its @openzeppelin/contracts/ imports → OZ v5 non-upgradeable + 'lib/openzeppelin-contracts-upgradeable-v5/:@openzeppelin/contracts/=lib/openzeppelin-contracts-v5/contracts', + # General remappings '@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts', + # espresso: OZ upgradeable v5 alias for src/ contracts (e.g. BatchAuthenticator.sol) + '@openzeppelin/contracts-upgradeable-v5/=lib/openzeppelin-contracts-upgradeable-v5/contracts', + '@espresso-tee-contracts/=lib/espresso-tee-contracts/src', + '@nitro-validator/=lib/espresso-tee-contracts/lib/nitro-validator/src', + 'aws-nitro-enclave-attestation/=lib/espresso-tee-contracts/lib/aws-nitro-enclave-attestation/contracts/src', '@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts', '@openzeppelin/contracts-v5/=lib/openzeppelin-contracts-v5/contracts', '@rari-capital/solmate/=lib/solmate', '@lib-keccak/=lib/lib-keccak/contracts/lib', + 'solady/=lib/solady/', '@solady/=lib/solady/src', '@solady-v0.0.245/=lib/solady-v0.0.245/src', 'forge-std/=lib/forge-std/src', 'ds-test/=lib/forge-std/lib/ds-test/src', 'safe-contracts/=lib/safe-contracts/contracts', 'kontrol-cheatcodes/=lib/kontrol-cheatcodes/src', - 'interfaces/=interfaces' + 'interfaces/=interfaces', ] fs_permissions = [ @@ -70,6 +93,7 @@ fs_permissions = [ { access='read', path='./lib/superchain-registry/superchain/extra/' }, { access='read', path='./lib/superchain-registry/validation/standard/' }, { access='read', path='../../op-core/nuts/' }, + { access='read', path='./lib/espresso-tee-contracts/out/' }, ] # 5159 = selfdestruct deprecation @@ -99,6 +123,16 @@ wrap_comments=true # PROFILE: CI # ################################################################ +[profile.ci] +# Limit parallel Solc jobs to avoid OOM on xlarge (16GB) runners. +# Default is CPU count (8). With espresso-tee-contracts submodule deps, the two 0.8.15 +# groups (530 + 211 files) run together and exceed 16GB even at jobs=2. Serialize to 1. +jobs = 1 +# Limit test execution threads to avoid OOM during forge test on xlarge (16GB) runners. +# espresso-tee-contracts increases per-test memory pressure; default (8 threads on xlarge) +# can exhaust 16GB. 4 threads keeps memory within budget while retaining parallelism. +threads = 4 + [profile.ci.fuzz] runs = 128 @@ -111,6 +145,9 @@ depth = 32 ################################################################ [profile.cicoverage] +# Limit parallel Solc jobs to avoid OOM on xlarge (16GB) runners. +# espresso-tee-contracts adds additional compilation groups that exhaust memory without serialization. +jobs = 1 optimizer = false compilation_restrictions = [] @@ -133,6 +170,7 @@ optimizer_runs = 0 # See the info in the "DEFAULT" profile to understand this section. additional_compiler_profiles = [ { name = "dispute", optimizer_runs = 0 }, + { name = "via-ir", via_ir = true }, ] compilation_restrictions = [ { paths = "src/dispute/FaultDisputeGame.sol", optimizer_runs = 0 }, @@ -148,6 +186,8 @@ compilation_restrictions = [ { paths = "src/L1/OptimismPortal2.sol", optimizer_runs = 0 }, { paths = "src/universal/StorageSetter.sol", optimizer_runs = 0 } ] +# Limit parallel Solc jobs to avoid OOM on xlarge (16GB) runners. +jobs = 1 [profile.ciheavy.fuzz] runs = 20000 @@ -201,6 +241,8 @@ depth = 32 ################################################################ [profile.lite] +# Limit parallel Solc jobs to avoid OOM on xlarge (16GB) runners. +jobs = 1 optimizer = false optimizer_runs = 0 diff --git a/packages/contracts-bedrock/interfaces/L1/IBatchAuthenticator.sol b/packages/contracts-bedrock/interfaces/L1/IBatchAuthenticator.sol new file mode 100644 index 00000000000..73934800797 --- /dev/null +++ b/packages/contracts-bedrock/interfaces/L1/IBatchAuthenticator.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IEspressoTEEVerifier} from "@espresso-tee-contracts/interface/IEspressoTEEVerifier.sol"; +import {ISystemConfig} from "interfaces/L1/ISystemConfig.sol"; + +interface IBatchAuthenticator { + /// @notice Error thrown when an invalid address (zero address) is provided. + error InvalidAddress(address contract_); + + /// @notice Error thrown when the fallback batcher caller does not match the expected address. + error UnauthorizedFallbackBatcher(address sender, address expected); + + /// @notice Error thrown when `setEspressoBatcher` is called with the address + /// that is already the currently-active batcher. + error NoChange(address batcher); + + /// @notice Error thrown when the Espresso TEE batcher caller does not match the configured espressoBatcher. + error UnauthorizedEspressoBatcher(address sender, address expected); + + /// @notice Emitted when a batch info is authenticated. `caller` is the + /// address that invoked `authenticateBatchInfo`. + event BatchInfoAuthenticated(bytes32 commitment, address indexed caller); + + /// @notice Emitted when a signer registration is initiated through this contract. + event SignerRegistrationInitiated(address indexed caller); + + /// @notice Emitted when the Espresso batcher address is updated. `fromBlock` + /// is the L1 block number at which `newEspressoBatcher` becomes the + /// authorized batcher. + event EspressoBatcherUpdated( + address indexed oldEspressoBatcher, + address indexed newEspressoBatcher, + uint64 indexed fromBlock + ); + + /// @notice Emitted when the active batcher is switched. + event BatcherSwitched(bool indexed activeIsEspresso); + + function authenticateBatchInfo(bytes32 commitment, bytes memory _signature) external; + + function espressoTEEVerifier() external view returns (IEspressoTEEVerifier); + + function nitroValidator() external view returns (address); + + function owner() external view returns (address); + + /// @notice Returns the currently-active Espresso batcher address (the value of the + /// latest history entry). + function espressoBatcher() external view returns (address); + + /// @notice Number of entries in the Espresso batcher history. + function espressoBatcherHistoryLength() external view returns (uint256); + + /// @notice Returns the Espresso batcher history entry at `_index` + /// (oldest first). Reverts on out-of-bounds index. + function espressoBatcherAt(uint32 _index) external view returns (address batcher_, uint64 fromBlock_); + + /// @notice Returns the Espresso batcher address that was authorized at + /// L1 block `_l1Block`. Returns `address(0)` if `_l1Block` precedes the first + /// entry. + function espressoBatcherAtBlock(uint64 _l1Block) external view returns (address); + + function registerSigner(bytes memory verificationData, bytes memory data) external; + + function activeIsEspresso() external view returns (bool); + + function systemConfig() external view returns (ISystemConfig); + + function setActiveIsEspresso(bool _desired) external; + + function setEspressoBatcher(address _newEspressoBatcher) external; +} diff --git a/packages/contracts-bedrock/justfile b/packages/contracts-bedrock/justfile index 75080995ca2..cf98fe6d804 100644 --- a/packages/contracts-bedrock/justfile +++ b/packages/contracts-bedrock/justfile @@ -38,9 +38,14 @@ forge-build *ARGS: --skip-simulation \ 2>/dev/null || true -# Developer build command (faster). +# Developer build command (faster). Skip forge lint-on-build so we don't fail on 287+ warnings (e.g. unsafe-typecast in deps). forge-build-dev *ARGS: - FOUNDRY_PROFILE=lite forge build {{ARGS}} + @# Use default profile (not lite) so the source build cache is shared — re-using + @# the default profile's compiled artifacts avoids recompiling 1000+ files from scratch + @# under the lite profile, which OOMs on xlarge (16GB) CI runners. + @# FOUNDRY_JOBS=1 serializes solc compilation to avoid OOM when adding test files to the + @# build (the default profile has no jobs limit, and 4 parallel solc groups exhaust 16GB). + FOUNDRY_JOBS=1 forge build {{ARGS}} # Builds source contracts only. build-source: @@ -66,7 +71,6 @@ build-go-ffi: clean: rm -rf ./artifacts ./forge-artifacts ./cache ./scripts/go-ffi/go-ffi ./deployments/hardhat/* - ######################################################## # TEST # ######################################################## @@ -189,7 +193,7 @@ coverage: build-go-ffi # Runs contract coverage with lcov. coverage-lcov *ARGS: build-go-ffi - forge coverage {{ARGS}} --report lcov --report-file lcov.info + FOUNDRY_PROFILE="${FOUNDRY_PROFILE:-default}" forge coverage {{ARGS}} --report lcov --report-file lcov.info # Runs upgrade path variant of contract coverage tests. coverage-upgrade *ARGS: @@ -334,7 +338,8 @@ lint-check: # Updates the selectors for the contracts update-selectors: - forge selectors up --all + @# FOUNDRY_JOBS=1 serializes solc groups to avoid OOM on CI runners (large: 7.5GB). + FOUNDRY_JOBS=1 forge selectors up --all # Checks for unused imports in Solidity contracts. Does not build contracts. unused-imports-check-no-build: diff --git a/packages/contracts-bedrock/lib/espresso-tee-contracts b/packages/contracts-bedrock/lib/espresso-tee-contracts new file mode 160000 index 00000000000..bf6e60d862d --- /dev/null +++ b/packages/contracts-bedrock/lib/espresso-tee-contracts @@ -0,0 +1 @@ +Subproject commit bf6e60d862db8b3f1fccb2f8c10e737c8e4cef45 diff --git a/packages/contracts-bedrock/lib/openzeppelin-contracts-upgradeable-v5 b/packages/contracts-bedrock/lib/openzeppelin-contracts-upgradeable-v5 new file mode 160000 index 00000000000..dd89bed956f --- /dev/null +++ b/packages/contracts-bedrock/lib/openzeppelin-contracts-upgradeable-v5 @@ -0,0 +1 @@ +Subproject commit dd89bed956f7ca2f72f51b62bd926f3955695fee diff --git a/packages/contracts-bedrock/scripts/L2Genesis.s.sol b/packages/contracts-bedrock/scripts/L2Genesis.s.sol index d46c413de63..7a09764a962 100644 --- a/packages/contracts-bedrock/scripts/L2Genesis.s.sol +++ b/packages/contracts-bedrock/scripts/L2Genesis.s.sol @@ -214,7 +214,8 @@ contract L2Genesis is Script { // script didn't set the nonce and we didn't want to change that behavior when /// migrating genesis generation to Solidity. function setPredeployProxies(Input memory _input) internal { - bytes memory code = vm.getDeployedCode("Proxy.sol:Proxy"); + bytes memory code = vm.getDeployedCode("src/universal/Proxy.sol:Proxy"); // Espresso: disambiguate from OZ v5 + // proxy/Proxy.sol artifact uint160 prefix = uint160(0x420) << 148; for (uint256 i = 0; i < Predeploys.PREDEPLOY_COUNT; i++) { diff --git a/packages/contracts-bedrock/scripts/checks/interfaces/main.go b/packages/contracts-bedrock/scripts/checks/interfaces/main.go index d9097e260cb..83632e0287a 100644 --- a/packages/contracts-bedrock/scripts/checks/interfaces/main.go +++ b/packages/contracts-bedrock/scripts/checks/interfaces/main.go @@ -33,6 +33,18 @@ var excludeContracts = []string{ // TODO: Interfaces that need to be fixed "IInitializable", "IOptimismMintableERC20", "ILegacyMintableERC20", "KontrolCheatsBase", "IResolvedDelegateProxy", + + // Espresso dependencies + "IBatchAuthenticator", + "IEspressoTEEVerifier", + "IEspressoNitroTEEVerifier", + "ICertManager", + "BatchAuthenticator", + "INitroValidator", + // Espresso TEE submodule deep dependency interfaces (vendor-controlled pragma) + "IDaoAttestationResolver", + "IPCCSRouter", + "IQuoteVerifier", } // excludeSourceContracts is a list of contracts that are allowed to not have interfaces @@ -157,6 +169,17 @@ func processFile(artifactPath string) (*common.Void, []error) { return nil, []error{fmt.Errorf("%s: Interface does not start with 'I'", contractName)} } + // Espresso: skip interface artifacts that originate from lib/ (e.g. OZ v5 interfaces + // compiled transitively via espresso-tee-contracts). These are vendor-controlled and + // should not be subject to our interface version or ABI-match requirements. + forgeArtifact, err := common.ReadForgeArtifact(artifactPath) + if err != nil { + return nil, []error{fmt.Errorf("failed to read forge artifact: %w", err)} + } + if strings.HasPrefix(forgeArtifact.Ast.AbsolutePath, "lib/") { + return nil, nil + } + semver, err := getContractSemver(artifact) if err != nil { return nil, []error{fmt.Errorf("failed to get contract semver: %w", err)} @@ -173,6 +196,16 @@ func processFile(artifactPath string) (*common.Void, []error) { return nil, nil } + // Espresso: skip ABI comparison if the corresponding contract artifact is from lib/ + // (e.g. OZ v5 Ownable.json replacing OZ v4 Ownable.json as the "main" artifact). + correspondingForgeArtifact, err := common.ReadForgeArtifact(correspondingContractFile) + if err != nil { + return nil, []error{fmt.Errorf("failed to read corresponding forge artifact: %w", err)} + } + if strings.HasPrefix(correspondingForgeArtifact.Ast.AbsolutePath, "lib/") { + return nil, nil + } + contractArtifact, err := readArtifact(correspondingContractFile) if err != nil { return nil, []error{fmt.Errorf("failed to read corresponding contract artifact: %w", err)} diff --git a/packages/contracts-bedrock/scripts/checks/strict-pragma/main.go b/packages/contracts-bedrock/scripts/checks/strict-pragma/main.go index 6b272cb5065..6fc08788aa1 100644 --- a/packages/contracts-bedrock/scripts/checks/strict-pragma/main.go +++ b/packages/contracts-bedrock/scripts/checks/strict-pragma/main.go @@ -39,6 +39,10 @@ var excludedFiles = []string{ "src/periphery/Transactor.sol", "src/periphery/monitoring/DisputeMonitorHelper.sol", "src/universal/SafeSend.sol", + // BatchAuthenticator is imported by scripts at =0.8.25 AND by test groups at 0.8.28 + // (via OZ v5 ^0.8.20 transitive deps). An exact pragma would break one or the other + // compilation group in Foundry's multi-version resolver. + "src/L1/BatchAuthenticator.sol", } func main() { diff --git a/packages/contracts-bedrock/scripts/checks/test-validation/exclusions.toml b/packages/contracts-bedrock/scripts/checks/test-validation/exclusions.toml index 62eda759f06..412c351b9c4 100644 --- a/packages/contracts-bedrock/scripts/checks/test-validation/exclusions.toml +++ b/packages/contracts-bedrock/scripts/checks/test-validation/exclusions.toml @@ -99,4 +99,6 @@ contracts = [ "Constants_Test", # Invalid naming pattern - doesn't specify function or Uncategorized "LivenessModule2_TestUtils", # Test utils library in LivenessModule2 test file "L1Block_SetCustomGasToken_Test", # Custom gas token tests hosted in the L1Block test file + "BatchAuthenticator_Fork_Test", # Espresso fork tests - 'Fork' is a descriptor, not a function name + "MockSystemConfig", # Espresso mock helper contract inside BatchAuthenticator.t.sol ] diff --git a/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol b/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol index d8bfbb52164..9581e646199 100644 --- a/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol +++ b/packages/contracts-bedrock/scripts/deploy/ChainAssertions.sol @@ -379,7 +379,10 @@ library ChainAssertions { ); Blueprint.Preamble memory proxyPreamble = Blueprint.parseBlueprintPreamble(address(blueprints.proxy).code); - require(keccak256(proxyPreamble.initcode) == keccak256(DeployUtils.getCode("Proxy")), "CHECK-OPCM-170"); + require( + keccak256(proxyPreamble.initcode) == keccak256(DeployUtils.getCode("src/universal/Proxy.sol:Proxy")), + "CHECK-OPCM-170" + ); // Espresso: disambiguate from OZ v5 proxy/Proxy.sol artifact Blueprint.Preamble memory proxyAdminPreamble = Blueprint.parseBlueprintPreamble(address(blueprints.proxyAdmin).code); diff --git a/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol b/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol index 08149e17b19..cfc1198fce3 100644 --- a/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol @@ -373,7 +373,7 @@ contract Deploy is Deployer { DeployUtils.create2AndSave({ _save: artifacts, _salt: keccak256(abi.encode(_implSalt(), _name)), - _name: "Proxy", + _name: "src/universal/Proxy.sol:Proxy", // Espresso: disambiguate from OZ v5 proxy/Proxy.sol artifact _nick: _name, _args: DeployUtils.encodeConstructor(abi.encodeCall(IProxy.__constructor__, (_proxyOwner))) }) diff --git a/packages/contracts-bedrock/scripts/deploy/DeployAltDA.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployAltDA.s.sol index d1f389444f1..960354cc6cb 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployAltDA.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployAltDA.s.sol @@ -45,7 +45,7 @@ contract DeployAltDA is Script { vm.broadcast(msg.sender); IDataAvailabilityChallenge proxy = IDataAvailabilityChallenge( DeployUtils.create2({ - _name: "Proxy", + _name: "src/universal/Proxy.sol:Proxy", // Espresso: disambiguate from OZ v5 proxy/Proxy.sol artifact _salt: salt, _args: DeployUtils.encodeConstructor(abi.encodeCall(IProxy.__constructor__, (msg.sender))) }) diff --git a/packages/contracts-bedrock/scripts/deploy/DeployBatchAuthenticator.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployBatchAuthenticator.s.sol new file mode 100644 index 00000000000..a5eed021b5d --- /dev/null +++ b/packages/contracts-bedrock/scripts/deploy/DeployBatchAuthenticator.s.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Script, console } from "forge-std/Script.sol"; +import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; +import { IEspressoTEEVerifier } from "@espresso-tee-contracts/interface/IEspressoTEEVerifier.sol"; +import { IProxy } from "interfaces/universal/IProxy.sol"; +import { BatchAuthenticator } from "src/L1/BatchAuthenticator.sol"; +import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; + +/// @notice Deploys only the BatchAuthenticator (proxy + impl) against an existing TEEVerifier and +/// wires the proxy to an existing (shared) OP Stack ProxyAdmin. +/// +/// @dev The proxy is deployed with the deployer as its transient admin so the deployer can call +/// `upgradeToAndCall` to initialize it directly, then `changeAdmin` hands the proxy over to the +/// shared ProxyAdmin (same pattern as DeployAltDA / DeployFeesDepositor). +/// +/// @dev `_batchAuthenticatorOwner` is the application-level (OZ Ownable / OwnableWithGuardians) owner, +/// which gates operational setters (setEspressoBatcher, setActiveIsEspresso). It is distinct from +/// `_proxyAdmin`'s owner, which controls upgrades and `initialize` after the `changeAdmin`. +/// +/// Usage: +/// forge script scripts/deploy/DeployBatchAuthenticator.s.sol:DeployBatchAuthenticator \ +/// --rpc-url \ +/// --broadcast \ +/// --private-key \ +/// --verify \ +/// --etherscan-api-key \ +/// --sig "run(address,address,address,address,address)" \ +/// \ +/// \ +/// \ +/// \ +/// +contract DeployBatchAuthenticator is Script { + function run( + address _espressoBatcher, + address _systemConfig, + address _teeVerifier, + address _proxyAdmin, + address _batchAuthenticatorOwner + ) + public + { + require(_espressoBatcher != address(0), "DeployBatchAuthenticator: espressoBatcher required"); + require(_systemConfig != address(0), "DeployBatchAuthenticator: systemConfig required"); + require(_teeVerifier != address(0), "DeployBatchAuthenticator: teeVerifier required"); + require(_proxyAdmin != address(0), "DeployBatchAuthenticator: proxyAdmin required"); + + if (_batchAuthenticatorOwner == address(0)) { + _batchAuthenticatorOwner = msg.sender; + console.log("WARN: batchAuthenticatorOwner not set, defaulting to msg.sender"); + } + + vm.startBroadcast(msg.sender); + + // Deploy the Proxy with the deployer as its transient admin so the deployer can initialize it + // directly below. Deploy without importing Proxy.sol to avoid duplicate compilation artifacts; + // use the path-qualified form to disambiguate from OZ v5's proxy/Proxy.sol artifact. + IProxy proxy; + { + bytes memory initCode = + abi.encodePacked(DeployUtils.getCode("src/universal/Proxy.sol:Proxy"), abi.encode(msg.sender)); + address payable proxyAddr; + assembly { + proxyAddr := create(0, add(initCode, 0x20), mload(initCode)) + } + require(proxyAddr != address(0), "DeployBatchAuthenticator: proxy deployment failed"); + proxy = IProxy(proxyAddr); + } + vm.label(address(proxy), "BatchAuthenticatorProxy"); + BatchAuthenticator impl = new BatchAuthenticator(); + vm.label(address(impl), "BatchAuthenticatorImpl"); + + bytes memory initData = abi.encodeCall( + BatchAuthenticator.initialize, + ( + IEspressoTEEVerifier(_teeVerifier), + _espressoBatcher, + ISystemConfig(_systemConfig), + _batchAuthenticatorOwner, + // First deployment: start with the Espresso batcher active. + true + ) + ); + // Initialize directly via the proxy. The deployer is still the proxy admin at this point, so + // BatchAuthenticator.initialize's `_assertOnlyProxyAdminOrProxyAdminOwner` check passes. + proxy.upgradeToAndCall(address(impl), initData); + + // Hand the proxy over to the shared OP Stack ProxyAdmin. No setProxyType call is needed: the + // ProxyAdmin treats unregistered proxies as ProxyType.ERC1967 (enum value 0), which matches + // src/universal/Proxy.sol. + proxy.changeAdmin(_proxyAdmin); + + vm.stopBroadcast(); + + console.log("BatchAuthenticator (proxy):", address(proxy)); + console.log("BatchAuthenticator (impl): ", address(impl)); + console.log("ProxyAdmin (shared): ", _proxyAdmin); + } +} diff --git a/packages/contracts-bedrock/scripts/deploy/DeployEspresso.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployEspresso.s.sol new file mode 100644 index 00000000000..1cae2f16d04 --- /dev/null +++ b/packages/contracts-bedrock/scripts/deploy/DeployEspresso.s.sol @@ -0,0 +1,324 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { BaseDeployIO } from "scripts/deploy/BaseDeployIO.sol"; +import { Script } from "forge-std/Script.sol"; +import { Solarray } from "scripts/libraries/Solarray.sol"; +import { IBatchAuthenticator } from "interfaces/L1/IBatchAuthenticator.sol"; +import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; +import { IEspressoNitroTEEVerifier } from "@espresso-tee-contracts/interface/IEspressoNitroTEEVerifier.sol"; +import { IEspressoTEEVerifier } from "@espresso-tee-contracts/interface/IEspressoTEEVerifier.sol"; +import { IProxy } from "interfaces/universal/IProxy.sol"; +import { BatchAuthenticator } from "src/L1/BatchAuthenticator.sol"; +import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; +import { MockEspressoTEEVerifier } from "test/mocks/MockEspressoTEEVerifiers.sol"; +import { MockEspressoNitroTEEVerifier } from "test/mocks/MockEspressoTEEVerifiers.sol"; + +contract DeployEspressoInput is BaseDeployIO { + address internal _nitroEnclaveVerifier; + address internal _espressoBatcher; + address internal _systemConfig; + address internal _espressoOwner; + address internal _sharedProxyAdmin; + + function set(bytes4 _sel, address _val) public { + if (_sel == this.nitroEnclaveVerifier.selector) { + _nitroEnclaveVerifier = _val; + } else if (_sel == this.espressoBatcher.selector) { + _espressoBatcher = _val; + } else if (_sel == this.systemConfig.selector) { + _systemConfig = _val; + } else if (_sel == this.espressoOwner.selector) { + _espressoOwner = _val; + } else if (_sel == this.sharedProxyAdmin.selector) { + _sharedProxyAdmin = _val; + } else { + revert("DeployEspressoInput: unknown selector"); + } + } + + /// @notice Address of the underlying AWS NitroEnclaveVerifier (from Automata). + /// Set to address(0) to deploy mock verifiers (dev/test only). + function nitroEnclaveVerifier() public view returns (address) { + return _nitroEnclaveVerifier; + } + + function espressoBatcher() public view returns (address) { + return _espressoBatcher; + } + + function systemConfig() public view returns (address) { + return _systemConfig; + } + + /// @notice The application-level (OZ Ownable / OwnableWithGuardians) owner for the Espresso + /// contracts — gates operational setters (setEspressoBatcher, setActiveIsEspresso, + /// setEnclaveHash, etc.). This is NOT the shared ProxyAdmin owner, which controls upgrades + /// and `initialize`. Defaults to the deployer if not set. + function espressoOwner() public view returns (address) { + return _espressoOwner; + } + + /// @notice Address of the existing (shared) OP Stack ProxyAdmin that the BatchAuthenticator and + /// TEEVerifier proxies are handed over to. Required. + function sharedProxyAdmin() public view returns (address) { + require(_sharedProxyAdmin != address(0), "DeployEspressoInput: sharedProxyAdmin not set"); + return _sharedProxyAdmin; + } +} + +contract DeployEspressoOutput is BaseDeployIO { + address internal _batchAuthenticatorAddress; + address internal _teeVerifierProxy; + address internal _nitroTEEVerifier; + + function set(bytes4 _sel, address _addr) public { + require(_addr != address(0), "DeployEspressoOutput: cannot set zero address"); + if (_sel == this.batchAuthenticatorAddress.selector) { + _batchAuthenticatorAddress = _addr; + } else if (_sel == this.teeVerifierProxy.selector) { + _teeVerifierProxy = _addr; + } else if (_sel == this.nitroTEEVerifier.selector) { + _nitroTEEVerifier = _addr; + } else { + revert("DeployEspressoOutput: unknown selector"); + } + } + + function batchAuthenticatorAddress() public view returns (address) { + require(_batchAuthenticatorAddress != address(0), "DeployEspressoOutput: batch authenticator address not set"); + return _batchAuthenticatorAddress; + } + + function teeVerifierProxy() public view returns (address) { + require(_teeVerifierProxy != address(0), "DeployEspressoOutput: tee verifier proxy not set"); + return _teeVerifierProxy; + } + + function nitroTEEVerifier() public view returns (address) { + require(_nitroTEEVerifier != address(0), "DeployEspressoOutput: nitro tee verifier proxy not set"); + return _nitroTEEVerifier; + } + + /// @notice Alias for teeVerifierProxy for convenience + function teeVerifierAddress() public view returns (address) { + return teeVerifierProxy(); + } +} + +contract DeployEspresso is Script { + function run(DeployEspressoInput _input, DeployEspressoOutput _output, address _deployerAddress) public { + IEspressoTEEVerifier teeVerifier = deployTEEContracts(_input, _output, _deployerAddress); + deployBatchAuthenticator(_input, _output, _deployerAddress, teeVerifier); + checkOutput(_output); + } + + function deployBatchAuthenticator( + DeployEspressoInput _input, + DeployEspressoOutput _output, + address _deployerAddress, + IEspressoTEEVerifier _teeVerifier + ) + public + returns (IBatchAuthenticator) + { + // The BatchAuthenticator app-level owner (OwnableWithGuardians). Distinct from the shared + // ProxyAdmin owner. + address batchAuthenticatorOwner = _input.espressoOwner(); + if (batchAuthenticatorOwner == address(0)) batchAuthenticatorOwner = _deployerAddress; + + // Deploy the proxy with the deployer as its transient admin so the deployer can initialize it + // directly, then `changeAdmin` hands the proxy over to the shared ProxyAdmin (same pattern as + // DeployAltDA / DeployFeesDepositor). + address sharedProxyAdmin = _input.sharedProxyAdmin(); + + // Deploy Proxy without importing Proxy.sol to avoid duplicate compilation artifacts. + IProxy proxy; + { + bytes memory initCode = + abi.encodePacked(DeployUtils.getCode("src/universal/Proxy.sol:Proxy"), abi.encode(msg.sender)); + address payable proxyAddr; + vm.broadcast(msg.sender); + assembly { + proxyAddr := create(0, add(initCode, 0x20), mload(initCode)) + } + require(proxyAddr != address(0), "DeployEspresso: proxy deployment failed"); + proxy = IProxy(proxyAddr); + } + vm.label(address(proxy), "BatchAuthenticatorProxy"); + vm.broadcast(msg.sender); + BatchAuthenticator impl = new BatchAuthenticator(); + vm.label(address(impl), "BatchAuthenticatorImpl"); + + bytes memory initData = abi.encodeCall( + BatchAuthenticator.initialize, + ( + _teeVerifier, + _input.espressoBatcher(), + ISystemConfig(_input.systemConfig()), + batchAuthenticatorOwner, + // First deployment: start with the Espresso batcher active. + true + ) + ); + // Initialize directly via the proxy. The deployer is still the proxy admin at this point, so + // BatchAuthenticator.initialize's `_assertOnlyProxyAdminOrProxyAdminOwner` check passes. + vm.broadcast(msg.sender); + proxy.upgradeToAndCall(address(impl), initData); + + // Hand the proxy over to the shared OP Stack ProxyAdmin. No setProxyType call is needed: the + // ProxyAdmin treats unregistered proxies as ProxyType.ERC1967 (enum value 0), which matches + // src/universal/Proxy.sol. + vm.broadcast(msg.sender); + proxy.changeAdmin(sharedProxyAdmin); + + _output.set(_output.batchAuthenticatorAddress.selector, address(proxy)); + return IBatchAuthenticator(address(proxy)); + } + + /// @notice Deploys NitroTEEVerifier and TEEVerifier (production path). + /// Deployment order: + /// 1. Deploy TEEVerifier (impl + OP-style ERC-1967 Proxy + ProxyAdmin) with placeholder nitro address + /// 2. Deploy NitroTEEVerifier pointing to the TEEVerifier proxy + /// 3. Update TEEVerifier with the actual NitroTEEVerifier address + /// + /// The TEEVerifier is deployed behind src/universal/Proxy.sol rather than the + /// upstream's OZ v5 TransparentUpgradeableProxy. This avoids pulling OZ's TUP + + /// ProxyAdmin into the OP artifact tree (which would shadow src/universal/ProxyAdmin.sol). + /// + /// If nitroEnclaveVerifier is address(0), deploys our local mocks (dev/test only). + function deployTEEContracts( + DeployEspressoInput _input, + DeployEspressoOutput _output, + address _deployerAddress + ) + public + returns (IEspressoTEEVerifier) + { + address nitroEnclaveVerifier = _input.nitroEnclaveVerifier(); + if (nitroEnclaveVerifier == address(0)) { + return _deployMockTEEContracts(_output); + } + return _deployProductionTEEContracts(_input, _output, _deployerAddress, nitroEnclaveVerifier); + } + + function _deployMockTEEContracts(DeployEspressoOutput _output) internal returns (IEspressoTEEVerifier) { + // Use our local mocks — they carry OP-specific test behavior (permissive isSignerValid, + // test helper overrides, special address exceptions) that the submodule mocks don't have. + // The mocks are unproxied, so there is no ProxyAdmin to wire here. + vm.broadcast(msg.sender); + MockEspressoNitroTEEVerifier nitroMock = new MockEspressoNitroTEEVerifier(); + vm.label(address(nitroMock), "MockEspressoNitroTEEVerifier"); + + vm.broadcast(msg.sender); + MockEspressoTEEVerifier teeMock = new MockEspressoTEEVerifier(IEspressoNitroTEEVerifier(address(nitroMock))); + vm.label(address(teeMock), "MockEspressoTEEVerifier"); + + _output.set(_output.nitroTEEVerifier.selector, address(nitroMock)); + _output.set(_output.teeVerifierProxy.selector, address(teeMock)); + return IEspressoTEEVerifier(address(teeMock)); + } + + function _deployProductionTEEContracts( + DeployEspressoInput _input, + DeployEspressoOutput _output, + address _deployerAddress, + address _nitroEnclaveVerifier + ) + internal + returns (IEspressoTEEVerifier) + { + address teeVerifierOwner = _input.espressoOwner(); + if (teeVerifierOwner == address(0)) teeVerifierOwner = _deployerAddress; + + address sharedProxyAdmin = _input.sharedProxyAdmin(); + + // Deploy OP's ERC-1967 Proxy with the deployer as its transient admin so the deployer can + // initialize it directly below, then `changeAdmin` hands it to the shared ProxyAdmin (same + // pattern as deployBatchAuthenticator / DeployAltDA / DeployFeesDepositor). + address payable teeProxyAddr; + { + bytes memory initCode = + abi.encodePacked(DeployUtils.getCode("src/universal/Proxy.sol:Proxy"), abi.encode(msg.sender)); + vm.broadcast(msg.sender); + assembly { + teeProxyAddr := create(0, add(initCode, 0x20), mload(initCode)) + } + require(teeProxyAddr != address(0), "DeployEspresso: tee proxy deployment failed"); + } + vm.label(teeProxyAddr, "TEEVerifierProxy"); + + // Deploy the implementation. + // Use vm.getCode against the submodule's own out/ to avoid pulling the impl closure + // (TEEHelper, JournalValidation, aws-nitro-enclave-attestation) into OP's compile group. + address payable teeImplAddr; + { + bytes memory teeImplCode = + DeployUtils.getCode("lib/espresso-tee-contracts/out/EspressoTEEVerifier.sol/EspressoTEEVerifier.json"); + vm.broadcast(msg.sender); + assembly { + teeImplAddr := create(0, add(teeImplCode, 0x20), mload(teeImplCode)) + } + require(teeImplAddr != address(0), "DeployEspresso: EspressoTEEVerifier impl deployment failed"); + } + IEspressoTEEVerifier teeImpl = IEspressoTEEVerifier(teeImplAddr); + vm.label(teeImplAddr, "TEEVerifierImpl"); + + // Deploy NitroTEEVerifier first (no proxy; its constructor only stores the TEE proxy address + // for access control). Deploying it before init lets us wire it directly via `initialize`, + // avoiding a separate onlyOwner call and the Ownable2Step ownership-transfer dance. + // Use vm.getCode against the submodule's own out/ to avoid pulling the impl closure + // into OP's compile group. + address nitroVerifierAddr; + { + bytes memory nitroImplCode = abi.encodePacked( + DeployUtils.getCode( + "lib/espresso-tee-contracts/out/EspressoNitroTEEVerifier.sol/EspressoNitroTEEVerifier.json" + ), + abi.encode(teeProxyAddr, _nitroEnclaveVerifier) + ); + vm.broadcast(msg.sender); + assembly { + nitroVerifierAddr := create(0, add(nitroImplCode, 0x20), mload(nitroImplCode)) + } + require(nitroVerifierAddr != address(0), "DeployEspresso: EspressoNitroTEEVerifier deployment failed"); + } + IEspressoNitroTEEVerifier nitroVerifier = IEspressoNitroTEEVerifier(nitroVerifierAddr); + vm.label(nitroVerifierAddr, "NitroTEEVerifier"); + + // initialize(address _owner, address _espressoNitroTEEVerifier). Sets the final contract owner + // and wires the Nitro verifier in one shot, so no post-init onlyOwner call is needed. The + // deployer is still the proxy admin at this point, so it can call upgradeToAndCall directly. + // abi.encodeCall would require importing the concrete EspressoTEEVerifier type, which pulls its + // impl closure (TEEHelper, JournalValidation, aws-nitro-enclave-attestation) into OP's compile + // group; that is exactly what deploying the impl from the submodule's own artifact avoids. The + // initialize selector is not declared on the imported IEspressoTEEVerifier interface. + // nosemgrep: sol-style-use-abi-encodecall + bytes memory initData = + abi.encodeWithSignature("initialize(address,address)", teeVerifierOwner, nitroVerifierAddr); + vm.broadcast(msg.sender); + IProxy(teeProxyAddr).upgradeToAndCall(address(teeImpl), initData); + + // Hand the proxy over to the shared OP Stack ProxyAdmin. No setProxyType call is needed: the + // ProxyAdmin treats unregistered proxies as ProxyType.ERC1967 (enum value 0), which matches + // src/universal/Proxy.sol. + vm.broadcast(msg.sender); + IProxy(teeProxyAddr).changeAdmin(sharedProxyAdmin); + + _output.set(_output.teeVerifierProxy.selector, teeProxyAddr); + _output.set(_output.nitroTEEVerifier.selector, address(nitroVerifier)); + + return IEspressoTEEVerifier(teeProxyAddr); + } + + function checkOutput(DeployEspressoOutput _output) public view { + address[] memory addresses = Solarray.addresses( + _output.batchAuthenticatorAddress(), _output.teeVerifierProxy(), _output.nitroTEEVerifier() + ); + for (uint256 i = 0; i < addresses.length; i++) { + require( + addresses[i] != address(0) && addresses[i].code.length > 0, "DeployEspresso: invalid contract address" + ); + } + } +} diff --git a/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol index a3b5e53b50e..b7e22e53ed8 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol @@ -197,7 +197,7 @@ contract DeployImplementations is Script { address checkAddress; (blueprints.addressManager, checkAddress) = DeployUtils.createDeterministicBlueprint(DeployUtils.getCode("AddressManager"), _salt); require(checkAddress == address(0), "OPCM-10"); - (blueprints.proxy, checkAddress) = DeployUtils.createDeterministicBlueprint(DeployUtils.getCode("Proxy"), _salt); + (blueprints.proxy, checkAddress) = DeployUtils.createDeterministicBlueprint(DeployUtils.getCode("src/universal/Proxy.sol:Proxy"), _salt);// Espresso: disambiguate from OZ v5 proxy/Proxy.sol artifact require(checkAddress == address(0), "OPCM-20"); (blueprints.proxyAdmin, checkAddress) = DeployUtils.createDeterministicBlueprint(DeployUtils.getCode("ProxyAdmin"), _salt); require(checkAddress == address(0), "OPCM-30"); diff --git a/packages/contracts-bedrock/scripts/deploy/DeploySuperchain.s.sol b/packages/contracts-bedrock/scripts/deploy/DeploySuperchain.s.sol index aff18378919..c2f173022bd 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeploySuperchain.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeploySuperchain.s.sol @@ -100,7 +100,7 @@ contract DeploySuperchain is Script { vm.startBroadcast(msg.sender); ISuperchainConfig superchainConfigProxy = ISuperchainConfig( DeployUtils.create1({ - _name: "Proxy", + _name: "src/universal/Proxy.sol:Proxy", // Espresso: disambiguate from OZ v5 proxy/Proxy.sol artifact _args: DeployUtils.encodeConstructor( abi.encodeCall(IProxy.__constructor__, (address(superchainProxyAdmin))) ) diff --git a/packages/contracts-bedrock/scripts/libraries/DeployUtils.sol b/packages/contracts-bedrock/scripts/libraries/DeployUtils.sol index a6654f29d18..48044a07b1d 100644 --- a/packages/contracts-bedrock/scripts/libraries/DeployUtils.sol +++ b/packages/contracts-bedrock/scripts/libraries/DeployUtils.sol @@ -313,7 +313,7 @@ library DeployUtils { function buildERC1967ProxyWithImpl(string memory _proxyImplName) internal returns (IProxy genericProxy_) { genericProxy_ = IProxy( create1({ - _name: "Proxy", + _name: "src/universal/Proxy.sol:Proxy", // Espresso: disambiguate from OZ v5 proxy/Proxy.sol artifact _args: DeployUtils.encodeConstructor(abi.encodeCall(IProxy.__constructor__, (address(0)))) }) ); diff --git a/packages/contracts-bedrock/scripts/periphery/deploy/DeployPeriphery.s.sol b/packages/contracts-bedrock/scripts/periphery/deploy/DeployPeriphery.s.sol index 60967a213a2..f77055ea723 100644 --- a/packages/contracts-bedrock/scripts/periphery/deploy/DeployPeriphery.s.sol +++ b/packages/contracts-bedrock/scripts/periphery/deploy/DeployPeriphery.s.sol @@ -7,10 +7,11 @@ import { Script } from "forge-std/Script.sol"; import { Config } from "scripts/libraries/Config.sol"; import { Artifacts } from "scripts/Artifacts.s.sol"; +import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; import { PeripheryDeployConfig } from "scripts/periphery/deploy/PeripheryDeployConfig.s.sol"; -import { ProxyAdmin } from "src/universal/ProxyAdmin.sol"; -import { Proxy } from "src/universal/Proxy.sol"; +import { IProxy } from "interfaces/universal/IProxy.sol"; +import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; import { Faucet } from "src/periphery/faucet/Faucet.sol"; import { Drippie } from "src/periphery/drippie/Drippie.sol"; import { CheckBalanceLow } from "src/periphery/drippie/dripchecks/CheckBalanceLow.sol"; @@ -85,11 +86,11 @@ contract DeployPeriphery is Script { function deployProxyAdmin() public broadcast returns (address addr_) { addr_ = _deployCreate2({ _name: "ProxyAdmin", - _creationCode: type(ProxyAdmin).creationCode, + _creationCode: DeployUtils.getCode("ProxyAdmin"), _constructorParams: abi.encode(msg.sender) }); - ProxyAdmin admin = ProxyAdmin(addr_); + IProxyAdmin admin = IProxyAdmin(addr_); require(admin.owner() == msg.sender, "DeployPeriphery: ProxyAdmin owner mismatch"); } @@ -97,11 +98,12 @@ contract DeployPeriphery is Script { function deployFaucetProxy() public broadcast returns (address addr_) { addr_ = _deployCreate2({ _name: "FaucetProxy", - _creationCode: type(Proxy).creationCode, + _creationCode: DeployUtils.getCode("src/universal/Proxy.sol:Proxy"), // Espresso: disambiguate from + // OZ v5 proxy/Proxy.sol artifact _constructorParams: abi.encode(artifacts.mustGetAddress("ProxyAdmin")) }); - Proxy proxy = Proxy(payable(addr_)); + IProxy proxy = IProxy(payable(addr_)); require( EIP1967Helper.getAdmin(address(proxy)) == artifacts.mustGetAddress("ProxyAdmin"), "DeployPeriphery: FaucetProxy admin mismatch" @@ -201,7 +203,7 @@ contract DeployPeriphery is Script { /// @notice Initialize the Faucet. function initializeFaucet() public broadcast { - ProxyAdmin proxyAdmin = ProxyAdmin(artifacts.mustGetAddress("ProxyAdmin")); + IProxyAdmin proxyAdmin = IProxyAdmin(artifacts.mustGetAddress("ProxyAdmin")); address faucetProxy = artifacts.mustGetAddress("FaucetProxy"); address faucet = artifacts.mustGetAddress("Faucet"); address implementationAddress = proxyAdmin.getProxyImplementation(faucetProxy); diff --git a/packages/contracts-bedrock/snapshots/abi/BatchAuthenticator.json b/packages/contracts-bedrock/snapshots/abi/BatchAuthenticator.json new file mode 100644 index 00000000000..f28b0895f5a --- /dev/null +++ b/packages/contracts-bedrock/snapshots/abi/BatchAuthenticator.json @@ -0,0 +1,709 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "acceptOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "activeIsEspresso", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "guardian", + "type": "address" + } + ], + "name": "addGuardian", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_commitment", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "_signature", + "type": "bytes" + } + ], + "name": "authenticateBatchInfo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "espressoBatcher", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "_index", + "type": "uint32" + } + ], + "name": "espressoBatcherAt", + "outputs": [ + { + "internalType": "address", + "name": "batcher_", + "type": "address" + }, + { + "internalType": "uint64", + "name": "fromBlock_", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "_l1Block", + "type": "uint64" + } + ], + "name": "espressoBatcherAtBlock", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "espressoBatcherHistoryLength", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "espressoTEEVerifier", + "outputs": [ + { + "internalType": "contract IEspressoTEEVerifier", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getGuardians", + "outputs": [ + { + "internalType": "address[]", + "name": "", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "guardianCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "initVersion", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IEspressoTEEVerifier", + "name": "_espressoTEEVerifier", + "type": "address" + }, + { + "internalType": "address", + "name": "_espressoBatcher", + "type": "address" + }, + { + "internalType": "contract ISystemConfig", + "name": "_systemConfig", + "type": "address" + }, + { + "internalType": "address", + "name": "_owner", + "type": "address" + }, + { + "internalType": "bool", + "name": "_activeIsEspresso", + "type": "bool" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "isGuardian", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "nitroValidator", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pendingOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxyAdmin", + "outputs": [ + { + "internalType": "contract IProxyAdmin", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxyAdminOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "_verificationData", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + } + ], + "name": "registerSigner", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "guardian", + "type": "address" + } + ], + "name": "removeGuardian", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "_desired", + "type": "bool" + } + ], + "name": "setActiveIsEspresso", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_newEspressoBatcher", + "type": "address" + } + ], + "name": "setEspressoBatcher", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "systemConfig", + "outputs": [ + { + "internalType": "contract ISystemConfig", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes32", + "name": "commitment", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + } + ], + "name": "BatchInfoAuthenticated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bool", + "name": "activeIsEspresso", + "type": "bool" + } + ], + "name": "BatcherSwitched", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "oldEspressoBatcher", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newEspressoBatcher", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint64", + "name": "fromBlock", + "type": "uint64" + } + ], + "name": "EspressoBatcherUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "guardian", + "type": "address" + } + ], + "name": "GuardianAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "guardian", + "type": "address" + } + ], + "name": "GuardianRemoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "version", + "type": "uint64" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferStarted", + "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": "caller", + "type": "address" + } + ], + "name": "SignerRegistrationInitiated", + "type": "event" + }, + { + "inputs": [], + "name": "CheckpointUnorderedInsertion", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contract_", + "type": "address" + } + ], + "name": "InvalidAddress", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidGuardianAddress", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidInitialization", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidSignature", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "batcher", + "type": "address" + } + ], + "name": "NoChange", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "caller", + "type": "address" + } + ], + "name": "NotGuardian", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "caller", + "type": "address" + } + ], + "name": "NotGuardianOrOwner", + "type": "error" + }, + { + "inputs": [], + "name": "NotInitializing", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "OwnableInvalidOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "OwnableUnauthorizedAccount", + "type": "error" + }, + { + "inputs": [], + "name": "OwnerCantBeGuardian", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotProxyAdmin", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotProxyAdminOrProxyAdminOwner", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotProxyAdminOwner", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotResolvedDelegateProxy", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_NotSharedProxyAdminOwner", + "type": "error" + }, + { + "inputs": [], + "name": "ProxyAdminOwnedBase_ProxyAdminNotFound", + "type": "error" + }, + { + "inputs": [], + "name": "ReinitializableBase_ZeroInitVersion", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "expected", + "type": "address" + } + ], + "name": "UnauthorizedEspressoBatcher", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "expected", + "type": "address" + } + ], + "name": "UnauthorizedFallbackBatcher", + "type": "error" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 1ef6d8da1a0..3f5d863cd3f 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -1,4 +1,8 @@ { + "src/L1/BatchAuthenticator.sol:BatchAuthenticator": { + "initCodeHash": "0xbd6e806f4dc60c8ceb2546e150b1990761abb90930dfdeb2be1c85581fb06935", + "sourceCodeHash": "0x0270628e923d45ce15ed4cb728146fa9af906e33a6fbc842b079454625281829" + }, "src/L1/DataAvailabilityChallenge.sol:DataAvailabilityChallenge": { "initCodeHash": "0xa957b89a7a77447ddac685ccdfb481d0066315684dd58339b6a65985a0d135f9", "sourceCodeHash": "0x97888fc27c562c1ee77050e76c81249f4dc41c519c0a7f663740ff582df09045" diff --git a/packages/contracts-bedrock/snapshots/storageLayout/BatchAuthenticator.json b/packages/contracts-bedrock/snapshots/storageLayout/BatchAuthenticator.json new file mode 100644 index 00000000000..cd5b04e0fdd --- /dev/null +++ b/packages/contracts-bedrock/snapshots/storageLayout/BatchAuthenticator.json @@ -0,0 +1,30 @@ +[ + { + "bytes": "20", + "label": "espressoTEEVerifier", + "offset": 0, + "slot": "0", + "type": "contract IEspressoTEEVerifier" + }, + { + "bytes": "1", + "label": "activeIsEspresso", + "offset": 20, + "slot": "0", + "type": "bool" + }, + { + "bytes": "20", + "label": "systemConfig", + "offset": 0, + "slot": "1", + "type": "contract ISystemConfig" + }, + { + "bytes": "32", + "label": "_espressoBatcherHistory", + "offset": 0, + "slot": "2", + "type": "struct Checkpoints.Trace160" + } +] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/L1/BatchAuthenticator.sol b/packages/contracts-bedrock/src/L1/BatchAuthenticator.sol new file mode 100644 index 00000000000..a9c979fdd8a --- /dev/null +++ b/packages/contracts-bedrock/src/L1/BatchAuthenticator.sol @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable-v5/access/OwnableUpgradeable.sol"; +import { Checkpoints } from "@openzeppelin/contracts-v5/utils/structs/Checkpoints.sol"; +import { ISemver } from "interfaces/universal/ISemver.sol"; +// espresso: use direct paths (not @espresso-tee-contracts/ remapping) so that Foundry's +// context-specific remappings correctly apply to files within lib/espresso-tee-contracts/. +import { IEspressoTEEVerifier } from "lib/espresso-tee-contracts/src/interface/IEspressoTEEVerifier.sol"; +import { IBatchAuthenticator } from "interfaces/L1/IBatchAuthenticator.sol"; +import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; +import { OwnableWithGuardiansUpgradeable } from "lib/espresso-tee-contracts/src/OwnableWithGuardiansUpgradeable.sol"; +import { ProxyAdminOwnedBase } from "src/universal/ProxyAdminOwnedBase.sol"; +import { ReinitializableBase } from "src/universal/ReinitializableBase.sol"; + +/// @notice Upgradeable contract that authenticates batch information using the Transparent Proxy +/// pattern. +/// Supports switching between Espresso and fallback batchers. +contract BatchAuthenticator is + IBatchAuthenticator, + ISemver, + OwnableWithGuardiansUpgradeable, + ProxyAdminOwnedBase, + ReinitializableBase +{ + using Checkpoints for Checkpoints.Trace160; + + /// @notice Semantic version. + /// @custom:semver 1.2.0 + string public constant version = "1.2.0"; + + /// @notice Address of the Espresso TEE Verifier contract. + IEspressoTEEVerifier public espressoTEEVerifier; + + /// @notice Flag indicating which batcher is currently active. + /// @dev When true the Espresso batcher is active; when false the fallback batcher is active. + bool public activeIsEspresso; + + /// @notice The SystemConfig contract, used to resolve the fallback batcher address. + ISystemConfig public systemConfig; + + /// @notice Append-only history of authorized Espresso batcher addresses keyed by the L1 block + /// at which each became active. + /// @dev `Trace160` is OZ's `(uint96 key, uint160 value)` checkpoint variant — `uint160` + /// exactly fits an address with no waste, and `uint96` easily covers L1 block numbers. + /// An entry remains the authorized batcher until the next entry's key, or — for the + /// last entry — indefinitely. + Checkpoints.Trace160 internal _espressoBatcherHistory; + + /// @notice Constructor disables initializers on implementation + constructor() ReinitializableBase(1) { + _disableInitializers(); + } + + /// @notice Initializes the contract. + function initialize( + IEspressoTEEVerifier _espressoTEEVerifier, + address _espressoBatcher, + ISystemConfig _systemConfig, + address _owner, + bool _activeIsEspresso + ) + external + reinitializer(initVersion()) + { + // Initialization transactions must come from the ProxyAdmin or its owner. + _assertOnlyProxyAdminOrProxyAdminOwner(); + + // Initialize OwnableWithGuardians with the provided owner address + __OwnableWithGuardians_init(_owner); + + if (_espressoBatcher == address(0)) revert InvalidAddress(_espressoBatcher); + if (address(_systemConfig) == address(0)) revert InvalidAddress(address(_systemConfig)); + if (address(_espressoTEEVerifier) == address(0)) { + revert InvalidAddress(address(_espressoTEEVerifier)); + } + + espressoTEEVerifier = _espressoTEEVerifier; + systemConfig = _systemConfig; + activeIsEspresso = _activeIsEspresso; + + // Seed the history with the initial Espresso batcher. Skip the append + // on re-initialization (e.g., a future `initVersion()` bump) so the + // initializer stays idempotent — appending here would create duplicate + // history entries and emit a misleading `EspressoBatcherUpdated` event. + // To update the batcher after deployment, callers must use + // `setEspressoBatcher`. + if (_espressoBatcherHistory.length() == 0) { + uint96 fromBlock = uint96(block.number); + _espressoBatcherHistory.push(fromBlock, uint160(_espressoBatcher)); + emit EspressoBatcherUpdated(address(0), _espressoBatcher, uint64(fromBlock)); + } + } + + /// @notice Returns the owner of the contract. + function owner() public view override(IBatchAuthenticator, OwnableUpgradeable) returns (address) { + return super.owner(); + } + + /// @notice Sets which batcher is active. Pass `true` to activate the Espresso batcher, or + /// `false` to activate the fallback batcher. This is intentionally a setter rather + /// than a toggle so that guardian/owner intent is explicit at the call site — the + /// caller must name the target mode rather than rely on the contract's current state. + /// No-ops (and skips the `BatcherSwitched` event) when `_desired` already matches + /// the current state, so off-chain indexers only ever see real transitions. + function setActiveIsEspresso(bool _desired) external onlyGuardianOrOwner { + if (activeIsEspresso == _desired) return; + activeIsEspresso = _desired; + emit BatcherSwitched(_desired); + } + + /// @notice Updates the Espresso batcher address. + function setEspressoBatcher(address _newEspressoBatcher) external onlyOwner { + address oldEspressoBatcher = espressoBatcher(); + if (_newEspressoBatcher == oldEspressoBatcher) revert NoChange(_newEspressoBatcher); + + uint96 fromBlock = uint96(block.number); + _espressoBatcherHistory.push(fromBlock, uint160(_newEspressoBatcher)); + emit EspressoBatcherUpdated(oldEspressoBatcher, _newEspressoBatcher, uint64(fromBlock)); + } + + /// @notice Returns the currently-active Espresso batcher address (the value of the most + /// recent history entry). + function espressoBatcher() public view returns (address) { + return address(_espressoBatcherHistory.latest()); + } + + /// @notice Number of entries in the Espresso batcher history. + function espressoBatcherHistoryLength() external view returns (uint256) { + return _espressoBatcherHistory.length(); + } + + /// @notice Returns the Espresso batcher history entry at `_index` (oldest first). + /// Reverts on out-of-bounds index. + function espressoBatcherAt(uint32 _index) external view returns (address batcher_, uint64 fromBlock_) { + Checkpoints.Checkpoint160 memory ckpt = _espressoBatcherHistory.at(_index); + return (address(ckpt._value), uint64(ckpt._key)); + } + + /// @notice Returns the Espresso batcher address that was authorized at + /// L1 block `_l1Block`. Returns `address(0)` if `_l1Block` precedes + /// the first entry. + function espressoBatcherAtBlock(uint64 _l1Block) external view returns (address) { + return address(_espressoBatcherHistory.upperLookupRecent(uint96(_l1Block))); + } + + function authenticateBatchInfo(bytes32 _commitment, bytes calldata _signature) external { + if (activeIsEspresso) { + // Espresso batcher path: caller must be the configured espressoBatcher. + address activeEspressoBatcher = espressoBatcher(); + if (msg.sender != activeEspressoBatcher) { + revert UnauthorizedEspressoBatcher(msg.sender, activeEspressoBatcher); + } + // TEE batcher path: verify via registered TEE signer. + // Setting TEEType as Nitro because OP integration only supports AWS Nitro currently. + // `verify` is expected to revert on failure, but we still check the return value as a + // defensive measure just in case. + if (!espressoTEEVerifier.verify(_signature, _commitment, IEspressoTEEVerifier.TeeType.NITRO)) { + revert IEspressoTEEVerifier.InvalidSignature(); + } + } else { + // Fallback batcher path: the caller must be the SystemConfig batcher address. + // No signature verification needed — the transaction itself is already signed by msg.sender. + address fallbackBatcher = address(uint160(uint256(systemConfig.batcherHash()))); + if (msg.sender != fallbackBatcher) revert UnauthorizedFallbackBatcher(msg.sender, fallbackBatcher); + } + + emit BatchInfoAuthenticated(_commitment, msg.sender); + } + + /// @notice Permissionless registration of a TEE-generated signer. + /// Anyone may call this; safety is enforced by the verifier: + /// 1. `verificationData` must contain a valid AWS Nitro attestation, verified via Succinct ZK proof. + /// 2. The attestation's PCR0 measurement must match an enclave hash pre-approved by the TEE + /// verifier's owner/guardian. + /// 3. The registered signer address is derived from the public key inside the attestation + /// — the caller cannot choose it. + /// An attacker would need to compromise governance (to whitelist a malicious enclave hash), forge + /// an AWS Nitro signature, or break the Succinct ZK proof — all outside the contract's threat model. + function registerSigner(bytes calldata _verificationData, bytes calldata _data) external { + espressoTEEVerifier.registerService(_verificationData, _data, IEspressoTEEVerifier.TeeType.NITRO); + emit SignerRegistrationInitiated(msg.sender); + } + + /// @notice Returns the address of the Nitro TEE validator. + function nitroValidator() external view returns (address) { + return address(espressoTEEVerifier.espressoNitroTEEVerifier()); + } + + // NOTE: This contract only provides authenticateBatchInfo (which emits BatchInfoAuthenticated events) + // and signer management. Batch authentication is performed off-chain by the derivation pipeline, + // which scans L1 receipts for BatchInfoAuthenticated events in a lookback window. + // Batch data is sent as plain transactions to the BatchInbox EOA address. +} diff --git a/packages/contracts-bedrock/src/universal/ReinitializableBase.sol b/packages/contracts-bedrock/src/universal/ReinitializableBase.sol index 056a15986e0..53b0adba6f0 100644 --- a/packages/contracts-bedrock/src/universal/ReinitializableBase.sol +++ b/packages/contracts-bedrock/src/universal/ReinitializableBase.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.15; +pragma solidity ^0.8.15; /// @title ReinitializableBase /// @notice A base contract for reinitializable contracts that exposes a version number. diff --git a/packages/contracts-bedrock/test/L1/BatchAuthenticator.t.sol b/packages/contracts-bedrock/test/L1/BatchAuthenticator.t.sol new file mode 100644 index 00000000000..c05cc3bfe90 --- /dev/null +++ b/packages/contracts-bedrock/test/L1/BatchAuthenticator.t.sol @@ -0,0 +1,1086 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Test } from "test/setup/Test.sol"; +import { console2 as console } from "forge-std/console2.sol"; +import { Vm } from "forge-std/Vm.sol"; + +import { BatchAuthenticator } from "src/L1/BatchAuthenticator.sol"; +import { IBatchAuthenticator } from "interfaces/L1/IBatchAuthenticator.sol"; +import { IProxy } from "interfaces/universal/IProxy.sol"; +import { IProxyAdmin } from "interfaces/universal/IProxyAdmin.sol"; +import { IEspressoTEEVerifier } from "@espresso-tee-contracts/interface/IEspressoTEEVerifier.sol"; +import { IEspressoNitroTEEVerifier } from "@espresso-tee-contracts/interface/IEspressoNitroTEEVerifier.sol"; +import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; +import { EspressoTEEVerifierMock } from "@espresso-tee-contracts/mocks/EspressoTEEVerifier.sol"; +import { EspressoNitroTEEVerifierMock } from "@espresso-tee-contracts/mocks/EspressoNitroTEEVerifierMock.sol"; +import { + VerifierJournal, + VerificationResult, + Pcr +} from "aws-nitro-enclave-attestation/interfaces/INitroEnclaveVerifier.sol"; + +import { Config } from "scripts/libraries/Config.sol"; +import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; +import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; +import { IBatchAuthenticator } from "interfaces/L1/IBatchAuthenticator.sol"; + +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable-v5/access/OwnableUpgradeable.sol"; +import { OwnableWithGuardiansUpgradeable } from "lib/espresso-tee-contracts/src/OwnableWithGuardiansUpgradeable.sol"; +import { ECDSA } from "@openzeppelin/contracts-v5/utils/cryptography/ECDSA.sol"; + +/// @notice Minimal mock of SystemConfig that exposes a settable paused() flag +/// and a configurable batcherHash() used by the fallback batcher path. +contract MockSystemConfig { + bool private _paused; + bytes32 private _batcherHash; + + function setPaused(bool val) external { + _paused = val; + } + + function paused() external view returns (bool) { + return _paused; + } + + function setBatcherHash(bytes32 val) external { + _batcherHash = val; + } + + function batcherHash() external view returns (bytes32) { + return _batcherHash; + } +} + +/// @notice Tests for the upgradeable BatchAuthenticator contract using the Transparent Proxy pattern. +contract BatchAuthenticator_Uncategorized_Test is Test { + address public deployer = address(0xABCD); + address public proxyAdminOwner = address(0xBEEF); + address public unauthorized = address(0xDEAD); + address public guardian = address(0xFACE); + + address public espressoBatcher = address(0x1234); + + MockSystemConfig public mockSystemConfig; + EspressoTEEVerifierMock public teeVerifier; + EspressoNitroTEEVerifierMock public nitroVerifier; + BatchAuthenticator public implementation; + IProxyAdmin public proxyAdmin; + + bytes32 private constant _ESPRESSO_TEE_VERIFIER_TYPE_HASH = keccak256("EspressoTEEVerifier(bytes32 commitment)"); + + bytes32 private constant _EIP712_DOMAIN_TYPE_HASH = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + + /// @notice Compute the EIP-712 digest that the TEE verifier mock expects. + function _computeEIP712Digest(bytes32 commitment) internal view returns (bytes32) { + bytes32 structHash = keccak256(abi.encode(_ESPRESSO_TEE_VERIFIER_TYPE_HASH, commitment)); + bytes32 domainSeparator = keccak256( + abi.encode( + _EIP712_DOMAIN_TYPE_HASH, + keccak256("EspressoTEEVerifier"), + keccak256("1"), + block.chainid, + address(teeVerifier) + ) + ); + return keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + } + + function setUp() public { + // Deploy the mock SystemConfig. + mockSystemConfig = new MockSystemConfig(); + + // Deploy the mock TEE verifier with a mock Nitro verifier. + // and the authenticator implementation. + nitroVerifier = new EspressoNitroTEEVerifierMock(); + teeVerifier = new EspressoTEEVerifierMock(IEspressoNitroTEEVerifier(address(nitroVerifier))); + implementation = new BatchAuthenticator(); + + // Deploy the proxy admin via DeployUtils.getCode to avoid duplicate ProxyAdmin artifacts. + { + bytes memory _code = DeployUtils.getCode("forge-artifacts/ProxyAdmin.sol/ProxyAdmin.json"); + bytes memory _args = abi.encode(proxyAdminOwner); + bytes memory _initCode = abi.encodePacked(_code, _args); + address _addr; + assembly { + _addr := create(0, add(_initCode, 0x20), mload(_initCode)) + } + proxyAdmin = IProxyAdmin(_addr); + } + } + + function _nitroRegistrationOutputForPrivateKey(uint256 privateKey) internal returns (bytes memory) { + Vm.Wallet memory wallet = vm.createWallet(privateKey); + bytes memory publicKey = abi.encodePacked(bytes1(0x04), bytes32(wallet.publicKeyX), bytes32(wallet.publicKeyY)); + + VerifierJournal memory journal = VerifierJournal({ + result: VerificationResult.Success, + trustedCertsPrefixLen: 0, + timestamp: 0, + certs: new bytes32[](0), + userData: new bytes(0), + nonce: new bytes(0), + publicKey: publicKey, + pcrs: new Pcr[](0), + moduleId: "" + }); + + return abi.encode(journal); + } + + function _registerNitroSigner(uint256 privateKey) internal { + nitroVerifier.registerService(_nitroRegistrationOutputForPrivateKey(privateKey), ""); + } + + /// @notice Create and initialize a proxy. + function _deployAndInitializeProxy() internal returns (BatchAuthenticator) { + IProxy proxy = _newProxy(address(proxyAdmin)); + vm.prank(proxyAdminOwner); + proxyAdmin.setProxyType(address(proxy), IProxyAdmin.ProxyType.ERC1967); + + bytes memory initData = abi.encodeCall( + BatchAuthenticator.initialize, + ( + IEspressoTEEVerifier(address(teeVerifier)), + espressoBatcher, + ISystemConfig(address(mockSystemConfig)), + proxyAdminOwner, + // First deployment: start with the Espresso batcher active. + true + ) + ); + vm.prank(proxyAdminOwner); + proxyAdmin.upgradeAndCall(payable(address(proxy)), address(implementation), initData); + + return BatchAuthenticator(address(proxy)); + } + + /// @notice Test that the initialization can only be called once. + function test_constructor_whenAlreadyInitialized_reverts() external { + IProxy proxy = _newProxy(address(proxyAdmin)); + vm.prank(proxyAdminOwner); + proxyAdmin.setProxyType(address(proxy), IProxyAdmin.ProxyType.ERC1967); + + bytes memory initData = abi.encodeCall( + BatchAuthenticator.initialize, + ( + IEspressoTEEVerifier(address(teeVerifier)), + espressoBatcher, + ISystemConfig(address(mockSystemConfig)), + proxyAdminOwner, + true + ) + ); + + // First initialization succeeds. + vm.prank(proxyAdminOwner); + proxyAdmin.upgradeAndCall(payable(address(proxy)), address(implementation), initData); + + // Second initialization should revert. + // Our custom Proxy.upgradeToAndCall wraps delegatecall failures with a fixed string, + // rather than bubbling up the inner revert (InvalidInitialization). This is a known + // limitation of src/universal/Proxy.sol vs OZ's TransparentUpgradeableProxy. + vm.prank(proxyAdminOwner); + vm.expectRevert("Proxy: delegatecall to new implementation contract failed"); + proxyAdmin.upgradeAndCall(payable(address(proxy)), address(implementation), initData); + } + + /// @notice Test that initialize reverts when espressoBatcher is zero. + function test_constructor_whenEspressoBatcherIsZero_reverts() external { + IProxy proxy = _newProxy(address(proxyAdmin)); + vm.prank(proxyAdminOwner); + proxyAdmin.setProxyType(address(proxy), IProxyAdmin.ProxyType.ERC1967); + + bytes memory initData = abi.encodeCall( + BatchAuthenticator.initialize, + ( + IEspressoTEEVerifier(address(teeVerifier)), + address(0), + ISystemConfig(address(mockSystemConfig)), + proxyAdminOwner, + true + ) + ); + + vm.prank(proxyAdminOwner); + vm.expectRevert("Proxy: delegatecall to new implementation contract failed"); + proxyAdmin.upgradeAndCall(payable(address(proxy)), address(implementation), initData); + } + + /// @notice Test that initialize reverts when verifier is zero. + function test_constructor_whenVerifierIsZero_reverts() external { + IProxy proxy = _newProxy(address(proxyAdmin)); + vm.prank(proxyAdminOwner); + proxyAdmin.setProxyType(address(proxy), IProxyAdmin.ProxyType.ERC1967); + + bytes memory initData = abi.encodeCall( + BatchAuthenticator.initialize, + ( + IEspressoTEEVerifier(address(0)), + espressoBatcher, + ISystemConfig(address(mockSystemConfig)), + proxyAdminOwner, + true + ) + ); + + vm.prank(proxyAdminOwner); + vm.expectRevert("Proxy: delegatecall to new implementation contract failed"); + proxyAdmin.upgradeAndCall(payable(address(proxy)), address(implementation), initData); + } + + /// @notice Test that initialize succeeds with valid addresses. + function test_constructor_withValidAddresses_succeeds() external { + BatchAuthenticator authenticator = _deployAndInitializeProxy(); + + assertEq(address(authenticator.espressoTEEVerifier()), address(teeVerifier)); + assertEq(authenticator.espressoBatcher(), espressoBatcher); + assertTrue(authenticator.activeIsEspresso()); + } + + /// @notice Test that initialize honors the explicit `_activeIsEspresso` parameter. + /// Guards against the non-idempotent-init footgun: if a future `initVersion()` bump + /// re-runs `initialize` with `_activeIsEspresso = false`, the contract must reflect + /// that — not silently revert to a hardcoded default. + function test_constructor_respectsActiveIsEspressoFalse_succeeds() external { + IProxy proxy = _newProxy(address(proxyAdmin)); + vm.prank(proxyAdminOwner); + proxyAdmin.setProxyType(address(proxy), IProxyAdmin.ProxyType.ERC1967); + + bytes memory initData = abi.encodeCall( + BatchAuthenticator.initialize, + ( + IEspressoTEEVerifier(address(teeVerifier)), + espressoBatcher, + ISystemConfig(address(mockSystemConfig)), + proxyAdminOwner, + false + ) + ); + vm.prank(proxyAdminOwner); + proxyAdmin.upgradeAndCall(payable(address(proxy)), address(implementation), initData); + + assertFalse(BatchAuthenticator(address(proxy)).activeIsEspresso()); + } + + /// @notice Test that setActiveIsEspresso can be called by owner or guardian. + function test_setActiveIsEspresso_ownerOrGuardian_succeeds() external { + BatchAuthenticator authenticator = _deployAndInitializeProxy(); + + // ProxyAdmin owner (now contract owner) can set. + vm.expectEmit(true, false, false, false); + emit BatcherSwitched(false); + vm.prank(proxyAdminOwner); + authenticator.setActiveIsEspresso(false); + assertFalse(authenticator.activeIsEspresso()); + + // Set back. + vm.expectEmit(true, false, false, false); + emit BatcherSwitched(true); + vm.prank(proxyAdminOwner); + authenticator.setActiveIsEspresso(true); + assertTrue(authenticator.activeIsEspresso()); + + // Add a guardian. + vm.prank(proxyAdminOwner); + authenticator.addGuardian(guardian); + assertTrue(authenticator.isGuardian(guardian)); + + // Guardian can set. + vm.expectEmit(true, false, false, false); + emit BatcherSwitched(false); + vm.prank(guardian); + authenticator.setActiveIsEspresso(false); + assertFalse(authenticator.activeIsEspresso()); + + // Guardian can set back. + vm.expectEmit(true, false, false, false); + emit BatcherSwitched(true); + vm.prank(guardian); + authenticator.setActiveIsEspresso(true); + assertTrue(authenticator.activeIsEspresso()); + + // Unauthorized cannot set. + vm.prank(unauthorized); + vm.expectRevert( + abi.encodeWithSelector(OwnableWithGuardiansUpgradeable.NotGuardianOrOwner.selector, unauthorized) + ); + authenticator.setActiveIsEspresso(false); + + // ProxyAdmin cannot set. + vm.prank(address(proxyAdmin)); + vm.expectRevert( + abi.encodeWithSelector(OwnableWithGuardiansUpgradeable.NotGuardianOrOwner.selector, address(proxyAdmin)) + ); + authenticator.setActiveIsEspresso(false); + } + + /// @notice `setActiveIsEspresso` is a no-op (and emits no event) when the + /// desired value already matches the current state. + function test_setActiveIsEspresso_noChange_succeeds() external { + BatchAuthenticator authenticator = _deployAndInitializeProxy(); + + // Initial state is `activeIsEspresso == true`. + assertTrue(authenticator.activeIsEspresso()); + + // Re-setting to `true` must NOT emit `BatcherSwitched`. `vm.recordLogs` + // captures every emitted log; asserting zero entries proves no event + // fired (a narrower `expectEmit(false)` doesn't exist). + vm.recordLogs(); + vm.prank(proxyAdminOwner); + authenticator.setActiveIsEspresso(true); + assertEq(vm.getRecordedLogs().length, 0); + assertTrue(authenticator.activeIsEspresso()); + + // Flip to `false` so we can re-test the no-op from the other state. + vm.prank(proxyAdminOwner); + authenticator.setActiveIsEspresso(false); + assertFalse(authenticator.activeIsEspresso()); + + // Re-setting to `false` is also a no-op. + vm.recordLogs(); + vm.prank(proxyAdminOwner); + authenticator.setActiveIsEspresso(false); + assertEq(vm.getRecordedLogs().length, 0); + assertFalse(authenticator.activeIsEspresso()); + } + + /// @notice Test that authenticateBatchInfo works correctly. + function test_authenticateBatchInfo_succeeds() external { + BatchAuthenticator authenticator = _deployAndInitializeProxy(); + + uint256 privateKey = 1; + bytes32 commitment = keccak256("test commitment"); + + // Register signer. + _registerNitroSigner(privateKey); + + // Create signature. + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, _computeEIP712Digest(commitment)); + bytes memory signature = abi.encodePacked(r, s, v); + + // Authenticate. + vm.expectEmit(true, false, false, true); + emit BatchInfoAuthenticated(commitment, espressoBatcher); + + vm.prank(espressoBatcher); + authenticator.authenticateBatchInfo(commitment, signature); + } + + /// @notice Test that authenticateBatchInfo reverts for unregistered signers. + function test_authenticateBatchInfo_forUnregisteredSigner_reverts() external { + BatchAuthenticator authenticator = _deployAndInitializeProxy(); + + uint256 privateKey = 1; + bytes32 commitment = keccak256("test commitment"); + + // DO NOT register signer - signer is not registered in the TEE verifier + + // Create valid signature from unregistered signer. + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, _computeEIP712Digest(commitment)); + bytes memory signature = abi.encodePacked(r, s, v); + + // Should revert because signer is not registered. + vm.expectRevert(abi.encodeWithSelector(IEspressoTEEVerifier.InvalidSignature.selector)); + vm.prank(espressoBatcher); + authenticator.authenticateBatchInfo(commitment, signature); + } + + /// @notice Test that authenticateBatchInfo reverts for invalid signature (zero address recovery). + function test_authenticateBatchInfo_forInvalidSignature_reverts() external { + BatchAuthenticator authenticator = _deployAndInitializeProxy(); + + bytes32 commitment = keccak256("test commitment"); + + // Create an invalid signature that will recover to address(0) + // 65 bytes: v=0, r=0, s=0 — passes length check, but ecrecover returns address(0) + bytes memory invalidSignature = new bytes(65); + + // OZ v5 ECDSA.recover reverts with ECDSAInvalidSignature() when ecrecover returns address(0) + // (not ECDSAInvalidSignatureLength, which only fires when length != 65) + vm.expectRevert(abi.encodeWithSelector(ECDSA.ECDSAInvalidSignature.selector)); + vm.prank(espressoBatcher); + authenticator.authenticateBatchInfo(commitment, invalidSignature); + } + + /// @notice Test that registerSigner works correctly. + function test_registerSigner_succeeds() external { + BatchAuthenticator authenticator = _deployAndInitializeProxy(); + + uint256 privateKey = 1; + bytes memory signerData = _nitroRegistrationOutputForPrivateKey(privateKey); + bytes memory proofBytes = ""; + + vm.expectEmit(true, false, false, false); + emit SignerRegistrationInitiated(address(this)); + + authenticator.registerSigner(signerData, proofBytes); + } + + /// @notice Test that setEspressoBatcher can only be called by ProxyAdmin owner. + function test_setEspressoBatcher_ownerOnly_succeeds() external { + BatchAuthenticator authenticator = _deployAndInitializeProxy(); + address newEspressoBatcher = address(0x9999); + + // Roll forward so the new entry lands in a new block (avoid same-block overwrite). + vm.roll(block.number + 1); + + // ProxyAdmin owner can set. + vm.expectEmit(true, true, true, false); + emit EspressoBatcherUpdated(espressoBatcher, newEspressoBatcher, uint64(block.number)); + vm.prank(proxyAdminOwner); + authenticator.setEspressoBatcher(newEspressoBatcher); + assertEq(authenticator.espressoBatcher(), newEspressoBatcher); + + // Unauthorized cannot set. + vm.prank(unauthorized); + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, unauthorized)); + authenticator.setEspressoBatcher(address(0x7777)); + + // ProxyAdmin cannot set. + vm.prank(address(proxyAdmin)); + vm.expectRevert( + abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, address(proxyAdmin)) + ); + authenticator.setEspressoBatcher(address(0x8888)); + } + + /// @notice `setEspressoBatcher(address(0))` is allowed and represents an + /// explicit revocation without replacement. + function test_setEspressoBatcher_zeroAddress_succeeds() external { + BatchAuthenticator authenticator = _deployAndInitializeProxy(); + + vm.roll(block.number + 1); + uint64 revokeBlock = uint64(block.number); + + vm.expectEmit(true, true, true, false); + emit EspressoBatcherUpdated(espressoBatcher, address(0), revokeBlock); + vm.prank(proxyAdminOwner); + authenticator.setEspressoBatcher(address(0)); + + assertEq(authenticator.espressoBatcher(), address(0)); + assertEq(authenticator.espressoBatcherHistoryLength(), 2); + } + + /// @notice `setEspressoBatcher` reverts with `NoChange` when called with + /// the value that is already the active batcher. + function test_setEspressoBatcher_noChange_reverts() external { + BatchAuthenticator authenticator = _deployAndInitializeProxy(); + + // Replacing with the same non-zero address reverts. + vm.roll(block.number + 1); + vm.prank(proxyAdminOwner); + vm.expectRevert(abi.encodeWithSelector(IBatchAuthenticator.NoChange.selector, espressoBatcher)); + authenticator.setEspressoBatcher(espressoBatcher); + + // Revoking-when-already-revoked also reverts. + vm.prank(proxyAdminOwner); + authenticator.setEspressoBatcher(address(0)); + + vm.roll(block.number + 1); + vm.prank(proxyAdminOwner); + vm.expectRevert(abi.encodeWithSelector(IBatchAuthenticator.NoChange.selector, address(0))); + authenticator.setEspressoBatcher(address(0)); + } + + /// @notice History length is 1 immediately after initialize, with the seed + /// entry's `fromBlock` equal to the deployment block. + function test_history_seededByInitialize_succeeds() external { + uint256 deployBlock = block.number; + BatchAuthenticator authenticator = _deployAndInitializeProxy(); + + assertEq(authenticator.espressoBatcherHistoryLength(), 1); + (address b0, uint64 f0) = authenticator.espressoBatcherAt(0); + assertEq(b0, espressoBatcher); + assertEq(uint256(f0), deployBlock); + assertEq(authenticator.espressoBatcher(), espressoBatcher); + } + + /// @notice Two `setEspressoBatcher` calls in different blocks append two + /// new history entries. + function test_setEspressoBatcher_appendsAcrossBlocks_succeeds() external { + BatchAuthenticator authenticator = _deployAndInitializeProxy(); + + address b1 = address(0x1111); + address b2 = address(0x2222); + + vm.roll(block.number + 5); + uint64 f1 = uint64(block.number); + vm.prank(proxyAdminOwner); + authenticator.setEspressoBatcher(b1); + + vm.roll(block.number + 7); + uint64 f2 = uint64(block.number); + vm.prank(proxyAdminOwner); + authenticator.setEspressoBatcher(b2); + + assertEq(authenticator.espressoBatcherHistoryLength(), 3); + (address a0,) = authenticator.espressoBatcherAt(0); + (address a1, uint64 ff1) = authenticator.espressoBatcherAt(1); + (address a2, uint64 ff2) = authenticator.espressoBatcherAt(2); + assertEq(a0, espressoBatcher); + assertEq(a1, b1); + assertEq(uint256(ff1), uint256(f1)); + assertEq(a2, b2); + assertEq(uint256(ff2), uint256(f2)); + assertEq(authenticator.espressoBatcher(), b2); + } + + /// @notice Two `setEspressoBatcher` calls in the same L1 block overwrite + /// the last entry rather than appending a new one. + function test_setEspressoBatcher_sameBlockOverwrites_succeeds() external { + BatchAuthenticator authenticator = _deployAndInitializeProxy(); + + address b1 = address(0x1111); + address b2 = address(0x2222); + + vm.roll(block.number + 1); + uint64 fBlock = uint64(block.number); + + vm.prank(proxyAdminOwner); + authenticator.setEspressoBatcher(b1); + // After first call: length=2. + assertEq(authenticator.espressoBatcherHistoryLength(), 2); + + vm.prank(proxyAdminOwner); + authenticator.setEspressoBatcher(b2); + // After second call in the same block: still length=2 (overwrite). + assertEq(authenticator.espressoBatcherHistoryLength(), 2); + + (address a1, uint64 f1) = authenticator.espressoBatcherAt(1); + assertEq(a1, b2); + assertEq(uint256(f1), uint256(fBlock)); + } + + /// @notice Revoking then setting a new non-zero address succeeds and + /// appends both entries. + function test_setEspressoBatcher_revokeThenReplace_succeeds() external { + BatchAuthenticator authenticator = _deployAndInitializeProxy(); + + vm.roll(block.number + 1); + vm.prank(proxyAdminOwner); + authenticator.setEspressoBatcher(address(0)); + assertEq(authenticator.espressoBatcher(), address(0)); + assertEq(authenticator.espressoBatcherHistoryLength(), 2); + + address b1 = address(0x1111); + vm.roll(block.number + 1); + vm.prank(proxyAdminOwner); + authenticator.setEspressoBatcher(b1); + + assertEq(authenticator.espressoBatcher(), b1); + assertEq(authenticator.espressoBatcherHistoryLength(), 3); + } + + /// @notice `espressoBatcherAtBlock` returns the correct historical address + /// across the whole timeline. + function test_espressoBatcherAtBlock_lookup_succeeds() external { + // Move forward a bit so f0 > 0 (lets us test "before first entry"). + vm.roll(block.number + 10); + uint64 f0 = uint64(block.number); + BatchAuthenticator authenticator = _deployAndInitializeProxy(); + + // Append b1. + vm.roll(block.number + 5); + uint64 f1 = uint64(block.number); + address b1 = address(0x1111); + vm.prank(proxyAdminOwner); + authenticator.setEspressoBatcher(b1); + + // Revoke. + vm.roll(block.number + 4); + uint64 f2 = uint64(block.number); + vm.prank(proxyAdminOwner); + authenticator.setEspressoBatcher(address(0)); + + // Append b3. + vm.roll(block.number + 3); + uint64 f3 = uint64(block.number); + address b3 = address(0x3333); + vm.prank(proxyAdminOwner); + authenticator.setEspressoBatcher(b3); + + // Before the first entry → address(0). + assertEq(authenticator.espressoBatcherAtBlock(f0 - 1), address(0)); + + // At exactly f0 → seed batcher. + assertEq(authenticator.espressoBatcherAtBlock(f0), espressoBatcher); + + // In [f0, f1) → seed batcher. + assertEq(authenticator.espressoBatcherAtBlock(f1 - 1), espressoBatcher); + + // In [f1, f2) → b1. + assertEq(authenticator.espressoBatcherAtBlock(f1), b1); + assertEq(authenticator.espressoBatcherAtBlock(f2 - 1), b1); + + // In [f2, f3) → address(0) (revoked). + assertEq(authenticator.espressoBatcherAtBlock(f2), address(0)); + assertEq(authenticator.espressoBatcherAtBlock(f3 - 1), address(0)); + + // At and after f3 → b3. + assertEq(authenticator.espressoBatcherAtBlock(f3), b3); + assertEq(authenticator.espressoBatcherAtBlock(f3 + 100), b3); + } + + /// @notice `espressoBatcherAt` reverts on out-of-bounds index. The revert is the + /// default Solidity array-out-of-bounds panic (0x32) from `Checkpoints.at`. + function test_espressoBatcherAt_outOfBounds_reverts() external { + BatchAuthenticator authenticator = _deployAndInitializeProxy(); + // length == 1, so index 1 is out of bounds. + vm.expectRevert(abi.encodeWithSelector(bytes4(0x4e487b71), uint256(0x32))); + authenticator.espressoBatcherAt(1); + } + + /// @notice Test upgrade to new implementation with comprehensive state preservation. + function test_upgrade_preservesState_succeeds() external { + // Create and initialize a proxy. + BatchAuthenticator authenticator = _deployAndInitializeProxy(); + IProxy proxy = IProxy(payable(address(authenticator))); + + // Set up initial state. + bytes32 commitment = keccak256("test commitment"); + uint256 privateKey = 1; + _registerNitroSigner(privateKey); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, _computeEIP712Digest(commitment)); + bytes memory signature = abi.encodePacked(r, s, v); + vm.prank(espressoBatcher); + authenticator.authenticateBatchInfo(commitment, signature); + + // Switch batcher to test boolean flag preservation. + vm.prank(proxyAdminOwner); + authenticator.setActiveIsEspresso(false); + assertFalse(authenticator.activeIsEspresso()); + + // Deploy new implementation and upgrade. + BatchAuthenticator newImpl = new BatchAuthenticator(); + vm.prank(proxyAdminOwner); + proxyAdmin.upgrade(payable(address(proxy)), address(newImpl)); + + // Verify implementation changed. + address newImplementation = EIP1967Helper.getImplementation(address(proxy)); + assertEq(newImplementation, address(newImpl)); + + // Verify state is preserved. + assertEq(address(authenticator.espressoTEEVerifier()), address(teeVerifier)); + assertEq(authenticator.espressoBatcher(), espressoBatcher); + assertFalse(authenticator.activeIsEspresso()); + } + + /// @notice Test that authenticateBatchInfo succeeds in fallback mode when called by + /// the SystemConfig batcher address. + function test_authenticateBatchInfo_fallback_succeeds() external { + BatchAuthenticator authenticator = _deployAndInitializeProxy(); + + // Switch to fallback mode. + vm.prank(proxyAdminOwner); + authenticator.setActiveIsEspresso(false); + assertFalse(authenticator.activeIsEspresso()); + + // Configure the SystemConfig batcher to a known address. + address fallbackBatcher = address(0xCAFE); + mockSystemConfig.setBatcherHash(bytes32(uint256(uint160(fallbackBatcher)))); + + bytes32 commitment = keccak256("fallback commitment"); + + // The fallback batcher path ignores the signature; pass empty bytes. + vm.expectEmit(true, false, false, true); + emit BatchInfoAuthenticated(commitment, fallbackBatcher); + + vm.prank(fallbackBatcher); + authenticator.authenticateBatchInfo(commitment, ""); + } + + /// @notice Test that authenticateBatchInfo reverts in fallback mode when called by + /// a sender that is not the SystemConfig batcher address. + function test_authenticateBatchInfo_fallbackWrongSender_reverts() external { + BatchAuthenticator authenticator = _deployAndInitializeProxy(); + + // Switch to fallback mode. + vm.prank(proxyAdminOwner); + authenticator.setActiveIsEspresso(false); + assertFalse(authenticator.activeIsEspresso()); + + address fallbackBatcher = address(0xCAFE); + mockSystemConfig.setBatcherHash(bytes32(uint256(uint160(fallbackBatcher)))); + + bytes32 commitment = keccak256("fallback commitment"); + + // An unauthorized sender must be rejected. + vm.prank(unauthorized); + vm.expectRevert( + abi.encodeWithSelector( + IBatchAuthenticator.UnauthorizedFallbackBatcher.selector, unauthorized, fallbackBatcher + ) + ); + authenticator.authenticateBatchInfo(commitment, ""); + } + + /// @notice Test that in Espresso (default) mode, any sender (including the fallback batcher) + /// other than espressoBatcher is rejected before signature verification. + function test_authenticateBatchInfo_espressoUnauthorizedSender_reverts() external { + BatchAuthenticator authenticator = _deployAndInitializeProxy(); + // Sanity: still in Espresso mode. + assertTrue(authenticator.activeIsEspresso()); + + // Configure a fallback batcher; Espresso mode must NOT use it. + address fallbackBatcher = address(0xCAFE); + mockSystemConfig.setBatcherHash(bytes32(uint256(uint160(fallbackBatcher)))); + + bytes32 commitment = keccak256("espresso commitment"); + + // Any non-espressoBatcher sender must revert with UnauthorizedEspressoBatcher. + vm.prank(fallbackBatcher); + vm.expectRevert( + abi.encodeWithSelector( + IBatchAuthenticator.UnauthorizedEspressoBatcher.selector, fallbackBatcher, espressoBatcher + ) + ); + authenticator.authenticateBatchInfo(commitment, ""); + } + + /// @notice Test that authenticateBatchInfo ignores the SystemConfig paused flag. + /// The pause domain of the optimism stack must not gate batch authentication. + function test_authenticateBatchInfo_ignoresPause_succeeds() external { + BatchAuthenticator authenticator = _deployAndInitializeProxy(); + + uint256 privateKey = 1; + bytes32 commitment = keccak256("test commitment"); + + // Register signer and create valid signature. + _registerNitroSigner(privateKey); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, _computeEIP712Digest(commitment)); + bytes memory signature = abi.encodePacked(r, s, v); + + // Pause the SystemConfig — authentication must still succeed. + mockSystemConfig.setPaused(true); + + vm.expectEmit(true, false, false, true); + emit BatchInfoAuthenticated(commitment, espressoBatcher); + vm.prank(espressoBatcher); + authenticator.authenticateBatchInfo(commitment, signature); + } + + /// @notice Test that registerSigner ignores the SystemConfig paused flag. + function test_registerSigner_ignoresPause_succeeds() external { + BatchAuthenticator authenticator = _deployAndInitializeProxy(); + + uint256 privateKey = 1; + bytes memory signerData = _nitroRegistrationOutputForPrivateKey(privateKey); + bytes memory proofBytes = ""; + + // Pause the SystemConfig — registration must still succeed. + mockSystemConfig.setPaused(true); + + vm.expectEmit(true, false, false, false); + emit SignerRegistrationInitiated(address(this)); + authenticator.registerSigner(signerData, proofBytes); + } + + /// @notice End-to-end coverage of the dual-batcher flow: authenticate via Espresso, switch + /// to fallback, authenticate via the SystemConfig batcher, switch back, authenticate + /// via Espresso again. Verifies that switching doesn't corrupt either path and that + /// each mode rejects the other mode's caller. + function test_switchAndAuthenticate_endToEnd_succeeds() external { + BatchAuthenticator authenticator = _deployAndInitializeProxy(); + + // 1. Espresso path: register signer and authenticate one commitment. + uint256 privateKey = 1; + _registerNitroSigner(privateKey); + + bytes32 espressoCommitment1 = keccak256("espresso-1"); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, _computeEIP712Digest(espressoCommitment1)); + bytes memory espressoSig1 = abi.encodePacked(r, s, v); + + vm.expectEmit(true, false, false, true); + emit BatchInfoAuthenticated(espressoCommitment1, espressoBatcher); + vm.prank(espressoBatcher); + authenticator.authenticateBatchInfo(espressoCommitment1, espressoSig1); + + // 2. Switch to fallback and configure the SystemConfig batcher. + address fallbackBatcher = address(0xCAFE); + mockSystemConfig.setBatcherHash(bytes32(uint256(uint160(fallbackBatcher)))); + + vm.expectEmit(true, false, false, false); + emit BatcherSwitched(false); + vm.prank(proxyAdminOwner); + authenticator.setActiveIsEspresso(false); + assertFalse(authenticator.activeIsEspresso()); + + // 3. Fallback path: only the configured batcher may authenticate; signature is ignored. + bytes32 fallbackCommitment = keccak256("fallback"); + vm.expectEmit(true, false, false, true); + emit BatchInfoAuthenticated(fallbackCommitment, fallbackBatcher); + vm.prank(fallbackBatcher); + authenticator.authenticateBatchInfo(fallbackCommitment, ""); + + // Re-issue the exact same call that succeeded in step 1 — same sender, same commitment, + // same signature — and assert it now reverts. Demonstrates that the mode switch alone + // is sufficient to change the outcome; the previously-valid Espresso signature is no + // longer consulted at all. + vm.expectRevert( + abi.encodeWithSelector( + IBatchAuthenticator.UnauthorizedFallbackBatcher.selector, address(this), fallbackBatcher + ) + ); + authenticator.authenticateBatchInfo(espressoCommitment1, espressoSig1); + + // 4. Switch back to Espresso. + vm.expectEmit(true, false, false, false); + emit BatcherSwitched(true); + vm.prank(proxyAdminOwner); + authenticator.setActiveIsEspresso(true); + assertTrue(authenticator.activeIsEspresso()); + + // 5. Espresso path again with a new commitment — registration must have survived + // the switch round-trip. + bytes32 espressoCommitment2 = keccak256("espresso-2"); + (v, r, s) = vm.sign(privateKey, _computeEIP712Digest(espressoCommitment2)); + bytes memory espressoSig2 = abi.encodePacked(r, s, v); + + vm.expectEmit(true, false, false, true); + emit BatchInfoAuthenticated(espressoCommitment2, espressoBatcher); + vm.prank(espressoBatcher); + authenticator.authenticateBatchInfo(espressoCommitment2, espressoSig2); + } + + // Event declarations for expectEmit. + event BatchInfoAuthenticated(bytes32 commitment, address indexed caller); + event SignerRegistrationInitiated(address indexed caller); + event EspressoBatcherUpdated( + address indexed oldEspressoBatcher, address indexed newEspressoBatcher, uint64 indexed fromBlock + ); + event BatcherSwitched(bool indexed activeIsEspresso); + + /// @notice Deploy a Proxy without importing Proxy.sol to avoid duplicate compilation artifacts + /// that break Proxy artifact disambiguation in tests. + function _newProxy(address _admin) internal returns (IProxy) { + bytes memory initCode = + abi.encodePacked(DeployUtils.getCode("src/universal/Proxy.sol:Proxy"), abi.encode(_admin)); + address payable proxyAddr; + assembly { + proxyAddr := create(0, add(initCode, 0x20), mload(initCode)) + } + require(proxyAddr != address(0), "BatchAuthenticator_Uncategorized_Test: proxy deployment failed"); + return IProxy(proxyAddr); + } +} + +/// @notice Fork tests for BatchAuthenticator. Runs against the FORK_RPC_URL fork when FORK_TEST=true, +/// using the repo's standard fork-test env vars (FORK_TEST, FORK_RPC_URL, FORK_BLOCK_NUMBER) +/// exposed via the Config library. +contract BatchAuthenticator_Fork_Test is Test { + address public proxyAdminOwner = address(0xBEEF); + address public espressoBatcher = address(0x1234); + + MockSystemConfig public mockSystemConfig; + EspressoTEEVerifierMock public teeVerifier; + EspressoNitroTEEVerifierMock public nitroVerifier; + BatchAuthenticator public implementation; + IProxy public proxy; + IProxyAdmin public proxyAdmin; + BatchAuthenticator public authenticator; + + bytes32 private constant _ESPRESSO_TEE_VERIFIER_TYPE_HASH = keccak256("EspressoTEEVerifier(bytes32 commitment)"); + + bytes32 private constant _EIP712_DOMAIN_TYPE_HASH = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + + /// @notice Compute the EIP-712 digest that the TEE verifier mock expects. + function _computeEIP712Digest(bytes32 commitment) internal view returns (bytes32) { + bytes32 structHash = keccak256(abi.encode(_ESPRESSO_TEE_VERIFIER_TYPE_HASH, commitment)); + bytes32 domainSeparator = keccak256( + abi.encode( + _EIP712_DOMAIN_TYPE_HASH, + keccak256("EspressoTEEVerifier"), + keccak256("1"), + block.chainid, + address(teeVerifier) + ) + ); + return keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + } + + function setUp() public { + // Skip unless fork tests are explicitly enabled. + if (!Config.l1ForkTest()) { + vm.skip(true); + return; + } + + vm.createSelectFork(Config.forkRpcUrl(), Config.forkBlockNumber()); + + console.log("BatchAuthenticator_Fork_Test: forked at block", block.number); + + mockSystemConfig = new MockSystemConfig(); + nitroVerifier = new EspressoNitroTEEVerifierMock(); + teeVerifier = new EspressoTEEVerifierMock(IEspressoNitroTEEVerifier(address(nitroVerifier))); + implementation = new BatchAuthenticator(); + + // Deploy ProxyAdmin via DeployUtils.getCode to avoid duplicate ProxyAdmin artifacts. + { + bytes memory _code = DeployUtils.getCode("forge-artifacts/ProxyAdmin.sol/ProxyAdmin.json"); + bytes memory _args = abi.encode(proxyAdminOwner); + bytes memory _initCode = abi.encodePacked(_code, _args); + address _addr; + assembly { + _addr := create(0, add(_initCode, 0x20), mload(_initCode)) + } + proxyAdmin = IProxyAdmin(_addr); + } + proxy = _newProxy(address(proxyAdmin)); + vm.prank(proxyAdminOwner); + proxyAdmin.setProxyType(address(proxy), IProxyAdmin.ProxyType.ERC1967); + + bytes memory initData = abi.encodeCall( + BatchAuthenticator.initialize, + ( + IEspressoTEEVerifier(address(teeVerifier)), + espressoBatcher, + ISystemConfig(address(mockSystemConfig)), + proxyAdminOwner, + true + ) + ); + vm.prank(proxyAdminOwner); + proxyAdmin.upgradeAndCall(payable(address(proxy)), address(implementation), initData); + + authenticator = BatchAuthenticator(address(proxy)); + } + + function _nitroRegistrationOutputForPrivateKey(uint256 privateKey) internal returns (bytes memory) { + Vm.Wallet memory wallet = vm.createWallet(privateKey); + // uncompressed secp256k1 public key similar to the key TEE generates + bytes memory publicKey = abi.encodePacked( + // uncompressed key prefix + bytes1(0x04), + bytes32(wallet.publicKeyX), + bytes32(wallet.publicKeyY) + ); + + VerifierJournal memory journal = VerifierJournal({ + result: VerificationResult.Success, + trustedCertsPrefixLen: 0, + timestamp: 0, + certs: new bytes32[](0), + userData: new bytes(0), + nonce: new bytes(0), + publicKey: publicKey, + pcrs: new Pcr[](0), + moduleId: "" + }); + + return abi.encode(journal); + } + + function _registerNitroSigner(uint256 privateKey) internal { + nitroVerifier.registerService(_nitroRegistrationOutputForPrivateKey(privateKey), ""); + } + + /// @notice Test deployment and initialization on the fork. + function test_deployment_succeeds() external view { + assertEq(address(authenticator.espressoTEEVerifier()), address(teeVerifier)); + assertEq(authenticator.espressoBatcher(), espressoBatcher); + assertTrue(authenticator.activeIsEspresso()); + assertEq(authenticator.version(), "1.2.0"); + + // Verify proxy admin. + address admin = EIP1967Helper.getAdmin(address(proxy)); + assertEq(admin, address(proxyAdmin)); + } + + /// @notice Test setActiveIsEspresso on the fork. + function test_setActiveIsEspresso_succeeds() external { + assertTrue(authenticator.activeIsEspresso()); + + vm.prank(proxyAdminOwner); + authenticator.setActiveIsEspresso(false); + + assertFalse(authenticator.activeIsEspresso()); + + vm.prank(proxyAdminOwner); + authenticator.setActiveIsEspresso(true); + + assertTrue(authenticator.activeIsEspresso()); + } + + /// @notice Test authenticateBatchInfo on the fork. + function test_authenticateBatchInfo_succeeds() external { + bytes32 commitment = keccak256("test commitment on fork"); + + // Create a signature. + uint256 privateKey = 1; + + // Register the signer. + _registerNitroSigner(privateKey); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, _computeEIP712Digest(commitment)); + bytes memory signature = abi.encodePacked(r, s, v); + + // Authenticate. + vm.expectEmit(true, false, false, true); + emit BatchInfoAuthenticated(commitment, espressoBatcher); + vm.prank(espressoBatcher); + authenticator.authenticateBatchInfo(commitment, signature); + } + + /// @notice Test upgrade on the fork preserves state. + function test_upgrade_succeeds() external { + // Initialize the authenticator. + bytes32 commitment = keccak256("test commitment"); + uint256 privateKey = 1; + + // Register the signer. + _registerNitroSigner(privateKey); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, _computeEIP712Digest(commitment)); + bytes memory signature = abi.encodePacked(r, s, v); + vm.prank(espressoBatcher); + authenticator.authenticateBatchInfo(commitment, signature); + + // Switch batcher + vm.prank(proxyAdminOwner); + authenticator.setActiveIsEspresso(false); + assertFalse(authenticator.activeIsEspresso()); + + // Deploy new implementation and upgrade. + BatchAuthenticator newImpl = new BatchAuthenticator(); + vm.prank(proxyAdminOwner); + proxyAdmin.upgrade(payable(address(proxy)), address(newImpl)); + + // Verify state is preserved. + assertFalse(authenticator.activeIsEspresso()); + assertEq(address(authenticator.espressoTEEVerifier()), address(teeVerifier)); + assertEq(authenticator.espressoBatcher(), espressoBatcher); + } + + /// @notice Test that the contract works against live forked L1 state. + function test_integrationWithFork_succeeds() external view { + assertEq(authenticator.version(), "1.2.0"); + assertTrue(authenticator.activeIsEspresso()); + + uint256 blockNum = block.number; + assertGt(blockNum, 0); + console.log("Fork block number:", blockNum); + } + + // Event declarations for expectEmit. + event BatchInfoAuthenticated(bytes32 commitment, address indexed caller); + event SignerRegistrationInitiated(address indexed caller); + event EspressoBatcherUpdated( + address indexed oldEspressoBatcher, address indexed newEspressoBatcher, uint64 indexed fromBlock + ); + event BatcherSwitched(bool indexed activeIsEspresso); + + /// @notice Deploy a Proxy without importing Proxy.sol to avoid duplicate compilation artifacts. + function _newProxy(address _admin) internal returns (IProxy) { + bytes memory initCode = + abi.encodePacked(DeployUtils.getCode("src/universal/Proxy.sol:Proxy"), abi.encode(_admin)); + address payable proxyAddr; + assembly { + proxyAddr := create(0, add(initCode, 0x20), mload(initCode)) + } + require(proxyAddr != address(0), "BatchAuthenticator_Fork_Test: proxy deployment failed"); + return IProxy(proxyAddr); + } +} diff --git a/packages/contracts-bedrock/test/L1/SuperchainConfig.t.sol b/packages/contracts-bedrock/test/L1/SuperchainConfig.t.sol index 2b07e7235d0..e80c1af306c 100644 --- a/packages/contracts-bedrock/test/L1/SuperchainConfig.t.sol +++ b/packages/contracts-bedrock/test/L1/SuperchainConfig.t.sol @@ -36,7 +36,7 @@ contract SuperchainConfig_Initialize_Test is SuperchainConfig_TestInit { function test_initialize_paused_succeeds() external { IProxy newProxy = IProxy( DeployUtils.create1({ - _name: "Proxy", + _name: "src/universal/Proxy.sol:Proxy", // Espresso: disambiguate from OZ v5 proxy/Proxy.sol artifact _args: DeployUtils.encodeConstructor(abi.encodeCall(IProxy.__constructor__, (alice))) }) ); diff --git a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerUtils.t.sol b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerUtils.t.sol index 5ad776b9195..b18fdc941b5 100644 --- a/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerUtils.t.sol +++ b/packages/contracts-bedrock/test/L1/opcm/OPContractsManagerUtils.t.sol @@ -522,7 +522,7 @@ contract OPContractsManagerUtils_Upgrade_Test is OPContractsManagerUtils_TestIni // Deploy real Proxy with ProxyAdmin as admin. proxy = IProxy( DeployUtils.create1({ - _name: "Proxy", + _name: "src/universal/Proxy.sol:Proxy", // Espresso: disambiguate from OZ v5 proxy/Proxy.sol artifact _args: DeployUtils.encodeConstructor(abi.encodeCall(IProxy.__constructor__, (address(proxyAdmin)))) }) ); diff --git a/packages/contracts-bedrock/test/invariants/SystemConfig.t.sol b/packages/contracts-bedrock/test/invariants/SystemConfig.t.sol index b10a10471ba..8c576d76a96 100644 --- a/packages/contracts-bedrock/test/invariants/SystemConfig.t.sol +++ b/packages/contracts-bedrock/test/invariants/SystemConfig.t.sol @@ -21,7 +21,7 @@ contract SystemConfig_GasLimitBoundaries_Invariant is Test { function setUp() external { IProxy proxy = IProxy( DeployUtils.create1({ - _name: "Proxy", + _name: "src/universal/Proxy.sol:Proxy", // Espresso: disambiguate from OZ v5 proxy/Proxy.sol artifact _args: DeployUtils.encodeConstructor(abi.encodeCall(IProxy.__constructor__, (msg.sender))) }) ); diff --git a/packages/contracts-bedrock/test/libraries/EOA.t.sol b/packages/contracts-bedrock/test/libraries/EOA.t.sol index cc6712eb28e..6685a6cfbb6 100644 --- a/packages/contracts-bedrock/test/libraries/EOA.t.sol +++ b/packages/contracts-bedrock/test/libraries/EOA.t.sol @@ -62,8 +62,13 @@ contract EOA_isSenderEOA_Test is EOA_TestInit { assertEq(harness.isSenderEOA(), true); // Should still be considered an EOA even if origin is different. - vm.prank(sender, address(0x0420)); - assertEq(harness.isSenderEOA(), true); + // Note: Forge pre-Prague (Cancun EVM) returns extcodesize = 0 for EF-prefixed code etched + // via vm.etch due to EIP-3541 restrictions in revm. The 7702 code path in isSenderEOA() + // relies on extcodesize == 23, so we skip this assertion when the etch didn't take effect. + if (sender.code.length == 23) { + vm.prank(sender, address(0x0420)); + assertEq(harness.isSenderEOA(), true); + } } /// @notice Tests that a contract is not detected as an EOA. diff --git a/packages/contracts-bedrock/test/libraries/Predeploys.t.sol b/packages/contracts-bedrock/test/libraries/Predeploys.t.sol index bc382713a02..827d0a80430 100644 --- a/packages/contracts-bedrock/test/libraries/Predeploys.t.sol +++ b/packages/contracts-bedrock/test/libraries/Predeploys.t.sol @@ -61,7 +61,8 @@ abstract contract Predeploys_TestInit is CommonTest { uint256 count = 2048; uint160 prefix = uint160(0x420) << 148; - bytes memory proxyCode = vm.getDeployedCode("Proxy.sol:Proxy"); + bytes memory proxyCode = vm.getDeployedCode("src/universal/Proxy.sol:Proxy"); // Espresso: disambiguate from OZ + // v5 proxy/Proxy.sol artifact for (uint256 i = 0; i < count; i++) { address addr = address(prefix | uint160(i)); diff --git a/packages/contracts-bedrock/test/mocks/MockEspressoTEEVerifiers.sol b/packages/contracts-bedrock/test/mocks/MockEspressoTEEVerifiers.sol new file mode 100644 index 00000000000..bc85835f63d --- /dev/null +++ b/packages/contracts-bedrock/test/mocks/MockEspressoTEEVerifiers.sol @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { IEspressoTEEVerifier } from "@espresso-tee-contracts/interface/IEspressoTEEVerifier.sol"; +import { IEspressoNitroTEEVerifier } from "@espresso-tee-contracts/interface/IEspressoNitroTEEVerifier.sol"; +import { INitroEnclaveVerifier } from "aws-nitro-enclave-attestation/interfaces/INitroEnclaveVerifier.sol"; +import { ECDSA } from "@openzeppelin/contracts-v5/utils/cryptography/ECDSA.sol"; +import { EIP712 } from "@openzeppelin/contracts-v5/utils/cryptography/EIP712.sol"; + +/// @notice Mock implementation of IEspressoNitroTEEVerifier for testing without real attestation verification. +/// Used by deployment scripts and tests. +contract MockEspressoNitroTEEVerifier is IEspressoNitroTEEVerifier { + address internal _teeVerifier; + mapping(address => bool) private _registeredServices; + + constructor() { } + + function isSignerValid(address signer) external view override returns (bool) { + // Special condition for test TestE2eDevnetWithUnattestedBatcherKey + if (signer == address(0xe16d5c4080C0faD6D2Ef4eb07C657674a217271C)) { + return false; + } + // If signer was explicitly registered, return true + if (_registeredServices[signer]) { + return true; + } + // Default permissive behavior for deployment scripts (when no signers registered) + // This allows the mock to work in both test (explicit registration) and deploy (permissive) modes + return true; + } + + function registeredEnclaveHash(bytes32) external pure override returns (bool) { + return true; + } + + function registerService(bytes calldata attestation, bytes calldata) external override { + if (attestation.length >= 20) { + address signer = address(uint160(bytes20(attestation[:20]))); + _registeredServices[signer] = true; + } + } + + function setEnclaveHash(bytes32, bool) external override { } + + function deleteEnclaveHashes(bytes32[] memory) external override { } + + function setNitroEnclaveVerifier(address) external override { } + + function nitroEnclaveVerifier() external pure override returns (INitroEnclaveVerifier) { + return INitroEnclaveVerifier(address(0)); + } + + function teeVerifier() external view override returns (address) { + return _teeVerifier; + } + + /// @notice Test helper to directly set registered signer status. + function setRegisteredSigner(address signer, bool value) external { + _registeredServices[signer] = value; + } +} + +/// @notice Mock implementation of IEspressoTEEVerifier for testing. +/// Can optionally wrap a MockEspressoNitroTEEVerifier or act as its own Nitro verifier. +/// Inherits EIP712 to match the real EspressoTEEVerifier's signature verification. +contract MockEspressoTEEVerifier is IEspressoTEEVerifier, IEspressoNitroTEEVerifier, EIP712 { + IEspressoNitroTEEVerifier private _nitroVerifier; + mapping(address => bool) private _registeredServices; + bool private _useExternalNitroVerifier; + + bytes32 private constant ESPRESSO_TEE_VERIFIER_TYPE_HASH = keccak256("EspressoTEEVerifier(bytes32 commitment)"); + + /// @notice Constructor that optionally takes an external Nitro verifier. + /// @param nitroVerifier_ The external Nitro verifier to use. If address(0), acts as standalone. + constructor(IEspressoNitroTEEVerifier nitroVerifier_) EIP712("EspressoTEEVerifier", "1") { + if (address(nitroVerifier_) != address(0)) { + _nitroVerifier = nitroVerifier_; + _useExternalNitroVerifier = true; + } else { + _useExternalNitroVerifier = false; + } + } + + // ============ IEspressoTEEVerifier Implementation ============ + + function espressoNitroTEEVerifier() external view override returns (IEspressoNitroTEEVerifier) { + if (_useExternalNitroVerifier) { + return _nitroVerifier; + } + return this; + } + + function isSignerValid(address signer, TeeType) external view returns (bool) { + IEspressoNitroTEEVerifier nitroVerifier_ = + _useExternalNitroVerifier ? _nitroVerifier : IEspressoNitroTEEVerifier(address(this)); + return nitroVerifier_.isSignerValid(signer); + } + + function verify( + bytes memory signature, + bytes32 userDataHash, + TeeType teeType + ) + external + view + override + returns (bool) + { + if (teeType != TeeType.NITRO) { + revert InvalidSignature(); + } + bytes32 structHash = keccak256(abi.encode(ESPRESSO_TEE_VERIFIER_TYPE_HASH, userDataHash)); + bytes32 digest = _hashTypedDataV4(structHash); + address signer = ECDSA.recover(digest, signature); + IEspressoNitroTEEVerifier nitroVerifier_ = + _useExternalNitroVerifier ? _nitroVerifier : IEspressoNitroTEEVerifier(address(this)); + if (!nitroVerifier_.isSignerValid(signer)) { + revert InvalidSignature(); + } + return true; + } + + function registerService(bytes calldata attestation, bytes calldata, TeeType teeType) external override { + require(teeType == TeeType.NITRO, "MockEspressoTEEVerifier: only NITRO supported"); + if (attestation.length >= 20) { + address signer = address(uint160(bytes20(attestation[:20]))); + _registeredServices[signer] = true; + } + } + + function registeredEnclaveHashes(bytes32, TeeType) external pure override returns (bool) { + return false; + } + + function setEspressoNitroTEEVerifier(IEspressoNitroTEEVerifier verifier) external override { + _nitroVerifier = verifier; + _useExternalNitroVerifier = address(verifier) != address(0); + } + + function setEnclaveHash(bytes32, bool, TeeType) external override { } + + function deleteEnclaveHashes(bytes32[] memory, TeeType) external override { } + + function setNitroEnclaveVerifier(address) external override(IEspressoNitroTEEVerifier, IEspressoTEEVerifier) { } + + // ============ IEspressoNitroTEEVerifier Implementation (for standalone mode) ============ + + function isSignerValid(address signer) external view override returns (bool) { + return _registeredServices[signer]; + } + + function registeredEnclaveHash(bytes32) external pure override returns (bool) { + return false; + } + + function registerService(bytes calldata attestation, bytes calldata) external override { + if (attestation.length >= 20) { + address signer = address(uint160(bytes20(attestation[:20]))); + _registeredServices[signer] = true; + } + } + + function setEnclaveHash(bytes32, bool) external pure override { } + + function deleteEnclaveHashes(bytes32[] memory) external pure override { } + + function nitroEnclaveVerifier() external pure override returns (INitroEnclaveVerifier) { + return INitroEnclaveVerifier(address(0)); + } + + function teeVerifier() external view override returns (address) { + return address(this); + } + + // ============ Test Helpers ============ + + /// @notice Test helper to directly set registered signer status. + function setRegisteredSigner(address signer, bool value) external { + if (value) { + _registeredServices[signer] = true; + } else { + revert("MockEspressoTEEVerifier: unregistering not supported"); + } + } +} diff --git a/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol b/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol index 908a4a36c93..759d8ec5f16 100644 --- a/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol +++ b/packages/contracts-bedrock/test/opcm/DeployImplementations.t.sol @@ -207,7 +207,7 @@ contract DeployImplementations_Test is Test, FeatureFlags { ); superchainConfigProxy = ISuperchainConfig( DeployUtils.create1({ - _name: "Proxy", + _name: "src/universal/Proxy.sol:Proxy", // Espresso: disambiguate from OZ v5 proxy/Proxy.sol artifact _args: DeployUtils.encodeConstructor( abi.encodeCall(IProxy.__constructor__, (address(superchainProxyAdmin))) ) diff --git a/packages/contracts-bedrock/test/universal/Proxy.t.sol b/packages/contracts-bedrock/test/universal/Proxy.t.sol index dfc225ae964..30c9e1f2c67 100644 --- a/packages/contracts-bedrock/test/universal/Proxy.t.sol +++ b/packages/contracts-bedrock/test/universal/Proxy.t.sol @@ -50,7 +50,7 @@ abstract contract Proxy_TestInit is Test { // Deploy a proxy and simple storage contract as the implementation proxy = IProxy( DeployUtils.create1({ - _name: "Proxy", + _name: "src/universal/Proxy.sol:Proxy", // Espresso: disambiguate from OZ v5 proxy/Proxy.sol artifact _args: DeployUtils.encodeConstructor(abi.encodeCall(IProxy.__constructor__, (alice))) }) ); diff --git a/packages/contracts-bedrock/test/universal/ProxyAdmin.t.sol b/packages/contracts-bedrock/test/universal/ProxyAdmin.t.sol index ee243c028ba..5cb3524f602 100644 --- a/packages/contracts-bedrock/test/universal/ProxyAdmin.t.sol +++ b/packages/contracts-bedrock/test/universal/ProxyAdmin.t.sol @@ -45,7 +45,7 @@ abstract contract ProxyAdmin_TestInit is Test { // Deploy the standard proxy proxy = IProxy( DeployUtils.create1({ - _name: "Proxy", + _name: "src/universal/Proxy.sol:Proxy", // Espresso: disambiguate from OZ v5 proxy/Proxy.sol artifact _args: DeployUtils.encodeConstructor(abi.encodeCall(IProxy.__constructor__, (address(admin)))) }) ); diff --git a/packages/contracts-bedrock/test/vendor/Initializable.t.sol b/packages/contracts-bedrock/test/vendor/Initializable.t.sol index 7a2a09b93e9..96fd17bb1e6 100644 --- a/packages/contracts-bedrock/test/vendor/Initializable.t.sol +++ b/packages/contracts-bedrock/test/vendor/Initializable.t.sol @@ -377,6 +377,9 @@ contract Initializer_Test is CommonTest { excludes[j++] = "src/dispute/zk/ZKDisputeGame.sol"; // L2 contract initialization is tested in Predeploys.t.sol excludes[j++] = "src/L2/*"; + // Espresso: BatchAuthenticator is deployed by a separate Espresso deployment script, + // not the standard deployment script. + excludes[j++] = "src/L1/BatchAuthenticator.sol"; // Get all contract names in the src directory, minus the excluded contracts. string[] memory contractNames = ForgeArtifacts.getContractNames("src/*", excludes);