diff --git a/services/requester/remote_cadence_arch.go b/services/requester/remote_cadence_arch.go new file mode 100644 index 00000000..da4287d3 --- /dev/null +++ b/services/requester/remote_cadence_arch.go @@ -0,0 +1,154 @@ +package requester + +import ( + "context" + "encoding/hex" + "fmt" + "math/big" + "strings" + + "github.com/onflow/cadence" + errs "github.com/onflow/flow-evm-gateway/models/errors" + evmImpl "github.com/onflow/flow-go/fvm/evm/impl" + evmTypes "github.com/onflow/flow-go/fvm/evm/types" + "github.com/onflow/flow-go/fvm/systemcontracts" + flowGo "github.com/onflow/flow-go/model/flow" + gethCommon "github.com/onflow/go-ethereum/common" + "github.com/onflow/go-ethereum/core/types" + "github.com/onflow/go-ethereum/crypto" +) + +var cadenceArchAddress = gethCommon.HexToAddress("0x0000000000000000000000010000000000000001") + +type RemoteCadenceArch struct { + blockHeight uint64 + client *CrossSporkClient + chainID flowGo.ChainID + cachedCalls map[string]evmTypes.Data +} + +var _ evmTypes.PrecompiledContract = (*RemoteCadenceArch)(nil) + +func NewRemoteCadenceArch( + blockHeight uint64, + client *CrossSporkClient, + chainID flowGo.ChainID, +) *RemoteCadenceArch { + return &RemoteCadenceArch{ + blockHeight: blockHeight, + client: client, + chainID: chainID, + cachedCalls: map[string]evmTypes.Data{}, + } +} + +func (rca *RemoteCadenceArch) Address() evmTypes.Address { + return evmTypes.NewAddress(cadenceArchAddress) +} + +func (rca *RemoteCadenceArch) RequiredGas(input []byte) uint64 { + evmResult, err := rca.runCall(input) + if err != nil { + return 0 + } + + key := hex.EncodeToString(crypto.Keccak256(input)) + rca.cachedCalls[key] = evmResult.ReturnedData + + return evmResult.GasConsumed +} + +func (rca *RemoteCadenceArch) Run(input []byte) ([]byte, error) { + key := hex.EncodeToString(crypto.Keccak256(input)) + result, ok := rca.cachedCalls[key] + + if !ok { + evmResult, err := rca.runCall(input) + if err != nil { + return nil, err + } + return evmResult.ReturnedData, nil + } + + return result, nil +} + +func (rca *RemoteCadenceArch) replaceAddresses(script []byte) []byte { + // make the list of all contracts we should replace address for + sc := systemcontracts.SystemContractsForChain(rca.chainID) + contracts := []systemcontracts.SystemContract{sc.EVMContract, sc.FungibleToken, sc.FlowToken} + + s := string(script) + // iterate over all the import name and address pairs and replace them in script + for _, contract := range contracts { + s = strings.ReplaceAll(s, + fmt.Sprintf("import %s", contract.Name), + fmt.Sprintf("import %s from %s", contract.Name, contract.Address.HexWithPrefix()), + ) + } + + return []byte(s) +} + +func (rca *RemoteCadenceArch) runCall(input []byte) (*evmTypes.ResultSummary, error) { + tx := types.NewTx( + &types.LegacyTx{ + Nonce: 0, + To: &cadenceArchAddress, + Value: big.NewInt(0), + Gas: 55_000, + GasPrice: big.NewInt(0), + Data: input, + }, + ) + encodedTx, err := tx.MarshalBinary() + if err != nil { + return nil, err + } + hexEncodedTx, err := cadence.NewString(hex.EncodeToString(encodedTx)) + if err != nil { + return nil, err + } + + hexEncodedAddress, err := addressToCadenceString(evmTypes.CoinbaseAddress.ToCommon()) + if err != nil { + return nil, err + } + + scriptResult, err := rca.client.ExecuteScriptAtBlockHeight( + context.Background(), + rca.blockHeight, + rca.replaceAddresses(dryRunScript), + []cadence.Value{hexEncodedTx, hexEncodedAddress}, + ) + if err != nil { + return nil, err + } + + evmResult, err := parseResult(scriptResult) + if err != nil { + return nil, err + } + + return evmResult, nil +} + +func addressToCadenceString(address gethCommon.Address) (cadence.String, error) { + return cadence.NewString(strings.TrimPrefix(address.Hex(), "0x")) +} + +func parseResult(res cadence.Value) (*evmTypes.ResultSummary, error) { + result, err := evmImpl.ResultSummaryFromEVMResultValue(res) + if err != nil { + return nil, fmt.Errorf("failed to decode EVM result of type: %s, with: %w", res.Type().ID(), err) + } + + if result.ErrorCode != 0 { + if result.ErrorCode == evmTypes.ExecutionErrCodeExecutionReverted { + return nil, errs.NewRevertError(result.ReturnedData) + } + return nil, errs.NewFailedTransactionError(result.ErrorMessage) + } + + return result, err +} diff --git a/services/requester/requester.go b/services/requester/requester.go index 2df729b9..a97b3167 100644 --- a/services/requester/requester.go +++ b/services/requester/requester.go @@ -398,12 +398,18 @@ func (e *EVM) Call( if tx.To != nil { to = *tx.To } + cdcHeight, err := e.evmToCadenceHeight(evmHeight) + if err != nil { + return nil, err + } + rca := NewRemoteCadenceArch(cdcHeight, e.client, e.config.FlowNetworkID) result, err := view.DryCall( from, to, tx.Data, tx.Value, tx.Gas, + query.WithExtraPrecompiledContracts([]evmTypes.PrecompiledContract{rca}), ) resultSummary := result.ResultSummary() @@ -432,12 +438,18 @@ func (e *EVM) EstimateGas( if tx.To != nil { to = *tx.To } + cdcHeight, err := e.evmToCadenceHeight(evmHeight) + if err != nil { + return 0, err + } + rca := NewRemoteCadenceArch(cdcHeight, e.client, e.config.FlowNetworkID) result, err := view.DryCall( from, to, tx.Data, tx.Value, tx.Gas, + query.WithExtraPrecompiledContracts([]evmTypes.PrecompiledContract{rca}), ) if err != nil { return 0, err @@ -669,3 +681,16 @@ func AddOne64th(n uint64) uint64 { // NOTE: Go's integer division floors, but that is desirable here return n + (n / 64) } + +func (e *EVM) evmToCadenceHeight(height int64) (uint64, error) { + cadenceHeight, err := e.blocks.GetCadenceHeight(uint64(height)) + if err != nil { + return 0, fmt.Errorf( + "failed to map evm height: %d to cadence height: %w", + height, + err, + ) + } + + return cadenceHeight, nil +} diff --git a/tests/e2e_web3js_test.go b/tests/e2e_web3js_test.go index 79d5d851..03c8b386 100644 --- a/tests/e2e_web3js_test.go +++ b/tests/e2e_web3js_test.go @@ -29,7 +29,6 @@ func TestWeb3_E2E(t *testing.T) { }) t.Run("verify Cadence arch calls", func(t *testing.T) { - t.Skip("not implemented yet") runWeb3Test(t, "verify_cadence_arch_calls_test") }) diff --git a/tests/web3js/verify_cadence_arch_calls_test.js b/tests/web3js/verify_cadence_arch_calls_test.js index f5249643..914dc352 100644 --- a/tests/web3js/verify_cadence_arch_calls_test.js +++ b/tests/web3js/verify_cadence_arch_calls_test.js @@ -12,7 +12,7 @@ it('should be able to use Cadence Arch calls', async () => { let contractAddress = deployed.receipt.contractAddress // submit a transaction that calls verifyArchCallToRandomSource(uint64 height) - let getRandomSourceData = deployed.contract.methods.verifyArchCallToRandomSource(120).encodeABI() + let getRandomSourceData = deployed.contract.methods.verifyArchCallToRandomSource(2).encodeABI() res = await helpers.signAndSend({ from: conf.eoa.address, to: contractAddress, @@ -23,7 +23,7 @@ it('should be able to use Cadence Arch calls', async () => { assert.equal(res.receipt.status, conf.successStatus) // make a contract call for verifyArchCallToRandomSource(uint64 height) - res = await web3.eth.call({ to: contractAddress, data: getRandomSourceData }, latest) + res = await web3.eth.call({ to: contractAddress, data: getRandomSourceData }, 'latest') assert.notEqual( res, '0x0000000000000000000000000000000000000000000000000000000000000000' @@ -42,7 +42,7 @@ it('should be able to use Cadence Arch calls', async () => { assert.equal(res.receipt.status, conf.successStatus) // make a contract call for verifyArchCallToRevertibleRandom() - res = await web3.eth.call({ to: contractAddress, data: revertibleRandomData }, latest) + res = await web3.eth.call({ to: contractAddress, data: revertibleRandomData }, 'latest') assert.notEqual( res, '0x0000000000000000000000000000000000000000000000000000000000000000' @@ -61,10 +61,10 @@ it('should be able to use Cadence Arch calls', async () => { assert.equal(res.receipt.status, conf.successStatus) // make a contract call for verifyArchCallToFlowBlockHeight() - res = await web3.eth.call({ to: contractAddress, data: flowBlockHeightData }, latest) + res = await web3.eth.call({ to: contractAddress, data: flowBlockHeightData }, 'latest') assert.equal( web3.eth.abi.decodeParameter('uint64', res), - latest, + 7n, ) // submit a transaction that calls verifyArchCallToVerifyCOAOwnershipProof(address,bytes32,bytes) @@ -84,7 +84,7 @@ it('should be able to use Cadence Arch calls', async () => { assert.equal(res.receipt.status, conf.successStatus) // make a contract call for verifyArchCallToVerifyCOAOwnershipProof(address,bytes32,bytes) - res = await web3.eth.call({ to: contractAddress, data: verifyCOAOwnershipProofData }, latest) + res = await web3.eth.call({ to: contractAddress, data: verifyCOAOwnershipProofData }, 'latest') assert.equal( web3.eth.abi.decodeParameter('bool', res), false,