Skip to content

Commit

Permalink
fix: missing gas costs and memory issues
Browse files Browse the repository at this point in the history
Add missing gas costs for multiple operations, mainly for FheBool,
FheUint160 and FheUint2048.

Note that verify for bool is more expensive due to lack of cast from
2048 bits to bool - doing not equal instead.

Make `castTo()` fail and not panic on bad type input.

Fixed a memory leak in `executeTernaryCiphertextOperation()` - the
`first_ptr` pointer was never freed.

Refactor some of the code such that it uses `defer` with destroy as
close to the point where memory is allocated as possible - that fixes
memory leaks on early returns in multiple places.

C code needs to be refactored and reduced. Maybe we can use codegen or
a tool for that.
  • Loading branch information
dartdart26 committed Jun 19, 2024
1 parent d22f070 commit 47dccc3
Show file tree
Hide file tree
Showing 6 changed files with 223 additions and 510 deletions.
137 changes: 0 additions & 137 deletions fhevm/contracts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ package fhevm
import (
"bytes"
"context"
"encoding/hex"
"errors"
"fmt"
"math/big"
"os"
Expand Down Expand Up @@ -164,46 +162,6 @@ func toPrecompileInputNoScalar(isScalar bool, hashes ...common.Hash) []byte {
return ret
}

func decryptRunWithoutKms(environment EVMEnvironment, caller common.Address, addr common.Address, input []byte, readOnly bool) ([]byte, error) {
logger := environment.GetLogger()
// if not gas estimation and not view function fail if decryptions are disabled in transactions
if environment.IsCommitting() && !environment.IsEthCall() && environment.FhevmParams().DisableDecryptionsInTransaction {
msg := "decryptions during transaction are disabled"
logger.Error(msg, "input", hex.EncodeToString(input))
return nil, errors.New(msg)
}
if len(input) != 32 {
msg := "decrypt input len must be 32 bytes"
logger.Error(msg, "input", hex.EncodeToString(input), "len", len(input))
return nil, errors.New(msg)
}
ct, _ := loadCiphertext(environment, common.BytesToHash(input))
if ct == nil {
msg := "decrypt unverified handle"
logger.Error(msg, "input", hex.EncodeToString(input))
return nil, errors.New(msg)
}

// If we are doing gas estimation, skip decryption and make sure we return the maximum possible value.
// We need that, because non-zero bytes cost more than zero bytes in some contexts (e.g. SSTORE or memory operations).
if !environment.IsCommitting() && !environment.IsEthCall() {
return bytes.Repeat([]byte{0xFF}, 32), nil
}

plaintext, err := ct.Decrypt()
if err != nil {
logger.Error("decrypt failed", "err", err)
return nil, err
}

logger.Info("decrypt success", "plaintext", plaintext)

// Always return a 32-byte big-endian integer.
ret := make([]byte, 32)
plaintext.FillBytes(ret)
return ret, nil
}

var scalarBytePadding = make([]byte, 31)

func toLibPrecompileInput(method string, isScalar bool, hashes ...common.Hash) []byte {
Expand Down Expand Up @@ -1943,31 +1901,6 @@ func LibDecrypt(t *testing.T, fheUintType tfhe.FheUintType) {
}
}

// TODO: can be enabled if mocking kms or running a kms during tests
// func TestLibReencrypt(t *testing.T) {
// signature := "reencrypt(uint256,uint256)"
// hashRes := crypto.Keccak256([]byte(signature))
// signatureBytes := hashRes[0:4]
// depth := 1
// environment := newTestEVMEnvironment()
// environment.depth = depth
// environment.ethCall = true
// toEncrypt := 7
// fheUintType := tfhe.FheUint8
// encCiphertext := loadCiphertextInTestMemory(environment, uint64(toEncrypt), depth, fheUintType).getHash()
// addr := tfheExecutorContractAddress
// readOnly := false
// input := make([]byte, 0)
// input = append(input, signatureBytes...)
// input = append(input, encCiphertext.Bytes()...)
// // just append twice not to generate public key
// input = append(input, encCiphertext.Bytes()...)
// _, err := FheLibRun(environment, addr, addr, input, readOnly)
// if err != nil {
// t.Fatalf("Reencrypt error: %s", err.Error())
// }
// }

func TestLibCast(t *testing.T) {
signature := "cast(uint256,bytes1)"
hashRes := crypto.Keccak256([]byte(signature))
Expand Down Expand Up @@ -3122,41 +3055,6 @@ func FheIfThenElse(t *testing.T, fheUintType tfhe.FheUintType, condition uint64)
}
}

