From 3e49cc0613d8e24c1a09803a17954df2431a928c Mon Sep 17 00:00:00 2001 From: Petar Ivanov <29689712+dartdart26@users.noreply.github.com> Date: Thu, 20 Apr 2023 10:45:30 +0300 Subject: [PATCH] Add gas costs for FHE operations Add gas costs for FHE-related operations implemented both as precompiled contracts and EVM opcodes. For operations whose runtime and/or storage depend on FHE integer types, we define a base cost for the smallest type and measure how much more resources bigger types require compared to the base. That is how we determine costs for bigger types. Base costs are somewhat arbitrary at that point. We would most likely have to tweak them once we have parallel tfhe-rs operations and/or hardware acceleration. Costs for reencryption and requires are arbitrary at that point, because we don't know how expensive they would be when done via threshold protocols. As of now, optimistic requires only support FheUint32. Reason is we don't have tfhe-rs type casting such that we could have different types for the control bit and the input values. Currently, we don't handle refunds for SSTORE when persisting a ciphertext handle. Also, we don't charge different costs for protected storage based on whether a byte of ciphertext is zero or non-zero. When doing SLOAD for a ciphertext that is already in memory, skip storage and just import the same ciphertext at the current depth. Finally, fix a bug in the `require` precompile that wasn't checking if the input ciphertxt is verified - it was only checking it is an existing one. --- core/vm/contracts.go | 280 +++++++++++++++++++++++--------------- core/vm/contracts_test.go | 18 ++- core/vm/gas_table.go | 18 +-- core/vm/instructions.go | 22 +-- core/vm/operations_acl.go | 36 ++++- params/protocol_params.go | 47 +++++++ 6 files changed, 283 insertions(+), 138 deletions(-) diff --git a/core/vm/contracts.go b/core/vm/contracts.go index 37bf15c5a447..359a197b00b8 100644 --- a/core/vm/contracts.go +++ b/core/vm/contracts.go @@ -51,7 +51,7 @@ type PrecompileAccessibleState interface { // requires a deterministic gas count based on the input size of the Run method of the // contract. type PrecompiledContract interface { - RequiredGas(input []byte) uint64 // RequiredGas calculates the contract gas use + RequiredGas(accessibleState PrecompileAccessibleState, input []byte) uint64 // RequiredGas calculates the contract gas use Run(accessibleState PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, readOnly bool) (ret []byte, err error) } @@ -238,7 +238,7 @@ func ActivePrecompiles(rules params.Rules) []common.Address { func RunPrecompiledContract(p PrecompiledContract, accessibleState PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) { accessibleState.Interpreter().evm.depth++ defer func() { accessibleState.Interpreter().evm.depth-- }() - gasCost := p.RequiredGas(input) + gasCost := p.RequiredGas(accessibleState, input) if suppliedGas < gasCost { return nil, 0, ErrOutOfGas } @@ -250,7 +250,7 @@ func RunPrecompiledContract(p PrecompiledContract, accessibleState PrecompileAcc // ECRECOVER implemented as a native contract. type ecrecover struct{} -func (c *ecrecover) RequiredGas(input []byte) uint64 { +func (c *ecrecover) RequiredGas(accessibleState PrecompileAccessibleState, input []byte) uint64 { return params.EcrecoverGas } @@ -292,7 +292,7 @@ type sha256hash struct{} // // This method does not require any overflow checking as the input size gas costs // required for anything significant is so high it's impossible to pay for. -func (c *sha256hash) RequiredGas(input []byte) uint64 { +func (c *sha256hash) RequiredGas(accessibleState PrecompileAccessibleState, input []byte) uint64 { return uint64(len(input)+31)/32*params.Sha256PerWordGas + params.Sha256BaseGas } func (c *sha256hash) Run(accessibleState PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, readOnly bool) ([]byte, error) { @@ -307,7 +307,7 @@ type ripemd160hash struct{} // // This method does not require any overflow checking as the input size gas costs // required for anything significant is so high it's impossible to pay for. -func (c *ripemd160hash) RequiredGas(input []byte) uint64 { +func (c *ripemd160hash) RequiredGas(accessibleState PrecompileAccessibleState, input []byte) uint64 { return uint64(len(input)+31)/32*params.Ripemd160PerWordGas + params.Ripemd160BaseGas } func (c *ripemd160hash) Run(accessibleState PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, readOnly bool) ([]byte, error) { @@ -323,7 +323,7 @@ type dataCopy struct{} // // This method does not require any overflow checking as the input size gas costs // required for anything significant is so high it's impossible to pay for. -func (c *dataCopy) RequiredGas(input []byte) uint64 { +func (c *dataCopy) RequiredGas(accessibleState PrecompileAccessibleState, input []byte) uint64 { return uint64(len(input)+31)/32*params.IdentityPerWordGas + params.IdentityBaseGas } func (c *dataCopy) Run(accessibleState PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, readOnly bool) ([]byte, error) { @@ -383,7 +383,7 @@ func modexpMultComplexity(x *big.Int) *big.Int { } // RequiredGas returns the gas required to execute the pre-compiled contract. -func (c *bigModExp) RequiredGas(input []byte) uint64 { +func (c *bigModExp) RequiredGas(accessibleState PrecompileAccessibleState, input []byte) uint64 { var ( baseLen = new(big.Int).SetBytes(getData(input, 0, 32)) expLen = new(big.Int).SetBytes(getData(input, 32, 32)) @@ -522,7 +522,7 @@ func runBn256Add(input []byte) ([]byte, error) { type bn256AddIstanbul struct{} // RequiredGas returns the gas required to execute the pre-compiled contract. -func (c *bn256AddIstanbul) RequiredGas(input []byte) uint64 { +func (c *bn256AddIstanbul) RequiredGas(accessibleState PrecompileAccessibleState, input []byte) uint64 { return params.Bn256AddGasIstanbul } @@ -535,7 +535,7 @@ func (c *bn256AddIstanbul) Run(accessibleState PrecompileAccessibleState, caller type bn256AddByzantium struct{} // RequiredGas returns the gas required to execute the pre-compiled contract. -func (c *bn256AddByzantium) RequiredGas(input []byte) uint64 { +func (c *bn256AddByzantium) RequiredGas(accessibleState PrecompileAccessibleState, input []byte) uint64 { return params.Bn256AddGasByzantium } @@ -560,7 +560,7 @@ func runBn256ScalarMul(input []byte) ([]byte, error) { type bn256ScalarMulIstanbul struct{} // RequiredGas returns the gas required to execute the pre-compiled contract. -func (c *bn256ScalarMulIstanbul) RequiredGas(input []byte) uint64 { +func (c *bn256ScalarMulIstanbul) RequiredGas(accessibleState PrecompileAccessibleState, input []byte) uint64 { return params.Bn256ScalarMulGasIstanbul } @@ -573,7 +573,7 @@ func (c *bn256ScalarMulIstanbul) Run(accessibleState PrecompileAccessibleState, type bn256ScalarMulByzantium struct{} // RequiredGas returns the gas required to execute the pre-compiled contract. -func (c *bn256ScalarMulByzantium) RequiredGas(input []byte) uint64 { +func (c *bn256ScalarMulByzantium) RequiredGas(accessibleState PrecompileAccessibleState, input []byte) uint64 { return params.Bn256ScalarMulGasByzantium } @@ -628,7 +628,7 @@ func runBn256Pairing(input []byte) ([]byte, error) { type bn256PairingIstanbul struct{} // RequiredGas returns the gas required to execute the pre-compiled contract. -func (c *bn256PairingIstanbul) RequiredGas(input []byte) uint64 { +func (c *bn256PairingIstanbul) RequiredGas(accessibleState PrecompileAccessibleState, input []byte) uint64 { return params.Bn256PairingBaseGasIstanbul + uint64(len(input)/192)*params.Bn256PairingPerPointGasIstanbul } @@ -641,7 +641,7 @@ func (c *bn256PairingIstanbul) Run(accessibleState PrecompileAccessibleState, ca type bn256PairingByzantium struct{} // RequiredGas returns the gas required to execute the pre-compiled contract. -func (c *bn256PairingByzantium) RequiredGas(input []byte) uint64 { +func (c *bn256PairingByzantium) RequiredGas(accessibleState PrecompileAccessibleState, input []byte) uint64 { return params.Bn256PairingBaseGasByzantium + uint64(len(input)/192)*params.Bn256PairingPerPointGasByzantium } @@ -651,7 +651,7 @@ func (c *bn256PairingByzantium) Run(accessibleState PrecompileAccessibleState, c type blake2F struct{} -func (c *blake2F) RequiredGas(input []byte) uint64 { +func (c *blake2F) RequiredGas(accessibleState PrecompileAccessibleState, input []byte) uint64 { // If the input is malformed, we can't calculate the gas, return 0 and let the // actual call choke and fault. if len(input) != blake2FInputLength { @@ -721,7 +721,7 @@ var ( type bls12381G1Add struct{} // RequiredGas returns the gas required to execute the pre-compiled contract. -func (c *bls12381G1Add) RequiredGas(input []byte) uint64 { +func (c *bls12381G1Add) RequiredGas(accessibleState PrecompileAccessibleState, input []byte) uint64 { return params.Bls12381G1AddGas } @@ -759,7 +759,7 @@ func (c *bls12381G1Add) Run(accessibleState PrecompileAccessibleState, caller co type bls12381G1Mul struct{} // RequiredGas returns the gas required to execute the pre-compiled contract. -func (c *bls12381G1Mul) RequiredGas(input []byte) uint64 { +func (c *bls12381G1Mul) RequiredGas(accessibleState PrecompileAccessibleState, input []byte) uint64 { return params.Bls12381G1MulGas } @@ -795,7 +795,7 @@ func (c *bls12381G1Mul) Run(accessibleState PrecompileAccessibleState, caller co type bls12381G1MultiExp struct{} // RequiredGas returns the gas required to execute the pre-compiled contract. -func (c *bls12381G1MultiExp) RequiredGas(input []byte) uint64 { +func (c *bls12381G1MultiExp) RequiredGas(accessibleState PrecompileAccessibleState, input []byte) uint64 { // Calculate G1 point, scalar value pair length k := len(input) / 160 if k == 0 { @@ -852,7 +852,7 @@ func (c *bls12381G1MultiExp) Run(accessibleState PrecompileAccessibleState, call type bls12381G2Add struct{} // RequiredGas returns the gas required to execute the pre-compiled contract. -func (c *bls12381G2Add) RequiredGas(input []byte) uint64 { +func (c *bls12381G2Add) RequiredGas(accessibleState PrecompileAccessibleState, input []byte) uint64 { return params.Bls12381G2AddGas } @@ -890,7 +890,7 @@ func (c *bls12381G2Add) Run(accessibleState PrecompileAccessibleState, caller co type bls12381G2Mul struct{} // RequiredGas returns the gas required to execute the pre-compiled contract. -func (c *bls12381G2Mul) RequiredGas(input []byte) uint64 { +func (c *bls12381G2Mul) RequiredGas(accessibleState PrecompileAccessibleState, input []byte) uint64 { return params.Bls12381G2MulGas } @@ -926,7 +926,7 @@ func (c *bls12381G2Mul) Run(accessibleState PrecompileAccessibleState, caller co type bls12381G2MultiExp struct{} // RequiredGas returns the gas required to execute the pre-compiled contract. -func (c *bls12381G2MultiExp) RequiredGas(input []byte) uint64 { +func (c *bls12381G2MultiExp) RequiredGas(accessibleState PrecompileAccessibleState, input []byte) uint64 { // Calculate G2 point, scalar value pair length k := len(input) / 288 if k == 0 { @@ -983,7 +983,7 @@ func (c *bls12381G2MultiExp) Run(accessibleState PrecompileAccessibleState, call type bls12381Pairing struct{} // RequiredGas returns the gas required to execute the pre-compiled contract. -func (c *bls12381Pairing) RequiredGas(input []byte) uint64 { +func (c *bls12381Pairing) RequiredGas(accessibleState PrecompileAccessibleState, input []byte) uint64 { return params.Bls12381PairingBaseGas + uint64(len(input)/384)*params.Bls12381PairingPerPairGas } @@ -1062,7 +1062,7 @@ func decodeBLS12381FieldElement(in []byte) ([]byte, error) { type bls12381MapG1 struct{} // RequiredGas returns the gas required to execute the pre-compiled contract. -func (c *bls12381MapG1) RequiredGas(input []byte) uint64 { +func (c *bls12381MapG1) RequiredGas(accessibleState PrecompileAccessibleState, input []byte) uint64 { return params.Bls12381MapG1Gas } @@ -1097,7 +1097,7 @@ func (c *bls12381MapG1) Run(accessibleState PrecompileAccessibleState, caller co type bls12381MapG2 struct{} // RequiredGas returns the gas required to execute the pre-compiled contract. -func (c *bls12381MapG2) RequiredGas(input []byte) uint64 { +func (c *bls12381MapG2) RequiredGas(accessibleState PrecompileAccessibleState, input []byte) uint64 { return params.Bls12381MapG2Gas } @@ -1272,25 +1272,75 @@ func importRandomCiphertext(accessibleState PrecompileAccessibleState, t fheUint return ctHash[:] } -type fheAdd struct{} - -func (e *fheAdd) RequiredGas(input []byte) uint64 { - // TODO - return 8 -} - -func (e *fheAdd) Run(accessibleState PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, readOnly bool) ([]byte, error) { +func get2VerifiedOperands(accessibleState PrecompileAccessibleState, input []byte) (lhs *verifiedCiphertext, rhs *verifiedCiphertext, err error) { if len(input) != 64 { - return nil, errors.New("input needs to contain two 256-bit sized values") + return nil, nil, errors.New("input needs to contain two 256-bit sized values") } - - lhs := getVerifiedCiphertext(accessibleState, common.BytesToHash(input[0:32])) + lhs = getVerifiedCiphertext(accessibleState, common.BytesToHash(input[0:32])) if lhs == nil { - return nil, errors.New("unverified ciphertext handle") + return nil, nil, errors.New("unverified ciphertext handle") } - rhs := getVerifiedCiphertext(accessibleState, common.BytesToHash(input[32:64])) + rhs = getVerifiedCiphertext(accessibleState, common.BytesToHash(input[32:64])) if rhs == nil { - return nil, errors.New("unverified ciphertext handle") + return nil, nil, errors.New("unverified ciphertext handle") + } + err = nil + return +} + +var fheAddSubGasCosts = map[fheUintType]uint64{ + FheUint8: params.FheUint8AddSubGas, + FheUint16: params.FheUint16AddSubGas, + FheUint32: params.FheUint32AddSubGas, +} + +var fheMulGasCosts = map[fheUintType]uint64{ + FheUint8: params.FheUint8MulGas, + FheUint16: params.FheUint16MulGas, + FheUint32: params.FheUint32MulGas, +} + +var fheLteGasCosts = map[fheUintType]uint64{ + FheUint8: params.FheUint8LteGas, + FheUint16: params.FheUint16LteGas, + FheUint32: params.FheUint32LteGas, +} + +var fheReencryptGasCosts = map[fheUintType]uint64{ + FheUint8: params.FheUint8ReencryptGas, + FheUint16: params.FheUint16ReencryptGas, + FheUint32: params.FheUint32ReencryptGas, +} + +var fheVerifyGasCosts = map[fheUintType]uint64{ + FheUint8: params.FheUint8VerifyGas, + FheUint16: params.FheUint16VerifyGas, + FheUint32: params.FheUint32VerifyGas, +} + +var fheRequireGasCosts = map[fheUintType]uint64{ + FheUint8: params.FheUint8RequireGas, + FheUint16: params.FheUint16RequireGas, + FheUint32: params.FheUint32RequireGas, +} + +type fheAdd struct{} + +func (e *fheAdd) RequiredGas(accessibleState PrecompileAccessibleState, input []byte) uint64 { + lhs, rhs, err := get2VerifiedOperands(accessibleState, input) + if err != nil { + return 0 + } + if lhs.ciphertext.fheUintType != rhs.ciphertext.fheUintType { + return 0 + } + return fheAddSubGasCosts[lhs.ciphertext.fheUintType] +} + +func (e *fheAdd) Run(accessibleState PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, readOnly bool) ([]byte, error) { + lhs, rhs, err := get2VerifiedOperands(accessibleState, input) + if err != nil { + return nil, err } // If we are doing gas estimation, skip execution and insert a random ciphertext as a result. @@ -1341,9 +1391,12 @@ func exitProcess() { type verifyCiphertext struct{} -func (e *verifyCiphertext) RequiredGas(input []byte) uint64 { - // TODO - return 8 +func (e *verifyCiphertext) RequiredGas(accessibleState PrecompileAccessibleState, input []byte) uint64 { + if len(input) <= 0 { + return 0 + } + ctType := fheUintType(input[len(input)-1]) + return fheVerifyGasCosts[ctType] } func (e *verifyCiphertext) Run(accessibleState PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, readOnly bool) ([]byte, error) { @@ -1384,9 +1437,15 @@ func toEVMBytes(input []byte) []byte { type reencrypt struct{} -func (e *reencrypt) RequiredGas(input []byte) uint64 { - // TODO - return 8 +func (e *reencrypt) RequiredGas(accessibleState PrecompileAccessibleState, input []byte) uint64 { + if len(input) != 32 { + return 0 + } + ct := getVerifiedCiphertext(accessibleState, common.BytesToHash(input)) + if ct == nil { + return 0 + } + return fheReencryptGasCosts[ct.ciphertext.fheUintType] } func (e *reencrypt) Run(accessibleState PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, readOnly bool) ([]byte, error) { @@ -1410,9 +1469,15 @@ func (e *reencrypt) Run(accessibleState PrecompileAccessibleState, caller common type require struct{} -func (e *require) RequiredGas(input []byte) uint64 { - // TODO - return 8 +func (e *require) RequiredGas(accessibleState PrecompileAccessibleState, input []byte) uint64 { + if len(input) != 32 { + return 0 + } + ct := getVerifiedCiphertext(accessibleState, common.BytesToHash(input)) + if ct == nil { + return 0 + } + return fheRequireGasCosts[ct.ciphertext.fheUintType] } type requireMessage struct { @@ -1519,8 +1584,8 @@ func (e *require) Run(accessibleState PrecompileAccessibleState, caller common.A if !accessibleState.Interpreter().evm.Commit { return nil, nil } - ct, ok := accessibleState.Interpreter().verifiedCiphertexts[common.BytesToHash(input)] - if !ok { + ct := getVerifiedCiphertext(accessibleState, common.BytesToHash(input)) + if ct == nil { return nil, errors.New("unverified ciphertext handle") } if !evaluateRequire(ct.ciphertext) { @@ -1531,9 +1596,21 @@ func (e *require) Run(accessibleState PrecompileAccessibleState, caller common.A type optimisticRequire struct{} -func (e *optimisticRequire) RequiredGas(input []byte) uint64 { - // TODO - return 8 +func (e *optimisticRequire) RequiredGas(accessibleState PrecompileAccessibleState, input []byte) uint64 { + if len(input) != 32 { + return 0 + } + ct := getVerifiedCiphertext(accessibleState, common.BytesToHash(input)) + if ct == nil { + return 0 + } + if ct.ciphertext.fheUintType != FheUint32 { + return 0 + } + if accessibleState.Interpreter().optimisticRequire == nil { + return params.FheUint32OptimisticRequireGas + } + return params.FheUint32OptimisticRequireMulGas } func (e *optimisticRequire) Run(accessibleState PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, readOnly bool) ([]byte, error) { @@ -1552,6 +1629,9 @@ func (e *optimisticRequire) Run(accessibleState PrecompileAccessibleState, calle if !ok { return nil, errors.New("unverified ciphertext handle") } + if ct.ciphertext.fheUintType != FheUint32 { + return nil, errors.New("optimistic require currently only supports 32 bit FHE unsigned intergers") + } // If this is the first optimistic require, just assign it. // If there is already an optimistic one, just multiply it with the incoming one. Here, we assume // that ciphertexts have value of either 0 or 1. Thus, multiplying all optimistic requires leads to @@ -1570,23 +1650,21 @@ func (e *optimisticRequire) Run(accessibleState PrecompileAccessibleState, calle type fheLte struct{} -func (e *fheLte) RequiredGas(input []byte) uint64 { - // TODO - return 8 +func (e *fheLte) RequiredGas(accessibleState PrecompileAccessibleState, input []byte) uint64 { + lhs, rhs, err := get2VerifiedOperands(accessibleState, input) + if err != nil { + return 0 + } + if lhs.ciphertext.fheUintType != rhs.ciphertext.fheUintType { + return 0 + } + return fheLteGasCosts[lhs.ciphertext.fheUintType] } func (e *fheLte) Run(accessibleState PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, readOnly bool) ([]byte, error) { - if len(input) != 64 { - return nil, errors.New("input needs to contain two 256-bit sized values") - } - - lhs := getVerifiedCiphertext(accessibleState, common.BytesToHash(input[0:32])) - if lhs == nil { - return nil, errors.New("unverified ciphertext handle") - } - rhs := getVerifiedCiphertext(accessibleState, common.BytesToHash(input[32:64])) - if rhs == nil { - return nil, errors.New("unverified ciphertext handle") + lhs, rhs, err := get2VerifiedOperands(accessibleState, input) + if err != nil { + return nil, err } if lhs.ciphertext.fheUintType != rhs.ciphertext.fheUintType { @@ -1617,23 +1695,16 @@ func (e *fheLte) Run(accessibleState PrecompileAccessibleState, caller common.Ad type fheSub struct{} -func (e *fheSub) RequiredGas(input []byte) uint64 { - // TODO - return 8 +func (e *fheSub) RequiredGas(accessibleState PrecompileAccessibleState, input []byte) uint64 { + // Implement in terms of add, because add and sub costs are currently the same. + add := fheAdd{} + return add.RequiredGas(accessibleState, input) } func (e *fheSub) Run(accessibleState PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, readOnly bool) ([]byte, error) { - if len(input) != 64 { - return nil, errors.New("input needs to contain two 256-bit sized values") - } - - lhs := getVerifiedCiphertext(accessibleState, common.BytesToHash(input[0:32])) - if lhs == nil { - return nil, errors.New("unverified ciphertext handle") - } - rhs := getVerifiedCiphertext(accessibleState, common.BytesToHash(input[32:64])) - if rhs == nil { - return nil, errors.New("unverified ciphertext handle") + lhs, rhs, err := get2VerifiedOperands(accessibleState, input) + if err != nil { + return nil, err } if lhs.ciphertext.fheUintType != rhs.ciphertext.fheUintType { @@ -1664,23 +1735,21 @@ func (e *fheSub) Run(accessibleState PrecompileAccessibleState, caller common.Ad type fheMul struct{} -func (e *fheMul) RequiredGas(input []byte) uint64 { - // TODO - return 8 +func (e *fheMul) RequiredGas(accessibleState PrecompileAccessibleState, input []byte) uint64 { + lhs, rhs, err := get2VerifiedOperands(accessibleState, input) + if err != nil { + return 0 + } + if lhs.ciphertext.fheUintType != rhs.ciphertext.fheUintType { + return 0 + } + return fheMulGasCosts[lhs.ciphertext.fheUintType] } func (e *fheMul) Run(accessibleState PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, readOnly bool) ([]byte, error) { - if len(input) != 64 { - return nil, errors.New("input needs to contain two 256-bit sized values") - } - - lhs := getVerifiedCiphertext(accessibleState, common.BytesToHash(input[0:32])) - if lhs == nil { - return nil, errors.New("unverified ciphertext handle") - } - rhs := getVerifiedCiphertext(accessibleState, common.BytesToHash(input[32:64])) - if rhs == nil { - return nil, errors.New("unverified ciphertext handle") + lhs, rhs, err := get2VerifiedOperands(accessibleState, input) + if err != nil { + return nil, err } if lhs.ciphertext.fheUintType != rhs.ciphertext.fheUintType { @@ -1711,23 +1780,16 @@ func (e *fheMul) Run(accessibleState PrecompileAccessibleState, caller common.Ad type fheLt struct{} -func (e *fheLt) RequiredGas(input []byte) uint64 { - // TODO - return 8 +func (e *fheLt) RequiredGas(accessibleState PrecompileAccessibleState, input []byte) uint64 { + // Implement in terms of lte, because lte and lt costs are currently the same. + lte := fheLte{} + return lte.RequiredGas(accessibleState, input) } func (e *fheLt) Run(accessibleState PrecompileAccessibleState, caller common.Address, addr common.Address, input []byte, readOnly bool) ([]byte, error) { - if len(input) != 64 { - return nil, errors.New("input needs to contain two 256-bit sized values") - } - - lhs := getVerifiedCiphertext(accessibleState, common.BytesToHash(input[0:32])) - if lhs == nil { - return nil, errors.New("unverified ciphertext handle") - } - rhs := getVerifiedCiphertext(accessibleState, common.BytesToHash(input[32:64])) - if rhs == nil { - return nil, errors.New("unverified ciphertext handle") + lhs, rhs, err := get2VerifiedOperands(accessibleState, input) + if err != nil { + return nil, err } // If we are doing gas estimation, skip execution and insert a random ciphertext as a result. @@ -1832,7 +1894,7 @@ func (e *fheLt) Run(accessibleState PrecompileAccessibleState, caller common.Add type cast struct{} -func (e *cast) RequiredGas(input []byte) uint64 { +func (e *cast) RequiredGas(accessibleState PrecompileAccessibleState, input []byte) uint64 { return 0 } @@ -1845,7 +1907,7 @@ func (e *cast) Run(accessibleState PrecompileAccessibleState, caller common.Addr type faucet struct{} -func (e *faucet) RequiredGas(input []byte) uint64 { +func (e *faucet) RequiredGas(accessibleState PrecompileAccessibleState, input []byte) uint64 { return 0 } diff --git a/core/vm/contracts_test.go b/core/vm/contracts_test.go index 58224287e422..d88db31b3382 100644 --- a/core/vm/contracts_test.go +++ b/core/vm/contracts_test.go @@ -98,9 +98,10 @@ func testPrecompiled(addr string, test precompiledTest, t *testing.T) { a := common.HexToAddress(addr) p := allStatelessPrecompiles[common.HexToAddress(addr)] in := common.Hex2Bytes(test.Input) - gas := p.RequiredGas(in) + state := newTestState() + gas := p.RequiredGas(state, in) t.Run(fmt.Sprintf("%s-Gas=%d", test.Name, gas), func(t *testing.T) { - if res, _, err := RunPrecompiledContract(p, newTestState(), a, a, in, gas, false); err != nil { + if res, _, err := RunPrecompiledContract(p, state, a, a, in, gas, false); err != nil { t.Error(err) } else if common.Bytes2Hex(res) != test.Expected { t.Errorf("Expected %v, got %v", test.Expected, common.Bytes2Hex(res)) @@ -119,11 +120,12 @@ func testPrecompiled(addr string, test precompiledTest, t *testing.T) { func testPrecompiledOOG(addr string, test precompiledTest, t *testing.T) { p := allStatelessPrecompiles[common.HexToAddress(addr)] in := common.Hex2Bytes(test.Input) - gas := p.RequiredGas(in) - 1 + state := newTestState() + gas := p.RequiredGas(state, in) - 1 t.Run(fmt.Sprintf("%s-Gas=%d", test.Name, gas), func(t *testing.T) { a := common.HexToAddress(addr) - _, _, err := RunPrecompiledContract(p, newTestState(), a, a, in, gas, false) + _, _, err := RunPrecompiledContract(p, state, a, a, in, gas, false) if err.Error() != "out of gas" { t.Errorf("Expected error [out of gas], got [%v]", err) } @@ -139,9 +141,10 @@ func testPrecompiledFailure(addr string, test precompiledFailureTest, t *testing a := common.HexToAddress(addr) p := allStatelessPrecompiles[common.HexToAddress(addr)] in := common.Hex2Bytes(test.Input) - gas := p.RequiredGas(in) + state := newTestState() + gas := p.RequiredGas(state, in) t.Run(test.Name, func(t *testing.T) { - _, _, err := RunPrecompiledContract(p, newTestState(), a, a, in, gas, false) + _, _, err := RunPrecompiledContract(p, state, a, a, in, gas, false) if err.Error() != test.ExpectedError { t.Errorf("Expected error [%v], got [%v]", test.ExpectedError, err) } @@ -160,7 +163,8 @@ func benchmarkPrecompiled(addr string, test precompiledTest, bench *testing.B) { a := common.HexToAddress(addr) p := allStatelessPrecompiles[common.HexToAddress(addr)] in := common.Hex2Bytes(test.Input) - reqGas := p.RequiredGas(in) + state := newTestState() + reqGas := p.RequiredGas(state, in) var ( res []byte diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go index 4c2cb3e5cf79..95173cb2749a 100644 --- a/core/vm/gas_table.go +++ b/core/vm/gas_table.go @@ -162,19 +162,19 @@ func gasSStore(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySi return params.NetSstoreDirtyGas, nil } -// 0. If *gasleft* is less than or equal to 2300, fail the current call. -// 1. If current value equals new value (this is a no-op), SLOAD_GAS is deducted. -// 2. If current value does not equal new value: -// 2.1. If original value equals current value (this storage slot has not been changed by the current execution context): +// 0. If *gasleft* is less than or equal to 2300, fail the current call. +// 1. If current value equals new value (this is a no-op), SLOAD_GAS is deducted. +// 2. If current value does not equal new value: +// 2.1. If original value equals current value (this storage slot has not been changed by the current execution context): // 2.1.1. If original value is 0, SSTORE_SET_GAS (20K) gas is deducted. // 2.1.2. Otherwise, SSTORE_RESET_GAS gas is deducted. If new value is 0, add SSTORE_CLEARS_SCHEDULE to refund counter. -// 2.2. If original value does not equal current value (this storage slot is dirty), SLOAD_GAS gas is deducted. Apply both of the following clauses: +// 2.2. If original value does not equal current value (this storage slot is dirty), SLOAD_GAS gas is deducted. Apply both of the following clauses: // 2.2.1. If original value is not 0: -// 2.2.1.1. If current value is 0 (also means that new value is not 0), subtract SSTORE_CLEARS_SCHEDULE gas from refund counter. -// 2.2.1.2. If new value is 0 (also means that current value is not 0), add SSTORE_CLEARS_SCHEDULE gas to refund counter. +// 2.2.1.1. If current value is 0 (also means that new value is not 0), subtract SSTORE_CLEARS_SCHEDULE gas from refund counter. +// 2.2.1.2. If new value is 0 (also means that current value is not 0), add SSTORE_CLEARS_SCHEDULE gas to refund counter. // 2.2.2. If original value equals new value (this storage slot is reset): -// 2.2.2.1. If original value is 0, add SSTORE_SET_GAS - SLOAD_GAS to refund counter. -// 2.2.2.2. Otherwise, add SSTORE_RESET_GAS - SLOAD_GAS gas to refund counter. +// 2.2.2.1. If original value is 0, add SSTORE_SET_GAS - SLOAD_GAS to refund counter. +// 2.2.2.2. Otherwise, add SSTORE_RESET_GAS - SLOAD_GAS gas to refund counter. func gasSStoreEIP2200(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { // If we fail the minimum gas availability invariant, fail (0) if contract.Gas <= params.SstoreSentryGasEIP2200 { diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 7256c551be71..9c81c54accc1 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -559,6 +559,13 @@ func newInt(buf []byte) *uint256.Int { var zero = uint256.NewInt(0).Bytes32() func verifyIfCiphertextHandle(val common.Hash, interpreter *EVMInterpreter, contractAddress common.Address) { + ct, ok := interpreter.verifiedCiphertexts[val] + if ok { + // If already existing in memory, skip storage and import the same ciphertext at the current depth. + importCiphertextToEVM(interpreter, ct.ciphertext) + return + } + protectedStorage := crypto.CreateProtectedStorageContractAddress(contractAddress) metadataInt := newInt(interpreter.evm.StateDB.GetState(protectedStorage, val).Bytes()) if !metadataInt.IsZero() { @@ -577,16 +584,11 @@ func verifyIfCiphertextHandle(val common.Hash, interpreter *EVMInterpreter, cont ctBytes = append(ctBytes, bytes[0:toAppend]...) protectedSlotIdx.AddUint64(protectedSlotIdx, 1) } - var ct *tfheCiphertext - verifiedCt := getVerifiedCiphertextFromEVM(interpreter, val) - if verifiedCt != nil { - ct = verifiedCt.ciphertext - } else { - ct = new(tfheCiphertext) - err := ct.deserialize(ctBytes, metadata.fheUintType) - if err != nil { - exitProcess() - } + + ct := new(tfheCiphertext) + err := ct.deserialize(ctBytes, metadata.fheUintType) + if err != nil { + exitProcess() } importCiphertextToEVM(interpreter, ct) } diff --git a/core/vm/operations_acl.go b/core/vm/operations_acl.go index 551e1f5f1188..85f91849d228 100644 --- a/core/vm/operations_acl.go +++ b/core/vm/operations_acl.go @@ -21,9 +21,22 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" ) +var gasProtectedStorageSstore = map[fheUintType]uint64{ + FheUint8: params.FheUint8ProtectedStorageSstore, + FheUint16: params.FheUint16ProtectedStorageSstore, + FheUint32: params.FheUint32ProtectedStorageSstore, +} + +var gasProtectedStorageSload = map[fheUintType]uint64{ + FheUint8: params.FheUint8ProtectedStorageSload, + FheUint16: params.FheUint16ProtectedStorageSload, + FheUint32: params.FheUint32ProtectedStorageSload, +} + func makeGasSStoreFunc(clearingRefund uint64) gasFunc { return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { // If we fail the minimum gas availability invariant, fail (0) @@ -50,12 +63,16 @@ func makeGasSStoreFunc(clearingRefund uint64) gasFunc { } } value := common.Hash(y.Bytes32()) - if current == value { // noop (1) // EIP 2200 original clause: // return params.SloadGasEIP2200, nil return cost + params.WarmStorageReadCostEIP2929, nil // SLOAD_GAS } + // TODO: For now, every SSTORE referring to a ciphertext incurs the same cost, irrespective + // of the original and current values of the slot. Refunds for protected storage are not taken into account. + if ct, ok := evm.interpreter.verifiedCiphertexts[value]; ok { + cost += gasProtectedStorageSstore[ct.ciphertext.fheUintType] + } original := evm.StateDB.GetCommittedState(contract.Address(), x.Bytes32()) if original == current { if original == (common.Hash{}) { // create slot (2.1.1) @@ -103,14 +120,27 @@ func makeGasSStoreFunc(clearingRefund uint64) gasFunc { func gasSLoadEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { loc := stack.peek() slot := common.Hash(loc.Bytes32()) + value := evm.StateDB.GetState(contract.Address(), slot) + + // If the value we load is a ciphertext handle, add to the gas cost + protectedStorageGas := uint64(0) + if _, ok := evm.interpreter.verifiedCiphertexts[value]; !ok { + protectedStorage := crypto.CreateProtectedStorageContractAddress(contract.Address()) + metadataInt := newInt(evm.StateDB.GetState(protectedStorage, value).Bytes()) + if !metadataInt.IsZero() { + metadata := newCiphertextMetadata(metadataInt.Bytes32()) + protectedStorageGas = gasProtectedStorageSload[metadata.fheUintType] + } + } + // Check slot presence in the access list if _, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot); !slotPresent { // If the caller cannot afford the cost, this change will be rolled back // If he does afford it, we can skip checking the same thing later on, during execution evm.StateDB.AddSlotToAccessList(contract.Address(), slot) - return params.ColdSloadCostEIP2929, nil + return params.ColdSloadCostEIP2929 + protectedStorageGas, nil } - return params.WarmStorageReadCostEIP2929, nil + return params.WarmStorageReadCostEIP2929 + protectedStorageGas, nil } // gasExtCodeCopyEIP2929 implements extcodecopy according to EIP-2929 diff --git a/params/protocol_params.go b/params/protocol_params.go index 0637e570c99f..f294e907a791 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -158,6 +158,53 @@ const ( // up to half the consumed gas could be refunded. Redefined as 1/5th in EIP-3529 RefundQuotient uint64 = 2 RefundQuotientEIP3529 uint64 = 5 + + // FHE operation costs depend on tfhe-rs performance and hardware acceleration. These values will most certainly change. + FheUint8AddSubGas uint64 = 5000 + FheUint16AddSubGas uint64 = FheUint8AddSubGas * 2 + FheUint32AddSubGas uint64 = FheUint16AddSubGas * 4 + FheUint8MulGas uint64 = 9000 + FheUint16MulGas uint64 = FheUint8MulGas * 3 + FheUint32MulGas uint64 = FheUint16MulGas * 10 + FheUint8LteGas uint64 = 3300 + FheUint16LteGas uint64 = 5000 + FheUint32LteGas uint64 = 11000 + + // TODO: Cost will depend on the complexity of doing reencryption by the oracle. + FheUint8ReencryptGas uint64 = 15000 + FheUint16ReencryptGas uint64 = FheUint8ReencryptGas * 2 + FheUint32ReencryptGas uint64 = FheUint16ReencryptGas * 4 + + // As of now, verification costs only cover ciphertext deserialization and assume there is no ZKPoK to verify. + FheUint8VerifyGas uint64 = 500 + FheUint16VerifyGas uint64 = 600 + FheUint32VerifyGas uint64 = 2000 + + // TODO: Cost will depend on the complexity of doing decryption by the oracle. + FheUint8RequireGas uint64 = 10000 + FheUint16RequireGas uint64 = FheUint8RequireGas * 2 + FheUint32RequireGas uint64 = FheUint16RequireGas * 4 + + // TODO: As of now, only support FheUint32 due to inability to cast between types. + // If there is at least one optimistic require, we need to decrypt it as it was a normal FHE require. + // For every subsequent optimistic require, we need to multiply it with the current require value. + FheUint32OptimisticRequireGas uint64 = FheUint32RequireGas + FheUint32OptimisticRequireMulGas uint64 = FheUint32MulGas + + // TODO: This will change once we have an FHE-based random generaration with different types. + FheRandGas uint64 = NetSstoreCleanGas + ColdSloadCostEIP2929 + + // TODO: The values here are chosen somewhat arbitrarily (at least the 8 bit ones). Also, we don't + // take into account whether a ciphertext existed (either "current" or "original") for the given handle. + // Finally, costs are likely to change in the future. + FheUint8ProtectedStorageSstore uint64 = NetSstoreInitGas * 3 + FheUint16ProtectedStorageSstore uint64 = FheUint8ProtectedStorageSstore * 2 + FheUint32ProtectedStorageSstore uint64 = FheUint16ProtectedStorageSstore * 4 + + // TODO: We don't take whether the slot is cold or warm into consideration. + FheUint8ProtectedStorageSload uint64 = ColdSloadCostEIP2929 * 3 + FheUint16ProtectedStorageSload uint64 = FheUint8ProtectedStorageSload * 2 + FheUint32ProtectedStorageSload uint64 = FheUint16ProtectedStorageSload * 4 ) // Gas discount table for BLS12-381 G1 and G2 multi exponentiation operations