diff --git a/contracts/clients/CkbClient.sol b/contracts/clients/CkbClient.sol index 941afd5..e340758 100644 --- a/contracts/clients/CkbClient.sol +++ b/contracts/clients/CkbClient.sol @@ -4,10 +4,12 @@ pragma solidity ^0.8.9; import "../core/02-client/ILightClient.sol"; import "../core/02-client/IBCHeight.sol"; import "../proto/Client.sol"; +import "./CkbProof.sol"; // MokkClient implements https://github.com/datachainlab/ibc-mock-client // WARNING: This client is intended to be used for testing purpose. Therefore, it is not generally available in a production, except in a fully trusted environment. contract CkbClient is ILightClient { + using CkbProof for *; uint64 private constant MAX_UINT64 = 18446744073709551615; constructor() {} @@ -66,12 +68,12 @@ contract CkbClient is ILightClient { Height.Data calldata, uint64, uint64, - bytes calldata, - bytes memory, + bytes calldata proof, bytes memory, - bytes calldata - ) external pure override returns (bool) { - return true; + bytes memory path, + bytes calldata value + ) external override returns (bool) { + return CkbProof.verifyProof(proof, path, value); } /** diff --git a/contracts/clients/CkbProof.sol b/contracts/clients/CkbProof.sol new file mode 100644 index 0000000..edbdf6a --- /dev/null +++ b/contracts/clients/CkbProof.sol @@ -0,0 +1,342 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.9; + +import "../utils/Molecule.sol"; +import "solidity-rlp/contracts/RLPReader.sol"; + +using Molecule for bytes; +using RLPReader for bytes; +using RLPReader for RLPReader.RLPItem; + +struct Proof { + uint32[] indices; + bytes32[] lemmas; + bytes32[] leaves; +} + +struct VerifyProofPayload { + uint8 verifyType; + bytes32 transactionsRoot; + bytes32 witnessesRoot; + bytes32 rawTransactionsRoot; + Proof proof; +} + +struct AxonObjectProof { + bytes ckbTransaction; + bytes32 blockHash; + VerifyProofPayload proofPayload; +} + +struct CKBHeader { + uint32 version; + uint32 compactTarget; + uint64 timestamp; + uint64 number; + uint64 epoch; + bytes32 parentHash; + bytes32 transactionsRoot; + bytes32 proposalsHash; + bytes32 extraHash; + bytes32 dao; + uint128 nonce; + bytes extension; + bytes32 blockHash; +} + +// Define the MsgType enum +enum MsgType { + MsgClientCreate +} + +// Define the CommitmentKV struct +struct CommitmentKV { + uint256 key; + uint256 value; +} + +// Define the Envelope struct +struct Envelope { + MsgType msg_type; + CommitmentKV[] commitments; + bytes content; +} + +library CkbLightClient { + event GetHeaderEvent(CKBHeader); + event NotGetHeaderEvent(); + + function getHeader(bytes32 blockHash) public returns (CKBHeader memory) { + // axon_precompile_address(0x02) + address get_header_addr = address(0x0102); + (bool isSuccess, bytes memory res) = get_header_addr.staticcall( + abi.encode(blockHash) + ); + + CKBHeader memory header; + if (isSuccess) { + header = abi.decode(res, (CKBHeader)); + /* + replace above decode into the following data can pass test + header = CKBHeader({ + version: 0, + compactTarget: 0, + timestamp: 0, + number: 0, + epoch: 0, + parentHash: bytes32(0), + transactionsRoot: 0x7c57536c95df426f5477c344f8f949e4dfd25443d6f586b4f350ae3e4b870433, + proposalsHash: bytes32(0), + extraHash: bytes32(0), + dao: bytes32(0), + nonce: uint128(0), + extension: "", + blockHash: bytes32(0) + }); + */ + emit GetHeaderEvent(header); + } else { + emit NotGetHeaderEvent(); + } + return header; + } +} + +using CkbLightClient for bytes32; + +// Define ckb blake2b +function blake2b(bytes memory data) view returns (bytes32) { + // axon_precompile_address(0x06) + address blake2b_addr = address(0x0106); + (bool isSuccess, bytes memory res) = blake2b_addr.staticcall(data); + + bytes32 hash; + if (isSuccess) { + hash = abi.decode(res, (bytes32)); + } + return hash; +} + +function ckbMbtVerify(VerifyProofPayload memory payload) view returns (bool) { + // axon_precompile_address(0x07) + address ckb_mbt_addr = address(0x0107); + (, bytes memory res) = ckb_mbt_addr.staticcall( + abi.encode( + payload.verifyType, + payload.transactionsRoot, + payload.witnessesRoot, + payload.rawTransactionsRoot, + payload.proof + ) + ); + + return uint8(res[0]) == 1; +} + +function calculateHashes( + bytes memory ckbTransaction +) view returns (bytes32 transactionHash, bytes32 witnessHash) { + // Calculate the hashes here + (uint256 offset, uint256 size) = ckbTransaction.readCKBTxRaw(); + bytes memory raw_tx = new bytes(size); + for (uint i = 0; i < size; i++) { + raw_tx[i] = ckbTransaction[i + offset]; + } + + return (blake2b(raw_tx), blake2b(ckbTransaction)); +} + +function decodeRlpEnvelope( + bytes memory rlpEncodedData +) pure returns (Envelope memory) { + RLPReader.RLPItem[] memory ls = rlpEncodedData.toRlpItem().toList(); + + // Decode the msg_type + // MsgType msg_type = MsgType(ls[0].toUint()); + MsgType msg_type = MsgType.MsgClientCreate; + + // Decode the commitments + RLPReader.RLPItem[] memory commitmentsRlp = ls[1].toList(); + CommitmentKV[] memory commitments = new CommitmentKV[]( + commitmentsRlp.length + ); + for (uint i = 0; i < commitmentsRlp.length; i++) { + RLPReader.RLPItem[] memory kvRlp = commitmentsRlp[i].toList(); + commitments[i] = CommitmentKV(kvRlp[0].toUint(), kvRlp[1].toUint()); + } + + // Decode the content + bytes memory content = ls[2].toBytes(); + + // Return the decoded Envelope + return Envelope(msg_type, commitments, content); +} + +function parseCommitment( + bytes memory ckbTransaction +) pure returns (CommitmentKV[] memory) { + uint256 witness_count = ckbTransaction.readCKBTxWitnessCount(); + uint8 output_type_index = 2; + (uint256 offset, uint256 size) = ckbTransaction.readCKBTxWitness( + uint8(witness_count - 1), + output_type_index + ); + bytes memory output_type_bytes = new bytes(size); + for (uint i = 0; i < size; i++) { + output_type_bytes[i] = ckbTransaction[i + offset]; + } + Envelope memory witness_struct = decodeRlpEnvelope(output_type_bytes); + return witness_struct.commitments; +} + +function verifyHashExist( + bytes32[] memory leaves, + bytes32 witnessHash +) pure returns (bool) { + bool isInLeaves = false; + for (uint i = 0; i < leaves.length; i++) { + if (leaves[i] == witnessHash) { + isInLeaves = true; + break; + } + } + + return isInLeaves; +} + +function isCommitInCommitments( + CommitmentKV[] memory commitments, + bytes memory key, + bytes calldata value +) pure returns (bool) { + uint256 keyHash = uint256(keccak256(key)); + uint256 valueHash = uint256(keccak256(value)); + for (uint i = 0; i < commitments.length; i++) { + if ( + commitments[i].key == keyHash && commitments[i].value == valueHash + ) { + return true; + } + } + return false; +} + +function decodeProof( + RLPReader.RLPItem[] memory items +) pure returns (Proof memory) { + require(items.length == 3, "Invalid proof length"); + + Proof memory proof; + + // Decode indices + RLPReader.RLPItem[] memory indicesItems = RLPReader.toList(items[0]); + proof.indices = new uint32[](indicesItems.length); + for (uint i = 0; i < indicesItems.length; i++) { + proof.indices[i] = uint32(RLPReader.toUint(indicesItems[i])); + } + + // Decode lemmas + RLPReader.RLPItem[] memory lemmasItems = RLPReader.toList(items[1]); + proof.lemmas = new bytes32[](lemmasItems.length); + for (uint i = 0; i < lemmasItems.length; i++) { + proof.lemmas[i] = bytes32(RLPReader.toBytes(lemmasItems[i])); + } + + // Decode leaves + RLPReader.RLPItem[] memory leavesItems = RLPReader.toList(items[2]); + proof.leaves = new bytes32[](leavesItems.length); + for (uint i = 0; i < leavesItems.length; i++) { + proof.leaves[i] = bytes32(RLPReader.toBytes(leavesItems[i])); + } + + return proof; +} + +function decodeAxonObjectProof( + bytes memory rlpData +) pure returns (AxonObjectProof memory) { + RLPReader.RLPItem[] memory items = RLPReader.toList( + RLPReader.toRlpItem(rlpData) + ); + require(items.length == 3, "Invalid RLP data length"); + + AxonObjectProof memory axonProof; + axonProof.ckbTransaction = RLPReader.toBytes(items[0]); + axonProof.blockHash = bytes32(RLPReader.toBytes(items[1])); + + RLPReader.RLPItem[] memory payloadItems = RLPReader.toList(items[2]); + require(payloadItems.length == 5, "Invalid payload length"); + + axonProof.proofPayload.verifyType = uint8( + RLPReader.toUint(payloadItems[0]) + ); + axonProof.proofPayload.transactionsRoot = bytes32( + RLPReader.toBytes(payloadItems[1]) + ); + axonProof.proofPayload.witnessesRoot = bytes32( + RLPReader.toBytes(payloadItems[2]) + ); + axonProof.proofPayload.rawTransactionsRoot = bytes32( + RLPReader.toBytes(payloadItems[3]) + ); + + require(payloadItems[4].isList(), "Invalid payload proof"); + axonProof.proofPayload.proof = decodeProof( + RLPReader.toList(payloadItems[4]) + ); + + return axonProof; +} + +library CkbProof { + event Log(string message); + + function verifyProof( + bytes calldata rlpiEncodedProof, + bytes memory path, + bytes calldata value + ) public returns (bool) { + // Parse the proof from the abi encoded data + AxonObjectProof memory axonObjProof = decodeAxonObjectProof( + rlpiEncodedProof + ); + + // Calculate the transaction hash and witness hash + (, bytes32 witnessHash) = calculateHashes(axonObjProof.ckbTransaction); + + // Check if the witness hash is in the leaves + if ( + !verifyHashExist( + axonObjProof.proofPayload.proof.leaves, + witnessHash + ) + ) { + return false; + } + + // Get the CKB header + CKBHeader memory header = axonObjProof.blockHash.getHeader(); + + // Create the VerifyProofPayload + VerifyProofPayload memory payload = VerifyProofPayload({ + verifyType: axonObjProof.proofPayload.verifyType, + transactionsRoot: header.transactionsRoot, + witnessesRoot: axonObjProof.proofPayload.witnessesRoot, + rawTransactionsRoot: axonObjProof.proofPayload.rawTransactionsRoot, + proof: axonObjProof.proofPayload.proof + }); + + // Verify the proof + if (!ckbMbtVerify(payload)) { + return false; + } + + // Parse the commitment from the witness + CommitmentKV[] memory commitments = parseCommitment( + axonObjProof.ckbTransaction + ); + + // Check if the commitment path/value matches the provided path/value + return isCommitInCommitments(commitments, path, value); + } +} diff --git a/contracts/clients/CkbTestProof.sol b/contracts/clients/CkbTestProof.sol new file mode 100644 index 0000000..cb1034d --- /dev/null +++ b/contracts/clients/CkbTestProof.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.9; + +import "./CkbProof.sol"; + +// counterpart for test case verifyTestProof to CkbLightClient::getHeader +function getTestHeader() pure returns (CKBHeader memory) { + CKBHeader memory ckbHeader = CKBHeader({ + version: 0, + compactTarget: 0, + timestamp: 0, + number: 0, + epoch: 0, + parentHash: bytes32(0), + transactionsRoot: 0x7c57536c95df426f5477c344f8f949e4dfd25443d6f586b4f350ae3e4b870433, + proposalsHash: bytes32(0), + extraHash: bytes32(0), + dao: bytes32(0), + nonce: uint128(0), + extension: "", + blockHash: bytes32(0) + }); + return ckbHeader; +} + +library CkbTestProof { + // !!!must be idenfical to CkbProof verifyProof except using getTestHeader to replace CkbLightClient::getHeader + function verifyTestProof( + bytes calldata rlpiEncodedProof, + bytes memory path, + bytes calldata value + ) public view returns (bool) { + // Parse the proof from the abi encoded data + AxonObjectProof memory axonObjProof = decodeAxonObjectProof( + rlpiEncodedProof + ); + + // Calculate the transaction hash and witness hash + (, bytes32 witnessHash) = calculateHashes(axonObjProof.ckbTransaction); + + // Check if the witness hash is in the leaves + if ( + !verifyHashExist( + axonObjProof.proofPayload.proof.leaves, + witnessHash + ) + ) { + return false; + } + + CKBHeader memory header = getTestHeader(); + require( + header.transactionsRoot == + 0x7c57536c95df426f5477c344f8f949e4dfd25443d6f586b4f350ae3e4b870433, + "getHeader transactionsRoot wrong" + ); + + // Create the VerifyProofPayload + VerifyProofPayload memory payload = VerifyProofPayload({ + verifyType: axonObjProof.proofPayload.verifyType, + // transactionsRoot: header.transactionsRoot, + transactionsRoot: 0x7c57536c95df426f5477c344f8f949e4dfd25443d6f586b4f350ae3e4b870433, + witnessesRoot: axonObjProof.proofPayload.witnessesRoot, + rawTransactionsRoot: axonObjProof.proofPayload.rawTransactionsRoot, + proof: axonObjProof.proofPayload.proof + }); + // require(false, "after VerifyProofPayload"); + + // Verify the proof + if (!ckbMbtVerify(payload)) { + return false; + } + // require(false, "after ckbMbtVerify"); + // Parse the commitment from the witness + CommitmentKV[] memory commitments = parseCommitment( + axonObjProof.ckbTransaction + ); + + // Check if the commitment path/value matches the provided path/value + return isCommitInCommitments(commitments, path, value); + } +} \ No newline at end of file diff --git a/contracts/utils/Molecule.sol b/contracts/utils/Molecule.sol index 2dd22a6..8a61fb1 100644 --- a/contracts/utils/Molecule.sol +++ b/contracts/utils/Molecule.sol @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.4; library Molecule { diff --git a/test/7c57_rlp.txt b/test/7c57_rlp.txt new file mode 100644 index 0000000..b8275e3 --- /dev/null +++ b/test/7c57_rlp.txt @@ -0,0 +1 @@ +0xf9088db907bbbb0700000c000000b5040000a90400001c00000020000000dd000000e100000095010000350400000000000005000000b5388231a41d3e7df140b0d53bb315d064aab373102189225a8565d72aef7c0d01000000003fcab0ccded82b900f70cbb930ab631b84c23d6acaf842ec605327cd39eaac4b0000000000d9812f0aaa786955967cf9ad69b959d97a9ea5a7b3df76b6f900c6ad622a2ad5000000000013118c41760cb0dcce5bf7e7e263d68584718efd2e66cfd227e4b6e6597eab77000000000029ed5663501cd171513155f8939ad2c9ffeb92aa4879d39cde987f8eb6274407000000000100000000040000000000000000000000503563ea7eb24d712a4ae3456146109a939599eb6c0c32c971f176bb97e7566c00000000000000000000000039d94387dc7331d614d598a0dce887291e5d413e47277e7223d91993b5d7d8a6000000000000000000000000048ab917630d62c1b187cb160a64c644951d1df26fb2287d5be49e9f6cdd790a00000000000000000000000039d94387dc7331d614d598a0dce887291e5d413e47277e7223d91993b5d7d8a601000000a002000014000000be0000003b0100003f020000aa0000001000000018000000aa00000000e66fdd03000000920000001000000030000000310000004eb3ffbbf4a2b0a14da17d4951860ab6ae6ebd654515ea58cdb5a62b4c0c5cf7015d000000437f66cc0d084f29fa03e02a92a5818a43f09bfdfe1d4ad39a4222dc6034c09fdc64a140aa3e981100a9beca4e685f962f0cf6c9010000000000000000ccdefc1fc781b8c1a9a946dfdeeb32829ef2f86e47e8e4d69f6e5bbbb960f42c7d00000010000000180000007d000000005937d102000000650000001000000030000000310000002a4909f913200af310f2655aeb8e967613beadbc8d65c3d98d9dcd1a7ab202a601300000000000000000000000ccdefc1fc781b8c1a9a946dfdeeb32829ef2f86e47e8e4d69f6e5bbbb960f42c0100000000000000040100001000000018000000af000000005c4d1f0500000097000000100000003000000031000000968b5df81187394143ff9cf13fef15cad183d316f01be23400f113dc87b0e1e10162000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000055000000100000003000000031000000cf6e0c0148123081af1deda0ef162d39cfdfe1ea6565d3689009c1f3562a5e820120000000c219351b150b900e50a7039f1e448b844110927e5fd9bd30425806cb8ddff1fd61000000100000001800000061000000182f81513d640100490000001000000030000000310000009bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce80114000000470dcdc5e44064909650113a274b3b36aecb6dc77400000014000000380000005c0000007000000020000000d66955911f3e4ca0298a2f3215370f7984dbdc2692ec7ad90b16cf1210e9245e20000000dc2b8931e47dd0db0c7a751d55f3eeb9d0e71da9ca08ee96af447a2cda23e63b10000000e70300000000000000000000000000000000000006030000140000004001000049020000a2020000280100002801000010000000100000009c00000088000000f88680b840636364656663316663373831623863316139613934366466646565623332383239656632663836653437653865346436396636653562626262393630663432630402c4010101c0e0887472616e73666572896368616e6e656c2d318c636f6e6e656374696f6e2d31d4936632306264382d636f6e6e656374696f6e2d3085696373323088000000f88680b840636364656663316663373831623863316139613934366466646565623332383239656632663836653437653865346436396636653562626262393630663432630402c4020101c0e0887472616e73666572896368616e6e656c2d318c636f6e6e656374696f6e2d31d4936632306264382d636f6e6e656374696f6e2d308569637332300501000005010000100000001000000010000000f1000000f8eff8eb01b84063636465666331666337383162386331613961393436646664656562333238323965663266383665343765386534643639663665356262626239363066343263896368616e6e656c2d30887472616e73666572896368616e6e656c2d31f8870a40633237313437666238393637656464623934643036633035653234616261336364336433613966336336623662356535303264393034303230613034386339361081e7071a1481c219351b150b81900e5081a703819f1e44818b8184411081927e221481f3819f81d681e51a81ad818881f681f481ce6a81b88182727981cf81ff81b92266808001c055000000550000001000000055000000550000004100000026205a16c4ac3773e8d5e29f3c7d4b34c7602dff940aee1020221e70d28f1576345fe117c1704cdf836e22e30cd029ad6117241cfc9f3333c6998f1fb85ed08d0160000000600000001000000010000000100000004c000000f84a0ef844f842a094afb8ce7ca6c3ebef33f7018fb3e20aa7e9e1ca7964fdff98c352edacfcb775a074cdbf0404b6403d2bac0adaa6b314c588644a6dc3a384f8e6130e6e376c76cbc281c0a0ad38b05c235f6d32412f1c18320b950324d4389f0f5d10b6d3339be9be7282b2f8ac01a07c57536c95df426f5477c344f8f949e4dfd25443d6f586b4f350ae3e4b870433a09f7c7df6cb0e59bc57b2ae92146ba8423c9010a69b16afb34f77162cd364ff8da0f92bfe5e211f7fd01a1ea4625d4e2431d211c48820cc5651289838be84df4258f846c102e1a06abc625a8faaa4c858c8196d6edf1b3dcfe2166da0f292e334f6888f6b644e83e1a093a5c7b2c59017c1d7a6c9565364af627984c8f80a31d0d6a66a062a0d4c4721 \ No newline at end of file diff --git a/test/verifyTestProof.js b/test/verifyTestProof.js new file mode 100644 index 0000000..c4ea794 --- /dev/null +++ b/test/verifyTestProof.js @@ -0,0 +1,31 @@ +const Molecule = artifacts.require("Molecule"); +const CkbTestProof = artifacts.require("CkbTestProof"); + +const fs = require('fs'); +const proof_path = require('path'); + +contract("verifyTestProof", (accounts) => { + it("test verifyMembership", async () => { + const molecule = await Molecule.new(); + console.log("molecule deployed on ", molecule.address); + await CkbTestProof.link(molecule); + + const ckbTestProof = await CkbTestProof.new(); + console.log("CkbTestProof deployed on ", ckbTestProof.address); + + console.log("rlpEncodedProof"); + const filePath = proof_path.join(__dirname, './7c57_rlp.txt'); + const hexString = fs.readFileSync(filePath, 'utf8'); + console.log("hexString len ", hexString.length); + const rlpEncodedProof = web3.utils.hexToBytes(hexString); + console.log("rlpEncodedProof len ", rlpEncodedProof.length); + + const path = "commitments/ports/ccdefc1fc781b8c1a9a946dfdeeb32829ef2f86e47e8e4d69f6e5bbbb960f42c/channels/channel-0/sequences/1"; + const value = "0xec577607291e6c583bdf479ab7f8b59f851419121e3d116befeeeb0f1b0a4f87"; + const pathBytes = Buffer.from(path); + const valueBytes = Buffer.from(value.slice(2), 'hex'); // remove the "0x" prefix and convert from hexadecimal + + const result = await ckbTestProof.verifyTestProof(rlpEncodedProof, pathBytes, valueBytes); + assert.equal(result, true, "The proof verification did not return the expected result"); + }); +});