func Decrypt(t *testing.T, fheUintType tfhe.FheUintType) {
var value uint64
switch fheUintType {
case tfhe.FheBool:
value = 1
case tfhe.FheUint4:
value = 2
case tfhe.FheUint8:
value = 2
case tfhe.FheUint16:
value = 4283
case tfhe.FheUint32:
value = 1333337
case tfhe.FheUint64:
value = 133333777777777
}
depth := 1
environment := newTestEVMEnvironment()
environment.depth = depth
addr := tfheExecutorContractAddress
readOnly := false
hash := loadCiphertextInTestMemory(environment, value, depth, fheUintType).GetHash()
out, err := decryptRunWithoutKms(environment, addr, addr, hash.Bytes(), readOnly)
if err != nil {
t.Fatalf(err.Error())
} else if len(out) != 32 {
t.Fatalf("decrypt expected output len of 32, got %v", len(out))
}
result := big.Int{}
result.SetBytes(out)
if result.Uint64() != value {
t.Fatalf("decrypt result not equal to value, result %v != value %v", result.Uint64(), value)
}
}

func FheRand(t *testing.T, fheUintType tfhe.FheUintType) {
depth := 1
environment := newTestEVMEnvironment()
Expand Down Expand Up @@ -4677,22 +4575,6 @@ func TestFheScalarMax64(t *testing.T) {
FheMax(t, tfhe.FheUint64, true)
}

func TestDecrypt8(t *testing.T) {
Decrypt(t, tfhe.FheUint8)
}

func TestDecrypt16(t *testing.T) {
Decrypt(t, tfhe.FheUint16)
}

func TestDecrypt32(t *testing.T) {
Decrypt(t, tfhe.FheUint32)
}

func TestDecrypt64(t *testing.T) {
Decrypt(t, tfhe.FheUint64)
}

func TestFheRand8(t *testing.T) {
FheRand(t, tfhe.FheUint8)
}
Expand Down Expand Up @@ -4883,25 +4765,6 @@ func newInterpreterFromEnvironment(environment *MockEVMEnvironment) *vm.EVMInter
return interpreter
}

func TestDecryptInTransactionDisabled(t *testing.T) {
depth := 0
environment := newTestEVMEnvironment()
environment.depth = depth
environment.commit = true
environment.ethCall = false
environment.fhevmParams.DisableDecryptionsInTransaction = true
addr := tfheExecutorContractAddress
readOnly := false
hash := loadCiphertextInTestMemory(environment, 1, depth, tfhe.FheUint8).GetHash()
// Call decrypt and expect it to fail due to disabling of decryptions during commit
_, err := decryptRunWithoutKms(environment, addr, addr, hash.Bytes(), readOnly)
if err == nil {
t.Fatalf("expected to error out in test")
} else if err.Error() != "decryptions during transaction are disabled" {
t.Fatalf("unexpected error for disabling decryption transactions, got %s", err.Error())
}
}

