diff --git a/boba_examples/turing-hello-world/contracts/HelloTuring.sol b/boba_examples/turing-hello-world/contracts/HelloTuring.sol index 0e27ca62cd..93da9a8d3b 100644 --- a/boba_examples/turing-hello-world/contracts/HelloTuring.sol +++ b/boba_examples/turing-hello-world/contracts/HelloTuring.sol @@ -37,9 +37,36 @@ contract HelloTuring { return product; } + // Tests error handling when a contract tries to make more than one call + // per Tx, using the "multFloatNumbers" offchain handler. + // Multiple calls from the same stack depth are permitted for legacy reasons + // but should not be used in new code. + function callTwice(string memory _url, string memory a, string memory b, uint32 mode) + public returns (uint256) { + + bytes memory encRequest; + bytes memory encResponse; + + if (mode == 2) { + // Call from a different stack depth + HelloTuring(address(this)).callTwice(_url, a, b, 0); + } else if (mode == 1) { + // Call from same stack depth + encRequest = abi.encode(b); + encResponse = myHelper.TuringTxV2(_url, encRequest); + } + + encRequest = abi.encode(a); + encResponse = myHelper.TuringTxV2(_url, encRequest); + + uint256 product = abi.decode(encResponse, (uint256)); + emit MultFloatNumbers(product); + return product; + } + // Tests a Turing method which returns a variable-length array. // The parameters 'a' and 'b' are passed in the request, returing - // an array of 'b' elements each with value 'a'. This function + // an array of 'a' elements each with value 'b'. This function // adds all of the returned values and returns a total of (a*b) function multArray(string memory _url, uint256 a, uint256 b) public { diff --git a/boba_examples/turing-hello-world/test/local-webserver.ts b/boba_examples/turing-hello-world/test/local-webserver.ts index 096bf454bf..8c47c72ef9 100644 --- a/boba_examples/turing-hello-world/test/local-webserver.ts +++ b/boba_examples/turing-hello-world/test/local-webserver.ts @@ -1,5 +1,4 @@ -import { BigNumber, Contract, ContractFactory, providers, Wallet, utils } from 'ethers' -import { ethers } from 'hardhat' +import { Contract, ContractFactory, providers, Wallet, utils } from 'ethers' import chai, { expect } from 'chai' import { solidity } from 'ethereum-waffle' chai.use(solidity) @@ -204,7 +203,7 @@ if (hre.network.name === "boba_local") { it('Should fund your Turing helper contract in turingCredit', async () => { - const depositAmount = utils.parseEther('0.5') + const depositAmount = utils.parseEther('1.5') const preBalance = await turingCredit.prepaidBalance(helper.address) const bobaBalance = await L2BOBAToken.balanceOf(deployerWallet.address) @@ -280,15 +279,26 @@ if (hre.network.name === "boba_local") { } }) - it("should charge extra gas for L1 calldata storage", async() => { - const g1 = (await hello.estimateGas.multArray(urlStr2, 1, 10, gasOverride)).toNumber() - const g2 = (await hello.estimateGas.multArray(urlStr2, 101, 10, gasOverride)).toNumber() + it("should charge extra gas for L1 calldata storage", async() => { + const eg1 = (await hello.estimateGas.multArray(urlStr2, 1, 10, gasOverride)).toNumber() + let tx1 = await hello.multArray(urlStr2, 1, 10, gasOverride) + const res1 = await tx1.wait() + expect(res1).to.be.ok + const ag1 = res1.gasUsed.toNumber() + + const eg2 = (await hello.estimateGas.multArray(urlStr2, 101, 10, gasOverride)).toNumber() + let tx2 = await hello.multArray(urlStr2, 101, 10, gasOverride) + const res2 = await tx2.wait() + expect(res2).to.be.ok + const ag2 = res2.gasUsed.toNumber() // Larger calldata costs more gas inside the contract itself. We need to test for // additional usage on top of this from the L1 calldata calculation. The exact value - // depends on the L1 gas price so this test doesn't look for a specific number - expect (g2 - g1).to.be.above(110000) - }) + // depends on the L1 gas price so this test doesn't look for a specific number. + // Actual tx is a different code path than estimateGas so both are checked. + expect (eg2 - eg1).to.be.above(110000) + expect (ag2 - ag1).to.be.above(110000) + }) it("should support a large response", async() => { const nElem = 2038 @@ -304,11 +314,57 @@ if (hre.network.name === "boba_local") { expect(result).to.equal(nElem * 55) }) + it("should allow repeated calls (legacy support)", async () => { + await hello.estimateGas.callTwice(urlStr, '6', '6', 1, gasOverride) + let tr = await hello.callTwice(urlStr, '6', '6', 1, gasOverride) + const res = await tr.wait() + expect(res).to.be.ok + + const ev = res.events.find(e => e.event === "MultFloatNumbers") + const result = parseInt(ev.data.slice(-64), 16) / 100 + expect(result.toFixed(5)).to.equal('904.78000') + }) + + it("should disallow repeated calls with different input", async () => { + try { + await hello.estimateGas.callTwice(urlStr, '6', '7', 1, gasOverride) + expect(1).to.equal(0) + } catch (e) { + // generic error code indicating that a tx reverted + expect(e.error.toString()).to.contain("gas required exceeds allowance") + } + + try { + let tr = await hello.callTwice(urlStr, '6', '7', 1, gasOverride) + const res = await tr.wait() + expect(1).to.equal(0) + } catch (e) { + expect(e.toString()).to.contain("transaction failed") + } + }) + + it("should disallow repeated calls at different depth", async () => { + try { + await hello.estimateGas.callTwice(urlStr, '8', '8', 2, gasOverride) + expect(1).to.equal(0) + } catch (e) { + expect(e.error.toString()).to.contain("gas required exceeds allowance") + } + try { + let tr = await hello.callTwice(urlStr, '8', '8', 2, gasOverride) + const res = await tr.wait() + expect(1).to.equal(0) + } catch (e) { + expect(e.toString()).to.contain("transaction failed") + } + }) + it("final balance", async () => { const postBalance = await turingCredit.prepaidBalance( helper.address ) - //expect(postBalance).to.equal( utils.parseEther('0.5')) + // Change expected value if tests are added or skipped above + expect(postBalance).to.equal(utils.parseEther('0.7')) }) }) } else { diff --git a/integration-tests/test/eth-l2/turing.spec.ts b/integration-tests/test/eth-l2/turing.spec.ts index 42438c9116..0eca6b2363 100644 --- a/integration-tests/test/eth-l2/turing.spec.ts +++ b/integration-tests/test/eth-l2/turing.spec.ts @@ -73,45 +73,48 @@ describe('Turing 256 Bit Random Number Test', async () => { ).attach(L1StandardBridgeAddress) /* eslint-disable */ const http = require('http') - const ip = require("ip") + const ip = require('ip') // start local server - const server = module.exports = http.createServer(async function (req, res) { - - if (req.headers['content-type'] === 'application/json') { - - let body = ''; - - req.on('data', function (chunk) { - body += chunk.toString() - }) - - req.on('end', async function () { - const jsonBody = JSON.parse(body) - const input = JSON.parse(body).params[0] - let result - - const args = utils.defaultAbiCoder.decode(['uint256','uint256'], input) - if (req.url === "/echo") { - const randomPrice = Math.floor(Math.random() * 1000) - result = input - let response = { - "jsonrpc": "2.0", - "id": jsonBody.id, - "result": result + const server = (module.exports = http + .createServer(async function (req, res) { + if (req.headers['content-type'] === 'application/json') { + let body = '' + + req.on('data', function (chunk) { + body += chunk.toString() + }) + + req.on('end', async function () { + const jsonBody = JSON.parse(body) + const input = JSON.parse(body).params[0] + let result + + const args = utils.defaultAbiCoder.decode( + ['uint256', 'uint256'], + input + ) + if (req.url === '/echo') { + const randomPrice = Math.floor(Math.random() * 1000) + result = input + let response = { + jsonrpc: '2.0', + id: jsonBody.id, + result: result, + } + res.end(JSON.stringify(response)) + server.emit('success', body) + } else { + res.writeHead(400, { 'Content-Type': 'text/plain' }) + res.end('Bad request') } - res.end(JSON.stringify(response)) - server.emit('success', body) - } else { - res.writeHead(400, { 'Content-Type': 'text/plain' }) - res.end('Bad request') - } - }); - } else { - console.log("Other request:", req) - res.writeHead(400, { 'Content-Type': 'text/plain' }) - res.end('Expected content-type: application/json') - } - }).listen(apiPort) + }) + } else { + console.log('Other request:', req) + res.writeHead(400, { 'Content-Type': 'text/plain' }) + res.end('Expected content-type: application/json') + } + }) + .listen(apiPort)) URL = `http://${ip.address()}:${apiPort}/echo` /* eslint-enable */ }) @@ -264,16 +267,26 @@ describe('Turing 256 Bit Random Number Test', async () => { } catch (e) { expect(e.error.toString()).to.contain('SERVER_ERROR') } + try { + await random.MixedInput(URL, 123, 999, { gasLimit: 11_000_000 }) + } catch (e) { + expect(e.error.toString()).to.contain('SERVER_ERROR') + } }) // Should reject a 2nd call from a different EVM depth. it('should disallow nested Turing calls', async () => { try { - const tr = await random.NestedRandom(1) + await random.estimateGas.NestedRandom(1) expect(1).to.equal(0) } catch (e) { expect(e.error.toString()).to.contain('SERVER_ERROR') } + try { + const tr = await random.NestedRandom(1, { gasLimit: 11_000_000 }) + } catch (e) { + expect(e.error.toString()).to.contain('SERVER_ERROR') + } }) it('should allow repeated Random calls (legacy support)', async () => { diff --git a/l2geth/core/state_processor.go b/l2geth/core/state_processor.go index e62fb363eb..45f366451e 100644 --- a/l2geth/core/state_processor.go +++ b/l2geth/core/state_processor.go @@ -124,6 +124,11 @@ func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *commo } } + p2 := msg.GasPrice() + if vmenv.ChainConfig().IsTuringCharge2Fork(vmenv.BlockNumber) && p2.BitLen() > 0 { + vmenv.Context.TuringGasMul = float64(l1GasPrice.Uint64()) / float64(p2.Uint64()) + } + // Determine the L2 Boba fee if users chose BOBA as the fee token feeTokenSelection := statedb.GetFeeTokenSelection(msg.From()) diff --git a/l2geth/core/vm/evm.go b/l2geth/core/vm/evm.go index e7039f115b..9f1fdb71ad 100644 --- a/l2geth/core/vm/evm.go +++ b/l2geth/core/vm/evm.go @@ -749,6 +749,31 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas // We are in Verifier/Replica mode // Turing for this Transaction has already been run elsewhere - replay using // information from the EVM context + if evm.Context.TuringInput == nil { + evm.Context.TuringInput = make([]byte, len(input)) + copy(evm.Context.TuringInput, input) + evm.Context.TuringVMDepth = evm.depth + } else if !bytes.Equal(input, evm.Context.TuringInput) || evm.depth != evm.Context.TuringVMDepth { + log.Debug("TURING ERROR: evm.Context.Turing already set") + return nil, gas, ErrTuringDepth + } + + // For compatibility, only apply a charge beyond the legacy size limit + if isTuring2 { + if len(evm.Context.Turing) > 160 { + feePerByte := evm.Context.TuringGasMul * 500.0 / 32.0 + turingGas = uint64(float64(len(evm.Context.Turing)) * feePerByte) + } + + if contract.Gas <= turingGas { + log.Debug("TURING ERROR: Insufficient gas for calldata", "have", contract.Gas, "need", turingGas) + return nil, 0, ErrTuringTooLong + } else { + log.Debug("TURING Deducting calldata gas", "had", contract.Gas, "len", len(evm.Context.Turing), "Mul", evm.Context.TuringGasMul, "deducting", turingGas) + contract.UseGas(turingGas) + } + } + ret, err = run(evm, contract, evm.Context.Turing, false) log.Trace("TURING REPLAY", "evm.Context.Turing", evm.Context.Turing) } diff --git a/l2geth/params/config.go b/l2geth/params/config.go index 2524d13640..b54c7c0e37 100644 --- a/l2geth/params/config.go +++ b/l2geth/params/config.go @@ -305,6 +305,36 @@ var ( // Enable the conditional logic to prevent Turing balances from reaching zero BobaOperaTestnetTuringChargeForkNum = big.NewInt(3000) + + // Enable the conditional logic to fix bug in charging for L1 Turing calldata + BobaMainnetTuringCharge2ForkNum = big.NewInt(1064000) + + // Enable the conditional logic to fix bug in charging for L1 Turing calldata + BobaGoerliTuringCharge2ForkNum = big.NewInt(114000) + + // Enable the conditional logic to fix bug in charging for L1 Turing calldata + BobaMoonbeamTuringCharge2ForkNum = big.NewInt(1580000) + + // Enable the conditional logic to fix bug in charging for L1 Turing calldata + BobaMoonbaseTuringCharge2ForkNum = big.NewInt(350000) + + // Enable the conditional logic to fix bug in charging for L1 Turing calldata + BobaAvaxTuringCharge2ForkNum = big.NewInt(101200) + + // Enable the conditional logic to fix bug in charging for L1 Turing calldata + BobaFujiTuringCharge2ForkNum = big.NewInt(4000) + + // Enable the conditional logic to fix bug in charging for L1 Turing calldata + BobaBnbTuringCharge2ForkNum = big.NewInt(25740000) + + // Enable the conditional logic to fix bug in charging for L1 Turing calldata + BobaBnbTestnetTuringCharge2ForkNum = big.NewInt(428000) + + // Enable the conditional logic to fix bug in charging for L1 Turing calldata + BobaOperaTuringCharge2ForkNum = big.NewInt(80000) + + // Enable the conditional logic to fix bug in charging for L1 Turing calldata + BobaOperaTestnetTuringCharge2ForkNum = big.NewInt(3000) ) // TrustedCheckpoint represents a set of post-processed trie roots (CHT and @@ -567,6 +597,43 @@ func (c *ChainConfig) IsTuringChargeFork(num *big.Int) bool { return true } +func (c *ChainConfig) IsTuringCharge2Fork(num *big.Int) bool { + if c.ChainID == nil { + return true + } + if c.ChainID.Cmp(OpMainnetChainID) == 0 { + return isForked(BobaMainnetTuringCharge2ForkNum, num) + } + if c.ChainID.Cmp(OpGoerliChainID) == 0 { + return isForked(BobaGoerliTuringCharge2ForkNum, num) + } + if c.ChainID.Cmp(OpMoonbeamChainID) == 0 { + return isForked(BobaMoonbeamTuringCharge2ForkNum, num) + } + if c.ChainID.Cmp(OpMoonbaseChainID) == 0 { + return isForked(BobaMoonbaseTuringCharge2ForkNum, num) + } + if c.ChainID.Cmp(OpBnbChainID) == 0 { + return isForked(BobaBnbTuringCharge2ForkNum, num) + } + if c.ChainID.Cmp(OpBnbTestnetChainID) == 0 { + return isForked(BobaBnbTestnetTuringCharge2ForkNum, num) + } + if c.ChainID.Cmp(OpAvaxChainID) == 0 { + return isForked(BobaAvaxTuringCharge2ForkNum, num) + } + if c.ChainID.Cmp(OpFujiChainID) == 0 { + return isForked(BobaFujiTuringCharge2ForkNum, num) + } + if c.ChainID.Cmp(OpOperaChainID) == 0 { + return isForked(BobaOperaTuringCharge2ForkNum, num) + } + if c.ChainID.Cmp(OpOperaTestnetChainID) == 0 { + return isForked(BobaOperaTestnetTuringCharge2ForkNum, num) + } + return true +} + func (c *ChainConfig) IsEthereumL2() bool { if os.Getenv("IS_ETHEREUM_L2") == "true" { return true diff --git a/ops/docker-compose.yml b/ops/docker-compose.yml index 3ff8133094..ad6d4cb804 100644 --- a/ops/docker-compose.yml +++ b/ops/docker-compose.yml @@ -418,7 +418,7 @@ services: environment: L1_NODE_WEB3_URL: http://l1_chain:8545 L1_CONFIRMATIONS: 8 - L2_NODE_WEB3_URL: http://l2geth:8545 + L2_NODE_WEB3_URL: http://replica:8545 L2_CHECK_INTERVAL: 10 VERIFIER_WEB3_URL: http://verifier:8545 ADDRESS_MANAGER_ADDRESS: "0x5FbDB2315678afecb367f032d93F642f64180aa3"