diff --git a/merkly/mtree.py b/merkly/mtree.py index dc15319..635e908 100644 --- a/merkly/mtree.py +++ b/merkly/mtree.py @@ -173,3 +173,61 @@ def human_leaves(self) -> List[str]: @property def human_short_leaves(self) -> List[str]: return [leaf.hex() for leaf in self.short_leaves] + + @staticmethod + def verify_proof(proof: List[Node], raw_leaf: str, root: str, **kwargs) -> bool: + """ + Verify the validity of a Merkle proof for a given leaf against the expected root hash. + + This method checks whether the provided proof can reconstruct the root hash + from the given raw leaf data. It uses the specified hash function to compute + the hashes along the proof path. + + Args: + proof (List[Node]): A list of Nodes representing the Merkle proof. Each Node + contains the hash of a sibling node and its position (left or right) in the tree. + raw_leaf (str): The raw leaf data (in string format) for which the proof is + being verified. This data should correspond to a leaf in the Merkle tree. + root (str): The expected root hash (in hexadecimal string format) that the + proof should reconstruct if valid. + **kwargs: Optional keyword arguments. Can include: + - hash_function (Callable[[bytes, bytes], bytes]): A custom hash function + that takes two byte inputs and returns a hash. If not provided, + the default `keccak` function is used. + + Returns: + bool: Returns True if the proof is valid and reconstructs the expected root + hash; otherwise, returns False. + + Example: + proof = [Node(data=b"abcd", side=Side.LEFT), Node(data=b"efgh", side=Side.RIGHT)] + leaf = "a" + root = "0xe35e6e14fdf91ecc6adfb74856bcd8a2c22544bd10bded94f2a9fecc77cf630b" + is_valid = MerkleTree.verify_proof(proof, leaf, root) + """ + if not kwargs.get("hash_function", None): + hash_function: Callable[[bytes, bytes], bytes] = lambda x, y: keccak(x + y) + else: + hash_function = kwargs["hash_function"] + + full_proof = [hash_function(raw_leaf.encode(), bytes())] + full_proof.extend(proof) + + def concat_nodes(left: Node, right: Node) -> Node: + if isinstance(left, Node) is not True: + start_node = left + if right.side == Side.RIGHT: + data = hash_function(start_node, right.data) + return Node(data=data, side=Side.LEFT) + else: + data = hash_function(right.data, start_node) + return Node(data=data, side=Side.RIGHT) + else: + if right.side == Side.RIGHT: + data = hash_function(left.data, right.data) + return Node(data=data, side=Side.LEFT) + else: + data = hash_function(right.data, left.data) + return Node(data=data, side=Side.RIGHT) + + return reduce(concat_nodes, full_proof).data.hex() == root diff --git a/pyproject.toml b/pyproject.toml index 34beedc..5179928 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "merkly" -version = "1.1.1" +version = "1.2.0" description = "🌳 The simple and easy implementation of Merkle Tree" authors = ["Lucas Oliveira "] repository = "https://github.com/olivmath/merkly.git" @@ -39,7 +39,7 @@ classifiers = [ pre-commit = "^3.0.3" coverage = "^7.2.7" pyclean = "^3.0.0" - pytest = "^7.2.1" + pytest = "^8.3.3" black = "^24.1.1" [build-system] diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..2217a1a --- /dev/null +++ b/test/README.md @@ -0,0 +1,2 @@ +> [!WARNING] +> For run tests you need install javascript deps! diff --git a/test/merkle_proof/test_merkle_proof.py b/test/merkle_proof/test_merkle_proof.py index b2e6a7a..5213abf 100644 --- a/test/merkle_proof/test_merkle_proof.py +++ b/test/merkle_proof/test_merkle_proof.py @@ -47,3 +47,51 @@ def test_verify_merkle(leaf: str): result = tree.proof(leaf) assert tree.verify(result, leaf) + + +def get_data_from_api(): + leaves = [ + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + ] + tree = MerkleTree(leaves) + + leaf = "1" + proof = tree.proof(leaf) + return proof, leaf + + +def test_verify_merkle_proof_without_leaves(): + proof, leaf = get_data_from_api() + + root = "3aa22c94ceb510827b04fa792ebdd7346eb2984ebb24e58dac66d7795c2af4e8" + + # Test valid proof + result = MerkleTree.verify_proof(proof, leaf, root) + assert result, "Expected proof to be valid" + + # Test invalid proof scenario + invalid_leaf = "invalid_leaf_data" + invalid_result = MerkleTree.verify_proof(proof, invalid_leaf, root) + assert not invalid_result, "Expected proof to be invalid for incorrect leaf" + + # Test with a different root + different_root = ( + "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + ) + different_result = MerkleTree.verify_proof(proof, leaf, different_root) + assert not different_result, "Expected proof to be invalid for different root"