func TestFheLibGetCiphertextInvalidInputSize(t *testing.T) {
environment := newTestEVMEnvironment()
addr := tfheExecutorContractAddress
Expand Down
12 changes: 0 additions & 12 deletions fhevm/fhelib.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,18 +227,6 @@ var fhelibMethods = []*FheLibMethod{
requiredGasFunction: trivialEncryptRequiredGas,
runFunction: trivialEncryptRun,
},
{
name: "decrypt",
argTypes: "(uint256)",
requiredGasFunction: decryptRequiredGas,
runFunction: decryptRun,
},
{
name: "reencrypt",
argTypes: "(uint256,uint256)",
requiredGasFunction: reencryptRequiredGas,
runFunction: reencryptRun,
},
{
name: "verifyCiphertext",
argTypes: "(bytes32,address,bytes,bytes1)",
Expand Down
129 changes: 0 additions & 129 deletions fhevm/operators_crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,135 +180,6 @@ func verifyCiphertextRun(environment EVMEnvironment, caller common.Address, addr
return handle[:], nil
}

func reencryptRun(environment EVMEnvironment, caller common.Address, addr common.Address, input []byte, readOnly bool, runSpan trace.Span) ([]byte, error) {
input = input[:minInt(64, len(input))]
// precompileBytes, err := reencryptRun(environment, caller, addr, bwCompatBytes, readOnly)

logger := environment.GetLogger()
if !environment.IsEthCall() {
msg := "reencrypt only supported on EthCall"
logger.Error(msg)
return nil, errors.New(msg)
}
if len(input) != 64 {
msg := "reencrypt input len must be 64 bytes"
logger.Error(msg, "input", hex.EncodeToString(input), "len", len(input))
return nil, errors.New(msg)
}
handle := common.BytesToHash(input[0:32])
ct, _ := loadCiphertext(environment, handle)
if ct != nil {
otelDescribeOperandsFheTypes(runSpan, ct.Type())

var fheType kms.FheType
switch ct.Type() {
case tfhe.FheBool:
fheType = kms.FheType_Bool
case tfhe.FheUint4:
fheType = kms.FheType_Euint4
case tfhe.FheUint8:
fheType = kms.FheType_Euint8
case tfhe.FheUint16:
fheType = kms.FheType_Euint16
case tfhe.FheUint32:
fheType = kms.FheType_Euint32
case tfhe.FheUint64:
fheType = kms.FheType_Euint64
case tfhe.FheUint160:
fheType = kms.FheType_Euint160
}

pubKey := input[32:64]

// TODO: generate merkle proof for some data
proof := &kms.Proof{
Height: 3,
MerklePatriciaProof: []byte{},
}

reencryptionRequest := &kms.ReencryptionRequest{
FheType: fheType,
Ciphertext: ct.Serialize(),
Request: pubKey, // TODO: change according to the structure of `Request`
Proof: proof,
}

conn, err := grpc.Dial(kms.KmsEndpointAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return nil, errors.New("kms unreachable")
}
defer conn.Close()

ep := kms.NewKmsEndpointClient(conn)

ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()

res, err := ep.Reencrypt(ctx, reencryptionRequest)
if err != nil {
return nil, err
}

// TODO: decide if `res.Signature` should be verified here

var reencryptedValue = res.ReencryptedCiphertext

logger.Info("reencrypt success", "input", hex.EncodeToString(input), "callerAddr", caller, "reencryptedValue", reencryptedValue, "len", len(reencryptedValue))
reencryptedValue = toEVMBytes(reencryptedValue)
// pad according to abi specification, first add offset to the dynamic bytes argument
outputBytes := make([]byte, 32, len(reencryptedValue)+32)
outputBytes[31] = 0x20
outputBytes = append(outputBytes, reencryptedValue...)
return padArrayTo32Multiple(outputBytes), nil
}
msg := "reencrypt could not load ciphertext handle"
logger.Error(msg, "input", hex.EncodeToString(input))
return nil, errors.New(msg)
}

func decryptRun(environment EVMEnvironment, caller common.Address, addr common.Address, input []byte, readOnly bool, runSpan trace.Span) ([]byte, error) {
input = input[:minInt(32, len(input))]

logger := environment.GetLogger()
// if not gas estimation and not view function fail if decryptions are disabled in transactions
if environment.IsCommitting() && !environment.IsEthCall() && environment.FhevmParams().DisableDecryptionsInTransaction {
msg := "decryptions during transaction are disabled"
logger.Error(msg, "input", hex.EncodeToString(input))
return nil, errors.New(msg)
}
if len(input) != 32 {
msg := "decrypt input len must be 32 bytes"
logger.Error(msg, "input", hex.EncodeToString(input), "len", len(input))
return nil, errors.New(msg)
}
ct, _ := loadCiphertext(environment, common.BytesToHash(input))
if ct == nil {
msg := "decrypt unverified handle"
logger.Error(msg, "input", hex.EncodeToString(input))
return nil, errors.New(msg)
}
otelDescribeOperandsFheTypes(runSpan, ct.Type())

// If we are doing gas estimation, skip decryption and make sure we return the maximum possible value.
// We need that, because non-zero bytes cost more than zero bytes in some contexts (e.g. SSTORE or memory operations).
if !environment.IsCommitting() && !environment.IsEthCall() {
return bytes.Repeat([]byte{0xFF}, 32), nil
}

plaintext, err := decryptValue(environment, ct)
if err != nil {
logger.Error("decrypt failed", "err", err)
return nil, err
}

logger.Info("decrypt success", "plaintext", plaintext)

// Always return a 32-byte big-endian integer.
ret := make([]byte, 32)
plaintext.FillBytes(ret)
return ret, nil
}

func getCiphertextRun(environment EVMEnvironment, caller common.Address, addr common.Address, input []byte, readOnly bool, runSpan trace.Span) ([]byte, error) {
input = input[:minInt(32, len(input))]

Expand Down
32 changes: 0 additions & 32 deletions fhevm/operators_crypto_gas.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,6 @@ func verifyCiphertextRequiredGas(environment EVMEnvironment, input []byte) uint6
return environment.FhevmParams().GasCosts.FheVerify[ct.Type()]
}

func reencryptRequiredGas(environment EVMEnvironment, input []byte) uint64 {
input = input[:minInt(64, len(input))]

logger := environment.GetLogger()
if len(input) != 64 {
logger.Error("reencrypt RequiredGas() input len must be 64 bytes", "input", hex.EncodeToString(input), "len", len(input))
return 0
}
ct, loadGas := loadCiphertext(environment, common.BytesToHash(input[0:32]))
if ct == nil {
logger.Error("reencrypt RequiredGas() input doesn't point to verified ciphertext", "input", hex.EncodeToString(input))
return loadGas
}
return environment.FhevmParams().GasCosts.FheReencrypt[ct.Type()] + loadGas
}

func getCiphertextRequiredGas(environment EVMEnvironment, input []byte) uint64 {
input = input[:minInt(32, len(input))]

Expand Down Expand Up @@ -71,22 +55,6 @@ func castRequiredGas(environment EVMEnvironment, input []byte) uint64 {
return environment.FhevmParams().GasCosts.FheCast + loadGas
}

func decryptRequiredGas(environment EVMEnvironment, input []byte) uint64 {
input = input[:minInt(32, len(input))]

logger := environment.GetLogger()
if len(input) != 32 {
logger.Error("decrypt RequiredGas() input len must be 32 bytes", "input", hex.EncodeToString(input), "len", len(input))
return 0
}
ct, loadGas := loadCiphertext(environment, common.BytesToHash(input))
if ct == nil {
logger.Error("decrypt RequiredGas() input doesn't point to verified ciphertext", "input", hex.EncodeToString(input))
return loadGas
}
return environment.FhevmParams().GasCosts.FheDecrypt[ct.Type()] + loadGas
}

func fhePubKeyRequiredGas(environment EVMEnvironment, input []byte) uint64 {
return environment.FhevmParams().GasCosts.FhePubKey
}
Expand Down
Loading

0 comments on commit 47dccc3

Please sign in to comment.