shardeum / bug-reporting

59 stars 33 forks source link

RPC ```eth_call``` in Golang works with ```client.Call(&result, "eth_call", req, "latest")``` but ```client.CallContract(context.Background(), callMsg, nil)``` returns ```Busy or error``` #28

Closed shreethejaBandit closed 1 year ago

shreethejaBandit commented 1 year ago

Shardeum RPC URL throws error when contract read methods are called via go client which is a official version of Geth client.

Impact : Read And Write To blockchain From Golang Applications are not possible.

How to reproduce the issue?
package main

import (
    "context"
    "fmt"

    "github.com/ethereum/go-ethereum"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/ethclient"
)

func main() {
    client, _ := ethclient.Dial("https://liberty20.shardeum.org")
    defer client.Close()

    contractAddr := common.HexToAddress("0x8f01876ccd727fd703787ed477b7f71e1e3adbb1")
    callMsg := ethereum.CallMsg{
        To:   &contractAddr,
        Data: common.FromHex("0x8da5cb5b"),
    }
    res, err := client.CallContract(context.Background(), callMsg, nil)
    if err != nil {
        fmt.Println("Error Found :")
        fmt.Print(err)
        return
    }
    fmt.Printf("Owner: %s\n", common.BytesToAddress(res).Hex())

}

The above mentioned snippet calls owner() function from a contract deployed onto shardeum. Works fine on metamask and other chains and even on JS applications.

to run this code just type in command go run main.go

Throws error saying : busy or error

What other resources can you share regarding this issue?
image
MarcusWentz commented 1 year ago

Did you setup the contract to have the generated Go file to handle method call types for the contract:

//Step 1: Create abi file by running: solc --abi Store.sol > store.abi
//Step 2: Create bin file by running: solc --bin Store.sol > store.bin
//Step 3: Remove comments above the abi and bin files.
//Step 4: Generate Go contract interaction file by running:  abigen --bin=store.bin --abi=store.abi --pkg=store --out=store.go
//Step 5: Run: getSetEvent.go

?

See this example:

https://github.com/MarcusWentz/Web3_Get_Set_Contract_Metamask/tree/main/Scripts/go

shreethejaBandit commented 1 year ago

Yes that is for contract call i have tried that method too but this doesnt need abigen stuff this directly calls rpc method eth_call with passing raw data. To make this issue simpler i have shared a raw example.

shreethejaBandit commented 1 year ago

If the reference to the abigen method needed attached the code below

func (rpc *EvmRpc) callTokenUri(contractAddress string, tokenId string) (string, error) {
    contract, err := erc721.NewMainCaller(common.HexToAddress(contractAddress), rpc)
    if err != nil {
        return "", err
    }

    token, err := strconv.Atoi(tokenId)
    if err != nil {
        return "", err
    }
    uri, err := contract.TokenURI(&bind.CallOpts{}, big.NewInt(int64(token)))
    if err != nil {
        return "", err
    }
    return uri, nil
}

Here erc721 is the contract generated from abigen method tried with this method too on eth bayc contract worked fine but in case of shardeum fails with above error.

MarcusWentz commented 1 year ago

I was able to run:

package main

import (
    "fmt"

    "github.com/ethereum/go-ethereum"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/ethclient"
)

func main() {
    client, _ := ethclient.Dial("https://liberty20.shardeum.org")
    defer client.Close()

    contractAddr := common.HexToAddress("0x8f01876ccd727fd703787ed477b7f71e1e3adbb1")
    callMsg := ethereum.CallMsg{
        To:   &contractAddr,
        Data: common.FromHex("0x8da5cb5b"),
    }
    fmt.Printf("callMsg: ", callMsg)

}

Are you able to use the equivalent to:

client.CallContract(context.Background(), callMsg, nil)

in Javascript, Python and Rust in the meantime while we look into this?

shreethejaBandit commented 1 year ago
fmt.Printf("callMsg: ", callMsg)

