# ABI for Ethereum Precompiles Libraries discussion-to: https://ethereum-magicians.org/t/abi-for-ethereum-precompiles-libraries/9263 ## Abstract The specification of the low-level C language ABI for libraries providing implementations of Ethereum Precompiles. ## Motivation The set of precompiles consist mostly of cryptography primitives and secure hash functions. From scratch implementations are hard to do and very much not recommended. Common solution is to integrate multiple third part libraries with implementations of individual precompiles. Depending on the toolchain this rises a number of complications from dependency management to correctly usage of third party libraries and properly implementing precompiles' gas metering. Many parties may prefer to treat precompiles as opaque box with little interest what's inside. E.g. 1. evmone project (EVM implementation) wants to execute State Transition Tests. These tests also invoke precompiles, but the evmone only cares about low-dependency correct solution with performance being secondary 2. Vendors of dapp tools. 3. Solidity may use a common precompiles library for more generic unit testing. 4. Ethereum Client may modularize their implementations with ability to share/exchange precompiles libraries. 5. Enables opaque box cross-testing/fuzzing of multiple precompiles libraries implementations. ## Existing prototypes 1. Ewasm Precompiles - https://github.com/ewasm/ewasm-precompiles - Rust 2. Silkpre - https://github.com/torquem-ch/silkpre - C++ ## List of Precompiles | id | name | input size | output size | can fail? | |-----|-----------|-|-------------|-----------| | 1 | ecrecover | 128* | 0, 32 | no | | 2 | sha256 | variable | 32 | no | | 3 | ripemd160 | variable | 32 | no | | 4 | identity | variable | input | no | | 5 | expmod <small>[EIP-198](https://eips.ethereum.org/EIPS/eip-198)</small> | variable | ~input | no | | 6 | ecadd <small>[EIP-196](https://eips.ethereum.org/EIPS/eip-196)</small> | 128* | 64 | yes | | 7 | ecmul <small>[EIP-196](https://eips.ethereum.org/EIPS/eip-196)</small> | 96* | 64 | yes | | 8 | ecpairing <small>[EIP-197](https://eips.ethereum.org/EIPS/eip-197)</small> | 0* | 32 | yes | | 9 | blake2bf <small>[EIP-152](https://eips.ethereum.org/EIPS/eip-152)</small> | 213 | 64 | yes | `*` allow short inputs where the trailing bytes are padded with zeroes ### Observations 1. A caller does not know the output size a priori (`ecrecover` gives empty output on "failure"). The output size is visible to contracts via `RETURNDATASIZE`. 2. A caller can compute the max output size by inspecting the input. ## Design (revision 2) ### API level 0 This API provides core computation functionality of precompiles without Ethereum-specific parts (gas cost computation, DOS protection). ```c int32_t ethprecompile_v1_name_execute( const uint8_t* input, size_t input_size, uint8_t* output, size_t output_size ); ``` Uniform function signature is provided for all precompiles. TODO: Consider heterogeneous function signatures. #### Rationale 1. The `size_t` is _natural_ type to represent array/buffer size in C and Rust. However, the type size depends on architecture (32 or 64 bits). The fixed size type `uint32_t` was also considered. 2. ##### Output type The function must return the output size (number of bytes written to the output buffer) _or_ error. This can be encoded in a single signed integer value: non-negative value means the output size, negative indicates an error. ## Design (revision 1) ### Gas metering The API should provide gas metering. The precompiles' gas costs are often non-trivial. It should be library responsibility to provide correct formulas. ### Gas parameter The gas metering should be combined with execution, i.e. there should be single API entry point doing both in the same time. Modeled by EVM execution a API function should take _gas limit_ as a parameter and return _gas left_ or _error_ in result. This limits the number of functions in the API. Users interested only in execution without gas metering can provide "infinite" value for the _gas left_ argument. Wrappers for such usage can be provided. **Alternative:** Silkpre provides separate functions for gas cost computation and for execution. ### Precompile ID We propose to use precompiles' names to identify them. The precompile name should be presented in the API function name. Mapping the names to Ethereum addresses is left to users. This allows enabling/disabling and assigning addresses to precompiles differently depending on blockchain configuration. Silkpre provides additional table mapping addresses to function pointers. ### Precompiles Versioning It may happen that the specification for a precompile changes. So far we have single precedence for this: the gas cost for `expmod` has been changed. In the very generic way we can mandatory version every precompile, e.g. by including arbitrary single-number version in the name. E.g. `sha256_v1`, `expmod_v2`. However, it is much more likely that only gas cost is subject for change. ### ABI versioning We also need to add a version to the API itself to prevent ABI breakage. There are multiple ways of doing so, but non of the look especially elegant. 1. Number of toolchains (e.g. GNU) can specify additional symbol versions in the final binary. This makes this transparent to API and users however is hard to standardize and combine with FFI. 2. We can reuse the already existing precompiles version number bump ABI version. However, this is quite confusing. E.g. both versions of `expmod_v1` and `expmod_v2` must exist in the library. In case of required ABI break they would need to be replaced by `expmod_v3` and `expmod_v4`. 3. Include the ABI version in the function name. So every name will have two version numbers. One for ABI and one for the precompile. ### Output Precompiles may have variadic length output. So here we have two options: 1. library allocated: where the library allocates the buffer for output and then the user need to free the buffer by additional function when the output is not needed any more, 2. user provided: user specifies the pointer to the output buffer and its size. This likely saves one allocation and output copy (EVM at least needs to place the output in _return data buffer_). However, we need a way to resolve the case then user provided buffer cannot fit the output. The worst case is `expmod` where the output size is the length of the modulus which by encoding can be up to `2**256` bytes and is only limited by the gas cost. Considering the realistic block gas limits (30M currently) the possible output size may be around 8–16 KB. So it is impractical to ask user to know the output size but leave gas cost computation to the library. Therefore, efficient output management conflicts with computing gas cost in the library together with the execution. ## Specification ### List of precompiles | id | name | output size | |-----|-----------|-------------| | 1 | ecrecover | 32 | | 2 | sha256 | 32 | | 3 | repemd160 | 32 | ### API For each precompile export a function under name: ``` ethprecompiled_v<ABI version>_<name>_execute ``` with the signature ```c struct Result { /// Value 0 for success, /// value != 0 for failure. /// One standard error code being "output buffer too small". int error_code; /// The size of the output in case of success /// (may be smaller than the provided output buffer). /// In case of failure this reports the required /// output buffer size. size_t output_size; }; Result execute( const uint8_t* input, size_t input_size, uint8_t* output_buffer, size_t output_buffer_size ); ```