CosmWasm / cosmwasm

Framework for building smart contracts in Wasm for the Cosmos SDK
https://www.cosmwasm.com/
Apache License 2.0
1.06k stars 328 forks source link

Light client verification #2224

Open en opened 2 weeks ago

en commented 2 weeks ago

I'd like to verify the state/events of a cosmwasm contract using a tendermint light client.

Since I haven’t really written cosmwasm contracts before, I’m concerned that without an event merkle tree, the events emitted by the contract might not be provable.

Is there a global path for the data written to the state by a cosmwasm contract? If such a path exists, it seems I could prove this data in a manner similar to IBC. verify_membership

I would appreciate any guidance or reference materials you could provide.

webmaster128 commented 2 weeks ago

Events in CosmWasm are just sent to CometBFT and handled there. As far as I can tell, they are not part of consensus and not provable. But I might be wrong.

What you can do is get storage proofs like you linked above for all state in the contract (everything you write via Storage::set). If you use a storage abstraction like cw-storage-plus or Storey, you just need to know how the keys are composed.

I would need to look up the exact encoding but the storage keys are something like this:

wasm | contract address | whatever key you use in the contract
en commented 1 week ago

@webmaster128 Thank you for your reply!

I managed to query the storage value of a cosmwasm contract through the abci_query interface. For example, for this key:

$ osmosisd --node tcp://142.132.202.86:46657 query wasm contract-state all osmo16cdpze425guzfnm6av90vqh5ptd0apu4dxrpdhk7yry4kkh65l8qx57fe0
- key: 0002616D0000000000000043
  value: eyJscCI6IjAuMDA1NzA2MzE2NjYxNTY4Mzg2IiwieGxwIjoiMCJ9

The core rust code is:

let request = QueryRawContractStateRequest {
    address: "osmo16cdpze425guzfnm6av90vqh5ptd0apu4dxrpdhk7yry4kkh65l8qx57fe0".to_string(),
    query_data: hex::decode("0002616D0000000000000043").unwrap(),
};
let data = request.to_proto_bytes();
...
abci_query(
        &self.rpc_client,
        &Url::from_str(&self.rpc_addr).unwrap(),
        "/cosmwasm.wasm.v1.Query/RawContractState".to_string(),
        data,
        Some(height),
        prove,
    ))?

The equivalent curl command is:

curl -X 'GET' \
  'https://rpc.osmosis.zone/abci_query?path=%22%2Fcosmwasm.wasm.v1.Query%2FRawContractState%22&data=0x0a3f6f736d6f31366364707a6534323567757a666e6d36617639307671683570746430617075346478727064686b37797279346b6b6836356c3871783537666530120c0002616d0000000000000043&height=0&prove=true' \
  -H 'accept: application/json'

Response body:

{
  "jsonrpc": "2.0",
  "id": -1,
  "result": {
    "response": {
      "code": 0,
      "log": "",
      "info": "",
      "index": "0",
      "key": null,
      "value": "Cid7ImxwIjoiMC4wMDU3MDYzMTY2NjE1NjgzODYiLCJ4bHAiOiIwIn0=",
      "proofOps": null,
      "height": "20199510",
      "codespace": ""
    }
  }
}

However, there are two issues:

  1. There is a character difference in the returned value. The value returned by osmosisd is eyJscCI6IjAuMDA1NzA2MzE2NjYxNTY4Mzg2IiwieGxwIjoiMCJ9, which decodes to {"lp":"0.005706316661568386","xlp":"0"}. The value returned by abci_query is Cid7ImxwIjoiMC4wMDU3MDYzMTY2NjE1NjgzODYiLCJ4bHAiOiIwIn0=, which decodes to '{"lp":"0.005706316661568386","xlp":"0"}.
  2. Even though I set prove=true, no proof is returned.

Do you have any insights on these two issues?

webmaster128 commented 1 week ago

The second response is a protobuf container which holds the data in field numer 1 of type bytes

This I see with decode_raw:

echo "Cid7ImxwIjoiMC4wMDU3MDYzMTY2NjE1NjgzODYiLCJ4bHAiOiIwIn0=" | base64 -d | decode_raw 
1: (39 bytes) '{"lp":"0.005706316661568386","xlp":"0"}'

The return type is probably QueryRawContractStateResponse.


