diff --git a/contracts/contracts/Sapphire.sol b/contracts/contracts/Sapphire.sol index 5ee4aed9..1099cc4c 100644 --- a/contracts/contracts/Sapphire.sol +++ b/contracts/contracts/Sapphire.sol @@ -23,6 +23,10 @@ library Sapphire { 0x0100000000000000000000000000000000000007; address internal constant CURVE25519_PUBLIC_KEY = 0x0100000000000000000000000000000000000008; + address internal constant GAS_USED = + 0x0100000000000000000000000000000000000009; + address internal constant PAD_GAS = + 0x010000000000000000000000000000000000000a; // Oasis-specific, general precompiles address internal constant SHA512_256 = @@ -225,6 +229,28 @@ library Sapphire { require(success, "verify: failed"); return abi.decode(v, (bool)); } + + /** + * @dev Set the current transactions gas usage to a specific amount + * @param toAmount Gas usage will be set to this amount + * @custom:see @oasisprotocol/oasis-sdk :: precompile/gas.rs :: call_pad_gas + * + * Will cause a reversion if the current usage is more than the amount + */ + function padGas(uint128 toAmount) internal view { + (bool success, ) = PAD_GAS.staticcall(abi.encode(toAmount)); + require(success, "verify: failed"); + } + + /** + * @dev Returns the amount of gas currently used by the transaction + * @custom:see @oasisprotocol/oasis-sdk :: precompile/gas.rs :: call_gas_used + */ + function gasUsed() internal view returns (uint64) { + (bool success, bytes memory v) = GAS_USED.staticcall(""); + require(success, "gasused: failed"); + return abi.decode(v, (uint64)); + } } /** diff --git a/contracts/contracts/tests/Gas.sol b/contracts/contracts/tests/Gas.sol new file mode 100644 index 00000000..cdcedf62 --- /dev/null +++ b/contracts/contracts/tests/Gas.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +import {Sapphire} from "../Sapphire.sol"; + +contract GasTests { + bytes32 tmp; + + function testConstantTime(uint256 useGas, uint128 padGasAmount) external { + if (useGas == 1) { + bytes32 x; + + for (uint256 i = 0; i < 100; i++) { + x = keccak256(abi.encodePacked(x, tmp)); + } + + tmp = x; + } + + Sapphire.padGas(padGasAmount); + } +} diff --git a/contracts/test/gas.ts b/contracts/test/gas.ts new file mode 100644 index 00000000..fe175350 --- /dev/null +++ b/contracts/test/gas.ts @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 + +import { expect } from 'chai'; +import { ethers } from 'hardhat'; +import { GasTests__factory } from '../typechain-types/factories/contracts/tests/Gas.sol'; +import { GasTests } from '../typechain-types/contracts/tests/Gas.sol/GasTests'; + +describe('Gas Padding', function () { + let contract: GasTests; + + before(async () => { + const factory = (await ethers.getContractFactory( + 'GasTests', + )) as GasTests__factory; + contract = await factory.deploy(); + }); + + it('Gas Padding works as Expected', async () => { + const expectedGas = 122735; + + let tx = await contract.testConstantTime(1, 100000); + let receipt = await tx.wait(); + expect(receipt.cumulativeGasUsed).eq(expectedGas); + + tx = await contract.testConstantTime(2, 100000); + receipt = await tx.wait(); + expect(receipt.cumulativeGasUsed).eq(expectedGas); + + tx = await contract.testConstantTime(1, 100000); + receipt = await tx.wait(); + expect(receipt.cumulativeGasUsed).eq(expectedGas); + + // Note: calldata isn't included in gas padding + // Thus when the value is 0 it will use 4 gas instead of 16 gas + tx = await contract.testConstantTime(0, 100000); + receipt = await tx.wait(); + expect(receipt.cumulativeGasUsed).eq(expectedGas - 12); + }); +});