diff --git a/evm/src/main/java/org/hyperledger/besu/evm/MainnetEVMs.java b/evm/src/main/java/org/hyperledger/besu/evm/MainnetEVMs.java index 7387ddaba2a..3afa09ee6e7 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/MainnetEVMs.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/MainnetEVMs.java @@ -33,6 +33,8 @@ import org.hyperledger.besu.evm.operation.AddOperation; import org.hyperledger.besu.evm.operation.AddressOperation; import org.hyperledger.besu.evm.operation.AndOperation; +import org.hyperledger.besu.evm.operation.AuthCallOperation; +import org.hyperledger.besu.evm.operation.AuthOperation; import org.hyperledger.besu.evm.operation.BalanceOperation; import org.hyperledger.besu.evm.operation.BaseFeeOperation; import org.hyperledger.besu.evm.operation.BlobBaseFeeOperation; @@ -948,6 +950,10 @@ public static void registerPragueOperations( final GasCalculator gasCalculator, final BigInteger chainID) { registerCancunOperations(registry, gasCalculator, chainID); + + // EIP-3074 AUTH and AUTHCALL + registry.put(new AuthOperation(gasCalculator)); + registry.put(new AuthCallOperation(gasCalculator)); } /** diff --git a/evm/src/main/java/org/hyperledger/besu/evm/frame/MessageFrame.java b/evm/src/main/java/org/hyperledger/besu/evm/frame/MessageFrame.java index d59e7e45b29..8673640d0fc 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/frame/MessageFrame.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/frame/MessageFrame.java @@ -247,6 +247,9 @@ public enum Type { /** The mark of the undoable collections at the creation of this message frame */ private final long undoMark; + /** mutated by AUTH operation */ + private Address authorizedBy = null; + /** * Builder builder. * @@ -1371,6 +1374,24 @@ public Optional> getVersionedHashes() { return txValues.versionedHashes(); } + /** + * Accessor for address that authorized future AUTHCALLs. + * + * @return the revert reason + */ + public Address getAuthorizedBy() { + return authorizedBy; + } + + /** + * Mutator for address that authorizes future AUTHCALLs, set by AUTH opcode + * + * @param authorizedBy the address that authorizes future AUTHCALLs + */ + public void setAuthorizedBy(final Address authorizedBy) { + this.authorizedBy = authorizedBy; + } + /** Reset. */ public void reset() { maybeUpdatedMemory = Optional.empty(); diff --git a/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/GasCalculator.java b/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/GasCalculator.java index 06f24c534e8..7a3f57c81dd 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/GasCalculator.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/GasCalculator.java @@ -210,6 +210,35 @@ long callOperationGasCost( Address contract, boolean accountIsWarm); + /** + * Returns the gas cost for AUTHCALL. + * + * @param frame The current frame + * @param stipend The gas stipend being provided by the CALL caller + * @param inputDataOffset The offset in memory to retrieve the CALL input data + * @param inputDataLength The CALL input data length + * @param outputDataOffset The offset in memory to place the CALL output data + * @param outputDataLength The CALL output data length + * @param transferValue The wei being transferred + * @param invoker The contract calling out on behalf of the authority + * @param invokee The address of the recipient (never null) + * @param accountIsWarm The address of the contract is "warm" as per EIP-2929 + * @return The gas cost for the CALL operation + */ + default long authCallOperationGasCost( + final MessageFrame frame, + final long stipend, + final long inputDataOffset, + final long inputDataLength, + final long outputDataOffset, + final long outputDataLength, + final Wei transferValue, + final Account invoker, + final Address invokee, + final boolean accountIsWarm) { + return 0L; + } + /** * Gets additional call stipend. * @@ -617,4 +646,18 @@ default long computeExcessBlobGas(final long parentExcessBlobGas, final int newB default long computeExcessBlobGas(final long parentExcessBlobGas, final long blobGasUsed) { return 0L; } + + /** + * Returns the gas cost of validating an auth commitment for an AUTHCALL + * + * @param frame the current frame, with memory to be read from + * @param offset start of memory read + * @param length amount of memory read + * @param authority address to check for warmup + * @return total gas cost for the operation + */ + default long authOperationGasCost( + final MessageFrame frame, final long offset, final long length, final Address authority) { + return 0L; + } } diff --git a/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/PragueGasCalculator.java b/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/PragueGasCalculator.java index 8aadbe5ffcd..79e901565e2 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/PragueGasCalculator.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/PragueGasCalculator.java @@ -16,6 +16,11 @@ import static org.hyperledger.besu.datatypes.Address.KZG_POINT_EVAL; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.evm.account.Account; +import org.hyperledger.besu.evm.frame.MessageFrame; + /** * Gas Calculator for Prague * @@ -27,6 +32,8 @@ * */ public class PragueGasCalculator extends CancunGasCalculator { + private final int AUTH_OP_FIXED_FEE = 3100; + private final long AUTH_CALL_VALUE_TRANSFER_GAS_COST = 6700; /** Instantiates a new Prague Gas Calculator. */ public PragueGasCalculator() { @@ -41,4 +48,55 @@ public PragueGasCalculator() { protected PragueGasCalculator(final int maxPrecompile) { super(maxPrecompile); } + + @Override + public long authOperationGasCost( + final MessageFrame frame, final long offset, final long length, final Address authority) { + final long memoryExpansionGasCost = memoryExpansionGasCost(frame, offset, length); + final long accessFee = frame.isAddressWarm(authority) ? 100 : 2600; + final long gasCost = AUTH_OP_FIXED_FEE + memoryExpansionGasCost + accessFee; + return gasCost; + } + + /** + * Returns the gas cost to call another contract on behalf of an authority + * + * @return the gas cost to call another contract on behalf of an authority + */ + @Override + public long authCallOperationGasCost( + final MessageFrame frame, + final long stipend, + final long inputDataOffset, + final long inputDataLength, + final long outputDataOffset, + final long outputDataLength, + final Wei transferValue, + final Account invoker, + final Address invokee, + final boolean accountIsWarm) { + + final long inputDataMemoryExpansionCost = + memoryExpansionGasCost(frame, inputDataOffset, inputDataLength); + final long outputDataMemoryExpansionCost = + memoryExpansionGasCost(frame, outputDataOffset, outputDataLength); + final long memoryExpansionCost = + Math.max(inputDataMemoryExpansionCost, outputDataMemoryExpansionCost); + + final long staticGasCost = getWarmStorageReadCost(); + + long dynamicGasCost = accountIsWarm ? 0 : getColdAccountAccessCost() - getWarmStorageReadCost(); + + if (!transferValue.isZero()) { + dynamicGasCost += AUTH_CALL_VALUE_TRANSFER_GAS_COST; + } + + if ((invoker == null || invoker.isEmpty()) && !transferValue.isZero()) { + dynamicGasCost += newAccountGasCost(); + } + + long cost = staticGasCost + memoryExpansionCost + dynamicGasCost; + + return cost; + } } diff --git a/evm/src/main/java/org/hyperledger/besu/evm/operation/AuthCallOperation.java b/evm/src/main/java/org/hyperledger/besu/evm/operation/AuthCallOperation.java new file mode 100644 index 00000000000..3d4fffffb3a --- /dev/null +++ b/evm/src/main/java/org/hyperledger/besu/evm/operation/AuthCallOperation.java @@ -0,0 +1,102 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.evm.operation; + +import static org.hyperledger.besu.evm.internal.Words.clampedToLong; + +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.evm.EVM; +import org.hyperledger.besu.evm.frame.ExceptionalHaltReason; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.gascalculator.GasCalculator; +import org.hyperledger.besu.evm.internal.Words; + +import org.apache.tuweni.bytes.Bytes32; + +/** Introduced via EIP-3074 to call another contract with a different authorization context. */ +public class AuthCallOperation extends AbstractCallOperation { + + /** + * Instantiates a new AuthCallOperation. + * + * @param gasCalculator a Prague or later gas calculator + */ + public AuthCallOperation(final GasCalculator gasCalculator) { + super(0xF7, "AUTHCALL", 7, 1, gasCalculator); + } + + @Override + protected Address to(final MessageFrame frame) { + return Words.toAddress(frame.getStackItem(1)); + } + + @Override + protected Wei value(final MessageFrame frame) { + return Wei.wrap(frame.getStackItem(2)); + } + + @Override + protected Wei apparentValue(final MessageFrame frame) { + return value(frame); + } + + @Override + protected long inputDataOffset(final MessageFrame frame) { + return clampedToLong(frame.getStackItem(3)); + } + + @Override + protected long inputDataLength(final MessageFrame frame) { + return clampedToLong(frame.getStackItem(4)); + } + + @Override + protected long outputDataOffset(final MessageFrame frame) { + return clampedToLong(frame.getStackItem(5)); + } + + @Override + protected long outputDataLength(final MessageFrame frame) { + return clampedToLong(frame.getStackItem(6)); + } + + @Override + protected Address address(final MessageFrame frame) { + return to(frame); + } + + @Override + protected Address sender(final MessageFrame frame) { + return frame.getAuthorizedBy(); + } + + @Override + public long gasAvailableForChildCall(final MessageFrame frame) { + return gasCalculator().gasAvailableForChildCall(frame, gas(frame), !value(frame).isZero()); + } + + @Override + public OperationResult execute(final MessageFrame frame, final EVM evm) { + if (frame.isStatic() && !value(frame).isZero()) { + return new OperationResult(cost(frame, true), ExceptionalHaltReason.ILLEGAL_STATE_CHANGE); + } else if (frame.getAuthorizedBy() != null) { + return super.execute(frame, evm); + } else { + frame.pushStackItem(Bytes32.ZERO); + return new OperationResult(cost(frame, true), null); + } + } +} diff --git a/evm/src/main/java/org/hyperledger/besu/evm/operation/AuthOperation.java b/evm/src/main/java/org/hyperledger/besu/evm/operation/AuthOperation.java new file mode 100644 index 00000000000..1ce8731e3db --- /dev/null +++ b/evm/src/main/java/org/hyperledger/besu/evm/operation/AuthOperation.java @@ -0,0 +1,109 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.evm.operation; + +import static org.hyperledger.besu.evm.internal.Words.clampedToLong; + +import org.hyperledger.besu.crypto.Hash; +import org.hyperledger.besu.crypto.SECPPublicKey; +import org.hyperledger.besu.crypto.SECPSignature; +import org.hyperledger.besu.crypto.SignatureAlgorithm; +import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.evm.EVM; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.gascalculator.GasCalculator; +import org.hyperledger.besu.evm.internal.Words; + +import java.util.Optional; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.apache.tuweni.units.bigints.UInt256; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** The AUTH operation. */ +public class AuthOperation extends AbstractOperation { + + /** The constant MAGIC defined by EIP-3074 */ + public static final byte MAGIC = 0x4; + + private static final Logger LOG = LoggerFactory.getLogger(AuthOperation.class); + + private static final SignatureAlgorithm signatureAlgorithm = + SignatureAlgorithmFactory.getInstance(); + + /** + * Instantiates a new AuthOperation. + * + * @param gasCalculator a Prague or later gas calculator + */ + public AuthOperation(final GasCalculator gasCalculator) { + super(0xF6, "AUTH", 3, 1, gasCalculator); + } + + @Override + public OperationResult execute(final MessageFrame frame, final EVM evm) { + // create authority from stack + Address authority = Words.toAddress(frame.getStackItem(0)); + long offset = clampedToLong(frame.getStackItem(1)); + long length = clampedToLong(frame.getStackItem(2)); + + byte yParity = frame.readMemory(offset, 1).get(0); + Bytes32 r = Bytes32.wrap(frame.readMemory(offset + 1, 32)); + Bytes32 s = Bytes32.wrap(frame.readMemory(offset + 33, 32)); + Bytes32 commit = Bytes32.wrap(frame.readMemory(offset + 65, 32)); + Bytes32 invoker = Bytes32.leftPad(frame.getContractAddress()); + Bytes32 senderNonce = + Bytes32.leftPad( + Bytes.ofUnsignedLong(frame.getWorldUpdater().getAccount(authority).getNonce())); + if (evm.getChainId().isEmpty()) { + frame.pushStackItem(UInt256.ZERO); + LOG.error("ChainId is not set"); + return new OperationResult(0, null); + } + Bytes authPreImage = + Bytes.concatenate( + Bytes.ofUnsignedShort(MAGIC), evm.getChainId().get(), senderNonce, invoker, commit); + Bytes32 messageHash = Hash.keccak256(authPreImage); + + final long gasCost = + super.gasCalculator().authOperationGasCost(frame, offset, length, authority); + Optional publicKey; + try { + SECPSignature signature = + signatureAlgorithm.createSignature( + r.toUnsignedBigInteger(), s.toUnsignedBigInteger(), yParity); + publicKey = signatureAlgorithm.recoverPublicKeyFromSignature(messageHash, signature); + } catch (IllegalArgumentException e) { + + frame.pushStackItem(UInt256.ZERO); + return new OperationResult(gasCost, null); + } + if (publicKey.isPresent()) { + Address signerAddress = Address.extract(publicKey.get()); + if (signerAddress.equals(authority)) { + frame.setAuthorizedBy(authority); + frame.pushStackItem(UInt256.ONE); + } else { + frame.pushStackItem(UInt256.ZERO); + } + } else { + frame.pushStackItem(UInt256.ZERO); + } + return new OperationResult(gasCost, null); + } +} diff --git a/evm/src/test/java/org/hyperledger/besu/evm/gascalculator/PragueGasCalculatorTest.java b/evm/src/test/java/org/hyperledger/besu/evm/gascalculator/PragueGasCalculatorTest.java new file mode 100644 index 00000000000..72cacd47cfe --- /dev/null +++ b/evm/src/test/java/org/hyperledger/besu/evm/gascalculator/PragueGasCalculatorTest.java @@ -0,0 +1,74 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.evm.gascalculator; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.evm.account.Account; +import org.hyperledger.besu.evm.account.MutableAccount; +import org.hyperledger.besu.evm.frame.MessageFrame; + +import org.junit.jupiter.api.Test; + +public class PragueGasCalculatorTest { + @Test + public void testAuthOperationGasCost() { + PragueGasCalculator pragueGasCalculator = new PragueGasCalculator(); + MessageFrame runningIn = mock(MessageFrame.class); + Address authority = Address.fromHexString("0xdeadbeef"); + when(runningIn.isAddressWarm(authority)).thenReturn(true); + long gasSpent = pragueGasCalculator.authOperationGasCost(runningIn, 0, 97, authority); + assertEquals( + 3100 + 100 + pragueGasCalculator.memoryExpansionGasCost(runningIn, 0, 97), gasSpent); + } + + @Test + public void testAuthCallOperationGasCostWithTransfer() { + PragueGasCalculator pragueGasCalculator = new PragueGasCalculator(); + MessageFrame runningIn = mock(MessageFrame.class); + Account invoker = mock(MutableAccount.class); + when(invoker.getAddress()).thenReturn(Address.fromHexString("0xCafeBabe")); + Address invokee = Address.fromHexString("0xdeadbeef"); + when(runningIn.isAddressWarm(invokee)).thenReturn(true); + long gasSpentInAuthCall = + pragueGasCalculator.authCallOperationGasCost( + runningIn, 63, 0, 97, 100, 97, Wei.ONE, invoker, invokee, true); + long gasSpentInCall = + pragueGasCalculator.callOperationGasCost( + runningIn, 63, 0, 97, 100, 97, Wei.ONE, invoker, invokee, true); + assertEquals(gasSpentInCall - 2300, gasSpentInAuthCall); + } + + @Test + public void testAuthCallOperationGasCostNoTransfer() { + PragueGasCalculator pragueGasCalculator = new PragueGasCalculator(); + MessageFrame runningIn = mock(MessageFrame.class); + Account invoker = mock(MutableAccount.class); + when(invoker.getAddress()).thenReturn(Address.fromHexString("0xCafeBabe")); + Address invokee = Address.fromHexString("0xdeadbeef"); + when(runningIn.isAddressWarm(invokee)).thenReturn(true); + long gasSpentInAuthCall = + pragueGasCalculator.authCallOperationGasCost( + runningIn, 63, 0, 97, 100, 97, Wei.ZERO, invoker, invokee, true); + long gasSpentInCall = + pragueGasCalculator.callOperationGasCost( + runningIn, 63, 0, 97, 100, 97, Wei.ZERO, invoker, invokee, true); + assertEquals(gasSpentInCall, gasSpentInAuthCall); + } +} diff --git a/evm/src/test/java/org/hyperledger/besu/evm/operations/AuthOperationTest.java b/evm/src/test/java/org/hyperledger/besu/evm/operations/AuthOperationTest.java new file mode 100644 index 00000000000..28df2b028aa --- /dev/null +++ b/evm/src/test/java/org/hyperledger/besu/evm/operations/AuthOperationTest.java @@ -0,0 +1,152 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.evm.operations; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.hyperledger.besu.crypto.Hash; +import org.hyperledger.besu.crypto.KeyPair; +import org.hyperledger.besu.crypto.SECPSignature; +import org.hyperledger.besu.crypto.SignatureAlgorithm; +import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.evm.EVM; +import org.hyperledger.besu.evm.account.MutableAccount; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.gascalculator.PragueGasCalculator; +import org.hyperledger.besu.evm.operation.AuthOperation; +import org.hyperledger.besu.evm.worldstate.WorldUpdater; + +import java.util.Optional; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.apache.tuweni.units.bigints.UInt256; +import org.junit.jupiter.api.Test; + +public class AuthOperationTest { + + @Test + public void testAuthOperation() { + SignatureAlgorithm algo = SignatureAlgorithmFactory.getInstance(); + KeyPair keys = algo.generateKeyPair(); + Address authingAddress = Address.extract(keys.getPublicKey()); + EVM fakeEVM = mock(EVM.class); + + Optional chainId = Optional.of(Bytes.of(1)); + when(fakeEVM.getChainId()).thenReturn(chainId); + long senderNonce = 0; + Address invokerAddress = Address.fromHexString("0xdeadbeef"); + Bytes32 invoker = Bytes32.leftPad(invokerAddress); + Bytes32 contractCommitment = Bytes32.leftPad(Bytes.fromHexString("0x1234")); + Bytes authPreImage = + Bytes.concatenate( + Bytes.ofUnsignedShort(AuthOperation.MAGIC), + chainId.get(), + Bytes32.leftPad(Bytes.ofUnsignedLong(senderNonce)), + invoker, + contractCommitment); + Bytes32 messageHash = Hash.keccak256(authPreImage); + SECPSignature signature = algo.sign(messageHash, keys); + + MessageFrame frame = mock(MessageFrame.class); + when(frame.getContractAddress()).thenReturn(invokerAddress); + MutableAccount authingAccount = mock(MutableAccount.class); + when(authingAccount.getAddress()).thenReturn(authingAddress); + when(authingAccount.getNonce()).thenReturn(senderNonce); + + WorldUpdater state = mock(WorldUpdater.class); + + when(state.getAccount(authingAddress)).thenReturn(authingAccount); + + when(frame.getWorldUpdater()).thenReturn(state); + + when(frame.getSenderAddress()).thenReturn(authingAddress); + when(state.getSenderAccount(frame)).thenReturn(authingAccount); + when(frame.getStackItem(0)).thenReturn(authingAddress); + when(frame.getStackItem(1)).thenReturn(Bytes.of(0)); + when(frame.getStackItem(2)).thenReturn(Bytes.of(97)); + Bytes encodedSignature = signature.encodedBytes(); + when(frame.readMemory(0, 1)).thenReturn(encodedSignature.slice(64, 1)); + when(frame.readMemory(1, 32)).thenReturn(Bytes32.wrap(encodedSignature.slice(0, 32).toArray())); + when(frame.readMemory(33, 32)) + .thenReturn(Bytes32.wrap(encodedSignature.slice(32, 32).toArray())); + when(frame.readMemory(65, 32)).thenReturn(contractCommitment); + + AuthOperation authOperation = new AuthOperation(new PragueGasCalculator()); + authOperation.execute(frame, fakeEVM); + verify(frame).setAuthorizedBy(authingAddress); + verify(frame).pushStackItem(UInt256.ONE); + } + + @Test + public void testAuthOperationNegative() { + SignatureAlgorithm algo = SignatureAlgorithmFactory.getInstance(); + KeyPair keys = algo.generateKeyPair(); + Address authingAddress = Address.extract(keys.getPublicKey()); + EVM fakeEVM = mock(EVM.class); + + Optional chainId = Optional.of(Bytes.of(1)); + when(fakeEVM.getChainId()).thenReturn(chainId); + long senderNonce = 0; + Address invokerAddress = Address.fromHexString("0xdeadbeef"); + Bytes32 invoker = Bytes32.leftPad(invokerAddress); + Bytes32 contractCommitment = Bytes32.leftPad(Bytes.fromHexString("0x1234")); + Bytes authPreImage = + Bytes.concatenate( + Bytes.ofUnsignedShort(AuthOperation.MAGIC), + chainId.get(), + Bytes32.leftPad(Bytes.ofUnsignedLong(senderNonce)), + invoker, + contractCommitment); + Bytes32 messageHash = Hash.keccak256(authPreImage); + + // Generate a new key pair to create an incorrect signature + KeyPair wrongKeys = algo.generateKeyPair(); + SECPSignature wrongSignature = algo.sign(messageHash, wrongKeys); + + MessageFrame frame = mock(MessageFrame.class); + when(frame.getContractAddress()).thenReturn(invokerAddress); + MutableAccount authingAccount = mock(MutableAccount.class); + when(authingAccount.getAddress()).thenReturn(authingAddress); + when(authingAccount.getNonce()).thenReturn(senderNonce); + + WorldUpdater state = mock(WorldUpdater.class); + + when(state.getAccount(authingAddress)).thenReturn(authingAccount); + + when(frame.getWorldUpdater()).thenReturn(state); + + when(frame.getSenderAddress()).thenReturn(authingAddress); + when(state.getSenderAccount(frame)).thenReturn(authingAccount); + when(frame.getStackItem(0)).thenReturn(authingAddress); + when(frame.getStackItem(1)).thenReturn(Bytes.of(0)); + when(frame.getStackItem(2)).thenReturn(Bytes.of(97)); + Bytes encodedSignature = wrongSignature.encodedBytes(); // Use the wrong signature + when(frame.readMemory(0, 1)).thenReturn(encodedSignature.slice(64, 1)); + when(frame.readMemory(1, 32)).thenReturn(Bytes32.wrap(encodedSignature.slice(0, 32).toArray())); + when(frame.readMemory(33, 32)) + .thenReturn(Bytes32.wrap(encodedSignature.slice(32, 32).toArray())); + when(frame.readMemory(65, 32)).thenReturn(contractCommitment); + + AuthOperation authOperation = new AuthOperation(new PragueGasCalculator()); + authOperation.execute(frame, fakeEVM); + verify(frame, never()).setAuthorizedBy(authingAddress); // The address should not be authorized + verify(frame).pushStackItem(UInt256.ZERO); // The stack should contain UInt256.ZERO + } +} diff --git a/evm/src/test/java/org/hyperledger/besu/evm/processor/AuthCallProcessorTest.java b/evm/src/test/java/org/hyperledger/besu/evm/processor/AuthCallProcessorTest.java new file mode 100644 index 00000000000..21144a4806b --- /dev/null +++ b/evm/src/test/java/org/hyperledger/besu/evm/processor/AuthCallProcessorTest.java @@ -0,0 +1,152 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.evm.processor; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import org.hyperledger.besu.crypto.Hash; +import org.hyperledger.besu.crypto.KeyPair; +import org.hyperledger.besu.crypto.SECPSignature; +import org.hyperledger.besu.crypto.SignatureAlgorithm; +import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.evm.EVM; +import org.hyperledger.besu.evm.MainnetEVMs; +import org.hyperledger.besu.evm.fluent.EVMExecutor; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.gascalculator.PragueGasCalculator; +import org.hyperledger.besu.evm.internal.EvmConfiguration; +import org.hyperledger.besu.evm.operation.AuthOperation; +import org.hyperledger.besu.evm.toy.ToyWorld; +import org.hyperledger.besu.evm.worldstate.WorldUpdater; + +import java.math.BigInteger; +import java.util.List; +import java.util.Optional; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +public class AuthCallProcessorTest extends MessageCallProcessorTest { + + MessageCallProcessor spyingMessageCallProcessor; + ArgumentCaptor frameCaptor = ArgumentCaptor.forClass(MessageFrame.class); + + WorldUpdater toyWorld = new ToyWorld(); + + @Test + public void authCallHappyPath() { + final EVM pragueEVM = + MainnetEVMs.prague(new PragueGasCalculator(), BigInteger.ONE, EvmConfiguration.DEFAULT); + final EVMExecutor executor = EVMExecutor.evm(pragueEVM); + this.spyingMessageCallProcessor = + spy(new MessageCallProcessor(pragueEVM, precompileContractRegistry)); + executor.messageCallProcessor(this.spyingMessageCallProcessor); + + executor.worldUpdater(toyWorld); + executor.gas(10_000_000_000L); + + SignatureAlgorithm algo = SignatureAlgorithmFactory.getInstance(); + KeyPair keys = algo.generateKeyPair(); + Optional chainId = Optional.of(Bytes.of(1)); + long senderNonce = 0; + Address invokerAddress = Address.fromHexString("0xdeadbeef"); + Bytes32 invoker = Bytes32.leftPad(invokerAddress); + Bytes32 contractCommitment = Bytes32.leftPad(Bytes.fromHexString("0x1234")); + Bytes authPreImage = + Bytes.concatenate( + Bytes.ofUnsignedShort(AuthOperation.MAGIC), + Bytes32.leftPad(chainId.get()), + Bytes32.leftPad(Bytes.ofUnsignedLong(senderNonce)), + invoker, + contractCommitment); + Bytes32 messageHash = Hash.keccak256(authPreImage); + SECPSignature signature = algo.sign(messageHash, keys); + Bytes encodedSignature = signature.encodedBytes(); + + Bytes authParam = + Bytes.concatenate( + encodedSignature.slice(64, 1), // y parity + encodedSignature.slice(0, 32), // r + encodedSignature.slice(32, 32), // s + contractCommitment); + + toyWorld.createAccount( + Address.extract(keys.getPublicKey()), 0, Wei.MAX_WEI); // initialize authority account + toyWorld.createAccount(invokerAddress, 0, Wei.MAX_WEI); // initialize invoker account + final Bytes codeBytes = + Bytes.fromHexString( + "0x" + + "6061" // push 97 the calldata length + + "6000" // push 0 the offset + + "6000" // push 0 the destination offset + + "37" // calldatacopy 97 bytes of the auth param to mem 0 + + "6061" // param is 97 bytes (0x61) + + "6000" // push 0 where in mem to find auth param + + "73" // push next 20 bytes for the authority address + + Address.extract(keys.getPublicKey()) + .toUnprefixedHexString() // push authority address + + "F6" // AUTH call, should work and set authorizedBy on the frame + + "6000" // push 0 for return length, we don't care about the return + + "6000" // push 0 for return offset, we don't care about the return + + "6000" // push 0 for input length + + "6000" // push 0 for input offset + + "60FF" // push 255 for the value being sent + + "73deadbeefdeadbeefdeadbeefdeadbeefdeadbeef" // push20 the invokee address + + "60FF" // push 255 gas + + "F7"); // AUTHCALL, should work + executor.contract(invokerAddress); + executor.execute(codeBytes, authParam, Wei.ZERO, invokerAddress); + verify(this.spyingMessageCallProcessor, times(2)) + .start(frameCaptor.capture(), any()); // one for parent frame, one for child + List frames = frameCaptor.getAllValues(); + assertThat(frames.get(0).getStackItem(0)).isEqualTo((Bytes.of(1))); + } + + @Test + public void unauthorizedAuthCall() { + final EVM pragueEVM = + MainnetEVMs.prague(new PragueGasCalculator(), BigInteger.ONE, EvmConfiguration.DEFAULT); + final EVMExecutor executor = EVMExecutor.evm(pragueEVM); + this.spyingMessageCallProcessor = + spy(new MessageCallProcessor(pragueEVM, precompileContractRegistry)); + executor.messageCallProcessor(this.spyingMessageCallProcessor); + + executor.gas(10_000_000_000L); + + final Bytes codeBytes = + Bytes.fromHexString( + "0x" + + "6000" // push 0 for return length + + "6000" // push 0 for return offset + + "6000" // push 0 for input length + + "6000" // push 0 for input offset + + "60FF" // push 255 for the value being sent + + "73deadbeefdeadbeefdeadbeefdeadbeefdeadbeef" // push20 the invokee address + + "60FF" // push 255 gas + + "F7"); // AUTHCALL without prior AUTH, should fail + + executor.execute(codeBytes, Bytes.EMPTY, Wei.ZERO, Address.ZERO); + verify(this.spyingMessageCallProcessor).start(frameCaptor.capture(), any()); + assertThat(frameCaptor.getValue().getStackItem(0)).isEqualTo(Bytes32.ZERO); + } +}