From 26427fd695fde3b613b611dbab6112652ecfc8d6 Mon Sep 17 00:00:00 2001 From: Petar Ivanov <29689712+dartdart26@users.noreply.github.com> Date: Wed, 21 Feb 2024 13:07:18 +0200 Subject: [PATCH] feat: a precompile to get ciphertext bytes Given a contract address and an ebool/euint value stored in it, return the underlying ciphertext. Returns an empty response if no such ciphertext exist. Only works via `eth_call`. The function selector for it, as of this commit, is `e4b808cb`. Move protected storage code into its own file. Add a `FheLib Library` section in the getting started doc section. Nit: rename `arg_types` to `argTypes` in instructions.go for naming consistency. --- docs/SUMMARY.md | 1 + docs/getting_started/FheLib.md | 63 +++++++++ fhevm/contracts_test.go | 210 ++++++++++++++++++++++++++++++ fhevm/fhelib.go | 70 +++++----- fhevm/fhelib_required_gas.go | 19 +++ fhevm/fhelib_run.go | 26 ++++ fhevm/instructions.go | 197 +--------------------------- fhevm/params.go | 9 ++ fhevm/protected_storage.go | 228 +++++++++++++++++++++++++++++++++ 9 files changed, 597 insertions(+), 226 deletions(-) create mode 100644 docs/getting_started/FheLib.md create mode 100644 fhevm/protected_storage.go diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 694a339..ce403c5 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -6,6 +6,7 @@ ## Getting Started - [Initial Setup](getting_started/README.md) - [Integration](getting_started/Integration.md) +- [FheLib Library](getting_started/FheLib.md) ## 🔗 Support diff --git a/docs/getting_started/FheLib.md b/docs/getting_started/FheLib.md new file mode 100644 index 0000000..c566824 --- /dev/null +++ b/docs/getting_started/FheLib.md @@ -0,0 +1,63 @@ +# FheLib + +`FheLib` is a library implemented inside fhevm-go. It offers FHE-related functionalities such as homomorphic operations, decryption/reencryption requests and so on. FheLib is exposed as a single `precompiled` contract (or a `precompile` for short) that is integrated into the underlying blockchain. + +FheLib functions can be called by calling the FheLib precompile with a respective EVM function selector. + +This page describes the required inputs, behaviours and outputs of some of these functions. + +## GetCiphertext Function (selector: e4b808cb) + +The `GetCiphertext` function returns a serialized TFHE ciphertext from protected storage given: + * contract address where the ciphertext is stored at + * the ebool/e(u)int value (also called a handle) for which the ciphertext is requested + +GetCiphertext only works via the `eth_call` RPC. + +To call GetCiphertext via `eth_call`, the following Python can serve as an example: + +```python +import http.client +import json + +# This is the address of the FheLib precompile. This value is hardcoded per blockchain. +fhe_lib_precompile_address = "0x000000000000000000000000000000000000005d" + +# The contract address where the ciphertext is stored at. +contract_address = "ACD7Be4EBF68Bf2A5b6eB0CaFb15460C169BC459" +# 12 bytes of 0s for padding the contract address. +address_zero_padding = "000000000000000000000000" + +# The ebool/e(u)int value for which the ciphertext is requested. +handle = "f038cdc8bf630e239f143abeb039b91ec82ec17a8460582e7a409fa551030c06" + +# The function selector of GetCiphertext. +get_ciphertext_selector = "e4b808cb" + +# Call the FheLib precompile with `data` being the concatenation of: +# - getCiphertext function selector; +# - 12 bytes of 0s to padd the contract address; +# - contract address; +# - the handle to the ciphertext. +payload = { + "jsonrpc": "2.0", + "method": "eth_call", + "params": [ + { + "to": fhe_lib_precompile_address, + "data": "0x" + get_ciphertext_selector + address_zero_padding + + contract_address + handle + }, + "latest" + ], + "id": 1, +} + +con = http.client.HTTPConnection("localhost", 8545) +con.request("POST", "/", body=json.dumps(payload), + headers={"Content-Type": "application/json"}) +resp = json.loads(con.getresponse().read()) + +# Remove leading "0x" and decode hex to get a byte buffer with the ciphertext. +ciphertext = bytes.fromhex(resp["result"][2:]) +``` \ No newline at end of file diff --git a/fhevm/contracts_test.go b/fhevm/contracts_test.go index 89c4c7c..889b0fd 100644 --- a/fhevm/contracts_test.go +++ b/fhevm/contracts_test.go @@ -4101,3 +4101,213 @@ func TestDecryptInTransactionDisabled(t *testing.T) { t.Fatalf("unexpected error for disabling decryption transactions, got %s", err.Error()) } } + +func TestFheLibGetCiphertextInvalidInputSize(t *testing.T) { + environment := newTestEVMEnvironment() + addr := common.Address{} + environment.ethCall = true + readOnly := true + input := make([]byte, 0) + zeroPadding := make([]byte, 12) + signature := crypto.Keccak256([]byte("getCiphertext(address,uint256)"))[0:4] + input = append(input, signature...) + input = append(input, zeroPadding...) + // missing input data... + _, err := FheLibRun(environment, addr, addr, input, readOnly) + if err == nil { + t.Fatalf("getCiphertext expected failure on bad input size") + } +} + +func TestFheLibGetCiphertextNonEthCall(t *testing.T) { + environment := newTestEVMEnvironment() + pc := uint64(0) + depth := 1 + environment.depth = depth + plaintext := uint64(2) + ct := verifyCiphertextInTestMemory(environment, plaintext, depth, FheUint32) + ctHash := ct.GetHash() + scope := newTestScopeConext() + loc := uint256.NewInt(10) + value := uint256FromBig(ctHash.Big()) + + // Setup and call SSTORE - it requires a location and a value to set there. + scope.pushToStack(value) + scope.pushToStack(loc) + _, err := OpSstore(&pc, environment, scope) + if err != nil { + t.Fatalf(err.Error()) + } + + // Call getCiphertext. + addr := common.Address{} + environment.ethCall = false + readOnly := true + input := make([]byte, 0) + zeroPadding := make([]byte, 12) + signature := crypto.Keccak256([]byte("getCiphertext(address,uint256)"))[0:4] + input = append(input, signature...) + input = append(input, zeroPadding...) + input = append(input, testContractAddress{}.Address().Bytes()...) + input = append(input, ctHash.Bytes()...) + _, err = FheLibRun(environment, addr, addr, input, readOnly) + if err == nil { + t.Fatalf("getCiphertext expected failure non-EthCall") + } +} + +func TestFheLibGetCiphertextNonExistentHandle(t *testing.T) { + environment := newTestEVMEnvironment() + pc := uint64(0) + depth := 1 + environment.depth = depth + plaintext := uint64(2) + ct := verifyCiphertextInTestMemory(environment, plaintext, depth, FheUint32) + ctHash := ct.GetHash() + scope := newTestScopeConext() + loc := uint256.NewInt(10) + value := uint256FromBig(ctHash.Big()) + + // Setup and call SSTORE - it requires a location and a value to set there. + scope.pushToStack(value) + scope.pushToStack(loc) + _, err := OpSstore(&pc, environment, scope) + if err != nil { + t.Fatalf(err.Error()) + } + + // Change ctHash to something that doesn't exist + ctHash[0]++ + + // Call getCiphertext. + addr := common.Address{} + environment.ethCall = true + readOnly := true + input := make([]byte, 0) + zeroPadding := make([]byte, 12) + signature := crypto.Keccak256([]byte("getCiphertext(address,uint256)"))[0:4] + input = append(input, signature...) + input = append(input, zeroPadding...) + input = append(input, testContractAddress{}.Address().Bytes()...) + input = append(input, ctHash.Bytes()...) + out, err := FheLibRun(environment, addr, addr, input, readOnly) + if err != nil { + t.Fatalf(err.Error()) + } + if len(out) != 0 { + t.Fatalf("getCiphertext expected empty output on non-existent handle") + } +} + +func TestFheLibGetCiphertextWrongContractAddress(t *testing.T) { + environment := newTestEVMEnvironment() + pc := uint64(0) + depth := 1 + environment.depth = depth + plaintext := uint64(2) + ct := verifyCiphertextInTestMemory(environment, plaintext, depth, FheUint32) + ctHash := ct.GetHash() + scope := newTestScopeConext() + loc := uint256.NewInt(10) + value := uint256FromBig(ctHash.Big()) + + // Setup and call SSTORE - it requires a location and a value to set there. + scope.pushToStack(value) + scope.pushToStack(loc) + _, err := OpSstore(&pc, environment, scope) + if err != nil { + t.Fatalf(err.Error()) + } + + // Call getCiphertext. + addr := common.Address{} + environment.ethCall = true + readOnly := true + contractAddress := testContractAddress{}.Address() + // Change address to another one that doesn't contain the handle. + contractAddress[0]++ + input := make([]byte, 0) + zeroPadding := make([]byte, 12) + signature := crypto.Keccak256([]byte("getCiphertext(address,uint256)"))[0:4] + input = append(input, signature...) + input = append(input, zeroPadding...) + input = append(input, contractAddress.Bytes()...) + input = append(input, ctHash.Bytes()...) + out, err := FheLibRun(environment, addr, addr, input, readOnly) + if err != nil { + t.Fatalf(err.Error()) + } + if len(out) != 0 { + t.Fatalf("getCiphertext expected empty output on wrong contract address") + } +} + +func FheLibGetCiphertext(t *testing.T, fheUintType FheUintType) { + environment := newTestEVMEnvironment() + pc := uint64(0) + depth := 1 + environment.depth = depth + plaintext := uint64(2) + ct := verifyCiphertextInTestMemory(environment, plaintext, depth, fheUintType) + ctHash := ct.GetHash() + scope := newTestScopeConext() + loc := uint256.NewInt(10) + value := uint256FromBig(ctHash.Big()) + + // Setup and call SSTORE - it requires a location and a value to set there. + scope.pushToStack(value) + scope.pushToStack(loc) + _, err := OpSstore(&pc, environment, scope) + if err != nil { + t.Fatalf(err.Error()) + } + + // Call getCiphertext. + addr := common.Address{} + environment.ethCall = true + readOnly := true + input := make([]byte, 0) + zeroPadding := make([]byte, 12) + signature := crypto.Keccak256([]byte("getCiphertext(address,uint256)"))[0:4] + input = append(input, signature...) + input = append(input, zeroPadding...) + input = append(input, testContractAddress{}.Address().Bytes()...) + input = append(input, ctHash.Bytes()...) + out, err := FheLibRun(environment, addr, addr, input, readOnly) + if err != nil { + t.Fatalf(err.Error()) + } + size, _ := GetExpandedFheCiphertextSize(fheUintType) + if size != uint(len(out)) { + t.Fatalf("getCiphertext returned ciphertext size of %d, expected %d", len(out), size) + } + + outCt := new(TfheCiphertext) + err = outCt.Deserialize(out, fheUintType) + if err != nil { + t.Fatalf(err.Error()) + } + decrypted, err := outCt.Decrypt() + if err != nil { + t.Fatalf(err.Error()) + } + if decrypted.Uint64() != plaintext { + t.Fatalf("getCiphertext returned ciphertext value of %d, expected %d", decrypted.Uint64(), plaintext) + } +} + +func TestFheLibGetCiphertext8(t *testing.T) { + FheLibGetCiphertext(t, FheUint8) +} + +func TestFheLibGetCiphertext16(t *testing.T) { + FheLibGetCiphertext(t, FheUint16) +} + +func TestFheLibGetCiphertext32(t *testing.T) { + FheLibGetCiphertext(t, FheUint32) +} + +func TestFheLibGetCiphertext64(t *testing.T) { + FheLibGetCiphertext(t, FheUint64) +} diff --git a/fhevm/fhelib.go b/fhevm/fhelib.go index 6845a34..cab695c 100644 --- a/fhevm/fhelib.go +++ b/fhevm/fhelib.go @@ -16,7 +16,7 @@ type FheLibMethod struct { // name of the fhelib function name string // types of the arguments that the fhelib function take. format is "(type1,type2...)" (e.g "(uint256,bytes1)") - arg_types string + argTypes string requiredGasFunction func(environment EVMEnvironment, input []byte) uint64 runFunction func(environment EVMEnvironment, caller common.Address, addr common.Address, input []byte, readOnly bool, runSpan trace.Span) ([]byte, error) } @@ -31,7 +31,7 @@ func makeKeccakSignature(input string) uint32 { // Return the computed signature by concatenating the name and the arg types of the method func (fheLibMethod *FheLibMethod) Signature() uint32 { - return makeKeccakSignature(fheLibMethod.name + fheLibMethod.arg_types) + return makeKeccakSignature(fheLibMethod.name + fheLibMethod.argTypes) } func (fheLibMethod *FheLibMethod) RequiredGas(environment EVMEnvironment, input []byte) uint64 { @@ -54,184 +54,190 @@ func GetFheLibMethod(signature uint32) (fheLibMethod *FheLibMethod, found bool) var fhelibMethods = []*FheLibMethod{ { name: "fheAdd", - arg_types: "(uint256,uint256,bytes1)", + argTypes: "(uint256,uint256,bytes1)", requiredGasFunction: fheAddSubRequiredGas, runFunction: fheAddRun, }, { name: "fheSub", - arg_types: "(uint256,uint256,bytes1)", + argTypes: "(uint256,uint256,bytes1)", requiredGasFunction: fheAddSubRequiredGas, runFunction: fheSubRun, }, { name: "fheMul", - arg_types: "(uint256,uint256,bytes1)", + argTypes: "(uint256,uint256,bytes1)", requiredGasFunction: fheMulRequiredGas, runFunction: fheMulRun, }, { name: "fheDiv", - arg_types: "(uint256,uint256,bytes1)", + argTypes: "(uint256,uint256,bytes1)", requiredGasFunction: fheDivRequiredGas, runFunction: fheDivRun, }, { name: "fheRem", - arg_types: "(uint256,uint256,bytes1)", + argTypes: "(uint256,uint256,bytes1)", requiredGasFunction: fheRemRequiredGas, runFunction: fheRemRun, }, { name: "fheMin", - arg_types: "(uint256,uint256,bytes1)", + argTypes: "(uint256,uint256,bytes1)", requiredGasFunction: fheMinRequiredGas, runFunction: fheMinRun, }, { name: "fheMax", - arg_types: "(uint256,uint256,bytes1)", + argTypes: "(uint256,uint256,bytes1)", requiredGasFunction: fheMaxRequiredGas, runFunction: fheMaxRun, }, { name: "fheRand", - arg_types: "(bytes1)", + argTypes: "(bytes1)", requiredGasFunction: fheRandRequiredGas, runFunction: fheRandRun, }, { name: "fheRandBounded", - arg_types: "(uint256,bytes1)", + argTypes: "(uint256,bytes1)", requiredGasFunction: fheRandBoundedRequiredGas, runFunction: fheRandBoundedRun, }, { name: "cast", - arg_types: "(uint256,bytes1)", + argTypes: "(uint256,bytes1)", requiredGasFunction: castRequiredGas, runFunction: castRun, }, { name: "fheLe", - arg_types: "(uint256,uint256,bytes1)", + argTypes: "(uint256,uint256,bytes1)", requiredGasFunction: fheLeRequiredGas, runFunction: fheLeRun, }, { name: "fheLt", - arg_types: "(uint256,uint256,bytes1)", + argTypes: "(uint256,uint256,bytes1)", requiredGasFunction: fheLtRequiredGas, runFunction: fheLtRun, }, { name: "fheEq", - arg_types: "(uint256,uint256,bytes1)", + argTypes: "(uint256,uint256,bytes1)", requiredGasFunction: fheEqRequiredGas, runFunction: fheEqRun, }, { name: "fheGe", - arg_types: "(uint256,uint256,bytes1)", + argTypes: "(uint256,uint256,bytes1)", requiredGasFunction: fheGeRequiredGas, runFunction: fheGeRun, }, { name: "fheGt", - arg_types: "(uint256,uint256,bytes1)", + argTypes: "(uint256,uint256,bytes1)", requiredGasFunction: fheGtRequiredGas, runFunction: fheGtRun, }, { name: "fheShl", - arg_types: "(uint256,uint256,bytes1)", + argTypes: "(uint256,uint256,bytes1)", requiredGasFunction: fheShlRequiredGas, runFunction: fheShlRun, }, { name: "fheShr", - arg_types: "(uint256,uint256,bytes1)", + argTypes: "(uint256,uint256,bytes1)", requiredGasFunction: fheShrRequiredGas, runFunction: fheShrRun, }, { name: "fheNe", - arg_types: "(uint256,uint256,bytes1)", + argTypes: "(uint256,uint256,bytes1)", requiredGasFunction: fheNeRequiredGas, runFunction: fheNeRun, }, { name: "fheNeg", - arg_types: "(uint256)", + argTypes: "(uint256)", requiredGasFunction: fheNegRequiredGas, runFunction: fheNegRun, }, { name: "fheNot", - arg_types: "(uint256)", + argTypes: "(uint256)", requiredGasFunction: fheNotRequiredGas, runFunction: fheNotRun, }, { name: "fheBitAnd", - arg_types: "(uint256,uint256,bytes1)", + argTypes: "(uint256,uint256,bytes1)", requiredGasFunction: fheBitAndRequiredGas, runFunction: fheBitAndRun, }, { name: "fheBitOr", - arg_types: "(uint256,uint256,bytes1)", + argTypes: "(uint256,uint256,bytes1)", requiredGasFunction: fheBitOrRequiredGas, runFunction: fheBitOrRun, }, { name: "fheBitXor", - arg_types: "(uint256,uint256,bytes1)", + argTypes: "(uint256,uint256,bytes1)", requiredGasFunction: fheBitXorRequiredGas, runFunction: fheBitXorRun, }, { name: "fheIfThenElse", - arg_types: "(uint256,uint256,uint256)", + argTypes: "(uint256,uint256,uint256)", requiredGasFunction: fheIfThenElseRequiredGas, runFunction: fheIfThenElseRun, }, { name: "fhePubKey", - arg_types: "(bytes1)", + argTypes: "(bytes1)", requiredGasFunction: fhePubKeyRequiredGas, runFunction: fhePubKeyRun, }, { name: "trivialEncrypt", - arg_types: "(uint256,bytes1)", + argTypes: "(uint256,bytes1)", requiredGasFunction: trivialEncryptRequiredGas, runFunction: trivialEncryptRun, }, { name: "decrypt", - arg_types: "(uint256)", + argTypes: "(uint256)", requiredGasFunction: decryptRequiredGas, runFunction: decryptRun, }, { name: "reencrypt", - arg_types: "(uint256,uint256)", + argTypes: "(uint256,uint256)", requiredGasFunction: reencryptRequiredGas, runFunction: reencryptRun, }, { name: "verifyCiphertext", - arg_types: "(bytes)", + argTypes: "(bytes)", requiredGasFunction: verifyCiphertextRequiredGas, runFunction: verifyCiphertextRun, }, { name: "optimisticRequire", - arg_types: "(uint256)", + argTypes: "(uint256)", requiredGasFunction: optimisticRequireRequiredGas, runFunction: optimisticRequireRun, }, + { + name: "getCiphertext", + argTypes: "(address,uint256)", + requiredGasFunction: getCiphertextRequiredGas, + runFunction: getCiphertextRun, + }, } func init() { diff --git a/fhevm/fhelib_required_gas.go b/fhevm/fhelib_required_gas.go index 85af89d..7497a7b 100644 --- a/fhevm/fhelib_required_gas.go +++ b/fhevm/fhelib_required_gas.go @@ -431,6 +431,25 @@ func optimisticRequireRequiredGas(environment EVMEnvironment, input []byte) uint return environment.FhevmParams().GasCosts.FheOptRequireBitAnd[FheUint8] } +func getCiphertextRequiredGas(environment EVMEnvironment, input []byte) uint64 { + input = input[:minInt(64, len(input))] + + logger := environment.GetLogger() + if len(input) != 64 { + logger.Error("getCiphertext RequiredGas() input len must be 64 bytes", + "input", hex.EncodeToString(input), "len", len(input)) + return 0 + } + + contractAddress := common.BytesToAddress(input[:32]) + handle := common.BytesToHash(input[32:]) + metadata := getCiphertextMetadataFromProtectedStorage(environment, contractAddress, handle) + if metadata == nil { + return GetNonExistentCiphertextGas + } + return environment.FhevmParams().GasCosts.FheGetCiphertext[metadata.fheUintType] +} + func castRequiredGas(environment EVMEnvironment, input []byte) uint64 { input = input[:minInt(33, len(input))] diff --git a/fhevm/fhelib_run.go b/fhevm/fhelib_run.go index 70f906d..b885f91 100644 --- a/fhevm/fhelib_run.go +++ b/fhevm/fhelib_run.go @@ -1601,6 +1601,32 @@ func decryptRun(environment EVMEnvironment, caller common.Address, addr common.A 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(64, len(input))] + + logger := environment.GetLogger() + if !environment.IsEthCall() { + msg := "getCiphertext only supported on EthCall" + logger.Error(msg) + return nil, errors.New(msg) + } + if len(input) != 64 { + msg := "getCiphertext input len must be 64 bytes" + logger.Error(msg, "input", hex.EncodeToString(input), "len", len(input)) + return nil, errors.New(msg) + } + + contractAddress := common.BytesToAddress(input[:32]) + handle := common.BytesToHash(input[32:]) + + ciphertext := getCiphertextFromProtectedStoage(environment, contractAddress, handle) + if ciphertext == nil { + return make([]byte, 0), nil + } + otelDescribeOperandsFheTypes(runSpan, ciphertext.metadata.fheUintType) + return ciphertext.bytes, nil +} + func decryptValue(environment EVMEnvironment, ct *TfheCiphertext) (uint64, error) { logger := environment.GetLogger() diff --git a/fhevm/instructions.go b/fhevm/instructions.go index 22033ad..1a90f0d 100644 --- a/fhevm/instructions.go +++ b/fhevm/instructions.go @@ -1,8 +1,6 @@ package fhevm import ( - "bytes" - "encoding/hex" "errors" "math/big" "strings" @@ -25,108 +23,6 @@ func contains(haystack []byte, needle []byte) bool { return strings.Contains(string(haystack), string(needle)) } -// Ciphertext metadata is stored in protected storage, in a 32-byte slot. -// Currently, we only utilize 17 bytes from the slot. -type ciphertextMetadata struct { - refCount uint64 - length uint64 - fheUintType FheUintType -} - -func (m ciphertextMetadata) serialize() [32]byte { - u := uint256.NewInt(0) - u[0] = m.refCount - u[1] = m.length - u[2] = uint64(m.fheUintType) - return u.Bytes32() -} - -func (m *ciphertextMetadata) deserialize(buf [32]byte) *ciphertextMetadata { - u := uint256.NewInt(0) - u.SetBytes(buf[:]) - m.refCount = u[0] - m.length = u[1] - m.fheUintType = FheUintType(u[2]) - return m -} - -func newCiphertextMetadata(buf [32]byte) *ciphertextMetadata { - m := ciphertextMetadata{} - return m.deserialize(buf) -} - -func minUint64(a, b uint64) uint64 { - if a < b { - return a - } - return b -} - -// If references are still left, reduce refCount by 1. Otherwise, zero out the metadata and the ciphertext slots. -func garbageCollectProtectedStorage(flagHandleLocation common.Hash, handle common.Hash, protectedStorage common.Address, env EVMEnvironment) { - // The location of ciphertext metadata is at Keccak256(handle). Doing so avoids attacks from users trying to garbage - // collect arbitrary locations in protected storage. Hashing the handle makes it hard to find a preimage such that - // it ends up in arbitrary non-zero places in protected stroage. - metadataKey := crypto.Keccak256Hash(handle.Bytes()) - - existingMetadataHash := env.GetState(protectedStorage, metadataKey) - existingMetadataInt := newInt(existingMetadataHash.Bytes()) - if !existingMetadataInt.IsZero() { - logger := env.GetLogger() - - // If no flag in protected storage for the location, ignore garbage collection. - // Else, set the value at the location to zero. - foundFlag := env.GetState(protectedStorage, flagHandleLocation) - if !bytes.Equal(foundFlag.Bytes(), flag.Bytes()) { - logger.Error("opSstore location flag not found for a ciphertext handle, ignoring garbage collection", - "expectedFlag", hex.EncodeToString(flag[:]), - "foundFlag", hex.EncodeToString(foundFlag[:]), - "flagHandleLocation", hex.EncodeToString(flagHandleLocation[:])) - return - } else { - env.SetState(protectedStorage, flagHandleLocation, zero) - } - - metadata := newCiphertextMetadata(existingMetadataInt.Bytes32()) - if metadata.refCount == 1 { - if env.IsCommitting() { - logger.Info("opSstore garbage collecting ciphertext", - "protectedStorage", hex.EncodeToString(protectedStorage[:]), - "metadataKey", hex.EncodeToString(metadataKey[:]), - "type", metadata.fheUintType, - "len", metadata.length) - } - - // Zero the metadata key-value. - env.SetState(protectedStorage, metadataKey, zero) - - // Set the slot to the one after the metadata one. - slot := newInt(metadataKey.Bytes()) - slot.AddUint64(slot, 1) - - // Zero the ciphertext slots. - slotsToZero := metadata.length / 32 - if metadata.length > 0 && metadata.length < 32 { - slotsToZero++ - } - for i := uint64(0); i < slotsToZero; i++ { - env.SetState(protectedStorage, slot.Bytes32(), zero) - slot.AddUint64(slot, 1) - } - } else if metadata.refCount > 1 { - if env.IsCommitting() { - logger.Info("opSstore decrementing ciphertext refCount", - "protectedStorage", hex.EncodeToString(protectedStorage[:]), - "metadataKey", hex.EncodeToString(metadataKey[:]), - "type", metadata.fheUintType, - "len", metadata.length) - } - metadata.refCount-- - env.SetState(protectedStorage, existingMetadataHash, metadata.serialize()) - } - } -} - func isVerifiedAtCurrentDepth(environment EVMEnvironment, ct *verifiedCiphertext) bool { return ct.verifiedDepths.has(environment.GetDepth()) } @@ -153,28 +49,10 @@ func verifyIfCiphertextHandle(handle common.Hash, env EVMEnvironment, contractAd return nil } - metadataKey := crypto.Keccak256Hash(handle.Bytes()) - protectedStorage := fhevm_crypto.CreateProtectedStorageContractAddress(contractAddress) - metadataInt := newInt(env.GetState(protectedStorage, metadataKey).Bytes()) - if !metadataInt.IsZero() { - metadata := newCiphertextMetadata(metadataInt.Bytes32()) - ctBytes := make([]byte, 0) - left := metadata.length - protectedSlotIdx := newInt(metadataKey.Bytes()) - protectedSlotIdx.AddUint64(protectedSlotIdx, 1) - for { - if left == 0 { - break - } - bytes := env.GetState(protectedStorage, protectedSlotIdx.Bytes32()) - toAppend := minUint64(uint64(len(bytes)), left) - left -= toAppend - ctBytes = append(ctBytes, bytes[0:toAppend]...) - protectedSlotIdx.AddUint64(protectedSlotIdx, 1) - } - + ciphertext := getCiphertextFromProtectedStoage(env, contractAddress, handle) + if ciphertext != nil { ct := new(TfheCiphertext) - err := ct.Deserialize(ctBytes, metadata.fheUintType) + err := ct.Deserialize(ciphertext.bytes, ciphertext.metadata.fheUintType) if err != nil { msg := "opSload failed to deserialize a ciphertext" env.GetLogger().Error(msg, "err", err) @@ -201,75 +79,6 @@ func OpSload(pc *uint64, env EVMEnvironment, scope ScopeContext) ([]byte, error) return nil, nil } -// An arbitrary constant value to flag locations in protected storage. -var flag = common.HexToHash("0xa145ffde0100a145ffde0100a145ffde0100a145ffde0100a145ffde0100fab3") - -// If a verified ciphertext: -// * if the ciphertext does not exist in protected storage, persist it with a refCount = 1 -// * if the ciphertexts exists in protected, bump its refCount by 1 -func persistIfVerifiedCiphertext(flagHandleLocation common.Hash, handle common.Hash, protectedStorage common.Address, env EVMEnvironment) { - verifiedCiphertext := getVerifiedCiphertextFromEVM(env, handle) - if verifiedCiphertext == nil { - return - } - logger := env.GetLogger() - - // Try to read ciphertext metadata from protected storage. - metadataKey := crypto.Keccak256Hash(handle.Bytes()) - metadataInt := newInt(env.GetState(protectedStorage, metadataKey).Bytes()) - metadata := ciphertextMetadata{} - - // Set flag in protected storage to mark the location as containing a handle. - env.SetState(protectedStorage, flagHandleLocation, flag) - - if metadataInt.IsZero() { - // If no metadata, it means this ciphertext itself hasn't been persisted to protected storage yet. We do that as part of SSTORE. - metadata.refCount = 1 - metadata.length = uint64(expandedFheCiphertextSize[verifiedCiphertext.ciphertext.fheUintType]) - metadata.fheUintType = verifiedCiphertext.ciphertext.fheUintType - ciphertextSlot := newInt(metadataKey.Bytes()) - ciphertextSlot.AddUint64(ciphertextSlot, 1) - if env.IsCommitting() { - logger.Info("opSstore persisting new ciphertext", - "protectedStorage", hex.EncodeToString(protectedStorage[:]), - "handle", hex.EncodeToString(handle.Bytes()), - "type", metadata.fheUintType, - "len", metadata.length, - "ciphertextSlot", hex.EncodeToString(ciphertextSlot.Bytes())) - } - ctPart32 := make([]byte, 32) - partIdx := 0 - ctBytes := verifiedCiphertext.ciphertext.Serialize() - for i, b := range ctBytes { - if i%32 == 0 && i != 0 { - env.SetState(protectedStorage, ciphertextSlot.Bytes32(), common.BytesToHash(ctPart32)) - ciphertextSlot.AddUint64(ciphertextSlot, 1) - ctPart32 = make([]byte, 32) - partIdx = 0 - } - ctPart32[partIdx] = b - partIdx++ - } - if len(ctPart32) != 0 { - env.SetState(protectedStorage, ciphertextSlot.Bytes32(), common.BytesToHash(ctPart32)) - } - } else { - // If metadata exists, bump the refcount by 1. - metadata = *newCiphertextMetadata(env.GetState(protectedStorage, metadataKey)) - metadata.refCount++ - if env.IsCommitting() { - logger.Info("opSstore bumping refcount of existing ciphertext", - "protectedStorage", hex.EncodeToString(protectedStorage[:]), - "handle", hex.EncodeToString(handle.Bytes()), - "type", metadata.fheUintType, - "len", metadata.length, - "refCount", metadata.refCount) - } - } - // Save the metadata in protected storage. - env.SetState(protectedStorage, metadataKey, metadata.serialize()) -} - func OpSstore(pc *uint64, env EVMEnvironment, scope ScopeContext) ([]byte, error) { // This function is a modified copy from https://github.com/ethereum/go-ethereum if otelCtx := env.OtelContext(); otelCtx != nil { diff --git a/fhevm/params.go b/fhevm/params.go index 577d205..83990ed 100644 --- a/fhevm/params.go +++ b/fhevm/params.go @@ -9,6 +9,8 @@ const EvmNetSstoreInitGas uint64 = 20000 const AdjustFHEGas uint64 = 10000 const ColdSloadCostEIP2929 uint64 = 2100 +const GetNonExistentCiphertextGas uint64 = 1000 + var ( // 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. @@ -59,6 +61,7 @@ type GasCosts struct { FheVerify map[FheUintType]uint64 FheOptRequire map[FheUintType]uint64 FheOptRequireBitAnd map[FheUintType]uint64 + FheGetCiphertext map[FheUintType]uint64 } func DefaultGasCosts() GasCosts { @@ -194,6 +197,12 @@ func DefaultGasCosts() GasCosts { FheUint16: 20000, FheUint32: 20000, }, + FheGetCiphertext: map[FheUintType]uint64{ + FheUint8: 12000, + FheUint16: 14000, + FheUint32: 18000, + FheUint64: 28000, + }, } } diff --git a/fhevm/protected_storage.go b/fhevm/protected_storage.go new file mode 100644 index 0000000..0e2ad23 --- /dev/null +++ b/fhevm/protected_storage.go @@ -0,0 +1,228 @@ +package fhevm + +import ( + "bytes" + "encoding/hex" + + "github.com/ethereum/go-ethereum/common" + crypto "github.com/ethereum/go-ethereum/crypto" + "github.com/holiman/uint256" + fhevm_crypto "github.com/zama-ai/fhevm-go/crypto" +) + +// An arbitrary constant value to flag locations in protected storage. +var flag = common.HexToHash("0xa145ffde0100a145ffde0100a145ffde0100a145ffde0100a145ffde0100fab3") + +func minUint64(a, b uint64) uint64 { + if a < b { + return a + } + return b +} + +// Ciphertext metadata is stored in protected storage, in a 32-byte slot. +// Currently, we only utilize 17 bytes from the slot. +type ciphertextMetadata struct { + refCount uint64 + length uint64 + fheUintType FheUintType +} + +func (m ciphertextMetadata) serialize() [32]byte { + u := uint256.NewInt(0) + u[0] = m.refCount + u[1] = m.length + u[2] = uint64(m.fheUintType) + return u.Bytes32() +} + +func (m *ciphertextMetadata) deserialize(buf [32]byte) *ciphertextMetadata { + u := uint256.NewInt(0) + u.SetBytes(buf[:]) + m.refCount = u[0] + m.length = u[1] + m.fheUintType = FheUintType(u[2]) + return m +} + +func newCiphertextMetadata(buf [32]byte) *ciphertextMetadata { + m := ciphertextMetadata{} + return m.deserialize(buf) +} + +type ciphertextData struct { + metadata *ciphertextMetadata + bytes []byte +} + +func getCiphertextMetadataKey(handle common.Hash) common.Hash { + return crypto.Keccak256Hash(handle.Bytes()) +} + +// Returns the ciphertext metadata for the given handle or nil if it doesn't point to a ciphertext. +func getCiphertextMetadataFromProtectedStorage(env EVMEnvironment, contractAddress common.Address, handle common.Hash) *ciphertextMetadata { + metadataKey := getCiphertextMetadataKey(handle) + protectedStorage := fhevm_crypto.CreateProtectedStorageContractAddress(contractAddress) + metadataInt := newInt(env.GetState(protectedStorage, metadataKey).Bytes()) + if metadataInt.IsZero() { + return nil + } + return newCiphertextMetadata(metadataInt.Bytes32()) +} + +// Returns the ciphertext data for the given handle or nil if it doesn't point to a ciphertext. +func getCiphertextFromProtectedStoage(env EVMEnvironment, contractAddress common.Address, handle common.Hash) *ciphertextData { + metadataKey := getCiphertextMetadataKey(handle) + protectedStorage := fhevm_crypto.CreateProtectedStorageContractAddress(contractAddress) + metadataInt := newInt(env.GetState(protectedStorage, metadataKey).Bytes()) + if metadataInt.IsZero() { + return nil + } + metadata := newCiphertextMetadata(metadataInt.Bytes32()) + ctBytes := make([]byte, 0) + left := metadata.length + protectedSlotIdx := newInt(metadataKey.Bytes()) + protectedSlotIdx.AddUint64(protectedSlotIdx, 1) + for { + if left == 0 { + break + } + bytes := env.GetState(protectedStorage, protectedSlotIdx.Bytes32()) + toAppend := minUint64(uint64(len(bytes)), left) + left -= toAppend + ctBytes = append(ctBytes, bytes[0:toAppend]...) + protectedSlotIdx.AddUint64(protectedSlotIdx, 1) + } + return &ciphertextData{metadata: metadata, bytes: ctBytes} +} + +// If a verified ciphertext: +// * if the ciphertext does not exist in protected storage, persist it with a refCount = 1 +// * if the ciphertexts exists in protected, bump its refCount by 1 +func persistIfVerifiedCiphertext(flagHandleLocation common.Hash, handle common.Hash, protectedStorage common.Address, env EVMEnvironment) { + verifiedCiphertext := getVerifiedCiphertextFromEVM(env, handle) + if verifiedCiphertext == nil { + return + } + logger := env.GetLogger() + + // Try to read ciphertext metadata from protected storage. + metadataKey := crypto.Keccak256Hash(handle.Bytes()) + metadataInt := newInt(env.GetState(protectedStorage, metadataKey).Bytes()) + metadata := ciphertextMetadata{} + + // Set flag in protected storage to mark the location as containing a handle. + env.SetState(protectedStorage, flagHandleLocation, flag) + + if metadataInt.IsZero() { + // If no metadata, it means this ciphertext itself hasn't been persisted to protected storage yet. We do that as part of SSTORE. + metadata.refCount = 1 + metadata.length = uint64(expandedFheCiphertextSize[verifiedCiphertext.ciphertext.fheUintType]) + metadata.fheUintType = verifiedCiphertext.ciphertext.fheUintType + ciphertextSlot := newInt(metadataKey.Bytes()) + ciphertextSlot.AddUint64(ciphertextSlot, 1) + if env.IsCommitting() { + logger.Info("opSstore persisting new ciphertext", + "protectedStorage", hex.EncodeToString(protectedStorage[:]), + "handle", hex.EncodeToString(handle.Bytes()), + "type", metadata.fheUintType, + "len", metadata.length, + "ciphertextSlot", hex.EncodeToString(ciphertextSlot.Bytes())) + } + ctPart32 := make([]byte, 32) + partIdx := 0 + ctBytes := verifiedCiphertext.ciphertext.Serialize() + for i, b := range ctBytes { + if i%32 == 0 && i != 0 { + env.SetState(protectedStorage, ciphertextSlot.Bytes32(), common.BytesToHash(ctPart32)) + ciphertextSlot.AddUint64(ciphertextSlot, 1) + ctPart32 = make([]byte, 32) + partIdx = 0 + } + ctPart32[partIdx] = b + partIdx++ + } + if len(ctPart32) != 0 { + env.SetState(protectedStorage, ciphertextSlot.Bytes32(), common.BytesToHash(ctPart32)) + } + } else { + // If metadata exists, bump the refcount by 1. + metadata = *newCiphertextMetadata(env.GetState(protectedStorage, metadataKey)) + metadata.refCount++ + if env.IsCommitting() { + logger.Info("opSstore bumping refcount of existing ciphertext", + "protectedStorage", hex.EncodeToString(protectedStorage[:]), + "handle", hex.EncodeToString(handle.Bytes()), + "type", metadata.fheUintType, + "len", metadata.length, + "refCount", metadata.refCount) + } + } + // Save the metadata in protected storage. + env.SetState(protectedStorage, metadataKey, metadata.serialize()) +} + +// If references are still left, reduce refCount by 1. Otherwise, zero out the metadata and the ciphertext slots. +func garbageCollectProtectedStorage(flagHandleLocation common.Hash, handle common.Hash, protectedStorage common.Address, env EVMEnvironment) { + // The location of ciphertext metadata is at Keccak256(handle). Doing so avoids attacks from users trying to garbage + // collect arbitrary locations in protected storage. Hashing the handle makes it hard to find a preimage such that + // it ends up in arbitrary non-zero places in protected stroage. + metadataKey := crypto.Keccak256Hash(handle.Bytes()) + + existingMetadataHash := env.GetState(protectedStorage, metadataKey) + existingMetadataInt := newInt(existingMetadataHash.Bytes()) + if !existingMetadataInt.IsZero() { + logger := env.GetLogger() + + // If no flag in protected storage for the location, ignore garbage collection. + // Else, set the value at the location to zero. + foundFlag := env.GetState(protectedStorage, flagHandleLocation) + if !bytes.Equal(foundFlag.Bytes(), flag.Bytes()) { + logger.Error("opSstore location flag not found for a ciphertext handle, ignoring garbage collection", + "expectedFlag", hex.EncodeToString(flag[:]), + "foundFlag", hex.EncodeToString(foundFlag[:]), + "flagHandleLocation", hex.EncodeToString(flagHandleLocation[:])) + return + } else { + env.SetState(protectedStorage, flagHandleLocation, zero) + } + + metadata := newCiphertextMetadata(existingMetadataInt.Bytes32()) + if metadata.refCount == 1 { + if env.IsCommitting() { + logger.Info("opSstore garbage collecting ciphertext", + "protectedStorage", hex.EncodeToString(protectedStorage[:]), + "metadataKey", hex.EncodeToString(metadataKey[:]), + "type", metadata.fheUintType, + "len", metadata.length) + } + + // Zero the metadata key-value. + env.SetState(protectedStorage, metadataKey, zero) + + // Set the slot to the one after the metadata one. + slot := newInt(metadataKey.Bytes()) + slot.AddUint64(slot, 1) + + // Zero the ciphertext slots. + slotsToZero := metadata.length / 32 + if metadata.length > 0 && metadata.length < 32 { + slotsToZero++ + } + for i := uint64(0); i < slotsToZero; i++ { + env.SetState(protectedStorage, slot.Bytes32(), zero) + slot.AddUint64(slot, 1) + } + } else if metadata.refCount > 1 { + if env.IsCommitting() { + logger.Info("opSstore decrementing ciphertext refCount", + "protectedStorage", hex.EncodeToString(protectedStorage[:]), + "metadataKey", hex.EncodeToString(metadataKey[:]), + "type", metadata.fheUintType, + "len", metadata.length) + } + metadata.refCount-- + env.SetState(protectedStorage, existingMetadataHash, metadata.serialize()) + } + } +}