diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..175af6b --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +.PHONY: build +build: + cd fhevm && go build . + +.PHONY: test +test: + cd fhevm && go test -v . diff --git a/fhevm/evm.go b/fhevm/evm.go index fd8d306..ae473ce 100644 --- a/fhevm/evm.go +++ b/fhevm/evm.go @@ -1,6 +1,15 @@ package fhevm -import "fmt" +import ( + "encoding/binary" + "errors" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/holiman/uint256" +) // A Logger interface for the EVM. type Logger interface { @@ -30,3 +39,93 @@ func (*DefaultLogger) Info(msg string, keyvals ...interface{}) { func (*DefaultLogger) Error(msg string, keyvals ...interface{}) { fmt.Println("Error: "+msg, toString(keyvals...)) } + +func makeKeccakSignature(input string) uint32 { + return binary.BigEndian.Uint32(crypto.Keccak256([]byte(input))[0:4]) +} + +func isScalarOp(environment *EVMEnvironment, input []byte) (bool, error) { + if len(input) != 65 { + return false, errors.New("input needs to contain two 256-bit sized values and 1 8-bit value") + } + isScalar := (input[64] == 1) + return isScalar, nil +} + +func getVerifiedCiphertext(environment *EVMEnvironment, ciphertextHash common.Hash) *verifiedCiphertext { + return getVerifiedCiphertextFromEVM(*environment, ciphertextHash) +} + +func get2VerifiedOperands(environment *EVMEnvironment, input []byte) (lhs *verifiedCiphertext, rhs *verifiedCiphertext, err error) { + if len(input) != 65 { + return nil, nil, errors.New("input needs to contain two 256-bit sized values and 1 8-bit value") + } + lhs = getVerifiedCiphertext(environment, common.BytesToHash(input[0:32])) + if lhs == nil { + return nil, nil, errors.New("unverified ciphertext handle") + } + rhs = getVerifiedCiphertext(environment, common.BytesToHash(input[32:64])) + if rhs == nil { + return nil, nil, errors.New("unverified ciphertext handle") + } + err = nil + return +} + +func getScalarOperands(environment *EVMEnvironment, input []byte) (lhs *verifiedCiphertext, rhs *big.Int, err error) { + if len(input) != 65 { + return nil, nil, errors.New("input needs to contain two 256-bit sized values and 1 8-bit value") + } + lhs = getVerifiedCiphertext(environment, common.BytesToHash(input[0:32])) + if lhs == nil { + return nil, nil, errors.New("unverified ciphertext handle") + } + rhs = &big.Int{} + rhs.SetBytes(input[32:64]) + return +} + +func importCiphertextToEVMAtDepth(environment *EVMEnvironment, ct *tfheCiphertext, depth int) *verifiedCiphertext { + existing, ok := (*environment).GetFhevmData().verifiedCiphertexts[ct.getHash()] + if ok { + existing.verifiedDepths.add(depth) + return existing + } else { + verifiedDepths := newDepthSet() + verifiedDepths.add(depth) + new := &verifiedCiphertext{ + verifiedDepths, + ct, + } + (*environment).GetFhevmData().verifiedCiphertexts[ct.getHash()] = new + return new + } +} + +func importCiphertextToEVM(environment *EVMEnvironment, ct *tfheCiphertext) *verifiedCiphertext { + return importCiphertextToEVMAtDepth(environment, ct, (*environment).GetDepth()) +} + +func importCiphertext(environment *EVMEnvironment, ct *tfheCiphertext) *verifiedCiphertext { + return importCiphertextToEVM(environment, ct) +} + +func importRandomCiphertext(environment *EVMEnvironment, t fheUintType) []byte { + nextCtHash := &(*environment).GetFhevmData().nextCiphertextHashOnGasEst + ctHashBytes := crypto.Keccak256(nextCtHash.Bytes()) + handle := common.BytesToHash(ctHashBytes) + ct := new(tfheCiphertext) + ct.fheUintType = t + ct.hash = &handle + importCiphertext(environment, ct) + temp := nextCtHash.Clone() + nextCtHash.Add(temp, uint256.NewInt(1)) + return ct.getHash().Bytes() +} + +func minInt(a int, b int) int { + if a < b { + return a + } + return b +} diff --git a/fhevm/interface.go b/fhevm/interface.go index abbd170..8046ad9 100644 --- a/fhevm/interface.go +++ b/fhevm/interface.go @@ -2,6 +2,7 @@ package fhevm import ( "github.com/ethereum/go-ethereum/common" + "github.com/holiman/uint256" ) type EVMEnvironment interface { @@ -29,4 +30,6 @@ type FhevmData struct { // All optimistic requires encountered up to that point in the txn execution optimisticRequires []*tfheCiphertext + + nextCiphertextHashOnGasEst uint256.Int } diff --git a/fhevm/precompiles.go b/fhevm/precompiles.go new file mode 100644 index 0000000..d58e97e --- /dev/null +++ b/fhevm/precompiles.go @@ -0,0 +1,161 @@ +package fhevm + +import ( + "encoding/binary" + "encoding/hex" + "errors" + + "github.com/ethereum/go-ethereum/common" + "github.com/zama-ai/fhevm-go/params" +) + +// PrecompiledContract is the basic interface for native Go contracts. The implementation +// requires a deterministic gas count based on the input size of the Run method of the +// contract. +type PrecompiledContract interface { + RequiredGas(environment *EVMEnvironment, input []byte) uint64 // RequiredGas calculates the contract gas use + Run(environment *EVMEnvironment, caller common.Address, addr common.Address, input []byte, readOnly bool) (ret []byte, err error) +} + +var PrecompiledContracts = map[common.Address]PrecompiledContract{ + common.BytesToAddress([]byte{93}): &fheLib{}, +} + +var signatureFheAdd = makeKeccakSignature("fheAdd(uint256,uint256,bytes1)") + +type fheLib struct{} + +func (e *fheLib) RequiredGas(environment *EVMEnvironment, input []byte) uint64 { + logger := (*environment).GetLogger() + if len(input) < 4 { + err := errors.New("input must contain at least 4 bytes for method signature") + logger.Error("fheLib precompile error", "err", err, "input", hex.EncodeToString(input)) + return 0 + } + signature := binary.BigEndian.Uint32(input[0:4]) + switch signature { + case signatureFheAdd: + bwCompatBytes := input[4:minInt(69, len(input))] + return fheAddRequiredGas(environment, bwCompatBytes) + default: + err := errors.New("precompile method not found") + logger.Error("fheLib precompile error", "err", err, "input", hex.EncodeToString(input)) + return 0 + } +} + +func (e *fheLib) Run(environment *EVMEnvironment, caller common.Address, addr common.Address, input []byte, readOnly bool) ([]byte, error) { + logger := (*environment).GetLogger() + if len(input) < 4 { + err := errors.New("input must contain at least 4 bytes for method signature") + logger.Error("fheLib precompile error", "err", err, "input", hex.EncodeToString(input)) + return nil, err + } + signature := binary.BigEndian.Uint32(input[0:4]) + switch signature { + case signatureFheAdd: + bwCompatBytes := input[4:minInt(69, len(input))] + return fheAddRun(environment, caller, addr, bwCompatBytes, readOnly) + default: + err := errors.New("precompile method not found") + logger.Error("fheLib precompile error", "err", err, "input", hex.EncodeToString(input)) + return nil, err + } +} + +var fheAddSubGasCosts = map[fheUintType]uint64{ + FheUint8: params.FheUint8AddSubGas, + FheUint16: params.FheUint16AddSubGas, + FheUint32: params.FheUint32AddSubGas, +} + +func fheAddRequiredGas(environment *EVMEnvironment, input []byte) uint64 { + logger := (*environment).GetLogger() + isScalar, err := isScalarOp(environment, input) + if err != nil { + logger.Error("fheAdd/Sub RequiredGas() can not detect if operator is meant to be scalar", "err", err, "input", hex.EncodeToString(input)) + return 0 + } + var lhs, rhs *verifiedCiphertext + if !isScalar { + lhs, rhs, err = get2VerifiedOperands(environment, input) + if err != nil { + logger.Error("fheAdd/Sub RequiredGas() ciphertext inputs not verified", "err", err, "input", hex.EncodeToString(input)) + return 0 + } + if lhs.ciphertext.fheUintType != rhs.ciphertext.fheUintType { + logger.Error("fheAdd/Sub RequiredGas() operand type mismatch", "lhs", lhs.ciphertext.fheUintType, "rhs", rhs.ciphertext.fheUintType) + return 0 + } + } else { + lhs, _, err = getScalarOperands(environment, input) + if err != nil { + logger.Error("fheAdd/Sub RequiredGas() scalar inputs not verified", "err", err, "input", hex.EncodeToString(input)) + return 0 + } + } + + return fheAddSubGasCosts[lhs.ciphertext.fheUintType] +} + +func fheAddRun(environment *EVMEnvironment, caller common.Address, addr common.Address, input []byte, readOnly bool) ([]byte, error) { + logger := (*environment).GetLogger() + + isScalar, err := isScalarOp(environment, input) + if err != nil { + logger.Error("fheAdd can not detect if operator is meant to be scalar", "err", err, "input", hex.EncodeToString(input)) + return nil, err + } + + if !isScalar { + lhs, rhs, err := get2VerifiedOperands(environment, input) + if err != nil { + logger.Error("fheAdd inputs not verified", "err", err, "input", hex.EncodeToString(input)) + return nil, err + } + if lhs.ciphertext.fheUintType != rhs.ciphertext.fheUintType { + msg := "fheAdd operand type mismatch" + logger.Error(msg, "lhs", lhs.ciphertext.fheUintType, "rhs", rhs.ciphertext.fheUintType) + return nil, errors.New(msg) + } + + // If we are doing gas estimation, skip execution and insert a random ciphertext as a result. + if !(*environment).IsCommitting() && !(*environment).IsEthCall() { + return importRandomCiphertext(environment, lhs.ciphertext.fheUintType), nil + } + + result, err := lhs.ciphertext.add(rhs.ciphertext) + if err != nil { + logger.Error("fheAdd failed", "err", err) + return nil, err + } + importCiphertext(environment, result) + + resultHash := result.getHash() + logger.Info("fheAdd success", "lhs", lhs.ciphertext.getHash().Hex(), "rhs", rhs.ciphertext.getHash().Hex(), "result", resultHash.Hex()) + return resultHash[:], nil + + } else { + lhs, rhs, err := getScalarOperands(environment, input) + if err != nil { + logger.Error("fheAdd scalar inputs not verified", "err", err, "input", hex.EncodeToString(input)) + return nil, err + } + + // If we are doing gas estimation, skip execution and insert a random ciphertext as a result. + if !(*environment).IsCommitting() && !(*environment).IsEthCall() { + return importRandomCiphertext(environment, lhs.ciphertext.fheUintType), nil + } + + result, err := lhs.ciphertext.scalarAdd(rhs.Uint64()) + if err != nil { + logger.Error("fheAdd failed", "err", err) + return nil, err + } + importCiphertext(environment, result) + + resultHash := result.getHash() + logger.Info("fheAdd scalar success", "lhs", lhs.ciphertext.getHash().Hex(), "rhs", rhs.Uint64(), "result", resultHash.Hex()) + return resultHash[:], nil + } +} diff --git a/go.sum b/go.sum index 4b1754c..24cad07 100644 --- a/go.sum +++ b/go.sum @@ -13,4 +13,4 @@ github.com/holiman/uint256 v1.2.2-0.20230321075855-87b91420868c/go.mod h1:SC8Ryt golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= \ No newline at end of file