This just prints the call arguments actual call is done by this line

client.CallContract(context.Background(), callMsg, nil)

Which throws error. JS Python looks good but go is throwing some problems is this related to the issue #23 ?

This shouldn't be a problem from package this is official go-ethereum client of geth

MarcusWentz commented 1 year ago

Does the callMsg printed output look correct?

MarcusWentz commented 1 year ago

Does the curl request work with this data as mentioned in https://github.com/Shardeum/shardeum-bug-reporting/issues/23?

MarcusWentz commented 1 year ago

I found another bug while testing this with a possible solution as well: https://github.com/Shardeum/shardeum-bug-reporting/issues/29

shreethejaBandit commented 1 year ago

Does the callMsg printed output look correct?

image

This is the call message output it seems to be proper is it accessList should be handled on sharded network.

shreethejaBandit commented 1 year ago

Does the curl request work with this data as mentioned in #23?

Yes Curl works fine as attached below

image
MarcusWentz commented 1 year ago

Thank you for testing the curl request.

We will look into what might be causing this issue.

Please use Javascript and Python (or maybe even Rust) in the meantime until this is fixed.

MarcusWentz commented 1 year ago

The curl request in text form to test:

curl 'https://liberty20.shardeum.org/'  -H 'content-type: application/json'  --data-raw '{"method":"eth_call","params":[
{
    "to":"0x8f01876ccd727fd703787ed477b7f71e1e3adbb1",
    "data":"0x8da5cb5b"
},"latest"],"id":46,"jsonrpc":"2.0"}' 

Result:

{"jsonrpc":"2.0","id":46,"result":"0x000000000000000000000000911027f9a8e982b8f6e6d6e3e844bae26490fca4"}
MarcusWentz commented 1 year ago

It is possible the Owner function data might not match this contract for some reason.

I recommend doing a contract call like this based on generated contract types:

func getstoredData(contract *store.Store) (storedData *big.Int) {

  storedData, err := contract.StoredData(&bind.CallOpts{})
  if err != nil {
        log.Fatal(err)
  }
  return

}

to find the Owner.

Golang example:

https://github.com/MarcusWentz/Web3_Get_Set_Contract_Metamask/blob/main/Scripts/go/getSetEvent.go

shreethejaBandit commented 1 year ago

It is possible the Owner function data might not match this contract for some reason.

I recommend doing a contract call like this based on generated contract types:

func getstoredData(contract *store.Store) (storedData *big.Int) {

  storedData, err := contract.StoredData(&bind.CallOpts{})
  if err != nil {
        log.Fatal(err)
  }
  return

}

to find the Owner.

Golang example:

https://github.com/MarcusWentz/Web3_Get_Set_Contract_Metamask/blob/main/Scripts/go/getSetEvent.go

The Original Method Follows the same as mentioned still the same error.

MarcusWentz commented 1 year ago

In Javascript I get:

const ethers = require("ethers");
(async () => {

  const provider = new ethers.providers.JsonRpcProvider("https://liberty20.shardeum.org")
  const contractAddress = "0x8f01876ccd727fd703787ed477b7f71e1e3adbb1";

  const rawCallData = "0x8da5cb5b";

  let callReturnData = await provider.call({
    to: contractAddress,
    data: rawCallData
  });

  console.log(callReturnData);
  console.log(typeof(callReturnData));

})();

which returns:

0x000000000000000000000000911027f9a8e982b8f6e6d6e3e844bae26490fca4
string

I will try to test this in Rust next to see if it has any type errors like Golang using ethers.rs:

https://docs.rs/ethers-providers/latest/ethers_providers/struct.Provider.html#method.call

MarcusWentz commented 1 year ago

Is this contract deployed to Goerli as well?

If not, please deploy the contract to Goerli, then share the contract address on Goerli so we can test the return value and type as well.

shreethejaBandit commented 1 year ago

Is this contract deployed to Goerli as well?

