This EIP proposes new EVM modular arithmetic opcodes which support operations on odd or power-of-two moduli between 3 and 2**768-1
Current opcodes for modular arithmetic only support values up to 256 bits wide. In addition, they are permissive and accept any representable value for the inputs.
Many cryptographic operations are heavily-bottlenecked by modular arithmetic. To expand the range of cryptographic primitives that can be implemented efficiently as EVM contracts, we propose new modular arithmetic opcodes designed for efficiency.
Name | Value | Description |
---|---|---|
COST_SETMODX_BASE |
1 | static cost component for the SETMODX opcode |
COST_STOREX_BASE |
1 | static cost for the STOREX opcode |
COST_LOADX_BASE |
1 | static cost for the LOADX opcode |
assert
implies that if the assertion fails, the current call frame will consume all call gas and terminate call execution in an exceptional state.The execution state of an EVM call frame is modified to include a mapping of id
(a number 0-256) to a field context. A field context comprises a modulus and an allocated space of virtual registers to perform modular arithmetic operations on.
An executing contract uses a new instruction SETMODX
to set the active field context, allocating a new one in the mapping if it does not already exist for id
.
New arithmetic opcodes perform modular addition, subtraction and multiplication with inputs/outputs from virtual registers of the active field context.
New load/store opcodes copy values to and from EVM memory and the virtual registers allocated by the active field context.
SETMODX(0xc0)
Input: <top of stack> id modulus_offset modulus_size alloc_count
.
Output: none
Charge COST_SETMODX_BASE
.
Assert 0 <= id <= 256
.
If a field context for id
exists in this call scope:
Otherwise:
modulus_size <= 96
.[modulus_offset, modulus_offset+modulus_size]
falls within EVM memory.modulus
.modulus
is odd or is a power of two.3 <= modulus <= 2**768 - 1
.0 < alloc_count <= 256
.element_size
as the size needed to represent the modulus padded to be a multiple of 64 bits. Implementations will align virtual register values along system-word lines.alloc_count * element_size
bytes:.alloc_count
initially-zeroed registers. Associate it with id
in the mapping.Modulus size (padded to nearest 64 bits) | cost |
---|---|
64 bits | 23 |
128 bits | 26 |
192 bits | 29 |
256 bits | 32 |
320 bits | 36 |
384 bits | 39 |
448 bits | 42 |
512 bits | 45 |
576 bits | 48 |
640 bits | 51 |
704 bits | 54 |
768 bits | 58 |
The values in this table are based on the following linear cost model:
def cost_setmodx_dynamic(modulus_size: int) -> int:
limb_count = (modulus_size + 7) / 8
return round(limb_count * 3.13 + 19.94)
Opcodes ADDMODX(0xc3)
, SUBMODX(0xc4)
, MULMODX(0xc5)
take a 7 byte immediate interpreted as byte values out_idx
, out_stride
, x_idx
, x_stride
, y_idx
, y_stride
, count
.
Define cost tables for each arithmetic operation based on the modulus:
Modulus size (padded to nearest 64 bits) | cost per add/sub | cost per mul |
---|---|---|
64 | 1 | 1 |
128 | 1 | 1 |
192 | 1 | 1 |
256 | 1 | 2 |
320 | 1 | 2 |
384 | 1 | 3 |
448 | 1 | 4 |
512 | 2 | 5 |
576 | 2 | 7 |
640 | 2 | 8 |
704 | 2 | 10 |
768 | 2 | 12 |
The values in this table correspond to the following cost models:
def cost_add_or_sub(modulus_size: int) -> int:
limb_count = (modulus_size + 7) // 8
return round(limb_count * 0.12 + 0.58)
def cost_mul(modulus_size: int) -> int:
limb_count = (modulus_size + 7) // 8
return round((0.08 * limb_count**2) + (-0.06 * limb_count) + 0.75)
Execution asserts:
max([out_idx + (out_stride * count), x_idx + (x_stride * count), y_idx + (y_stride * count)]) < len(field_context.registers)
out_stride != 0
count != 0
active_ctx
is set.Then, charge count * operation_cost
where operation_cost
is drawn from the cost table. Compute:
for i in range(count):
active_ctx.registers[out_idx+i*out_stride] = operation(active_ctx.registers[x_idx+i*x_stride], active_ctx.registers[y_idx+i*y_stride])
Where operation
computes modular addition, subtraction or multiplication depending on the opcode.
Note: Inputs/outputs can overlap. active_ctx.registers
is not modified until all operations in the loop have completed.
Note: serialization format for values in EVM memory: big-endian padded to active_ctx.element_size
bytes.
LOADX(0xc1)
Stack in: (top of stack) dest source count
Stack out: none
Description: copies values from registers in the currently-active field context to EVM memory.
[source, source + count]
falls within the active field context's zero-indexed value space.[dest, dest + count * active_ctx.element_size]
falls entirely within EVM memory.(active_ctxt.element_size + 31) // 32 * 3
gas (the same as the memory copying component for the MCOPY
opcode)count * operation_cost
gas, where operation_cost
is looked up from the multiplication cost table, given active_ctxt.element_size
.[source, source + count]
to memory [dest, dest + count * active_ctx.element_size]
.STOREX(0xc2)
Stack in: dest source count
Stack out: none
Description: copies values from EVM memory into registers of the currently-active field context.
dest + count
is less than or equal to the number of the active field context's virtual registers.cost_mem_copy(count * active_ctx.element_size)
(TBD: deliberately didn't choose mcopy
gas model to see if this can consider a cheaper one).count * operation_cost
gas, where operation_cost
is looked up from the multiplication cost table, given active_ctxt.element_size
.[source, source + count * active_context.element_size]
falls entirely within EVM memory. Interpret it as count
number of values, asserting that each is less than the modulus and storing them in the registers [dest, dest + count]
.When expanding EVM memory or allocating a new field context via SETMODX
, expansion cost will now consider the size of all allocated virtual registers in the current call frame.
It is assumed that optimized implementations will not store values in EVM-compatible big-endian serialization format, but instead convert them to an internal working representation. The costs in the spec explicitly reflect the choice of Montgomery form as an optimal internal representation.
Values represented in Montgomery form can make use of optimized modular reduction for multiplication. See the dedicated section on Montgomery multiplication in the rationale for more details.
24576 bytes is chosen as the per-call-context allocation limit with the goal of ensuring that the virtual register space can be contained in the L1 CPU data cache of most machines (with room to spare), in order that arithmetic operation costs do not have to account for memory access latency.
For odd moduli, SETMODX
precomputes two values used for optimized modular multiplication via Montgomery reduction. This is dominated by a modular reduction by the modulus, so the cost model is assumed to be linear.
Power-of-two moduli require no precomputation for optimized modular arithmetic.
For power-of-two moduli, LOADX
/STOREX
are assumed to copy values directly between EVM/EVMMAX memories without measurable conversion cost to convert between EVM big-endian serialization and whatever internal representation is used.
For odd moduli, the spec assumes that the internal representation is Montgomery form. Conversion between canonical/Montgomery form requires one modular multiplication per value per direction. The cost of memory copying is baked in to the multiplication price model.
Addition/subtraction/multiplication are assumed to have constant-time implementations. Addition/subtraction can be implemented with linear complexity, while multiplication is quadratic in the size of the modulus.
The costs for power-of-two modular multiplication are provisional, and likely be brought down when an optimized arithmetic implementation is rolled out and benchmarked.
For a value A
, an odd modulus M
and a value R
(must be coprime and greater than M
, chosen as a power of two for efficient performance), the Montgomery representation is A * R % M
.
Define the Montgomery modular multiplication of two values A
and B
: mulmont(A, B, M): A * B * R**-1 % M
where R**-1 % M
is the modular inverse of R
with respect to M
. There is various literature and algorithms for computing mulmont
which have been published since 1985, and the operation is used ubiquitously where execution is bottlenecked by operations over odd moduli.
Note that normal modular addition and subtraction algorithms work for Montgomery form values.
mulmont(canon_val, R**2 % M, M) = mont_val
mulmont(mont_val, 1, M) = canon_val
# Basic Montgomery Multiplication
# x, y, mod (int) - x < mod and y < mod
# mod (int) - an odd modulus
# R (int) - a power of two, and greater than mod
# mont_const (int) - pow(-mod, -1, R), precomputed
# output:
# (x * y * pow(R, -1, mod)) % mod
#
def mulmont(x: int, y: int, mod: int, monst_const: int, R: int) -> int:
t = x * y
m = ((t % R) * mont_const) % R
t = t + m * mod
t /= R
if t >= mod:
t -= mod
return t
There are various optimized algorithms for computing Montgomery Multiplication of bigint numbers expressed as arrays of system words.
These all use a different precomputed constant mont_const = pow(-mod, -1, 2**SYSTEM_WORD_SIZE_BITS)
.
There are tests contained in the Geth implementation. However, no cross-client tests exist yet.
There is an implementation of this EIP in an open PR to Go Ethereum.
Copyright and related rights waived via CC0.