From 8b54add07566121d66043e6bab94374edcf33726 Mon Sep 17 00:00:00 2001 From: topologoanatom Date: Fri, 23 Jan 2026 14:05:59 +0200 Subject: [PATCH 1/2] implement inclusion proof verif --- .../src/assets/test-tx-incl-block.hex | 1 + crates/simplicityhl-core/src/lib.rs | 2 + crates/simplicityhl-core/src/tx_inclusion.rs | 159 ++++++++++++++++++ 3 files changed, 162 insertions(+) create mode 100644 crates/simplicityhl-core/src/assets/test-tx-incl-block.hex create mode 100644 crates/simplicityhl-core/src/tx_inclusion.rs diff --git a/crates/simplicityhl-core/src/assets/test-tx-incl-block.hex b/crates/simplicityhl-core/src/assets/test-tx-incl-block.hex new file mode 100644 index 0000000..e957c2b --- /dev/null +++ b/crates/simplicityhl-core/src/assets/test-tx-incl-block.hex @@ -0,0 +1 @@ +000000207e3dba98460e4136659f0fccf3e59338dfe53ed5f094fb0bb94d771c48341854d875900105c87e5dd46c740cb1129c06f8f4007e868f61b25e37cffa946c718d8742805b01000000015100030200000001010000000000000000000000000000000000000000000000000000000000000000ffffffff03510101ffffffff0201230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b2010000000000009b64001976a914608c0ea8194a8ceb57f0196f44a6b48a54fc065988ac01230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b201000000000000000000266a24aa21a9ed8f8a98e5623643b24167266c2648ead4a50d18b0491c6f34e11398aaee0ca6e8000000000000012000000000000000000000000000000000000000000000000000000000000000000000000000020000000001eb04b68e9a26d116046c76e8ff47332fb71dda90ff4bef5370f25226d3bc09fc0000000000feffffff0201230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b20100000002540bd71c001976a91448633e2c0ee9495dd3f9c43732c47f4702a362c888ac01230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b2010000000000000ce4000000000000020000000101f23ceddac67cfbbc997199daa651384d0746fb2a5482b8c8629ba8df4b788f75000000006b483045022100e0feb3e2f292000d67e24b821d87c9532230dac1de428d6a0068c9f416583abf02200e76f072788dd411b2327267cd91c6b1659809598cd4fae35be475efe1e4bbad01210201e15c23c021652d07c1557b607ea0379fca0462aca840d6c33c4d4927524547feffffff030b60424a423335923c15ae387d95d4f80d944722020bfa55b9f0a0e67579e3c13c081c4f215239c77456d121eb73bd9914a9a6398fe369b4eb8f88a5f78e257fcaa303301ee46349950886ae115c9556607fcda9381c2f72368f4b5286488c62aa0b081976a9148bb6c4d5814d43fefb9e330575e326632136389c88ac0bd436b0539f5497af792d7cb281f09b73d8a5abc198b3ce6239d79e68893e5e5d0923899fd35071ba8a209d85b556d5747b6c35539c3b2f8631a27c0d477a1f45a603d1d350b8cbf900f7666da66541bf6252fc4c162141ad49c670884c93c57db6ba1976a9148c7ab6e0fca387d03643d4846f708bf39d47c1e988ac01230f4f5d4b7c6fa845806ee4f67713459e1b69e8e60fcee2e4940c7a0d5de1b2010000000000008e800000000000000000000043010001dc65ae13f76fde4a7172e0fb380b1a5cc8dc88eaa0659e638a25eac8ae30d79bf93eb7e487eeee323e4ac8e3a2fe6523bdeba6acce32b9b085f2286174c04655fd6c0a6020000000000000000178ad016b3e5d8165423e56d8b37e3eaee96009b2f970043ccf65d61b5c3c1e1ef343e0c479bdba442717dc861c9591566010240b9d4607efb9252a5fcef05edf640e0bb6b606729246ad07baa49d0d3b52042c65a03ca737744e45b2d2d6d177c36569ae9d6eb4437305b169bbc59f85cabff3bc49a2d6d08c177cce3121a509d3c47961bd22e35c932b79d4ec5ccaf913fac04034bfebdadbc4ff3127af96344b02ee6b967bb08326cbe6a4e1c924485e64a8c0fdf70b98c99f38acaa15aa0adb2b5b7335ed5502443891bcd657310347cbd928f40f38f1dec087a2b947c9cf7d304798f77bbc4a2c843796b2d49acce91de4e88a0a9c261277df28ffc3320d7f7d64790f592ddded48a1068ef88271395fa5606389ef90856ddd6bd6710a8d27e0147983b5dde2a7efae44e83ad02a3c3da04be43d5f2c05c205f1e17b48554c2177670f46dbb6600bd2e6c75dd5ea2e1072c5f22483dcf05d8124e3f9063a5ddb179a29c23a2d15d6e89f2192f03dae5938f66fcdcff000c5a96ffd2920f23881880af72153c96a56dd80c218bb48b44a18e54a8050ff32c869c1264ee574cdb4002f86e0779c724d11dc4a768dbec1bd22054886f1fdf2e7347e4c247b829159d1375f881c6ce0a5c4da8534000e7fec3a980afb1edc99b725c29de80f260dcf144c873bf589ae1812ef6cb05f2234f9c66c23e874a0d5d0dc52f2209e015bbcf74ee449a397f6b0318c915b7e58dea5904abbe35285e90ccf548ad1f3f52f60c3b19b3cd67644d633e68aef42d8ef1782f22a8edd0620f55f29070720ca7a078ac83e87b9ebd2783ecad17dd854ef1bbd319f1a6d3a1e4931f9097422f5a3c4af037b99e06c7610ee61102c6eea763af108e9a16b93b2dc0891658d5c6a197df6aae9b306b2c895d21c79cb6cb6dd85b4018b0a9fe7468336e3907eb4adcaf930cacc97e8e951d2d6b25744a4143679bad1f31b210c9a2ed54b80d8f5d7dc1f1c985681534c1926920cd683d95dca7e8ea285f9906d2e89cd8bfa76a98e38ee4b5152522d55f79610fe8d5278fe6ed5866b5da4dcf330ea84307c34f30e1a66eb1934dafebb0074fc27c2ff73d8c0bae8416cc87bf611f81119aba9e2a911beaf3ac9507e621fc1ed1cf15dfb31408cf55e2bfdd2880db2d3489a336d6f8348347648d882f9f376331e469e809115c6cc82468f363c910673e9ded172ded90a369e1cdd135676f623e11a1531ed221177812b1ef0c65e5ca92c0df8de7fe664710f3228a226e019c99607fe1395ecd5643e1c7ad8a132bf5131737cb970a7f0dabc00029755bf71b3f47bd69ba39b3ab104c74f04239f4919dca1dfce7c9c41cba9d449073e106ebabe3c313b598ee8b11702ec46e9ee53fb9422f0326371898b8fa4c21a951684c687398e0bebd6f6fd91b829e8666b9a19a4273cfda0f34b8ecb902f7adc6539fb9a0cba6f87a63a957acfb2dfa18973f4a3063668767b2be780311513c63f1814f082176f6a953f2ffaa49ec9b39fecc2eab603be7a969bb4c1dbebf8d39fa90f802d5ea52378b5025a19b64a8c2c2dd6a6133bd8d29730bd5724b5bf50c158b238d1137082937ad91a176aaf91577868db7581b457c917e612b242ce0065ad47e11dcdc1fc6158687142249bcf312497a547b6f43e795af7d4ae8cd022e44e417987e35e83de21e39dcdf86b97bd421e6e61881a432fa2284f20be80e32459443736b875d9036468ceb881589394441e2d10aa10b6c93332951e8ba56f89fac70baf415b4511873c0f3e418ca4fe8954a28f1f7b5f590d34470119f694e2712f184882d90396c8e6aa850eaa3c2ae51990543638c46c59512167a2c5ad593532dc2142ffb6560476e4159213b9ef017ec75310d2e4624a405bb26f7192a485a94890674928c9caa4a5819ca4ddcba8fa71afc1a6baf63f039452c8fe994f8b63d58c876dfddd61a476345eaed4f66bdc0fcfc38d485c6a5b0e27d0fbc50427ff591ba38d63445c01642cfbd7d4c032f2546a6fe80bc3b598362502c552049523fe360c3bcf1cc572feb04386f97d55871dd8cea0393cdd964e724082adc98126e6f2fe1d576be4bf911e9aca70e35538175f8382bbcd614bbecc97c9607ef25da2ff08a6e5b6f76cbe9ccb0e0fdc3528e3e2c3675a5c897d295bb76524ec8a73a70b97909368f44d92f9aceaef0b03f3dafa1faa89fc663a92da3c19b4952463fac0e825e78cf046e266cfb9975af72e9d50d2c2cafee88fe2cecae2b1465fc07b280d83b66062dc9e7a372f81aec8e0bb9e97877814a5a6813c67746e35cd068d45d8664528bd00d5a306a5319e1bea7f38345da92d3a10d91476a26aed6b8441f0f72fbbad5d5e0f8ae5cabc9f4f08e6be7902b5c53632db5264afee7422c87b3237a32d5213ad0eb807b61977d9d90666cbb0c70500526b0eb762c99351796db41166b0aa2f221b5607e0d629fac4e938488245c11557381a4f8addcc49913b11d42481cf8668e37bacbad4a20509e4fe4ccbcee7aea2909a2abe59052f7f28b9340cd92f69729d615b8d3b530941c0b30506498cd4e561a9c82d915266bb7115967bc76c5593c06d094bdf4294b868afc5fa52742d3bdbd5932df599f0e1187c49f0dba8679c771a514cc9da75e03506957800bf470d4a07c4bb8918d6085499bb8ceeaba23c0b465863327e9ab8b6b8cf8b3ca530ca7b02cfadf85437b750f305e8fbc8855c95bee8595a7e9e1f0993a03adbadc68665a18936cc99b6530b4518c0754990d7bfdfdac76f88cfcbcb7b3d9a71ee10cbd3a1bdbc2e50b642c1fef56511962f845bbec6eab727b1d4add335db8d80c4c07e8356ad05adad68b012489fa5bb5d9019a667778ddf7f5edd80f1d3c4abd64397a89e554c8007809336ddc2b2e7d5219c39fdf39aad33b9350f6b18fe3b98c690b9068f36d4b7669530fd216373842fbf70fe9bbe80854b31eed4bd515d6caeb065d6c609846c9bfae1b3fce3db70b5bfb448ec69512e7f25019c789301b77a75f2a0f81c65ec29f41bf96d597a00c310e8ba4b48ac82b5a735c1e83f22394eb2fc9b35d42a35533c938f26290a5860175637982f1733c99be39c44ac4a09187406306bde2fd3d28e4e7bda73719912c338804dea03987757dac4d73def665e11da126f9414f71624a3b753797eb0472bd334094515c4f9fe57fdd8d185f22b4bf82e4b5f6b800870cce19a0c8174dc11ee9f1cb9ffe0ac6f6fff1ebf7c915c7ae20172bb70390e3759912e0e0a4e83a0a2d2318f4386314a89f6438ccb331f89377ff7947fe4b24f788aef85c1656ca87ee41c959f1b09bde09f20c2a51ac481646b28e9b0fc2ff49cfe8cf28577bf5bf6f261f54f97fcd2875da4210c6dfe685450280b68e378d9a486243cc682ed4ec747c37de1fde848e4a8f70498d22e40c462c469c884cd67330e77b694e759232313f31a1624e0e1960f23ddae47b68ff553d0de0910c8abe2e8e5fb063aa744ff77465fc731c7af79a84dcaa9b3f741a46dd3c932877d49242c6d883e14392b8c4530986605812b636a73590ef437f27e40d1af37ed1cbd68fb4e9ca5b0b41e5daee0142c1bf59c9d71f6c19b25e6148dfbb9fb142107aabe3701e36611a7e0b13ea32d3c5f8a51f63c5f34415baa15f6ca77300eb323241ffe73c5acd97fcb682c21dc8911392979e9cb81be5218acf452b5b93f6681d323b7989fdd10efe6fe9e2ac88d0d76a4cf3ee45e3b5c430100014142c1fc7e8a658eff437594a25cf34d269556d8511918f27fdc7e9d6dd73f0e4790b91f225e9d131e6abb3dbfb66549a9aa57948fbd2f183fcd951b1d2305bffd6c0a602000000000000000016f5cdf9fb6c1b5e98a36befdc2c55bd4fd8793d554b2506f51c909362495e1216ee83cd270ddb0a00785600ba23bd3363f0798e3a7a117990415adec88e61be65170bd587ab4d2ee38edb22a91e5c29afa397dd5a73465c51c6263f5fbde47fa801ce84464acc32589acaafadfe44d6558774b7085612a88f3424b6dca3c6f07217d1cbd5c41bda46a6a492a0119c1de4d25b58c94250bee3fba6b8223777535673a2f4da6af27598030f88144f408120f07ca9c98d5d9edcdf6cdc9073f118fce55e6c9d0be80b5e87992ddaa9c22053b3a00d42bdedc9768de25c0b37a5c4fb4e86710b33cebed5588d88adde607f6bca14f0279ce35126d403ffa50f288c87f528c19749ed43bd846c513fcd92c173fe76d8f2e69770439d3d075cb19b1094a42ee07ae1de197e8c136e2bc688a75a74db24adb0fbb73872dc80074f61c9cce9bd33861bdd921ee3edacab1d6e7cec325c172b6b6e82ada11687e4fc931225074dd1f20a0f9342dbce1fc3fdbf5bb6cb74ab6475e574e9f5f247a2f7e4fcfcc354d4da8c8066e574642c7fccbbb9ef0aa592ecab5366fe87eb8e14cd64aee34578aa48f68f8f4c5372df2c3fc429f5a3e39ef6c034c87f9c52b2ea35e28c7bf3be737c3817efd6569466dc859e8ff8965c5249b6f045934d3d08b0ffd388aec58df8194ac2c4fec2152942d2626595e65664b1fa33b5dae8ee796a840a56d885cbf7ae6483fad05e507ada3f075ebce0d791b626c6dfe93f8492c4dd3b34aafc33d7644c5c8e38bfd8c19194f65be88fcb4538778632e489a626896372fdd2498b16e64daa7d3c5cfac688d6f9cdf3717261b0a1f25be1bdd6be6558ddb826fa04b5f668810a291aea51a6f05ff7c34dcf81c74849a8015bad5e4e416989b10ef01de304775db725fa0b665f4330dc9c540dc29aab144837362a97d6bb0165cb3272338c2d32386cd95ee3e66d876b591a25a6907237523cf908f736d2fdc8e54ea8d9c7562697161d1f72fc4d7b775052415cd0e5ae5bdf6edfab5776b6ff75ce5e1f8f2beea6ec74252b63966cca58abd638279dc5c998a1068079f3e5dcc8a69165c304c3d8c362ccfadab05ad12208a5655ab389eb727e8ed5f86b300331a13be26e2fbabf89fbfd2b98481dd5edb52ed456a0e03a84b6f89761f91ff251412f5cfa286e35fb9f48ef0e044c4742b6e860a08767ecb80548c2f3df3b371cdb40e86dbe118f64e84faf45ecb78d73364e9e31e3412ca2a3fad0a35983370ea9e6264a222edd1fd4aca30e3c169d7ca2d07609262e786ecd019c1417a06b7dfa32a54e0897afdc6492f26611555cbff47dba3b76381f239d597a8f687669333e0b47b53d5bcc4fea1919490bad3c6f0b6a58a50aca7ddeb9745ead454e0a38d9486fb52aefe0dbb92bf7fd6c215078aba3482b11274ec8cddff92c359bbc6d20bd823ad0bbf859cfaadf8e775b3d37b3078319f46c6d2a112cf60a673fee467538c70f1687d97fbe9d9f8a0856061592a4e00b6d10e979e674dd2cd0ba8b853f733877cd508062d5f723d58d215ad69c2be6be742496aef54eb87338622eb36a9bbc5a7a602d280a45e095b1e078dab54479e783a513c722066acaae44ccc15f9560da91ed053ec05c36d82f6809766876c45c4fbeb2321d50f48f7995437d0c5fc365974a571fb0352d28cb1cdbd21d69fab576a2e68d6b881776027bcdb7f01be22b1c847d91f26e680ef6ab2c128a89b59432383d9bd661b0b01432cf8a25319426d38ac2e2114825f59b4250569c798b1094920bb31130728313ff56a6eef2e6c4b275215dce3786d0f9024952b5f572566c53597e7ef4ab1f75743e605a564054d667f48906b5481d924769ef65751e349891d725a2c1bf8b102fea4c25c874d2fc2ce1bfec4b39bea76fbf7a28855725d52b595a4fc96892c3f1f961d46310ebd5221df729c02060035c559baf0fd7efa73a2213ca29642857aeb8ebf7efdf9d2f5c84746b6fc35ab355a8dca56e7dde4831e47ca1be6b62af30cfcf807c384e56ab84ff03bbe786251e6c4b932c9217bf671046217bd0511fdc06aa69050c1480281e4843eb73d80095a2fb8e68a2c0c98c9aea637b99d87ad847a3a76d59ea308c751f9cb4a4fce2989822bd6ba2f901f09df647536dc30730ea3160dd35b8c6dcc9aa815b79ed492a8a299a298ccdf784b9b0211ca877ec1723817c98529acaa4d3727162b5740b0fc9b498dfb2212a3cbf0c63dc4f7663fafad7905643a792862b651e8497b0f0da632b897ecf9ee63f2b20b54fa5eb2f2e424dcce5a075f50b856af266655be3a815fc83ed8027508b2536976982196b160e2219ffdb5c7a56dd3e6b700860c711f4439dbf72973f4f26fe3260ec43a3446fe14444b9787d877e107be610147eec4a3574745e95a1f424aff062f84c559d13b1e6b59e8dc2221515c229f07db8eb39c515a321d8bd07b1bd6c9a79dac6d951c04415553c7a2ce1eb77495c7f89c4d5b4cffd289435b69bc53585095083cc5a1b191781342266e204e1566aca8175e2ae84a8bd711d188b666dfb65a6442776d3e23c1b5192af09ec712537f2157d0ccbc1bb3b3a1969d9705671f16bdc266e615ad2e50a8cbd666f3ee7465cc430c6cd69d30c91e717b12f7094b6f0ef89134d6c1620d28d8f238c181146448b348e4ca2e93c737210350f18fb878fb91b70ecc5689e5b6101ecfc545f6a1c903115b0c6419c91a50fb2dbe2edd362f2815f0c75070974507c34130ac9b29747ff7efbe6e37ee4c62be3ecfedfa817fdf3309163aaff677775b77f0d288c9858cfe59cb0fa18afa591e7d574eaef43c82e79d71542c4177de4e5bd724b18cfd33c68530665728a9d5ef192772094acbf3d885d5146c1634e74754e3fbcb94fa349eac8280cfd7d1f46a0813b57a83bd078b1f7cb5a60a59b59380fe04e1c600c33b33d1add69a9ff1be546f0ec5c0083979fce940b23711f382ac0d011c1103f02cb6082c18e39cf7a9c3bf4c081f905ae7b87951a7880b57e934465ccd634e5a17fd8d8866abfdfebd33b2c3d2c5be58144900c04e9c18de0c80270660e62a3c185277555f89da4c41bd33cec1359f4ed21abdb586e1d97f720a92d16014d7f1822f1836f74c97cb7f7b38e073477c6ab064fde835916c1e624de81f2ad90f6260073c5e1848582860f033630bde225821b39c2572b30c36adf8fdb8317c33df05f6413447f4985d12e9012629df09dc8f43373a6d0db4b0048453a6f1ec662472c77a30d5cf4ac7084f736d0d598c251f2aefc986052fbf12a657885d7140ad36b07c63ab86388a2be12d943747f3f29ef9f2e11e1444cc873df0ed7826eef675389a0d5a0388a8504fe89c4791ea4a572bfd406d5f01418b4f888c9a7a566e32811936bf6950bbf786b86c41c28f2045d31953fcd15f179e7bc00c72870890537921f7deff82270b0e44b88720aa738f60a85567deb7c90b0c2444467621e53e1c079436d31d3d0b34dd237fc281eb9d87175237a9a433142db4bb7f8c4cb6a34e2dc73f074045d216695ce88ef68e18564c935c9cbd902e939655c258de2ab78def8746bffd972083afce3b6881b7147262e1a44e0224689fafa1a3cb823c8da6eb7df091bec0638bf728b7b10aa95f2bce512ec8d3252938d2eb77b44ace7a2f976588032cac5af670f9e5ca25cb0721bc1baec26f9c3a9f41b02fb62997d6cb0a01314845e9d0e78139ea49f2ead8736e0000 \ No newline at end of file diff --git a/crates/simplicityhl-core/src/lib.rs b/crates/simplicityhl-core/src/lib.rs index 164eaf8..aeee21d 100644 --- a/crates/simplicityhl-core/src/lib.rs +++ b/crates/simplicityhl-core/src/lib.rs @@ -8,6 +8,7 @@ mod error; mod fee_rate_fetcher; mod runner; mod scripts; +mod tx_inclusion; #[cfg(feature = "encoding")] pub mod encoding { @@ -72,6 +73,7 @@ pub use error::EncodingError; pub use runner::*; pub use scripts::*; +pub use tx_inclusion::*; pub use fee_rate_fetcher::*; diff --git a/crates/simplicityhl-core/src/tx_inclusion.rs b/crates/simplicityhl-core/src/tx_inclusion.rs new file mode 100644 index 0000000..6c7202d --- /dev/null +++ b/crates/simplicityhl-core/src/tx_inclusion.rs @@ -0,0 +1,159 @@ +use simplicityhl::elements::hashes::{Hash, HashEngine}; +use simplicityhl::elements::{Block, TxMerkleNode, Txid}; + +pub type MerkleProof = (usize, Vec); + +/// Creates inclusion proof for tx in block's Merkle tree +#[must_use] +pub fn merkle_branch(tx: &Txid, block: &Block) -> Option { + if block.txdata.is_empty() { + return None; + } + + let tx_index = block.txdata.iter().position(|t| &t.txid() == tx)?; + + Some((tx_index, build_merkle_branch(tx_index, block))) +} + +/// Verifies proof of tx inclusion +#[must_use] +pub fn verify_tx(tx: &Txid, root: &TxMerkleNode, proof: &MerkleProof) -> bool { + root.eq(&compute_merkle_root_from_branch(tx, proof.0, &proof.1)) +} + +fn build_merkle_branch(tx_index: usize, block: &Block) -> Vec { + if block.txdata.is_empty() || block.txdata.len() == 1 { + return vec![]; + } + + let mut branch = vec![]; + let mut layer = block + .txdata + .iter() + .map(|tx| TxMerkleNode::from_raw_hash(*tx.txid().as_raw_hash())) + .collect::>(); + let mut index = tx_index; + + while layer.len() > 1 { + let mut next_layer = vec![]; + + for i in (0..layer.len()).step_by(2) { + let left = layer[i]; + let right = if i + 1 < layer.len() { + layer[i + 1] + } else { + layer[i] + }; + + let mut eng = TxMerkleNode::engine(); + eng.input(left.as_raw_hash().as_byte_array()); + eng.input(right.as_raw_hash().as_byte_array()); + + next_layer.push(TxMerkleNode::from_engine(eng)); + + if index / 2 == i / 2 { + let sibling = if index.is_multiple_of(2) { right } else { left }; + branch.push(sibling); + } + } + + index /= 2; + layer = next_layer; + } + + branch +} + +fn compute_merkle_root_from_branch( + tx: &Txid, + tx_index: usize, + branch: &[TxMerkleNode], +) -> TxMerkleNode { + let mut res = TxMerkleNode::from_raw_hash(*tx.as_raw_hash()); + let mut pos = tx_index; + + for leaf in branch { + let mut eng = TxMerkleNode::engine(); + + res = if pos & 1 == 0 { + eng.input(res.as_raw_hash().as_byte_array()); + eng.input(leaf.as_raw_hash().as_byte_array()); + TxMerkleNode::from_engine(eng) + } else { + eng.input(leaf.as_raw_hash().as_byte_array()); + eng.input(res.as_raw_hash().as_byte_array()); + TxMerkleNode::from_engine(eng) + }; + + pos >>= 1; + } + + res +} + +#[cfg(test)] +mod test { + + use super::*; + + /// Taken from rust-elements + /// + macro_rules! hex_deserialize( + ($e:expr) => ({ + use simplicityhl::elements::encode::deserialize; + + fn hex_char(c: char) -> u8 { + match c { + '0' => 0, + '1' => 1, + '2' => 2, + '3' => 3, + '4' => 4, + '5' => 5, + '6' => 6, + '7' => 7, + '8' => 8, + '9' => 9, + 'a' | 'A' => 10, + 'b' | 'B' => 11, + 'c' | 'C' => 12, + 'd' | 'D' => 13, + 'e' | 'E' => 14, + 'f' | 'F' => 15, + x => panic!("Invalid character {} in hex string", x), + } + } + + let mut ret = Vec::with_capacity($e.len() / 2); + let mut byte = 0; + for (ch, store) in $e.chars().zip([false, true].iter().cycle()) { + byte = (byte << 4) + hex_char(ch); + if *store { + ret.push(byte); + byte = 0; + } + } + deserialize(&ret).expect("deserialize object") + }); + ); + + // Unfortunately, `hex_deserialize` macro aforehead returns error trying deserialize + // blocks from elements-cli regtest, so this block, taken from `elements::Block::block`, is + // the only test case I have found so far. + const BLOCK_STR: &str = include_str!("./assets/test-tx-incl-block.hex"); + + #[test] + fn test_merkle_branch_construction() { + let block: Block = hex_deserialize!(BLOCK_STR); + + assert_eq!(block.txdata.len(), 3); + + let tx = block.txdata[1].txid(); + let proof = merkle_branch(&tx, &block).expect("Failed to find tx in block"); + + assert!( + verify_tx(&tx, &block.header.merkle_root, &proof), + "Invalid merkle proof" + ); + } +} From d811af9b418cf09761dd9492f15583486d81f80e Mon Sep 17 00:00:00 2001 From: topologoanatom Date: Mon, 26 Jan 2026 16:14:10 +0200 Subject: [PATCH 2/2] improve docs --- crates/simplicityhl-core/src/tx_inclusion.rs | 25 ++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/crates/simplicityhl-core/src/tx_inclusion.rs b/crates/simplicityhl-core/src/tx_inclusion.rs index 6c7202d..34c2c2e 100644 --- a/crates/simplicityhl-core/src/tx_inclusion.rs +++ b/crates/simplicityhl-core/src/tx_inclusion.rs @@ -1,9 +1,22 @@ +//! Transaction inclusion verification using Merkle proofs for Liquid/Elements blocks. +//! +//! This module provides SPV (Simplified Payment Verification) functionality to prove +//! a transaction exists in a block without downloading all transactions. + use simplicityhl::elements::hashes::{Hash, HashEngine}; use simplicityhl::elements::{Block, TxMerkleNode, Txid}; +/// Merkle proof: (`transaction_index`, `sibling_hashes`) pub type MerkleProof = (usize, Vec); -/// Creates inclusion proof for tx in block's Merkle tree +/// Constructs a Merkle inclusion proof (Merkle branch) for a transaction TXID +/// in a block, using Bitcoin consensus Merkle tree construction rules +/// (pairwise double-SHA256 hashing with odd-hash duplication). +/// +/// Liquid inherits the same Merkle tree semantics via the Elements codebase: +/// +/// +/// Returns `None` if the transaction is not present in the block. #[must_use] pub fn merkle_branch(tx: &Txid, block: &Block) -> Option { if block.txdata.is_empty() { @@ -15,7 +28,14 @@ pub fn merkle_branch(tx: &Txid, block: &Block) -> Option { Some((tx_index, build_merkle_branch(tx_index, block))) } -/// Verifies proof of tx inclusion +/// Verifies a Merkle inclusion proof (Merkle branch) for a transaction TXID +/// against the given Merkle root using Bitcoin consensus Merkle tree rules +/// (pairwise double-SHA256 hashing with left/right ordering). +/// +/// Liquid inherits the same Merkle tree semantics via the Elements codebase: +/// +/// +/// Returns `true` if the proof commits the transaction to the given root. #[must_use] pub fn verify_tx(tx: &Txid, root: &TxMerkleNode, proof: &MerkleProof) -> bool { root.eq(&compute_merkle_root_from_branch(tx, proof.0, &proof.1)) @@ -34,6 +54,7 @@ fn build_merkle_branch(tx_index: usize, block: &Block) -> Vec { .collect::>(); let mut index = tx_index; + // Bottom-up traversal: pair nodes, hash parents, collect siblings along path to root while layer.len() > 1 { let mut next_layer = vec![];