If not, please deploy the contract to Goerli, then share the contract address on Goerli so we can test the return value and type as well.

Yes Goerli tested against all standard networks and JS also no problem only on golang we get this error. same code with goerlli rpc will work.

MarcusWentz commented 1 year ago

Rust test working:

// use std::env;

use std::str::FromStr;
use std::str;

use ethers::types::transaction::eip2718::TypedTransaction;

use ethers_providers::{Provider};
use ethers_core::types::{Address};

use ethers::{
    prelude::*,
};

use eyre::Result;

#[tokio::main]
async fn main() -> Result<()> {

    // let rpc_goerli_infura_https = env::var("goerliHTTPS_InfuraAPIKey").expect("$goerliHTTPS_InfuraAPIKey is not set");

    // let provider = Provider::<Http>::try_from(rpc_goerli_infura_https).expect("could not instantiate HTTP Provider");

    let rpc_shardeum_https = "https://liberty20.shardeum.org/";

    let provider = Provider::<Http>::try_from(rpc_shardeum_https).expect("could not instantiate HTTP Provider");

    let chain_id_connected = provider.get_chainid().await?;
    println!("Got chain_id_connected: {}", chain_id_connected);

    let mut tx_raw = TypedTransaction::Legacy(TransactionRequest::new());

    if chain_id_connected == U256::from(5) {

        let contract_address = "0x326C977E6efc84E512bB9C30f76E30c160eD06FB".parse::<Address>()?; //Chainlink Address Goerli

        let from_wallet = "0x66C1d8A5ee726b545576A75380391835F8AAA43c";

        let hex_string_function = "balanceOf(address)";
        let hex_string_function_hashed = ethers::utils::keccak256(hex_string_function);

        let function_selector:           String = prefix_hex::encode(&hex_string_function_hashed[0..4]);
        let padded_zeroes:               &str = "000000000000000000000000";
        let slice_wallet_to_add_to_data: &str = &from_wallet[2..42];

        let raw_string = function_selector + padded_zeroes + slice_wallet_to_add_to_data;

        let raw_call_data = Bytes::from_str(&raw_string).unwrap();

        tx_raw.set_to(contract_address);
        tx_raw.set_data(raw_call_data);

    }
    if chain_id_connected == U256::from(8081) {
        let contract_address = "0x8f01876ccd727fd703787ed477b7f71e1e3adbb1".parse::<Address>()?;

        let raw_call_data = Bytes::from_str("0x8da5cb5b").unwrap(); //ERC-20: balanceOf(0x66C1d8A5ee726b545576A75380391835F8AAA43c)

        tx_raw.set_to(contract_address);
        tx_raw.set_data(raw_call_data);

    }

    println!("{:?}", tx_raw);

    let call_return_data = provider.call(&tx_raw,None).await?;

    println!("{:?}", call_return_data);
    // println!("{:?}", call_return_data.whatisthis); //Shows type as "ethers::types::Bytes" in error message. Credit: https://stackoverflow.com/a/21747400

    Ok(())

}

Returns:

Legacy(TransactionRequest { from: None, to: Some(Address(0x8f01876ccd727fd703787ed477b7f71e1e3adbb1)), gas: None, gas_price: None, value: None, data: Some(Bytes(0x8da5cb5b)), nonce: None, chain_id: Some(8081) })
Bytes(0x000000000000000000000000911027f9a8e982b8f6e6d6e3e844bae26490fca4)

I see the response is type Bytes (Javascript showed type String):

error[E0609]: no field `whatisthis` on type `ethers::types::Bytes`
  --> src/main.rs:50:39
   |