To get a proof you might need to query the sore directly, like this CosmJS code snippet is doing:

  public async queryRawProof(
    store: string,
    queryKey: Uint8Array,
    desiredHeight?: number,
  ): Promise<ProvenQuery> {
    const { key, value, height, proof, code, log } = await this.cometClient.abciQuery({
      // we need the StoreKey for the module, not the module name
      // https://github.com/cosmos/cosmos-sdk/blob/8cab43c8120fec5200c3459cbf4a92017bb6f287/x/auth/types/keys.go#L12
      path: `/store/${store}/key`,
      data: queryKey,
      prove: true,
      height: desiredHeight,
    });

But I don't know the value for store and key right now. There are a few IBC and bank examples in the CosmJS codebase.

en commented 1 week ago

Thank you! So the value I retrieved is correct. I was missing a step for protobuf decoding.

I don’t think there is such a path like /store/${store}/key for the state in cosmwasm.

From this unit test as a starting point, when querying the state of cosmwasm using abci_query, it seems that only /cosmwasm.wasm.v1.Query/RawContractState is a valid path, and the data is the protobuf-serialized QueryRawContractStateRequest.

I can use this method to retrieve any value from the cosmwasm state. For example, I want to get the contract_version. from the contract osmo16cdpze425guzfnm6av90vqh5ptd0apu4dxrpdhk7yry4kkh65l8qx57fe0

We can construct the following request:

let request = QueryRawContractStateRequest {
    address: "osmo16cdpze425guzfnm6av90vqh5ptd0apu4dxrpdhk7yry4kkh65l8qx57fe0".to_string(),
    query_data: b"contract_info".to_vec(),
};
let data = request.to_proto_bytes();
println!("data: {:?}", hex::encode(data.clone()));

We get:

data: "0a3f6f736d6f31366364707a6534323567757a666e6d36617639307671683570746430617075346478727064686b37797279346b6b6836356c3871783537666530120d636f6e74726163745f696e666f"

Then, we can retrieve the version stored in the state using abci_query:

$ curl -X 'GET' \
  'https://rpc.osmosis.zone/abci_query?path=%22%2Fcosmwasm.wasm.v1.Query%2FRawContractState%22&data=0x0a3f6f736d6f31366364707a6534323567757a666e6d36617639307671683570746430617075346478727064686b37797279346b6b6836356c3871783537666530120d636f6e74726163745f696e666f&height=0&prove=true' \
  -H 'accept: application/json'

The response is:

{
  "jsonrpc": "2.0",
  "id": -1,
  "result": {
    "response": {
      "code": 0,
      "log": "",
      "info": "",
      "index": "0",
      "key": null,
      "value": "CjZ7ImNvbnRyYWN0IjoibGV2YW5hLmZpbmFuY2U6bWFya2V0IiwidmVyc2lvbiI6IjAuMS4yIn0=",
      "proofOps": null,
      "height": "20318338",
      "codespace": ""
    }
  }
}

This decodes to:

{"contract":"levana.finance:market","version":"0.1.2"}

However, the proof is empty.

en commented 1 week ago

Taking it a step further, for the above example, there is a prefix store key relative to the wasm store: 0363AF2FBB3E8F2BCA901BC7B2E6233307A7E9332095C5FA5427EB0663E5CA3306.

This key is composed of 0x03 and the contract data hash, which can be obtained like this:

$ osmosisd --node tcp://142.132.202.86:46657 query wasm contract osmo16cdpze425guzfnm6av90vqh5ptd0apu4dxrpdhk7yry4kkh65l8qx57fe0

Output:

address: osmo16cdpze425guzfnm6av90vqh5ptd0apu4dxrpdhk7yry4kkh65l8qx57fe0
contract_info:
  admin: osmo1m3zqg7r669v3rceqygzv8nc9ctuggvpzjm8m8h4452kzgsxev0kslgs6qc
  code_id: "1040"
  created:
    block_height: "14780873"
    tx_index: "0"
  creator: osmo1ssw6x553kzqher0earlkwlxasfm2stnl3ms3ma2zz4tnajxyyaaqlucd45
  extension: null
  ibc_port_id: ""
  label: Levana Perps Market - SHIB_USDC

Then, to find the data hash:

$osmosisd --node tcp://142.132.202.86:46657 query wasm code-info 1040

Output:

code_id: "1040"
creator: osmo1lqyn9ncwkcqj8e0pnugu72tyyfehe2tre98c5qfzjg4d3vdw7n5q5a0x37
data_hash: 63AF2FBB3E8F2BCA901BC7B2E6233307A7E9332095C5FA5427EB0663E5CA3306
instantiate_permission:
  address: ""
  permission: Everybody

However, I haven’t found a method to query using /store/wasm/.../prefixStoreKey. If such a method exists, it’s definitely not the abci_query.

en commented 1 week ago

ibc-related keys are stored under store/ibc/key. For example, to get the client state for client_id 07-tendermint-12, we can construct a key like this: clients/07-tendermint-12/clientState, convert it to hexadecimal, and get 0x636C69656E74732F30372D74656E6465726D696E742D31322F636C69656E745374617465.

We can then query the client state and its proof using:

curl -X 'GET' \
  'https://rpc.osmosis.zone/abci_query?path=%22store%2Fibc%2Fkey%22&data=0x636C69656E74732F30372D74656E6465726D696E742D31322F636C69656E745374617465&height=0&prove=true' \
  -H 'accept: application/json'

Similarly, I found some keys for cosmwasm. For example, to query code info, use the path store/wasm/key with a key of 0x01 + code_id. For instance:

curl -X 'GET' \
  'https://rpc.osmosis.zone/abci_query?path=%22store%2Fwasm%2Fkey%22&data=0x010000000000000001&height=0&prove=true' \
  -H 'accept: application/json'

This also returns the correct store value and proof.

For client state and code info, they are stored similarly in the KVStore and can be proven.

But, for cosmwasm state, it is stored in vmstore, and the query and store methods differ from the two examples above, which seems to make it impossible to generate proofs.

chipshort commented 4 days ago

But, for cosmwasm state, it is stored in vmstore, and the query and store methods differ from the two examples above, which seems to make it impossible to generate proofs.

What do you mean with "cosmwasm state"? AFAIK, all state, including contract info, contract history and state written by a contract is stored under various keys in the KVStore provided by the Cosmos SDK. The only exception are the Wasm files themselves. They are only stored on disk.

en commented 3 days ago

Hi @chipshort,

Yes, you are right. I have figured out the key for the cosmwasm state. There are many errors in my previous post.

And the conclusion is:

The path is store/wasm/key or /store/wasm/key. The key for the state is 0x03 + Address (hex) + raw key.

For example, here is a query for a state:

$ ~/go/bin/osmosisd --node tcp://142.132.202.86:46657 query wasm contract-state raw osmo16cdpze425guzfnm6av90vqh5ptd0apu4dxrpdhk7yry4kkh65l8qx57fe0 contract_info --ascii
data: eyJjb250cmFjdCI6ImxldmFuYS5maW5hbmNlOm1hcmtldCIsInZlcnNpb24iOiIwLjEuMiJ9

The corresponding hex address for the contract is:

$ ~/go/bin/osmosisd  debug addr osmo16cdpze425guzfnm6av90vqh5ptd0apu4dxrpdhk7yry4kkh65l8qx57fe0
Address (hex): D61A1166AAA23824CF7AEB0AF602F40ADAFE8795698616DEDE20C95B5AFAA7CE

The hexadecimal representation of contract_info is 636F6E74726163745F696E666F.

So the final key is: 0x03D61A1166AAA23824CF7AEB0AF602F40ADAFE8795698616DEDE20C95B5AFAA7CE636F6E74726163745F696E666F.

Now, using abci_query to query the value and proof:

$ curl -X 'GET' \
  'https://rpc.osmosis.zone/abci_query?path=%22%2Fstore%2Fwasm%2Fkey%22&data=0x03D61A1166AAA23824CF7AEB0AF602F40ADAFE8795698616DEDE20C95B5AFAA7CE636F6E74726163745F696E666F&height=0&prove=true' \
  -H 'accept: application/json'
{
  "jsonrpc": "2.0",
  "id": -1,
  "result": {
    "response": {
      "code": 0,
      "log": "",
      "info": "",
      "index": "0",
      "key": "A9YaEWaqojgkz3rrCvYC9Ara/oeVaYYW3t4gyVta+qfOY29udHJhY3RfaW5mbw==",
      "value": "eyJjb250cmFjdCI6ImxldmFuYS5maW5hbmNlOm1hcmtldCIsInZlcnNpb24iOiIwLjEuMiJ9",
      "proofOps": {
        "ops": [
          {
            "type": "ics23:iavl",
            "key": "A9YaEWaqojgkz3rrCvYC9Ara/oeVaYYW3t4gyVta+qfOY29udHJhY3RfaW5mbw==",
            "data": "..."
          },
          {
            "type": "ics23:simple",
            "key": "d2FzbQ==",
            "data": "..."
          }
        ]
      },
      "height": "20662929",
      "codespace": ""
    }
  }
}

It looks like the data is correct.