50 |     println!("{:?}", call_return_data.whatisthis); //Shows type as "ethers::types::Bytes" in error message. Credit: https://stackoverflow...
   |                                       ^^^^^^^^^^ unknown field
   |
   = note: available fields are: `0

It is possible that Golang is sensitive to this return type potentially being wrong when trying to unmarshal the JSON RPC response.

MarcusWentz commented 1 year ago

Golang

client.CallContract(context.Background(), callMsg, nil)

test:

package main

import (
    "context"
    "encoding/hex"
    "fmt"
    "log"
    "os"

    "github.com/ethereum/go-ethereum"
    "github.com/ethereum/go-ethereum/ethclient"
    "github.com/ethereum/go-ethereum/common"
)

func main() {

    client, err := ethclient.Dial(os.Getenv("goerliHTTPS_InfuraAPIKey"))
    // client, err := ethclient.Dial("https://liberty20.shardeum.org")
    if err != nil {
        log.Fatal(err)
    }

    //Goerli
    tx_to := common.HexToAddress("0x326C977E6efc84E512bB9C30f76E30c160eD06FB")
    tx_data := common.FromHex("0x70a0823100000000000000000000000066C1d8A5ee726b545576A75380391835F8AAA43c")

    //Liberty 2.X
    // tx_to := common.HexToAddress("0x8f01876ccd727fd703787ed477b7f71e1e3adbb1")
    // tx_data := common.FromHex("0x8da5cb5b")

    callMsg := ethereum.CallMsg{
        To:   &tx_to,
        Data: tx_data,
    }

    fmt.Printf("callMsg: ")
    fmt.Println(callMsg)

    res, err := client.CallContract(context.Background(), callMsg, nil)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("eth_call: " + hex.EncodeToString(res) + "\n")

}

Goerli:

callMsg: {0x0000000000000000000000000000000000000000 0x326C977E6efc84E512bB9C30f76E30c160eD06FB 0 <nil> <nil> <nil> <nil> [112 160 130 49 0 0 0 0 0 0 0 0 0 0 0 0 102 193 216 165 238 114 107 84 85 118 167 83 128 57 24 53 248 170 164 60] []}
eth_call: 0000000000000000000000000000000000000000000000008ac7230489e80000

Liberty 2.X:

callMsg: {0x0000000000000000000000000000000000000000 0x8F01876CCd727Fd703787eD477B7f71E1E3ADbb1 0 <nil> <nil> <nil> <nil> [141 165 203 91] []}
2023/02/14 12:59:37 Busy or error
exit status 1
MarcusWentz commented 1 year ago

Golang test with eth_call working by sending raw JSON to the RPC client:

package main

import (
    "fmt"
    "log"
    // "os"

    "github.com/ethereum/go-ethereum/rpc"
)

func main() {
    // client, err := rpc.DialHTTP(os.Getenv("goerliHTTPS_InfuraAPIKey"))
    client, err := rpc.DialHTTP("https://liberty20.shardeum.org")
    if err != nil {
        log.Fatal(err)
    }

    type request struct {
        To   string `json:"to"`
        Data string `json:"data"`
    }

    var result string

    // req := request{"0x326C977E6efc84E512bB9C30f76E30c160eD06FB", "0x70a0823100000000000000000000000066C1d8A5ee726b545576A75380391835F8AAA43c"}
    req := request{"0x8f01876ccd727fd703787ed477b7f71e1e3adbb1", "0x8da5cb5b"}
    if err := client.Call(&result, "eth_call", req, "latest"); err != nil {
        log.Fatal(err)
    }

    fmt.Printf(result + "\n")
}

Goerli:

0x0000000000000000000000000000000000000000000000008ac7230489e80000

Liberty 2.X:

0x000000000000000000000000911027f9a8e982b8f6e6d6e3e844bae26490fca4
MarcusWentz commented 1 year ago

Test with contract IDEX.sol on Ethereum Mainnet:

https://etherscan.io/address/0xcc13fc627effd6e35d2d2706ea3c4d7396c610ea#code

from:

https://ethereum.stackexchange.com/a/63813

Betatnet 1.X contract address:

https://explorer-sphinx.shardeum.org/account/0xa66cc96316a4df10b96dc3e62dae184d04e93ad9

RPC eth_call in Golang works with client.Call(&result, "eth_call", req, "latest"):

package main

import (
    "fmt"
    "log"
    "os"

    "github.com/ethereum/go-ethereum/rpc"
)

func main() {
    client, err := rpc.DialHTTP(os.Getenv("mainnetHTTPS_InfuraAPIKey"))
    // client, err := rpc.DialHTTP("https://sphinx.shardeum.org/")
    if err != nil {
        log.Fatal(err)
    }

    type request struct {
        To   string `json:"to"`
        Data string `json:"data"`
    }

    var result string

    req := request{"0xcc13fc627effd6e35d2d2706ea3c4d7396c610ea", "0x8da5cb5b"}
    // req := request{"0xA66CC96316A4dF10b96Dc3e62dAE184d04E93Ad9", "0x8da5cb5b"}
    if err := client.Call(&result, "eth_call", req, "latest"); err != nil {
        log.Fatal(err)
    }

    fmt.Printf(result + "\n")
}

client.CallContract(context.Background(), callMsg, nil) returns Busy or error:

package main

import (
    "context"
    "encoding/hex"
    "fmt"
    "log"
    "os"

    "github.com/ethereum/go-ethereum"
    "github.com/ethereum/go-ethereum/ethclient"
    "github.com/ethereum/go-ethereum/common"
)

func main() {

    client, err := ethclient.Dial(os.Getenv("mainnetHTTPS_InfuraAPIKey"))

    // client, err := ethclient.Dial("https://sphinx.shardeum.org/")

    if err != nil {
        log.Fatal(err)
    }

    //Ethereum Mainnet
    tx_to := common.HexToAddress("0xcc13fc627effd6e35d2d2706ea3c4d7396c610ea")

    //Betanet 1.X
    // tx_to := common.HexToAddress("0xA66CC96316A4dF10b96Dc3e62dAE184d04E93Ad9")

    tx_data := common.FromHex("0x8da5cb5b")

    callMsg := ethereum.CallMsg{
        To:   &tx_to,
        Data: tx_data,
    }

    fmt.Printf("callMsg: ")
    fmt.Println(callMsg)

    res, err := client.CallContract(context.Background(), callMsg, nil)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("eth_call: " + hex.EncodeToString(res) + "\n")

}
skundu42 commented 1 year ago

The golang eth implementation aways sends the message with the "From" address, so if you do not provide it it will send the request with the zero value for address: 0x0000000000000000000000000000000000000000

like

curl 'https://sphinx.shardeum.org/' -H 'content-type: application/json' --data-raw '{"method":"eth_call","params":[ { "from": "0x0000000000000000000000000000000000000000", "to":"0x8f01876ccd727fd703787ed477b7f71e1e3adbb1", "data":"0x8da5cb5b" },"latest"],"id":46,"jsonrpc":"2.0"}'

which gives a {"jsonrpc":"2.0","id":46,"error":{"code":500,"message":"Busy or error"}} response from our network

fix: make our rpc identify 0x0000000000000000000000000000000000000000 as no Value (null)

workaround: use a address in the golang request

func main() {

client, err := ethclient.Dial("https://sphinx.shardeum.org/%22/)
defer client.Close()

contractAddr := common.HexToAddress("0x8f01876ccd727fd703787ed477b7f71e1e3adbb1")
callMsg := ethereum.CallMsg{
    From: common.HexToAddress("0xfC321461191148FfC45Df25829717795909Ef8Ce"),
    To:   &contractAddr,
    Data: common.FromHex("0x8da5cb5b"),
}
res, err := client.CallContract(context.Background(), callMsg, nil)
if err != nil {
    fmt.Println("Error Found 😊
    fmt.Print(err)
    return
}
fmt.Printf("Owner: %s\n", common.BytesToAddress(res).Hex())

}

this will give you a 200 response!

rmagon commented 1 year ago

Thanks, this helps for now