├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── rustfmt.toml ├── src ├── helpers │ ├── builders.rs │ ├── mod.rs │ └── parsers.rs ├── lib.rs ├── rpc.rs ├── service.rs ├── spec │ ├── address.rs │ ├── blob.rs │ ├── block.rs │ ├── block_hash.rs │ ├── header.rs │ ├── header_stream.rs │ ├── mod.rs │ ├── proof.rs │ ├── transaction.rs │ └── utxo.rs └── verifier.rs └── test_data ├── blob.txt └── mock_txs.txt /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | .DS_Store 4 | 5 | # reveal tx logs 6 | reveal_*.tx 7 | 8 | # coverage report 9 | tarpaulin-report.html 10 | build_rs_cov.profraw -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bitcoin-da" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license = "MIT OR Apache-2.0" 6 | authors = ["Chainway "] 7 | homepage = "https://www.chainway.xyz" 8 | publish = false 9 | repository = "https://github.com/chainway/bitcoin-da" 10 | rust-version = "1.66" 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [dependencies] 15 | sov-rollup-interface = { git = "https://github.com/Sovereign-Labs/sovereign-sdk", rev = "617926c" } 16 | 17 | tokio = { version = "1", features = [ 18 | "full", 19 | ], optional = true, default-features = false } 20 | 21 | reqwest = { version = "0.11.13", features = [ 22 | "blocking", 23 | "json", 24 | ], optional = true, default-features = false } 25 | base64 = "0.13.1" 26 | hex = { version = "0.4.3", features = ["serde"] } 27 | tracing = "0.1.37" 28 | rand = "0.8.5" 29 | serde = "1.0.188" 30 | serde_json = { version = "1.0.105", features = ["raw_value"], optional = true } 31 | async-trait = "0.1.73" 32 | borsh = "0.10.3" 33 | anyhow = "1.0.75" 34 | thiserror = "1.0.50" 35 | futures = { version = "0.3", optional = true } 36 | pin-project = { version = "1.1.3", optional = true } 37 | 38 | bitcoin = { version = "0.30.1", features = ["serde", "rand"] } 39 | brotli = "3.3.4" 40 | async-recursion = "1.0.5" 41 | 42 | 43 | [features] 44 | default = [] 45 | native = [ 46 | "dep:tokio", 47 | "dep:reqwest", 48 | "dep:futures", 49 | "dep:pin-project", 50 | "dep:serde_json", 51 | "sov-rollup-interface/native", 52 | ] 53 | verifier = [] 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2023 Chainway 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BitcoinDA 2 | 3 | > [!WARNING] 4 | > This adapter is now maintained in [Citrea monorepo](https://github.com/chainwayxyz/citrea) 5 | 6 | BitcoinDA is a Data Availability adapter making Bitcoin compatible with the Sovereign SDK. None of its code is 7 | suitable for production use. It contains known security flaws and numerous inefficiencies. 8 | 9 | ## BitcoinDA 10 | 11 | BitcoinDA is a research prototype. It contains known vulnerabilities and should not be used in production under any 12 | circumstances. 13 | 14 | ## How it Works 15 | 16 | All of BitcoinDA boils down to two trait implementations: [`DaVerifier`](https://github.com/Sovereign-Labs/sovereign-sdk/blob/8388dc2176940bc6a909076e5ed43feb5a87bf7a/sdk/src/state_machine/da.rs#L36) and [`DaService`](https://github.com/Sovereign-Labs/sovereign-sdk/blob/8388dc2176940bc6a909076e5ed43feb5a87bf7a/sdk/src/node/services/da.rs#L13). 17 | 18 | ### The DaVerifier Trait 19 | 20 | The DaVerifier trait is the simpler of the two core traits. Its job is to take a list of BlobTransactions from a DA layer block 21 | and verify that the list is _complete_ and _correct_. Once deployed in a rollup, the data verified by this trait 22 | will be passed to the state transition function, so non-determinism should be strictly avoided. 23 | 24 | The logic inside this trait will get compiled down into your rollup's proof system, so it's important to gain a high 25 | degree of confidence in its correctness (upgrading SNARKs is hard!) and think carefully about performance. 26 | 27 | At a bare minimum, you should ensure that the verifier rejects... 28 | 29 | 1. If the order of the blobs in an otherwise valid input is changed 30 | 1. If the sender of any of the blobs is tampered with 31 | 1. If any blob is omitted 32 | 1. If any blob is duplicated 33 | 1. If any extra blobs are added 34 | 35 | We also recommend (but don't require) that any logic in the `DaVerifier` be able to build with `no_std`. 36 | This maximizes your odds of being compatible with new zk proof systems as they become available. However, 37 | it's worth noting that some Rust-compatible SNARKs (including Risc0) support limited versions of `std`. If you only care 38 | about compatibility with these proof systems, then `no_std` isn't a requirement. 39 | 40 | **BitcoinDA's DA Verifier** 41 | 42 | In Bitcoin, checking _completeness_ of data is not easy. Unfortunately, in order to prove DAService included all of the transactions that can be related to the rollup we need to go through each transaction in our SNARK. 43 | We are iterating over non-included transactions of a block and asserting there are no related script (inscription) inside them. 44 | 45 | Checking _inclusion_, is easy. We can simply check that the transaction is included in the block using _txroot_ field of the block header. This is a merkle root of all transactions in the block. We are extracting the blob sender in a unique way due to the Bitcoin's UTXO architecture. We require sender's to include their `public_key` and `signature(hash(blob))` inside their inscription, after the namespace and the blob itself. This way we can verify the sender of the blob and the blob itself. 46 | 47 | ### The DaService Trait 48 | 49 | The `DaService` trait is slightly more complicated than the `DaVerifier`. Thankfully, it exists entirely outside of the 50 | rollup's state machine - so it never has to be proven in zk. This means that its perfomance is less critical, and that 51 | upgrading it in response to a vulnerability is much easier. 52 | 53 | The job of the `DAService` is to allow the Sovereign SDK's node software to communicate with a DA layer. It has two related 54 | responsibilities. The first is to interact with DA layer nodes via RPC - retrieving data for the rollup as it becomes 55 | available. The second is to process that data into the form expected by the `DaVerifier`. For example, almost all DA layers 56 | provide data in JSON format via RPC - but, parsing JSON in a zk-SNARK would be horribly inefficient. So, the `DaService` 57 | is responsible for both querying the RPC service and transforming its responses into a more useful format. 58 | 59 | **BitcoinDA's DA Service** 60 | BitcoinDA's DA service currently communicates with a local bitcoin-core node via JSON-RPC. Each time a Bitcoin block is 61 | created, the DA service makes a series of RPC requests to obtain all of the relevant blob data. Then, it packages 62 | that data into the format expected by the DA verifier and returns. 63 | 64 | ## License 65 | 66 | Licensed under the [Apache License, Version 67 | 2.0](./LICENSE). 68 | 69 | Unless you explicitly state otherwise, any contribution intentionally submitted 70 | for inclusion in this repository by you, as defined in the Apache-2.0 license, shall be 71 | licensed as above, without any additional terms or conditions. 72 | 73 | --- 74 | Built by [Chainway](https://github.com/chainwayxyz) ❤️ 75 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | group_imports = "StdExternalCrate" 2 | imports_granularity = "Module" 3 | -------------------------------------------------------------------------------- /src/helpers/builders.rs: -------------------------------------------------------------------------------- 1 | use core::result::Result::Ok; 2 | use core::str::FromStr; 3 | use std::fs::File; 4 | use std::io::{BufWriter, Write}; 5 | 6 | use anyhow::anyhow; 7 | use bitcoin::absolute::LockTime; 8 | use bitcoin::blockdata::opcodes::all::{OP_CHECKSIG, OP_ENDIF, OP_IF}; 9 | use bitcoin::blockdata::opcodes::OP_FALSE; 10 | use bitcoin::blockdata::script; 11 | use bitcoin::hashes::{sha256d, Hash}; 12 | use bitcoin::key::{TapTweak, TweakedPublicKey, UntweakedKeyPair}; 13 | use bitcoin::psbt::Prevouts; 14 | use bitcoin::script::PushBytesBuf; 15 | use bitcoin::secp256k1::constants::SCHNORR_SIGNATURE_SIZE; 16 | use bitcoin::secp256k1::schnorr::Signature; 17 | use bitcoin::secp256k1::{self, Secp256k1, SecretKey, XOnlyPublicKey}; 18 | use bitcoin::sighash::SighashCache; 19 | use bitcoin::taproot::{ControlBlock, LeafVersion, TapLeafHash, TaprootBuilder}; 20 | use bitcoin::{ 21 | Address, Network, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Witness, 22 | }; 23 | use brotli::{CompressorWriter, DecompressorWriter}; 24 | 25 | use crate::helpers::{BODY_TAG, PUBLICKEY_TAG, RANDOM_TAG, ROLLUP_NAME_TAG, SIGNATURE_TAG}; 26 | use crate::spec::utxo::UTXO; 27 | 28 | pub fn compress_blob(blob: &[u8]) -> Vec { 29 | let mut writer = CompressorWriter::new(Vec::new(), 4096, 11, 22); 30 | writer.write_all(blob).unwrap(); 31 | writer.into_inner() 32 | } 33 | 34 | pub fn decompress_blob(blob: &[u8]) -> Vec { 35 | let mut writer = DecompressorWriter::new(Vec::new(), 4096); 36 | writer.write_all(blob).unwrap(); 37 | writer.into_inner().expect("decompression failed") 38 | } 39 | 40 | // Signs a message with a private key 41 | pub fn sign_blob_with_private_key( 42 | blob: &[u8], 43 | private_key: &SecretKey, 44 | ) -> Result<(Vec, Vec), ()> { 45 | let message = sha256d::Hash::hash(blob).to_byte_array(); 46 | let secp = Secp256k1::new(); 47 | let public_key = secp256k1::PublicKey::from_secret_key(&secp, private_key); 48 | let msg = secp256k1::Message::from_slice(&message).unwrap(); 49 | let sig = secp.sign_ecdsa(&msg, private_key); 50 | Ok(( 51 | sig.serialize_compact().to_vec(), 52 | public_key.serialize().to_vec(), 53 | )) 54 | } 55 | 56 | #[allow(clippy::ptr_arg)] 57 | fn get_size( 58 | inputs: &Vec, 59 | outputs: &Vec, 60 | script: Option<&ScriptBuf>, 61 | control_block: Option<&ControlBlock>, 62 | ) -> usize { 63 | let mut tx = Transaction { 64 | input: inputs.clone(), 65 | output: outputs.clone(), 66 | lock_time: LockTime::ZERO, 67 | version: 2, 68 | }; 69 | 70 | for i in 0..tx.input.len() { 71 | tx.input[i].witness.push( 72 | Signature::from_slice(&[0; SCHNORR_SIGNATURE_SIZE]) 73 | .unwrap() 74 | .as_ref(), 75 | ); 76 | } 77 | 78 | #[allow(clippy::unnecessary_unwrap)] 79 | if tx.input.len() == 1 && script.is_some() && control_block.is_some() { 80 | tx.input[0].witness.push(script.unwrap()); 81 | tx.input[0].witness.push(control_block.unwrap().serialize()); 82 | } 83 | 84 | tx.vsize() 85 | } 86 | 87 | fn choose_utxos(utxos: &[UTXO], amount: u64) -> Result<(Vec, u64), anyhow::Error> { 88 | let mut bigger_utxos: Vec<&UTXO> = utxos.iter().filter(|utxo| utxo.amount >= amount).collect(); 89 | let mut sum: u64 = 0; 90 | 91 | if !bigger_utxos.is_empty() { 92 | // sort vec by amount (small first) 93 | bigger_utxos.sort_by(|a, b| a.amount.cmp(&b.amount)); 94 | 95 | // single utxo will be enough 96 | // so return the transaction 97 | let utxo = bigger_utxos[0]; 98 | sum += utxo.amount; 99 | 100 | Ok((vec![utxo.clone()], sum)) 101 | } else { 102 | let mut smaller_utxos: Vec<&UTXO> = 103 | utxos.iter().filter(|utxo| utxo.amount < amount).collect(); 104 | 105 | // sort vec by amount (large first) 106 | smaller_utxos.sort_by(|a, b| b.amount.cmp(&a.amount)); 107 | 108 | let mut chosen_utxos: Vec = vec![]; 109 | 110 | for utxo in smaller_utxos { 111 | sum += utxo.amount; 112 | chosen_utxos.push(utxo.clone()); 113 | 114 | if sum >= amount { 115 | break; 116 | } 117 | } 118 | 119 | if sum < amount { 120 | return Err(anyhow!("not enough UTXOs")); 121 | } 122 | 123 | Ok((chosen_utxos, sum)) 124 | } 125 | } 126 | 127 | fn build_commit_transaction( 128 | utxos: Vec, 129 | recipient: Address, 130 | change_address: Address, 131 | output_value: u64, 132 | fee_rate: f64, 133 | ) -> Result { 134 | // get single input single output transaction size 135 | let mut size = get_size( 136 | &vec![TxIn { 137 | previous_output: OutPoint { 138 | txid: Txid::from_str( 139 | "0000000000000000000000000000000000000000000000000000000000000000", 140 | ) 141 | .unwrap(), 142 | vout: 0, 143 | }, 144 | script_sig: script::Builder::new().into_script(), 145 | witness: Witness::new(), 146 | sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, 147 | }], 148 | &vec![TxOut { 149 | script_pubkey: recipient.clone().script_pubkey(), 150 | value: output_value, 151 | }], 152 | None, 153 | None, 154 | ); 155 | let mut last_size = size; 156 | 157 | let utxos: Vec = utxos 158 | .iter() 159 | .filter(|utxo| utxo.spendable && utxo.solvable && utxo.amount > 546) 160 | .cloned() 161 | .collect(); 162 | 163 | if utxos.is_empty() { 164 | return Err(anyhow::anyhow!("no spendable UTXOs")); 165 | } 166 | 167 | let tx = loop { 168 | let fee = ((last_size as f64) * fee_rate).ceil() as u64; 169 | 170 | let input_total = output_value + fee; 171 | 172 | let res = choose_utxos(&utxos, input_total)?; 173 | 174 | let (chosen_utxos, sum) = res; 175 | 176 | let mut outputs: Vec = vec![]; 177 | 178 | outputs.push(TxOut { 179 | value: output_value, 180 | script_pubkey: recipient.script_pubkey(), 181 | }); 182 | 183 | let mut direct_return = false; 184 | if let Some(excess) = sum.checked_sub(input_total) { 185 | if excess >= 546 { 186 | outputs.push(TxOut { 187 | value: excess, 188 | script_pubkey: change_address.script_pubkey(), 189 | }); 190 | } else { 191 | // if dust is left, leave it for fee 192 | direct_return = true; 193 | } 194 | } 195 | 196 | let inputs = chosen_utxos 197 | .iter() 198 | .map(|u| TxIn { 199 | previous_output: OutPoint { 200 | txid: u.tx_id, 201 | vout: u.vout, 202 | }, 203 | script_sig: script::Builder::new().into_script(), 204 | witness: Witness::new(), 205 | sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, 206 | }) 207 | .collect(); 208 | 209 | size = get_size(&inputs, &outputs, None, None); 210 | 211 | if size == last_size || direct_return { 212 | break Transaction { 213 | lock_time: LockTime::ZERO, 214 | version: 2, 215 | input: inputs, 216 | output: outputs, 217 | }; 218 | } 219 | 220 | last_size = size; 221 | }; 222 | 223 | Ok(tx) 224 | } 225 | 226 | #[allow(clippy::too_many_arguments)] 227 | fn build_reveal_transaction( 228 | input_utxo: TxOut, 229 | input_txid: Txid, 230 | input_vout: u32, 231 | recipient: Address, 232 | output_value: u64, 233 | fee_rate: f64, 234 | reveal_script: &ScriptBuf, 235 | control_block: &ControlBlock, 236 | ) -> Result { 237 | let outputs: Vec = vec![TxOut { 238 | value: output_value, 239 | script_pubkey: recipient.script_pubkey(), 240 | }]; 241 | 242 | let inputs = vec![TxIn { 243 | previous_output: OutPoint { 244 | txid: input_txid, 245 | vout: input_vout, 246 | }, 247 | script_sig: script::Builder::new().into_script(), 248 | witness: Witness::new(), 249 | sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, 250 | }]; 251 | 252 | let size = get_size(&inputs, &outputs, Some(reveal_script), Some(control_block)); 253 | 254 | let fee = ((size as f64) * fee_rate).ceil() as u64; 255 | 256 | let input_total = output_value + fee; 257 | 258 | if input_utxo.value < 546 || input_utxo.value < input_total { 259 | return Err(anyhow::anyhow!("input UTXO not big enough")); 260 | } 261 | 262 | let tx = Transaction { 263 | lock_time: LockTime::ZERO, 264 | version: 2, 265 | input: inputs, 266 | output: outputs, 267 | }; 268 | 269 | Ok(tx) 270 | } 271 | 272 | // TODO: parametrize hardness 273 | // so tests are easier 274 | // Creates the inscription transactions (commit and reveal) 275 | #[allow(clippy::too_many_arguments)] 276 | pub fn create_inscription_transactions( 277 | rollup_name: &str, 278 | body: Vec, 279 | signature: Vec, 280 | sequencer_public_key: Vec, 281 | utxos: Vec, 282 | recipient: Address, 283 | reveal_value: u64, 284 | commit_fee_rate: f64, 285 | reveal_fee_rate: f64, 286 | network: Network, 287 | reveal_tx_prefix: &[u8], 288 | ) -> Result<(Transaction, Transaction), anyhow::Error> { 289 | // Create commit key 290 | let secp256k1 = Secp256k1::new(); 291 | let key_pair = UntweakedKeyPair::new(&secp256k1, &mut rand::thread_rng()); 292 | let (public_key, _parity) = XOnlyPublicKey::from_keypair(&key_pair); 293 | 294 | // start creating inscription content 295 | let reveal_script_builder = script::Builder::new() 296 | .push_x_only_key(&public_key) 297 | .push_opcode(OP_CHECKSIG) 298 | .push_opcode(OP_FALSE) 299 | .push_opcode(OP_IF) 300 | .push_slice(PushBytesBuf::try_from(ROLLUP_NAME_TAG.to_vec()).expect("Cannot push tag")) 301 | .push_slice( 302 | PushBytesBuf::try_from(rollup_name.as_bytes().to_vec()) 303 | .expect("Cannot push rollup name"), 304 | ) 305 | .push_slice( 306 | PushBytesBuf::try_from(SIGNATURE_TAG.to_vec()).expect("Cannot push signature tag"), 307 | ) 308 | .push_slice(PushBytesBuf::try_from(signature).expect("Cannot push signature")) 309 | .push_slice( 310 | PushBytesBuf::try_from(PUBLICKEY_TAG.to_vec()).expect("Cannot push public key tag"), 311 | ) 312 | .push_slice( 313 | PushBytesBuf::try_from(sequencer_public_key).expect("Cannot push sequencer public key"), 314 | ) 315 | .push_slice(PushBytesBuf::try_from(RANDOM_TAG.to_vec()).expect("Cannot push random tag")); 316 | // This envelope is not finished yet. The random number will be added later and followed by the body 317 | 318 | // Start loop to find a 'nonce' i.e. random number that makes the reveal tx hash starting with zeros given length 319 | let mut nonce: i64 = 0; 320 | loop { 321 | let utxos = utxos.clone(); 322 | let recipient = recipient.clone(); 323 | // ownerships are moved to the loop 324 | let mut reveal_script_builder = reveal_script_builder.clone(); 325 | 326 | // push first random number and body tag 327 | reveal_script_builder = reveal_script_builder 328 | .push_int(nonce) 329 | .push_slice(PushBytesBuf::try_from(BODY_TAG.to_vec()).expect("Cannot push body tag")); 330 | 331 | // push body in chunks of 520 bytes 332 | for chunk in body.chunks(520) { 333 | reveal_script_builder = reveal_script_builder.push_slice( 334 | PushBytesBuf::try_from(chunk.to_vec()).expect("Cannot push body chunk"), 335 | ); 336 | } 337 | // push end if 338 | reveal_script_builder = reveal_script_builder.push_opcode(OP_ENDIF); 339 | 340 | // finalize reveal script 341 | let reveal_script = reveal_script_builder.into_script(); 342 | 343 | // create spend info for tapscript 344 | let taproot_spend_info = TaprootBuilder::new() 345 | .add_leaf(0, reveal_script.clone()) 346 | .expect("Cannot add reveal script to taptree") 347 | .finalize(&secp256k1, public_key) 348 | .expect("Cannot finalize taptree"); 349 | 350 | // create control block for tapscript 351 | let control_block = taproot_spend_info 352 | .control_block(&(reveal_script.clone(), LeafVersion::TapScript)) 353 | .expect("Cannot create control block"); 354 | 355 | // create commit tx address 356 | let commit_tx_address = Address::p2tr( 357 | &secp256k1, 358 | public_key, 359 | taproot_spend_info.merkle_root(), 360 | network, 361 | ); 362 | 363 | let commit_value = (get_size( 364 | &vec![TxIn { 365 | previous_output: OutPoint { 366 | txid: Txid::from_str( 367 | "0000000000000000000000000000000000000000000000000000000000000000", 368 | ) 369 | .unwrap(), 370 | vout: 0, 371 | }, 372 | script_sig: script::Builder::new().into_script(), 373 | witness: Witness::new(), 374 | sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, 375 | }], 376 | &vec![TxOut { 377 | script_pubkey: recipient.clone().script_pubkey(), 378 | value: reveal_value, 379 | }], 380 | Some(&reveal_script), 381 | Some(&control_block), 382 | ) as f64 383 | * reveal_fee_rate 384 | + reveal_value as f64) 385 | .ceil() as u64; 386 | 387 | // build commit tx 388 | let unsigned_commit_tx = build_commit_transaction( 389 | utxos, 390 | commit_tx_address.clone(), 391 | recipient.clone(), 392 | commit_value, 393 | commit_fee_rate, 394 | )?; 395 | 396 | let output_to_reveal = unsigned_commit_tx.output[0].clone(); 397 | 398 | let mut reveal_tx = build_reveal_transaction( 399 | output_to_reveal.clone(), 400 | unsigned_commit_tx.txid(), 401 | 0, 402 | recipient, 403 | reveal_value, 404 | reveal_fee_rate, 405 | &reveal_script, 406 | &control_block, 407 | )?; 408 | 409 | let reveal_hash = reveal_tx.txid().as_raw_hash().to_byte_array(); 410 | 411 | // check if first N bytes equal to the given prefix 412 | if reveal_hash.starts_with(reveal_tx_prefix) { 413 | // start signing reveal tx 414 | let mut sighash_cache = SighashCache::new(&mut reveal_tx); 415 | 416 | // create data to sign 417 | let signature_hash = sighash_cache 418 | .taproot_script_spend_signature_hash( 419 | 0, 420 | &Prevouts::All(&[output_to_reveal]), 421 | TapLeafHash::from_script(&reveal_script, LeafVersion::TapScript), 422 | bitcoin::sighash::TapSighashType::Default, 423 | ) 424 | .expect("Cannot create hash for signature"); 425 | 426 | // sign reveal tx data 427 | let signature = secp256k1.sign_schnorr_with_rng( 428 | &secp256k1::Message::from_slice(signature_hash.as_byte_array()) 429 | .expect("should be cryptographically secure hash"), 430 | &key_pair, 431 | &mut rand::thread_rng(), 432 | ); 433 | 434 | // add signature to witness and finalize reveal tx 435 | let witness = sighash_cache.witness_mut(0).unwrap(); 436 | witness.push(signature.as_ref()); 437 | witness.push(reveal_script); 438 | witness.push(&control_block.serialize()); 439 | 440 | // check if inscription locked to the correct address 441 | let recovery_key_pair = 442 | key_pair.tap_tweak(&secp256k1, taproot_spend_info.merkle_root()); 443 | let (x_only_pub_key, _parity) = recovery_key_pair.to_inner().x_only_public_key(); 444 | assert_eq!( 445 | Address::p2tr_tweaked( 446 | TweakedPublicKey::dangerous_assume_tweaked(x_only_pub_key), 447 | network, 448 | ), 449 | commit_tx_address 450 | ); 451 | 452 | return Ok((unsigned_commit_tx, reveal_tx)); 453 | } 454 | 455 | nonce += 1; 456 | } 457 | } 458 | 459 | pub fn write_reveal_tx(tx: &[u8], tx_id: String) { 460 | let reveal_tx_file = File::create(format!("reveal_{}.tx", tx_id)).unwrap(); 461 | let mut reveal_tx_writer = BufWriter::new(reveal_tx_file); 462 | reveal_tx_writer.write_all(tx).unwrap(); 463 | } 464 | 465 | #[cfg(test)] 466 | mod tests { 467 | use core::str::FromStr; 468 | 469 | use bitcoin::hashes::Hash; 470 | use bitcoin::secp256k1::constants::SCHNORR_SIGNATURE_SIZE; 471 | use bitcoin::secp256k1::schnorr::Signature; 472 | use bitcoin::taproot::ControlBlock; 473 | use bitcoin::{Address, ScriptBuf, TxOut, Txid}; 474 | 475 | use crate::helpers::builders::{compress_blob, decompress_blob}; 476 | use crate::helpers::parsers::parse_transaction; 477 | use crate::spec::utxo::UTXO; 478 | 479 | #[test] 480 | fn compression_decompression() { 481 | let blob = std::fs::read("test_data/blob.txt").unwrap(); 482 | 483 | // compress and measure time 484 | let time = std::time::Instant::now(); 485 | let compressed_blob = compress_blob(&blob); 486 | println!("compression time: {:?}", time.elapsed()); 487 | 488 | // decompress and measure time 489 | let time = std::time::Instant::now(); 490 | let decompressed_blob = decompress_blob(&compressed_blob); 491 | println!("decompression time: {:?}", time.elapsed()); 492 | 493 | assert_eq!(blob, decompressed_blob); 494 | 495 | // size 496 | println!("blob size: {}", blob.len()); 497 | println!("compressed blob size: {}", compressed_blob.len()); 498 | println!( 499 | "compression ratio: {}", 500 | (blob.len() as f64) / (compressed_blob.len() as f64) 501 | ); 502 | } 503 | 504 | #[test] 505 | fn write_reveal_tx() { 506 | let tx = vec![100, 100, 100]; 507 | let tx_id = "test_tx".to_string(); 508 | 509 | super::write_reveal_tx(tx.as_slice(), tx_id); 510 | 511 | let file = std::fs::read("reveal_test_tx.tx").unwrap(); 512 | 513 | assert_eq!(tx, file); 514 | 515 | std::fs::remove_file("reveal_test_tx.tx").unwrap(); 516 | } 517 | 518 | #[allow(clippy::type_complexity)] 519 | fn get_mock_data() -> (&'static str, Vec, Vec, Vec, Address, Vec) { 520 | let rollup_name = "test_rollup"; 521 | let body = vec![100; 1000]; 522 | let signature = vec![100; 64]; 523 | let sequencer_public_key = vec![100; 33]; 524 | let address = 525 | Address::from_str("bc1pp8qru0ve43rw9xffmdd8pvveths3cx6a5t6mcr0xfn9cpxx2k24qf70xq9") 526 | .unwrap() 527 | .require_network(bitcoin::Network::Bitcoin) 528 | .unwrap(); 529 | let utxos = vec![ 530 | UTXO { 531 | tx_id: Txid::from_str( 532 | "4cfbec13cf1510545f285cceceb6229bd7b6a918a8f6eba1dbee64d26226a3b7", 533 | ) 534 | .unwrap(), 535 | vout: 0, 536 | address: "bc1pp8qru0ve43rw9xffmdd8pvveths3cx6a5t6mcr0xfn9cpxx2k24qf70xq9" 537 | .to_string(), 538 | script_pubkey: address.script_pubkey().to_hex_string(), 539 | amount: 1_000_000, 540 | confirmations: 100, 541 | spendable: true, 542 | solvable: true, 543 | }, 544 | UTXO { 545 | tx_id: Txid::from_str( 546 | "44990141674ff56ed6fee38879e497b2a726cddefd5e4d9b7bf1c4e561de4347", 547 | ) 548 | .unwrap(), 549 | vout: 0, 550 | address: "bc1pp8qru0ve43rw9xffmdd8pvveths3cx6a5t6mcr0xfn9cpxx2k24qf70xq9" 551 | .to_string(), 552 | script_pubkey: address.script_pubkey().to_hex_string(), 553 | amount: 100_000, 554 | confirmations: 100, 555 | spendable: true, 556 | solvable: true, 557 | }, 558 | UTXO { 559 | tx_id: Txid::from_str( 560 | "4dbe3c10ee0d6bf16f9417c68b81e963b5bccef3924bbcb0885c9ea841912325", 561 | ) 562 | .unwrap(), 563 | vout: 0, 564 | address: "bc1pp8qru0ve43rw9xffmdd8pvveths3cx6a5t6mcr0xfn9cpxx2k24qf70xq9" 565 | .to_string(), 566 | script_pubkey: address.script_pubkey().to_hex_string(), 567 | amount: 10_000, 568 | confirmations: 100, 569 | spendable: true, 570 | solvable: true, 571 | }, 572 | ]; 573 | 574 | ( 575 | rollup_name, 576 | body, 577 | signature, 578 | sequencer_public_key, 579 | address, 580 | utxos, 581 | ) 582 | } 583 | 584 | #[test] 585 | fn choose_utxos() { 586 | let (_, _, _, _, _, utxos) = get_mock_data(); 587 | 588 | let (chosen_utxos, sum) = super::choose_utxos(&utxos, 105_000).unwrap(); 589 | 590 | assert_eq!(sum, 1_000_000); 591 | assert_eq!(chosen_utxos.len(), 1); 592 | assert_eq!(chosen_utxos[0], utxos[0]); 593 | 594 | let (chosen_utxos, sum) = super::choose_utxos(&utxos, 1_005_000).unwrap(); 595 | 596 | assert_eq!(sum, 1_100_000); 597 | assert_eq!(chosen_utxos.len(), 2); 598 | assert_eq!(chosen_utxos[0], utxos[0]); 599 | assert_eq!(chosen_utxos[1], utxos[1]); 600 | 601 | let (chosen_utxos, sum) = super::choose_utxos(&utxos, 100_000).unwrap(); 602 | 603 | assert_eq!(sum, 100_000); 604 | assert_eq!(chosen_utxos.len(), 1); 605 | assert_eq!(chosen_utxos[0], utxos[1]); 606 | 607 | let (chosen_utxos, sum) = super::choose_utxos(&utxos, 90_000).unwrap(); 608 | 609 | assert_eq!(sum, 100_000); 610 | assert_eq!(chosen_utxos.len(), 1); 611 | assert_eq!(chosen_utxos[0], utxos[1]); 612 | 613 | let res = super::choose_utxos(&utxos, 100_000_000); 614 | 615 | assert!(res.is_err()); 616 | assert_eq!(format!("{}", res.unwrap_err()), "not enough UTXOs"); 617 | } 618 | 619 | #[test] 620 | fn build_commit_transaction() { 621 | let (_, _, _, _, address, utxos) = get_mock_data(); 622 | 623 | let recipient = 624 | Address::from_str("bc1p2e37kuhnsdc5zvc8zlj2hn6awv3ruavak6ayc8jvpyvus59j3mwqwdt0zc") 625 | .unwrap() 626 | .require_network(bitcoin::Network::Bitcoin) 627 | .unwrap(); 628 | let mut tx = super::build_commit_transaction( 629 | utxos.clone(), 630 | recipient.clone(), 631 | address.clone(), 632 | 5_000, 633 | 8.0, 634 | ) 635 | .unwrap(); 636 | 637 | tx.input[0].witness.push( 638 | Signature::from_slice(&[0; SCHNORR_SIGNATURE_SIZE]) 639 | .unwrap() 640 | .as_ref(), 641 | ); 642 | 643 | // 154 vB * 8 sat/vB = 1232 sats 644 | // 5_000 + 1232 = 6232 645 | // input: 10000 646 | // outputs: 5_000 + 3.768 647 | assert_eq!(tx.vsize(), 154); 648 | assert_eq!(tx.input.len(), 1); 649 | assert_eq!(tx.output.len(), 2); 650 | assert_eq!(tx.output[0].value, 5_000); 651 | assert_eq!(tx.output[0].script_pubkey, recipient.script_pubkey()); 652 | assert_eq!(tx.output[1].value, 3_768); 653 | assert_eq!(tx.output[1].script_pubkey, address.script_pubkey()); 654 | 655 | let mut tx = super::build_commit_transaction( 656 | utxos.clone(), 657 | recipient.clone(), 658 | address.clone(), 659 | 5_000, 660 | 45.0, 661 | ) 662 | .unwrap(); 663 | 664 | tx.input[0].witness.push( 665 | Signature::from_slice(&[0; SCHNORR_SIGNATURE_SIZE]) 666 | .unwrap() 667 | .as_ref(), 668 | ); 669 | 670 | // 111 vB * 45 sat/vB = 4.995 sats 671 | // 5_000 + 4928 = 9995 672 | // input: 10000 673 | // outputs: 5_000 674 | assert_eq!(tx.vsize(), 111); 675 | assert_eq!(tx.input.len(), 1); 676 | assert_eq!(tx.output.len(), 1); 677 | assert_eq!(tx.output[0].value, 5_000); 678 | assert_eq!(tx.output[0].script_pubkey, recipient.script_pubkey()); 679 | 680 | let mut tx = super::build_commit_transaction( 681 | utxos.clone(), 682 | recipient.clone(), 683 | address.clone(), 684 | 5_000, 685 | 32.0, 686 | ) 687 | .unwrap(); 688 | 689 | tx.input[0].witness.push( 690 | Signature::from_slice(&[0; SCHNORR_SIGNATURE_SIZE]) 691 | .unwrap() 692 | .as_ref(), 693 | ); 694 | 695 | // you expect 696 | // 154 vB * 32 sat/vB = 4.928 sats 697 | // 5_000 + 4928 = 9928 698 | // input: 10000 699 | // outputs: 5_000 72 700 | // instead do 701 | // input: 10000 702 | // outputs: 5_000 703 | // so size is actually 111 704 | assert_eq!(tx.vsize(), 111); 705 | assert_eq!(tx.input.len(), 1); 706 | assert_eq!(tx.output.len(), 1); 707 | assert_eq!(tx.output[0].value, 5_000); 708 | assert_eq!(tx.output[0].script_pubkey, recipient.script_pubkey()); 709 | 710 | let mut tx = super::build_commit_transaction( 711 | utxos.clone(), 712 | recipient.clone(), 713 | address.clone(), 714 | 1_050_000, 715 | 5.0, 716 | ) 717 | .unwrap(); 718 | 719 | tx.input[0].witness.push( 720 | Signature::from_slice(&[0; SCHNORR_SIGNATURE_SIZE]) 721 | .unwrap() 722 | .as_ref(), 723 | ); 724 | tx.input[1].witness.push( 725 | Signature::from_slice(&[0; SCHNORR_SIGNATURE_SIZE]) 726 | .unwrap() 727 | .as_ref(), 728 | ); 729 | 730 | // 212 vB * 5 sat/vB = 1060 sats 731 | // 1_050_000 + 1060 = 1_051_060 732 | // inputs: 1_000_000 100_000 733 | // outputs: 1_050_000 8940 734 | assert_eq!(tx.vsize(), 212); 735 | assert_eq!(tx.input.len(), 2); 736 | assert_eq!(tx.output.len(), 2); 737 | assert_eq!(tx.output[0].value, 1_050_000); 738 | assert_eq!(tx.output[0].script_pubkey, recipient.script_pubkey()); 739 | assert_eq!(tx.output[1].value, 48940); 740 | assert_eq!(tx.output[1].script_pubkey, address.script_pubkey()); 741 | 742 | let tx = super::build_commit_transaction( 743 | utxos.clone(), 744 | recipient.clone(), 745 | address.clone(), 746 | 100_000_000_000, 747 | 32.0, 748 | ); 749 | 750 | assert!(tx.is_err()); 751 | assert_eq!(format!("{}", tx.unwrap_err()), "not enough UTXOs"); 752 | 753 | let tx = super::build_commit_transaction( 754 | vec![UTXO { 755 | tx_id: Txid::from_str( 756 | "4cfbec13cf1510545f285cceceb6229bd7b6a918a8f6eba1dbee64d26226a3b7", 757 | ) 758 | .unwrap(), 759 | vout: 0, 760 | address: "bc1pp8qru0ve43rw9xffmdd8pvveths3cx6a5t6mcr0xfn9cpxx2k24qf70xq9" 761 | .to_string(), 762 | script_pubkey: address.script_pubkey().to_hex_string(), 763 | amount: 152, 764 | confirmations: 100, 765 | spendable: true, 766 | solvable: true, 767 | }], 768 | recipient.clone(), 769 | address.clone(), 770 | 100_000_000_000, 771 | 32.0, 772 | ); 773 | 774 | assert!(tx.is_err()); 775 | assert_eq!(format!("{}", tx.unwrap_err()), "no spendable UTXOs"); 776 | } 777 | 778 | #[test] 779 | fn build_reveal_transaction() { 780 | let (_, _, _, _, address, utxos) = get_mock_data(); 781 | 782 | let utxo = utxos.first().unwrap(); 783 | let script = ScriptBuf::from_hex("62a58f2674fd840b6144bea2e63ebd35c16d7fd40252a2f28b2a01a648df356343e47976d7906a0e688bf5e134b6fd21bd365c016b57b1ace85cf30bf1206e27").unwrap(); 784 | let control_block = ControlBlock::decode(&[ 785 | 193, 165, 246, 250, 6, 222, 28, 9, 130, 28, 217, 67, 171, 11, 229, 62, 48, 206, 219, 786 | 111, 155, 208, 6, 7, 119, 63, 146, 90, 227, 254, 231, 232, 249, 787 | ]) 788 | .unwrap(); // should be 33 bytes 789 | 790 | let mut tx = super::build_reveal_transaction( 791 | TxOut { 792 | value: utxo.amount, 793 | script_pubkey: ScriptBuf::from_hex(utxo.script_pubkey.as_str()).unwrap(), 794 | }, 795 | utxo.tx_id, 796 | utxo.vout, 797 | address.clone(), 798 | 546, 799 | 8.0, 800 | &script, 801 | &control_block, 802 | ) 803 | .unwrap(); 804 | 805 | tx.input[0].witness.push([0; SCHNORR_SIGNATURE_SIZE]); 806 | tx.input[0].witness.push(script.clone()); 807 | tx.input[0].witness.push(control_block.serialize()); 808 | 809 | assert_eq!(tx.input.len(), 1); 810 | assert_eq!(tx.input[0].previous_output.txid, utxo.tx_id); 811 | assert_eq!(tx.input[0].previous_output.vout, utxo.vout); 812 | 813 | assert_eq!(tx.output.len(), 1); 814 | assert_eq!(tx.output[0].value, 546); 815 | assert_eq!(tx.output[0].script_pubkey, address.script_pubkey()); 816 | 817 | let utxo = utxos.get(2).unwrap(); 818 | 819 | let tx = super::build_reveal_transaction( 820 | TxOut { 821 | value: utxo.amount, 822 | script_pubkey: ScriptBuf::from_hex(utxo.script_pubkey.as_str()).unwrap(), 823 | }, 824 | utxo.tx_id, 825 | utxo.vout, 826 | address.clone(), 827 | 546, 828 | 75.0, 829 | &script, 830 | &control_block, 831 | ); 832 | 833 | assert!(tx.is_err()); 834 | assert_eq!(format!("{}", tx.unwrap_err()), "input UTXO not big enough"); 835 | 836 | let utxo = utxos.get(2).unwrap(); 837 | 838 | let tx = super::build_reveal_transaction( 839 | TxOut { 840 | value: utxo.amount, 841 | script_pubkey: ScriptBuf::from_hex(utxo.script_pubkey.as_str()).unwrap(), 842 | }, 843 | utxo.tx_id, 844 | utxo.vout, 845 | address.clone(), 846 | 9999, 847 | 1.0, 848 | &script, 849 | &control_block, 850 | ); 851 | 852 | assert!(tx.is_err()); 853 | assert_eq!(format!("{}", tx.unwrap_err()), "input UTXO not big enough"); 854 | } 855 | #[test] 856 | fn create_inscription_transactions() { 857 | let (rollup_name, body, signature, sequencer_public_key, address, utxos) = get_mock_data(); 858 | 859 | let tx_prefix = &[0u8]; 860 | let (commit, reveal) = super::create_inscription_transactions( 861 | rollup_name, 862 | body.clone(), 863 | signature.clone(), 864 | sequencer_public_key.clone(), 865 | utxos.clone(), 866 | address.clone(), 867 | 546, 868 | 12.0, 869 | 10.0, 870 | bitcoin::Network::Bitcoin, 871 | tx_prefix, 872 | ) 873 | .unwrap(); 874 | 875 | // check pow 876 | assert!(reveal.txid().as_byte_array().starts_with(tx_prefix)); 877 | 878 | // check outputs 879 | assert_eq!(commit.output.len(), 2, "commit tx should have 2 outputs"); 880 | 881 | assert_eq!(reveal.output.len(), 1, "reveal tx should have 1 output"); 882 | 883 | assert_eq!( 884 | commit.input[0].previous_output.txid, utxos[2].tx_id, 885 | "utxo to inscribe should be chosen correctly" 886 | ); 887 | assert_eq!( 888 | commit.input[0].previous_output.vout, utxos[2].vout, 889 | "utxo to inscribe should be chosen correctly" 890 | ); 891 | 892 | assert_eq!( 893 | reveal.input[0].previous_output.txid, 894 | commit.txid(), 895 | "reveal should use commit as input" 896 | ); 897 | assert_eq!( 898 | reveal.input[0].previous_output.vout, 0, 899 | "reveal should use commit as input" 900 | ); 901 | 902 | assert_eq!( 903 | reveal.output[0].script_pubkey, 904 | address.script_pubkey(), 905 | "reveal should pay to the correct address" 906 | ); 907 | 908 | // check inscription 909 | let inscription = parse_transaction(&reveal, rollup_name).unwrap(); 910 | 911 | assert_eq!(inscription.body, body, "body should be correct"); 912 | assert_eq!( 913 | inscription.signature, signature, 914 | "signature should be correct" 915 | ); 916 | assert_eq!( 917 | inscription.public_key, sequencer_public_key, 918 | "sequencer public key should be correct" 919 | ); 920 | } 921 | } 922 | -------------------------------------------------------------------------------- /src/helpers/mod.rs: -------------------------------------------------------------------------------- 1 | // Tags that are used to seperate the different parts of the script 2 | const ROLLUP_NAME_TAG: &[u8] = &[1]; 3 | const SIGNATURE_TAG: &[u8] = &[2]; 4 | const PUBLICKEY_TAG: &[u8] = &[3]; 5 | const RANDOM_TAG: &[u8] = &[4]; 6 | const BODY_TAG: &[u8] = &[]; 7 | 8 | pub mod builders; 9 | pub mod parsers; 10 | -------------------------------------------------------------------------------- /src/helpers/parsers.rs: -------------------------------------------------------------------------------- 1 | use core::iter::Peekable; 2 | 3 | use bitcoin::blockdata::opcodes::all::{OP_ENDIF, OP_IF}; 4 | use bitcoin::blockdata::script::{Instruction, Instructions}; 5 | use bitcoin::consensus::Decodable; 6 | use bitcoin::hashes::{sha256d, Hash}; 7 | use bitcoin::opcodes::OP_FALSE; 8 | use bitcoin::secp256k1::{ecdsa, Message, Secp256k1}; 9 | use bitcoin::{secp256k1, Script, Transaction}; 10 | use serde::{Deserialize, Serialize}; 11 | 12 | use super::{BODY_TAG, PUBLICKEY_TAG, RANDOM_TAG, ROLLUP_NAME_TAG, SIGNATURE_TAG}; 13 | 14 | #[derive(Debug, Clone, Serialize, Deserialize)] 15 | pub struct ParsedInscription { 16 | pub body: Vec, 17 | pub signature: Vec, 18 | pub public_key: Vec, 19 | } 20 | 21 | impl ParsedInscription { 22 | /// Verifies the signature of the inscription and returns the hash of the body 23 | pub fn get_sig_verified_hash(&self) -> Option<[u8; 32]> { 24 | let public_key = secp256k1::PublicKey::from_slice(&self.public_key); 25 | let signature = ecdsa::Signature::from_compact(&self.signature); 26 | let hash = sha256d::Hash::hash(&self.body).to_byte_array(); 27 | let message = Message::from_slice(&hash).unwrap(); // cannot fail 28 | 29 | let secp = Secp256k1::new(); 30 | 31 | if public_key.is_ok() 32 | && signature.is_ok() 33 | && secp 34 | .verify_ecdsa(&message, &signature.unwrap(), &public_key.unwrap()) 35 | .is_ok() 36 | { 37 | Some(hash) 38 | } else { 39 | None 40 | } 41 | } 42 | } 43 | 44 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 45 | pub enum ParserError { 46 | InvalidRollupName, 47 | EnvelopeHasNonPushOp, 48 | EnvelopeHasIncorrectFormat, 49 | NonTapscriptWitness, 50 | IncorrectSignature, 51 | } 52 | 53 | pub fn parse_transaction( 54 | tx: &Transaction, 55 | rollup_name: &str, 56 | ) -> Result { 57 | let script = get_script(tx)?; 58 | let mut instructions = script.instructions().peekable(); 59 | parse_relevant_inscriptions(&mut instructions, rollup_name) 60 | } 61 | 62 | // Returns the script from the first input of the transaction 63 | fn get_script(tx: &Transaction) -> Result<&Script, ParserError> { 64 | tx.input[0] 65 | .witness 66 | .tapscript() 67 | .ok_or(ParserError::NonTapscriptWitness) 68 | } 69 | 70 | // TODO: discuss removing tags 71 | // Parses the inscription from script if it is relevant to the rollup 72 | fn parse_relevant_inscriptions( 73 | instructions: &mut Peekable, 74 | rollup_name: &str, 75 | ) -> Result { 76 | let mut last_op = None; 77 | let mut inside_envelope = false; 78 | let mut inside_envelope_index = 0; 79 | 80 | let mut body: Vec = Vec::new(); 81 | let mut signature: Vec = Vec::new(); 82 | let mut public_key: Vec = Vec::new(); 83 | 84 | // this while loop is optimized for the least amount of iterations 85 | // for a strict envelope structure 86 | // nothing other than data pushes should be inside the envelope 87 | // the loop will break after the first envelope is parsed 88 | while let Some(Ok(instruction)) = instructions.next() { 89 | match instruction { 90 | Instruction::Op(OP_IF) => { 91 | if last_op == Some(OP_FALSE) { 92 | inside_envelope = true; 93 | } else if inside_envelope { 94 | return Err(ParserError::EnvelopeHasNonPushOp); 95 | } 96 | } 97 | Instruction::Op(OP_ENDIF) => { 98 | if inside_envelope { 99 | break; // we are done parsing 100 | } 101 | } 102 | Instruction::Op(another_op) => { 103 | // don't allow anything except data pushes inside envelope 104 | if inside_envelope { 105 | return Err(ParserError::EnvelopeHasNonPushOp); 106 | } 107 | 108 | last_op = Some(another_op); 109 | } 110 | Instruction::PushBytes(bytes) => { 111 | if inside_envelope { 112 | // this looks ugly but we need to have least amount of 113 | // iterations possible in a malicous case 114 | // so if any of the conditions does not hold 115 | // we return an error 116 | if (inside_envelope_index == 0 && bytes.as_bytes() != ROLLUP_NAME_TAG) 117 | || (inside_envelope_index == 2 && bytes.as_bytes() != SIGNATURE_TAG) 118 | || (inside_envelope_index == 4 && bytes.as_bytes() != PUBLICKEY_TAG) 119 | || (inside_envelope_index == 6 && bytes.as_bytes() != RANDOM_TAG) 120 | || (inside_envelope_index == 8 && bytes.as_bytes() != BODY_TAG) 121 | { 122 | return Err(ParserError::EnvelopeHasIncorrectFormat); 123 | } else if inside_envelope_index == 1 124 | && bytes.as_bytes() != rollup_name.as_bytes() 125 | { 126 | return Err(ParserError::InvalidRollupName); 127 | } else if inside_envelope_index == 3 { 128 | signature.extend(bytes.as_bytes()); 129 | } else if inside_envelope_index == 5 { 130 | public_key.extend(bytes.as_bytes()); 131 | } else if inside_envelope_index >= 9 { 132 | body.extend(bytes.as_bytes()); 133 | } 134 | 135 | inside_envelope_index += 1; 136 | } else if bytes.is_empty() { 137 | last_op = Some(OP_FALSE); // rust bitcoin pushes [] instead of op_false 138 | } 139 | } 140 | } 141 | } 142 | 143 | if body.is_empty() || signature.is_empty() || public_key.is_empty() { 144 | return Err(ParserError::EnvelopeHasIncorrectFormat); 145 | } 146 | 147 | Ok(ParsedInscription { 148 | body, 149 | signature, 150 | public_key, 151 | }) 152 | } 153 | 154 | pub fn parse_hex_transaction( 155 | tx_hex: &str, 156 | ) -> Result { 157 | if let Ok(reader) = hex::decode(tx_hex) { 158 | Transaction::consensus_decode(&mut &reader[..]) 159 | } else { 160 | Err(bitcoin::consensus::encode::Error::ParseFailed( 161 | "Could not decode hex", 162 | )) 163 | } 164 | } 165 | #[cfg(test)] 166 | mod tests { 167 | use bitcoin::key::XOnlyPublicKey; 168 | use bitcoin::opcodes::all::{OP_CHECKSIG, OP_ENDIF, OP_IF}; 169 | use bitcoin::opcodes::{OP_FALSE, OP_TRUE}; 170 | use bitcoin::script::{self, PushBytesBuf}; 171 | use bitcoin::Transaction; 172 | 173 | use super::{ 174 | parse_relevant_inscriptions, BODY_TAG, PUBLICKEY_TAG, RANDOM_TAG, ROLLUP_NAME_TAG, 175 | SIGNATURE_TAG, 176 | }; 177 | use crate::helpers::parsers::{parse_transaction, ParserError}; 178 | 179 | #[test] 180 | fn correct() { 181 | let reveal_script_builder = script::Builder::new() 182 | .push_x_only_key(&XOnlyPublicKey::from_slice(&[1; 32]).unwrap()) 183 | .push_opcode(OP_CHECKSIG) 184 | .push_opcode(OP_FALSE) 185 | .push_opcode(OP_IF) 186 | .push_slice(PushBytesBuf::try_from(ROLLUP_NAME_TAG.to_vec()).unwrap()) 187 | .push_slice(PushBytesBuf::try_from("sov-btc".as_bytes().to_vec()).unwrap()) 188 | .push_slice(PushBytesBuf::try_from(SIGNATURE_TAG.to_vec()).unwrap()) 189 | .push_slice(PushBytesBuf::try_from(vec![0u8; 64]).unwrap()) 190 | .push_slice(PushBytesBuf::try_from(PUBLICKEY_TAG.to_vec()).unwrap()) 191 | .push_slice(PushBytesBuf::try_from(vec![0u8; 64]).unwrap()) 192 | .push_slice(PushBytesBuf::try_from(RANDOM_TAG.to_vec()).unwrap()) 193 | .push_int(0) 194 | .push_slice(PushBytesBuf::try_from(BODY_TAG.to_vec()).unwrap()) 195 | .push_slice(PushBytesBuf::try_from(vec![0u8; 128]).unwrap()) 196 | .push_opcode(OP_ENDIF); 197 | 198 | let reveal_script = reveal_script_builder.into_script(); 199 | 200 | let result = 201 | parse_relevant_inscriptions(&mut reveal_script.instructions().peekable(), "sov-btc"); 202 | 203 | assert!(result.is_ok()); 204 | 205 | let result = result.unwrap(); 206 | 207 | assert_eq!(result.body, vec![0u8; 128]); 208 | assert_eq!(result.signature, vec![0u8; 64]); 209 | assert_eq!(result.public_key, vec![0u8; 64]); 210 | } 211 | 212 | #[test] 213 | fn wrong_rollup_tag() { 214 | let reveal_script_builder = script::Builder::new() 215 | .push_x_only_key(&XOnlyPublicKey::from_slice(&[1; 32]).unwrap()) 216 | .push_opcode(OP_CHECKSIG) 217 | .push_opcode(OP_FALSE) 218 | .push_opcode(OP_IF) 219 | .push_slice(PushBytesBuf::try_from(ROLLUP_NAME_TAG.to_vec()).unwrap()) 220 | .push_slice(PushBytesBuf::try_from("not-sov-btc".as_bytes().to_vec()).unwrap()) 221 | .push_slice(PushBytesBuf::try_from(SIGNATURE_TAG.to_vec()).unwrap()) 222 | .push_slice(PushBytesBuf::try_from(vec![0u8; 64]).unwrap()) 223 | .push_slice(PushBytesBuf::try_from(PUBLICKEY_TAG.to_vec()).unwrap()) 224 | .push_slice(PushBytesBuf::try_from(vec![0u8; 64]).unwrap()) 225 | .push_slice(PushBytesBuf::try_from(RANDOM_TAG.to_vec()).unwrap()) 226 | .push_int(0) 227 | .push_slice(PushBytesBuf::try_from(BODY_TAG.to_vec()).unwrap()) 228 | .push_slice(PushBytesBuf::try_from(vec![0u8; 128]).unwrap()) 229 | .push_opcode(OP_ENDIF); 230 | 231 | let reveal_script = reveal_script_builder.into_script(); 232 | 233 | let result = 234 | parse_relevant_inscriptions(&mut reveal_script.instructions().peekable(), "sov-btc"); 235 | 236 | assert!(result.is_err()); 237 | assert_eq!(result.unwrap_err(), ParserError::InvalidRollupName); 238 | } 239 | 240 | #[test] 241 | fn leave_out_tags() { 242 | // name 243 | let reveal_script_builder = script::Builder::new() 244 | .push_x_only_key(&XOnlyPublicKey::from_slice(&[1; 32]).unwrap()) 245 | .push_opcode(OP_CHECKSIG) 246 | .push_opcode(OP_FALSE) 247 | .push_opcode(OP_IF) 248 | .push_slice(PushBytesBuf::try_from(SIGNATURE_TAG.to_vec()).unwrap()) 249 | .push_slice(PushBytesBuf::try_from(vec![0u8; 64]).unwrap()) 250 | .push_slice(PushBytesBuf::try_from(PUBLICKEY_TAG.to_vec()).unwrap()) 251 | .push_slice(PushBytesBuf::try_from(vec![0u8; 64]).unwrap()) 252 | .push_slice(PushBytesBuf::try_from(RANDOM_TAG.to_vec()).unwrap()) 253 | .push_int(0) 254 | .push_slice(PushBytesBuf::try_from(BODY_TAG.to_vec()).unwrap()) 255 | .push_slice(PushBytesBuf::try_from(vec![0u8; 128]).unwrap()) 256 | .push_opcode(OP_ENDIF); 257 | 258 | let reveal_script = reveal_script_builder.into_script(); 259 | 260 | let result = 261 | parse_relevant_inscriptions(&mut reveal_script.instructions().peekable(), "sov-btc"); 262 | 263 | assert!(result.is_err(), "Failed to error on no name tag."); 264 | assert_eq!(result.unwrap_err(), ParserError::EnvelopeHasIncorrectFormat); 265 | 266 | // signature 267 | let reveal_script_builder = script::Builder::new() 268 | .push_x_only_key(&XOnlyPublicKey::from_slice(&[1; 32]).unwrap()) 269 | .push_opcode(OP_CHECKSIG) 270 | .push_opcode(OP_FALSE) 271 | .push_opcode(OP_IF) 272 | .push_slice(PushBytesBuf::try_from(ROLLUP_NAME_TAG.to_vec()).unwrap()) 273 | .push_slice(PushBytesBuf::try_from("sov-btc".as_bytes().to_vec()).unwrap()) 274 | .push_slice(PushBytesBuf::try_from(PUBLICKEY_TAG.to_vec()).unwrap()) 275 | .push_slice(PushBytesBuf::try_from(vec![0u8; 64]).unwrap()) 276 | .push_slice(PushBytesBuf::try_from(RANDOM_TAG.to_vec()).unwrap()) 277 | .push_int(0) 278 | .push_slice(PushBytesBuf::try_from(BODY_TAG.to_vec()).unwrap()) 279 | .push_slice(PushBytesBuf::try_from(vec![0u8; 128]).unwrap()) 280 | .push_opcode(OP_ENDIF); 281 | 282 | let reveal_script = reveal_script_builder.into_script(); 283 | 284 | let result = 285 | parse_relevant_inscriptions(&mut reveal_script.instructions().peekable(), "sov-btc"); 286 | 287 | assert!(result.is_err(), "Failed to error on no signature tag."); 288 | assert_eq!(result.unwrap_err(), ParserError::EnvelopeHasIncorrectFormat); 289 | 290 | // publickey 291 | let reveal_script_builder = script::Builder::new() 292 | .push_x_only_key(&XOnlyPublicKey::from_slice(&[1; 32]).unwrap()) 293 | .push_opcode(OP_CHECKSIG) 294 | .push_opcode(OP_FALSE) 295 | .push_opcode(OP_IF) 296 | .push_slice(PushBytesBuf::try_from(ROLLUP_NAME_TAG.to_vec()).unwrap()) 297 | .push_slice(PushBytesBuf::try_from("sov-btc".as_bytes().to_vec()).unwrap()) 298 | .push_slice(PushBytesBuf::try_from(SIGNATURE_TAG.to_vec()).unwrap()) 299 | .push_slice(PushBytesBuf::try_from(vec![0u8; 64]).unwrap()) 300 | .push_slice(PushBytesBuf::try_from(RANDOM_TAG.to_vec()).unwrap()) 301 | .push_int(0) 302 | .push_slice(PushBytesBuf::try_from(BODY_TAG.to_vec()).unwrap()) 303 | .push_slice(PushBytesBuf::try_from(vec![0u8; 128]).unwrap()) 304 | .push_opcode(OP_ENDIF); 305 | 306 | let reveal_script = reveal_script_builder.into_script(); 307 | 308 | let result = 309 | parse_relevant_inscriptions(&mut reveal_script.instructions().peekable(), "sov-btc"); 310 | 311 | assert!(result.is_err(), "Failed to error on no publickey tag."); 312 | assert_eq!(result.unwrap_err(), ParserError::EnvelopeHasIncorrectFormat); 313 | 314 | // body 315 | let reveal_script_builder = script::Builder::new() 316 | .push_x_only_key(&XOnlyPublicKey::from_slice(&[1; 32]).unwrap()) 317 | .push_opcode(OP_CHECKSIG) 318 | .push_opcode(OP_FALSE) 319 | .push_opcode(OP_IF) 320 | .push_slice(PushBytesBuf::try_from(ROLLUP_NAME_TAG.to_vec()).unwrap()) 321 | .push_slice(PushBytesBuf::try_from("sov-btc".as_bytes().to_vec()).unwrap()) 322 | .push_slice(PushBytesBuf::try_from(SIGNATURE_TAG.to_vec()).unwrap()) 323 | .push_slice(PushBytesBuf::try_from(vec![0u8; 64]).unwrap()) 324 | .push_slice(PushBytesBuf::try_from(PUBLICKEY_TAG.to_vec()).unwrap()) 325 | .push_slice(PushBytesBuf::try_from(vec![0u8; 64]).unwrap()) 326 | .push_slice(PushBytesBuf::try_from(RANDOM_TAG.to_vec()).unwrap()) 327 | .push_int(0) 328 | .push_opcode(OP_ENDIF); 329 | 330 | let reveal_script = reveal_script_builder.into_script(); 331 | 332 | let result = 333 | parse_relevant_inscriptions(&mut reveal_script.instructions().peekable(), "sov-btc"); 334 | 335 | assert!(result.is_err(), "Failed to error on no body tag."); 336 | 337 | // random 338 | let reveal_script_builder = script::Builder::new() 339 | .push_x_only_key(&XOnlyPublicKey::from_slice(&[1; 32]).unwrap()) 340 | .push_opcode(OP_CHECKSIG) 341 | .push_opcode(OP_FALSE) 342 | .push_opcode(OP_IF) 343 | .push_slice(PushBytesBuf::try_from(ROLLUP_NAME_TAG.to_vec()).unwrap()) 344 | .push_slice(PushBytesBuf::try_from("sov-btc".as_bytes().to_vec()).unwrap()) 345 | .push_slice(PushBytesBuf::try_from(SIGNATURE_TAG.to_vec()).unwrap()) 346 | .push_slice(PushBytesBuf::try_from(vec![0u8; 64]).unwrap()) 347 | .push_slice(PushBytesBuf::try_from(PUBLICKEY_TAG.to_vec()).unwrap()) 348 | .push_slice(PushBytesBuf::try_from(vec![0u8; 64]).unwrap()) 349 | .push_slice(PushBytesBuf::try_from(BODY_TAG.to_vec()).unwrap()) 350 | .push_slice(PushBytesBuf::try_from(vec![0u8; 128]).unwrap()) 351 | .push_opcode(OP_ENDIF); 352 | 353 | let reveal_script = reveal_script_builder.into_script(); 354 | 355 | let result = 356 | parse_relevant_inscriptions(&mut reveal_script.instructions().peekable(), "sov-btc"); 357 | 358 | assert!(result.is_err(), "Failed to error on no random tag."); 359 | assert_eq!(result.unwrap_err(), ParserError::EnvelopeHasIncorrectFormat); 360 | } 361 | 362 | #[test] 363 | fn non_parseable_tx() { 364 | let hex_tx = "020000000001013a66019bfcc719ba12586a83ebbb0b3debdc945f563cd64fd44c8044e3d3a1790100000000fdffffff028fa2aa060000000017a9147ba15d4e0d8334de3a68cf3687594e2d1ee5b00d879179e0090000000016001493c93ad222e57d65438545e048822ede2d418a3d0247304402202432e6c422b93705fbc57b350ea43e4ef9441c0907988eff051eaac807fc8cf2022046c92b540b5f04f8da11febb5d2a478aed1b8bc088e769da8b78fffcae8c9a9a012103e2991b47d9c788f55379f9ef519b642d79d7dfe0e7555ec5575ee934b2dca1223f5d0c00"; 365 | 366 | let tx: Transaction = 367 | bitcoin::consensus::deserialize(&hex::decode(hex_tx).unwrap()).unwrap(); 368 | 369 | let result = parse_transaction(&tx, "sov-btc"); 370 | 371 | assert!(result.is_err(), "Failed to error on non-parseable tx."); 372 | assert_eq!(result.unwrap_err(), ParserError::EnvelopeHasIncorrectFormat); 373 | } 374 | 375 | #[test] 376 | fn only_checksig() { 377 | let reveal_script = script::Builder::new() 378 | .push_x_only_key(&XOnlyPublicKey::from_slice(&[1; 32]).unwrap()) 379 | .push_opcode(OP_CHECKSIG) 380 | .into_script(); 381 | 382 | let result = 383 | parse_relevant_inscriptions(&mut reveal_script.instructions().peekable(), "sov-btc"); 384 | 385 | assert!(result.is_err()); 386 | assert_eq!(result.unwrap_err(), ParserError::EnvelopeHasIncorrectFormat); 387 | } 388 | 389 | #[test] 390 | fn complex_envelope() { 391 | let reveal_script = script::Builder::new() 392 | .push_x_only_key(&XOnlyPublicKey::from_slice(&[1; 32]).unwrap()) 393 | .push_opcode(OP_CHECKSIG) 394 | .push_opcode(OP_FALSE) 395 | .push_opcode(OP_IF) 396 | .push_slice(PushBytesBuf::try_from(ROLLUP_NAME_TAG.to_vec()).unwrap()) 397 | .push_slice(PushBytesBuf::try_from("sov-btc".as_bytes().to_vec()).unwrap()) 398 | .push_slice(PushBytesBuf::try_from(SIGNATURE_TAG.to_vec()).unwrap()) 399 | .push_slice(PushBytesBuf::try_from(vec![0u8; 64]).unwrap()) 400 | .push_opcode(OP_TRUE) 401 | .push_opcode(OP_IF) 402 | .push_x_only_key(&XOnlyPublicKey::from_slice(&[1; 32]).unwrap()) 403 | .push_opcode(OP_CHECKSIG) 404 | .push_opcode(OP_ENDIF) 405 | .push_slice(PushBytesBuf::try_from(PUBLICKEY_TAG.to_vec()).unwrap()) 406 | .push_slice(PushBytesBuf::try_from(vec![0u8; 64]).unwrap()) 407 | .push_slice(PushBytesBuf::try_from(RANDOM_TAG.to_vec()).unwrap()) 408 | .push_int(0) 409 | .push_slice(PushBytesBuf::try_from(BODY_TAG.to_vec()).unwrap()) 410 | .push_slice(PushBytesBuf::try_from(vec![0u8; 128]).unwrap()) 411 | .push_opcode(OP_ENDIF) 412 | .into_script(); 413 | 414 | let result = 415 | parse_relevant_inscriptions(&mut reveal_script.instructions().peekable(), "sov-btc"); 416 | 417 | assert!(result.is_err()); 418 | assert_eq!(result.unwrap_err(), ParserError::EnvelopeHasNonPushOp); 419 | } 420 | 421 | #[test] 422 | fn two_envelopes() { 423 | let reveal_script = script::Builder::new() 424 | .push_opcode(OP_FALSE) 425 | .push_opcode(OP_IF) 426 | .push_slice(PushBytesBuf::try_from(ROLLUP_NAME_TAG.to_vec()).unwrap()) 427 | .push_slice(PushBytesBuf::try_from("sov-btc".as_bytes().to_vec()).unwrap()) 428 | .push_slice(PushBytesBuf::try_from(SIGNATURE_TAG.to_vec()).unwrap()) 429 | .push_slice(PushBytesBuf::try_from(vec![0u8; 64]).unwrap()) 430 | .push_slice(PushBytesBuf::try_from(PUBLICKEY_TAG.to_vec()).unwrap()) 431 | .push_slice(PushBytesBuf::try_from(vec![0u8; 64]).unwrap()) 432 | .push_slice(PushBytesBuf::try_from(RANDOM_TAG.to_vec()).unwrap()) 433 | .push_int(0) 434 | .push_slice(PushBytesBuf::try_from(BODY_TAG.to_vec()).unwrap()) 435 | .push_slice(PushBytesBuf::try_from(vec![0u8; 128]).unwrap()) 436 | .push_opcode(OP_ENDIF) 437 | .push_x_only_key(&XOnlyPublicKey::from_slice(&[1; 32]).unwrap()) 438 | .push_opcode(OP_CHECKSIG) 439 | .push_opcode(OP_FALSE) 440 | .push_opcode(OP_IF) 441 | .push_slice(PushBytesBuf::try_from(ROLLUP_NAME_TAG.to_vec()).unwrap()) 442 | .push_slice(PushBytesBuf::try_from("sov-btc".as_bytes().to_vec()).unwrap()) 443 | .push_slice(PushBytesBuf::try_from(SIGNATURE_TAG.to_vec()).unwrap()) 444 | .push_slice(PushBytesBuf::try_from(vec![1u8; 64]).unwrap()) 445 | .push_slice(PushBytesBuf::try_from(PUBLICKEY_TAG.to_vec()).unwrap()) 446 | .push_slice(PushBytesBuf::try_from(vec![1u8; 64]).unwrap()) 447 | .push_slice(PushBytesBuf::try_from(RANDOM_TAG.to_vec()).unwrap()) 448 | .push_int(1) 449 | .push_slice(PushBytesBuf::try_from(BODY_TAG.to_vec()).unwrap()) 450 | .push_slice(PushBytesBuf::try_from(vec![1u8; 128]).unwrap()) 451 | .push_opcode(OP_ENDIF) 452 | .into_script(); 453 | 454 | let result = 455 | parse_relevant_inscriptions(&mut reveal_script.instructions().peekable(), "sov-btc"); 456 | 457 | assert!(result.is_ok()); 458 | 459 | let result = result.unwrap(); 460 | 461 | assert_eq!(result.body, vec![0u8; 128]); 462 | assert_eq!(result.signature, vec![0u8; 64]); 463 | assert_eq!(result.public_key, vec![0u8; 64]); 464 | } 465 | 466 | #[test] 467 | fn big_push() { 468 | let reveal_script = script::Builder::new() 469 | .push_opcode(OP_FALSE) 470 | .push_opcode(OP_IF) 471 | .push_slice(PushBytesBuf::try_from(ROLLUP_NAME_TAG.to_vec()).unwrap()) 472 | .push_slice(PushBytesBuf::try_from("sov-btc".as_bytes().to_vec()).unwrap()) 473 | .push_slice(PushBytesBuf::try_from(SIGNATURE_TAG.to_vec()).unwrap()) 474 | .push_slice(PushBytesBuf::try_from(vec![0u8; 64]).unwrap()) 475 | .push_slice(PushBytesBuf::try_from(PUBLICKEY_TAG.to_vec()).unwrap()) 476 | .push_slice(PushBytesBuf::try_from(vec![0u8; 64]).unwrap()) 477 | .push_slice(PushBytesBuf::try_from(RANDOM_TAG.to_vec()).unwrap()) 478 | .push_int(0) 479 | .push_slice(PushBytesBuf::try_from(BODY_TAG.to_vec()).unwrap()) 480 | .push_slice(PushBytesBuf::try_from(vec![1u8; 512]).unwrap()) 481 | .push_slice(PushBytesBuf::try_from(vec![1u8; 512]).unwrap()) 482 | .push_slice(PushBytesBuf::try_from(vec![1u8; 512]).unwrap()) 483 | .push_slice(PushBytesBuf::try_from(vec![1u8; 512]).unwrap()) 484 | .push_slice(PushBytesBuf::try_from(vec![1u8; 512]).unwrap()) 485 | .push_slice(PushBytesBuf::try_from(vec![1u8; 512]).unwrap()) 486 | .push_opcode(OP_ENDIF) 487 | .push_x_only_key(&XOnlyPublicKey::from_slice(&[1; 32]).unwrap()) 488 | .push_opcode(OP_CHECKSIG) 489 | .into_script(); 490 | 491 | let result = 492 | parse_relevant_inscriptions(&mut reveal_script.instructions().peekable(), "sov-btc"); 493 | 494 | assert!(result.is_ok()); 495 | 496 | let result = result.unwrap(); 497 | 498 | assert_eq!(result.body, vec![1u8; 512 * 6]); 499 | assert_eq!(result.signature, vec![0u8; 64]); 500 | assert_eq!(result.public_key, vec![0u8; 64]); 501 | } 502 | } 503 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod helpers; 2 | #[cfg(feature = "native")] 3 | mod rpc; 4 | pub mod spec; 5 | 6 | #[cfg(feature = "native")] 7 | pub mod service; 8 | pub mod verifier; 9 | 10 | const REVEAL_OUTPUT_AMOUNT: u64 = 546; 11 | -------------------------------------------------------------------------------- /src/rpc.rs: -------------------------------------------------------------------------------- 1 | use core::fmt::Display; 2 | use core::str::FromStr; 3 | 4 | use anyhow::anyhow; 5 | use async_recursion::async_recursion; 6 | use bitcoin::block::{Header, Version}; 7 | use bitcoin::hash_types::TxMerkleNode; 8 | use bitcoin::{Address, BlockHash, CompactTarget, Network}; 9 | use reqwest::header::HeaderMap; 10 | use serde::{Deserialize, Serialize}; 11 | use serde_json::value::RawValue; 12 | use serde_json::{json, to_value}; 13 | 14 | use crate::helpers::parsers::parse_hex_transaction; 15 | use crate::spec::block::BitcoinBlock; 16 | use crate::spec::header::HeaderWrapper; 17 | use crate::spec::transaction::Transaction; 18 | use crate::spec::utxo::UTXO; 19 | 20 | // RPCError is a struct that represents an error returned by the Bitcoin RPC 21 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 22 | pub struct RPCError { 23 | pub code: i32, 24 | pub message: String, 25 | } 26 | impl Display for RPCError { 27 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 28 | write!(f, "RPCError {}: {}", self.code, self.message) 29 | } 30 | } 31 | 32 | // Response is a struct that represents a response returned by the Bitcoin RPC 33 | // It is generic over the type of the result field, which is usually a String in Bitcoin Core 34 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 35 | struct Response { 36 | pub result: Option, 37 | pub error: Option, 38 | pub id: String, 39 | } 40 | 41 | // BitcoinNode is a struct that represents a connection to a Bitcoin RPC node 42 | #[derive(Debug, Clone)] 43 | pub struct BitcoinNode { 44 | url: String, 45 | client: reqwest::Client, 46 | network: Network, 47 | } 48 | impl BitcoinNode { 49 | pub fn new(url: String, username: String, password: String, network: Network) -> Self { 50 | let mut headers = HeaderMap::new(); 51 | headers.insert( 52 | "Authorization", 53 | format!( 54 | "Basic {}", 55 | base64::encode(format!("{}:{}", username, password)) 56 | ) 57 | .parse() 58 | .expect("Failed to parse auth header!"), 59 | ); 60 | headers.insert( 61 | "Content-Type", 62 | "application/json" 63 | .parse() 64 | .expect("Failed to parse content type header!"), 65 | ); 66 | let client = reqwest::Client::builder() 67 | .default_headers(headers) 68 | .build() 69 | .expect("Failed to build client!"); 70 | 71 | Self { 72 | url, 73 | client, 74 | network, 75 | } 76 | } 77 | 78 | // TODO: add max retries 79 | #[async_recursion] 80 | async fn call( 81 | &self, 82 | method: &str, 83 | params: Vec, 84 | ) -> Result { 85 | let response = self 86 | .client 87 | .post(&self.url) 88 | .json(&json!({ 89 | "jsonrpc": "1.0", 90 | "id": method, 91 | "method": method, 92 | "params": params 93 | })) 94 | .send() 95 | .await; 96 | 97 | // sometimes requests to bitcoind are dropped without a reason 98 | // so impl. recursive retry 99 | // TODO: add max retries 100 | if let Err(error) = response { 101 | // TODO: maybe remove is_request() check? 102 | if error.is_connect() || error.is_timeout() || error.is_request() { 103 | tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; 104 | return self.call(method, params).await; 105 | } 106 | return Err(anyhow!(error)); 107 | } 108 | 109 | let response = response.unwrap().json::>().await?; 110 | 111 | if let Some(error) = response.error { 112 | return Err(anyhow!(error)); 113 | } 114 | 115 | Ok(response.result.unwrap()) 116 | } 117 | 118 | // get_block_count returns the current block height 119 | pub async fn get_block_count(&self) -> Result { 120 | self.call::("getblockcount", vec![]).await 121 | } 122 | 123 | // get_block_hash returns the block hash of the block at the given height 124 | pub async fn get_block_hash(&self, height: u64) -> Result { 125 | self.call::("getblockhash", vec![to_value(height)?]) 126 | .await 127 | } 128 | 129 | // get_best_blockhash returns the best blockhash of the chain 130 | pub async fn get_best_blockhash(&self) -> Result { 131 | self.call::("getbestblockhash", vec![]).await 132 | } 133 | 134 | // get_block_header returns a particular block header with a given hash 135 | pub async fn get_block_header(&self, hash: String) -> Result { 136 | let result = self 137 | .call::>("getblockheader", vec![to_value(hash.clone())?]) 138 | .await? 139 | .to_string(); 140 | 141 | let full_header: serde_json::Value = serde_json::from_str(&result)?; 142 | 143 | let header: Header = Header { 144 | bits: CompactTarget::from_consensus(u32::from_str_radix( 145 | full_header["bits"].as_str().unwrap(), 146 | 16, 147 | )?), 148 | merkle_root: TxMerkleNode::from_str(full_header["merkleroot"].as_str().unwrap())?, 149 | nonce: full_header["nonce"].as_u64().unwrap() as u32, 150 | prev_blockhash: BlockHash::from_str( 151 | full_header["previousblockhash"].as_str().unwrap(), 152 | )?, 153 | time: full_header["time"].as_u64().unwrap() as u32, 154 | version: Version::from_consensus(full_header["version"].as_u64().unwrap() as i32), 155 | }; 156 | 157 | let header_wrapper: HeaderWrapper = HeaderWrapper::new( 158 | header, 159 | full_header["nTx"].as_u64().unwrap() as u32, 160 | full_header["height"].as_u64().unwrap(), 161 | ); 162 | 163 | Ok(header_wrapper) 164 | } 165 | 166 | // get_block returns the block at the given hash 167 | pub async fn get_block(&self, hash: String) -> Result { 168 | let result = self 169 | .call::>("getblock", vec![to_value(hash.clone())?, to_value(3)?]) 170 | .await? 171 | .to_string(); 172 | 173 | let full_block: serde_json::Value = serde_json::from_str(&result)?; 174 | 175 | let header: Header = Header { 176 | bits: CompactTarget::from_consensus(u32::from_str_radix( 177 | full_block["bits"].as_str().unwrap(), 178 | 16, 179 | )?), 180 | merkle_root: TxMerkleNode::from_str(full_block["merkleroot"].as_str().unwrap())?, 181 | nonce: full_block["nonce"].as_u64().unwrap() as u32, 182 | prev_blockhash: BlockHash::from_str(full_block["previousblockhash"].as_str().unwrap())?, 183 | time: full_block["time"].as_u64().unwrap() as u32, 184 | version: Version::from_consensus(full_block["version"].as_u64().unwrap() as i32), 185 | }; 186 | 187 | let txdata = full_block["tx"].as_array().unwrap(); 188 | 189 | let txs: Vec = txdata 190 | .iter() 191 | .map(|tx| { 192 | let tx_hex = tx["hex"].as_str().unwrap(); 193 | 194 | parse_hex_transaction(tx_hex).unwrap() // hex from rpc cannot be invalid 195 | }) 196 | .collect(); 197 | 198 | let height = full_block["height"].as_u64().unwrap(); 199 | 200 | Ok(BitcoinBlock { 201 | header: HeaderWrapper::new(header, txs.len() as u32, height), 202 | txdata: txs, 203 | }) 204 | } 205 | 206 | // get_utxos returns all unspent transaction outputs for the wallets of bitcoind 207 | pub async fn get_utxos(&self) -> Result, anyhow::Error> { 208 | let utxos = self 209 | .call::>("listunspent", vec![to_value(0)?, to_value(9999999)?]) 210 | .await?; 211 | 212 | if utxos.is_empty() { 213 | return Err(anyhow!("No UTXOs found")); 214 | } 215 | 216 | Ok(utxos) 217 | } 218 | 219 | // get_change_address returns a change address for the wallet of bitcoind 220 | async fn get_change_address(&self) -> Result { 221 | let address_string = self.call::("getrawchangeaddress", vec![]).await?; 222 | Ok(Address::from_str(&address_string)?.require_network(self.network)?) 223 | } 224 | 225 | pub async fn get_change_addresses(&self) -> Result<[Address; 2], anyhow::Error> { 226 | let change_address = self.get_change_address().await?; 227 | let change_address_2 = self.get_change_address().await?; 228 | 229 | Ok([change_address, change_address_2]) 230 | } 231 | 232 | // estimate_smart_fee estimates the fee to confirm a transaction in the next block 233 | pub async fn estimate_smart_fee(&self) -> Result { 234 | let result = self 235 | .call::>("estimatesmartfee", vec![to_value(1)?]) 236 | .await? 237 | .to_string(); 238 | 239 | let result_map: serde_json::Value = serde_json::from_str(&result)?; 240 | 241 | // Issue: https://github.com/chainwayxyz/bitcoin-da/issues/3 242 | let btc_vkb = result_map 243 | .get("feerate") 244 | .unwrap_or(&serde_json::Value::from_str("0.00001").unwrap()) 245 | .as_f64() 246 | .unwrap(); 247 | 248 | // convert to sat/vB and round up 249 | Ok((btc_vkb * 100_000_000.0 / 1000.0).ceil()) 250 | } 251 | 252 | // sign_raw_transaction_with_wallet signs a raw transaction with the wallet of bitcoind 253 | pub async fn sign_raw_transaction_with_wallet( 254 | &self, 255 | tx: String, 256 | ) -> Result { 257 | let result = self 258 | .call::>("signrawtransactionwithwallet", vec![to_value(tx)?]) 259 | .await? 260 | .to_string(); 261 | 262 | let signed_tx: serde_json::Value = serde_json::from_str(&result)?; 263 | 264 | Ok(signed_tx["hex"].as_str().unwrap().to_string()) 265 | } 266 | 267 | // send_raw_transaction sends a raw transaction to the network 268 | pub async fn send_raw_transaction(&self, tx: String) -> Result { 269 | self.call::("sendrawtransaction", vec![to_value(tx)?]) 270 | .await 271 | } 272 | 273 | pub async fn list_wallets(&self) -> Result, anyhow::Error> { 274 | self.call::>("listwallets", vec![]).await 275 | } 276 | 277 | #[cfg(test)] 278 | pub async fn generate_to_address( 279 | &self, 280 | address: Address, 281 | blocks: u32, 282 | ) -> Result, anyhow::Error> { 283 | if self.network == Network::Regtest { 284 | self.call::>( 285 | "generatetoaddress", 286 | vec![to_value(blocks)?, to_value(address.to_string())?], 287 | ) 288 | .await 289 | } else { 290 | Err(anyhow!("Cannot generate blocks on non-regtest network")) 291 | } 292 | } 293 | } 294 | 295 | #[cfg(test)] 296 | mod tests { 297 | use crate::rpc::BitcoinNode; 298 | 299 | fn get_bitcoin_node() -> BitcoinNode { 300 | BitcoinNode::new( 301 | "http://localhost:38332".to_string(), 302 | "chainway".to_string(), 303 | "topsecret".to_string(), 304 | bitcoin::Network::Regtest, 305 | ) 306 | } 307 | 308 | #[tokio::test] 309 | async fn get_utxos() { 310 | let node = get_bitcoin_node(); 311 | 312 | let utxos = node.get_utxos().await.unwrap(); 313 | 314 | utxos.iter().for_each(|utxo| { 315 | println!("address: {}, amount: {}", utxo.address, utxo.amount); 316 | }); 317 | } 318 | 319 | #[tokio::test] 320 | async fn list_wallets() { 321 | let node = get_bitcoin_node(); 322 | 323 | let wallets = node.list_wallets().await.unwrap(); 324 | 325 | wallets.iter().for_each(|wallet| { 326 | println!("wallet: {}", wallet); 327 | }); 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /src/service.rs: -------------------------------------------------------------------------------- 1 | use core::result::Result::Ok; 2 | use core::str::FromStr; 3 | use core::time::Duration; 4 | 5 | use async_trait::async_trait; 6 | use bitcoin::address::NetworkUnchecked; 7 | use bitcoin::consensus::encode; 8 | use bitcoin::hashes::{sha256d, Hash}; 9 | use bitcoin::secp256k1::SecretKey; 10 | use bitcoin::{Address, Txid}; 11 | use hex::ToHex; 12 | use serde::{Deserialize, Serialize}; 13 | use sov_rollup_interface::da::DaSpec; 14 | use sov_rollup_interface::services::da::DaService; 15 | use tracing::info; 16 | 17 | use crate::helpers::builders::{ 18 | compress_blob, create_inscription_transactions, decompress_blob, sign_blob_with_private_key, 19 | write_reveal_tx, 20 | }; 21 | use crate::helpers::parsers::parse_transaction; 22 | use crate::rpc::{BitcoinNode, RPCError}; 23 | use crate::spec::blob::BlobWithSender; 24 | use crate::spec::block::BitcoinBlock; 25 | use crate::spec::header_stream::BitcoinHeaderStream; 26 | use crate::spec::proof::InclusionMultiProof; 27 | use crate::spec::utxo::UTXO; 28 | use crate::spec::{BitcoinSpec, RollupParams}; 29 | use crate::verifier::BitcoinVerifier; 30 | use crate::REVEAL_OUTPUT_AMOUNT; 31 | 32 | /// A service that provides data and data availability proofs for Bitcoin 33 | #[derive(Debug, Clone)] 34 | pub struct BitcoinService { 35 | client: BitcoinNode, 36 | rollup_name: String, 37 | network: bitcoin::Network, 38 | address: Address, 39 | sequencer_da_private_key: SecretKey, 40 | reveal_tx_id_prefix: Vec, 41 | } 42 | 43 | /// Runtime configuration for the DA service 44 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 45 | pub struct DaServiceConfig { 46 | /// The URL of the Bitcoin node to connect to 47 | pub node_url: String, 48 | pub node_username: String, 49 | pub node_password: String, 50 | 51 | // network of the bitcoin node 52 | pub network: String, 53 | 54 | // taproot address that holds the funds of the sequencer 55 | // will be used as the change address for the inscribe transaction 56 | pub address: String, 57 | 58 | // da private key of the sequencer 59 | pub sequencer_da_private_key: Option, 60 | 61 | // number of last paid fee rates to average if estimation fails 62 | pub fee_rates_to_avg: Option, 63 | } 64 | 65 | const FINALITY_DEPTH: u64 = 4; // blocks 66 | const POLLING_INTERVAL: u64 = 10; // seconds 67 | 68 | impl BitcoinService { 69 | // Create a new instance of the DA service from the given configuration. 70 | pub async fn new(config: DaServiceConfig, chain_params: RollupParams) -> Self { 71 | let network = 72 | bitcoin::Network::from_str(&config.network).expect("Invalid bitcoin network name"); 73 | 74 | let client = BitcoinNode::new( 75 | config.node_url, 76 | config.node_username, 77 | config.node_password, 78 | network, 79 | ); 80 | 81 | let address = Address::from_str(&config.address).expect("Invalid bitcoin address"); 82 | 83 | let private_key = 84 | SecretKey::from_str(&config.sequencer_da_private_key.unwrap_or("".to_owned())) 85 | .expect("Invalid private key"); 86 | 87 | Self::with_client( 88 | client, 89 | chain_params.rollup_name, 90 | network, 91 | address, 92 | private_key, 93 | chain_params.reveal_tx_id_prefix, 94 | ) 95 | .await 96 | } 97 | 98 | pub async fn with_client( 99 | client: BitcoinNode, 100 | rollup_name: String, 101 | network: bitcoin::Network, 102 | address: Address, 103 | sequencer_da_private_key: SecretKey, 104 | reveal_tx_id_prefix: Vec, 105 | ) -> Self { 106 | // We can't store address with the network check because it's not serializable 107 | address 108 | .clone() 109 | .require_network(network) 110 | .expect("Invalid address for network!"); 111 | 112 | let wallets = client 113 | .list_wallets() 114 | .await 115 | .expect("Failed to list loaded wallets"); 116 | 117 | if wallets.is_empty() { 118 | panic!("No loaded wallet found!"); 119 | } 120 | 121 | Self { 122 | client, 123 | rollup_name, 124 | network, 125 | address, 126 | sequencer_da_private_key, 127 | reveal_tx_id_prefix, 128 | } 129 | } 130 | 131 | pub async fn send_transaction_with_fee_rate( 132 | &self, 133 | blob: &[u8], 134 | fee_sat_per_vbyte: f64, 135 | ) -> Result<::TransactionId, anyhow::Error> { 136 | let client = self.client.clone(); 137 | 138 | let blob = blob.to_vec(); 139 | let network = self.network; 140 | let address = self 141 | .address 142 | .clone() 143 | .require_network(network) 144 | .expect("Invalid network for address"); 145 | let rollup_name = self.rollup_name.clone(); 146 | let sequencer_da_private_key = self.sequencer_da_private_key; 147 | 148 | // Compress the blob 149 | let blob = compress_blob(&blob); 150 | 151 | // get all available utxos 152 | let utxos: Vec = client.get_utxos().await?; 153 | 154 | // sign the blob for authentication of the sequencer 155 | let (signature, public_key) = sign_blob_with_private_key(&blob, &sequencer_da_private_key) 156 | .expect("Sequencer sign the blob"); 157 | 158 | // create inscribe transactions 159 | let (unsigned_commit_tx, reveal_tx) = create_inscription_transactions( 160 | &rollup_name, 161 | blob, 162 | signature, 163 | public_key, 164 | utxos, 165 | address, 166 | REVEAL_OUTPUT_AMOUNT, 167 | fee_sat_per_vbyte, 168 | fee_sat_per_vbyte, 169 | network, 170 | self.reveal_tx_id_prefix.as_slice(), 171 | )?; 172 | 173 | // sign inscribe transactions 174 | let serialized_unsigned_commit_tx = &encode::serialize(&unsigned_commit_tx); 175 | let signed_raw_commit_tx = client 176 | .sign_raw_transaction_with_wallet(serialized_unsigned_commit_tx.encode_hex()) 177 | .await?; 178 | 179 | // send inscribe transactions 180 | client.send_raw_transaction(signed_raw_commit_tx).await?; 181 | 182 | // serialize reveal tx 183 | let serialized_reveal_tx = &encode::serialize(&reveal_tx); 184 | 185 | // write reveal tx to file, it can be used to continue revealing blob if something goes wrong 186 | write_reveal_tx( 187 | serialized_reveal_tx, 188 | unsigned_commit_tx.txid().to_raw_hash().to_string(), 189 | ); 190 | 191 | // send reveal tx 192 | let reveal_tx_hash = client 193 | .send_raw_transaction(serialized_reveal_tx.encode_hex()) 194 | .await?; 195 | 196 | info!("Blob inscribe tx sent. Hash: {}", reveal_tx_hash); 197 | 198 | Ok(Txid::from_str(reveal_tx_hash.as_str()) 199 | .expect("Failed to parse txid from reveal tx hash")) 200 | } 201 | 202 | pub async fn get_fee_rate(&self) -> Result { 203 | if self.network == bitcoin::Network::Regtest { 204 | // sometimes local mempool is empty, node cannot estimate 205 | return Ok(2.0); 206 | } 207 | 208 | self.client.estimate_smart_fee().await 209 | } 210 | } 211 | 212 | #[async_trait] 213 | impl DaService for BitcoinService { 214 | type Spec = BitcoinSpec; 215 | 216 | type Verifier = BitcoinVerifier; 217 | 218 | type FilteredBlock = BitcoinBlock; 219 | 220 | type HeaderStream = BitcoinHeaderStream; 221 | 222 | type TransactionId = Txid; 223 | 224 | type Error = anyhow::Error; 225 | 226 | // Make an RPC call to the node to get the block at the given height 227 | // If no such block exists, block until one does. 228 | async fn get_block_at(&self, height: u64) -> Result { 229 | let client = self.client.clone(); 230 | info!("Getting block at height {}", height); 231 | 232 | let block_hash; 233 | loop { 234 | block_hash = match client.get_block_hash(height).await { 235 | Ok(block_hash_response) => block_hash_response, 236 | Err(error) => { 237 | match error.downcast_ref::() { 238 | Some(error) => { 239 | if error.code == -8 { 240 | info!("Block not found, waiting"); 241 | tokio::time::sleep(Duration::from_secs(POLLING_INTERVAL)).await; 242 | continue; 243 | } else { 244 | // other error, return message 245 | return Err(anyhow::anyhow!(error.message.clone())); 246 | } 247 | } 248 | None => { 249 | return Err(anyhow::anyhow!(error)); 250 | } 251 | } 252 | } 253 | }; 254 | 255 | break; 256 | } 257 | let block = client.get_block(block_hash).await?; 258 | 259 | Ok(block) 260 | } 261 | 262 | // Fetch the [`DaSpec::BlockHeader`] of the last finalized block. 263 | async fn get_last_finalized_block_header( 264 | &self, 265 | ) -> Result<::BlockHeader, Self::Error> { 266 | let block_count = self.client.get_block_count().await?; 267 | 268 | let finalized_blockhash = self 269 | .client 270 | .get_block_hash(block_count - FINALITY_DEPTH) 271 | .await?; 272 | 273 | let finalized_block_header = self.client.get_block_header(finalized_blockhash).await?; 274 | 275 | Ok(finalized_block_header) 276 | } 277 | 278 | async fn subscribe_finalized_header(&self) -> Result { 279 | todo!() 280 | } 281 | 282 | // Fetch the head block of DA. 283 | async fn get_head_block_header( 284 | &self, 285 | ) -> Result<::BlockHeader, Self::Error> { 286 | let best_blockhash = self.client.get_best_blockhash().await?; 287 | 288 | let head_block_header = self.client.get_block_header(best_blockhash).await?; 289 | 290 | Ok(head_block_header) 291 | } 292 | 293 | // Extract the blob transactions relevant to a particular rollup from a block. 294 | fn extract_relevant_blobs( 295 | &self, 296 | block: &Self::FilteredBlock, 297 | ) -> Vec<::BlobTransaction> { 298 | let mut txs = Vec::new(); 299 | 300 | info!( 301 | "Extracting relevant txs from block {:?}", 302 | block.header.block_hash() 303 | ); 304 | 305 | // iterate over all transactions in the block 306 | for tx in block.txdata.iter() { 307 | if !tx 308 | .txid() 309 | .to_byte_array() 310 | .as_slice() 311 | .starts_with(self.reveal_tx_id_prefix.as_slice()) 312 | { 313 | continue; 314 | } 315 | 316 | // check if the inscription in script is relevant to the rollup 317 | let parsed_inscription = parse_transaction(tx, &self.rollup_name); 318 | 319 | if let Ok(inscription) = parsed_inscription { 320 | if inscription.get_sig_verified_hash().is_some() { 321 | // Decompress the blob 322 | let decompressed_blob = decompress_blob(&inscription.body); 323 | 324 | let relevant_tx = BlobWithSender::new( 325 | decompressed_blob, 326 | inscription.public_key, 327 | sha256d::Hash::hash(&inscription.body).to_byte_array(), 328 | ); 329 | 330 | txs.push(relevant_tx); 331 | } 332 | } 333 | } 334 | txs 335 | } 336 | 337 | async fn get_extraction_proof( 338 | &self, 339 | block: &Self::FilteredBlock, 340 | _blobs: &[::BlobTransaction], 341 | ) -> ( 342 | ::InclusionMultiProof, 343 | ::CompletenessProof, 344 | ) { 345 | info!( 346 | "Getting extraction proof for block {:?}", 347 | block.header.block_hash() 348 | ); 349 | 350 | let mut completeness_proof = Vec::with_capacity(block.txdata.len()); 351 | 352 | let block_txs = block 353 | .txdata 354 | .iter() 355 | .map(|tx| { 356 | let tx_hash = tx.txid().to_raw_hash().to_byte_array(); 357 | 358 | // if tx_hash has two leading zeros, it is in the completeness proof 359 | if tx_hash[0..2] == [0, 0] { 360 | completeness_proof.push(tx.clone()); 361 | } 362 | 363 | tx_hash 364 | }) 365 | .collect::>(); 366 | 367 | let inclusion_proof = InclusionMultiProof { txs: block_txs }; 368 | 369 | (inclusion_proof, completeness_proof) 370 | } 371 | 372 | // Extract the list blob transactions relevant to a particular rollup from a block, along with inclusion and 373 | // completeness proofs for that set of transactions. The output of this method will be passed to the verifier. 374 | async fn extract_relevant_blobs_with_proof( 375 | &self, 376 | block: &Self::FilteredBlock, 377 | ) -> ( 378 | Vec<::BlobTransaction>, 379 | ::InclusionMultiProof, 380 | ::CompletenessProof, 381 | ) { 382 | info!( 383 | "Extracting relevant txs with proof from block {:?}", 384 | block.header.block_hash() 385 | ); 386 | 387 | let txs = self.extract_relevant_blobs(block); 388 | let (inclusion_proof, completeness_proof) = 389 | self.get_extraction_proof(block, txs.as_slice()).await; 390 | 391 | (txs, inclusion_proof, completeness_proof) 392 | } 393 | 394 | async fn send_transaction( 395 | &self, 396 | blob: &[u8], 397 | ) -> Result<::TransactionId, Self::Error> { 398 | let fee_sat_per_vbyte = self.get_fee_rate().await?; 399 | self.send_transaction_with_fee_rate(blob, fee_sat_per_vbyte) 400 | .await 401 | } 402 | 403 | async fn send_aggregated_zk_proof( 404 | &self, 405 | _aggregated_proof_data: &[u8], 406 | ) -> Result { 407 | todo!(); 408 | } 409 | 410 | async fn get_aggregated_proofs_at(&self, _height: u64) -> Result>, Self::Error> { 411 | todo!(); 412 | } 413 | } 414 | 415 | #[cfg(test)] 416 | mod tests { 417 | use core::str::FromStr; 418 | use std::collections::HashSet; 419 | 420 | use bitcoin::hashes::{sha256d, Hash}; 421 | use bitcoin::secp256k1::KeyPair; 422 | use bitcoin::{merkle_tree, Address, Txid}; 423 | use sov_rollup_interface::services::da::DaService; 424 | 425 | use super::{BitcoinService, FINALITY_DEPTH}; 426 | use crate::helpers::parsers::parse_transaction; 427 | use crate::rpc::BitcoinNode; 428 | use crate::service::DaServiceConfig; 429 | use crate::spec::RollupParams; 430 | 431 | async fn get_service() -> BitcoinService { 432 | let rpc = BitcoinNode::new( 433 | "http://localhost:38332".to_string(), 434 | "chainway".to_string(), 435 | "topsecret".to_string(), 436 | bitcoin::Network::Regtest, 437 | ); 438 | 439 | // empty regtest mempool 440 | rpc.generate_to_address( 441 | Address::from_str("bcrt1qxuds94z3pqwqea2p4f4ev4f25s6uu7y3avljrl") 442 | .unwrap() 443 | .require_network(bitcoin::Network::Regtest) 444 | .unwrap(), 445 | 1, 446 | ) 447 | .await 448 | .unwrap(); 449 | 450 | let runtime_config = DaServiceConfig { 451 | node_url: "http://localhost:38332".to_string(), 452 | node_username: "chainway".to_string(), 453 | node_password: "topsecret".to_string(), 454 | network: "regtest".to_string(), 455 | address: "bcrt1qy85zdv5se9d9ceg9nvay36t6j86z95fny4rdzu".to_string(), 456 | sequencer_da_private_key: Some( 457 | "E9873D79C6D87DC0FB6A5778633389F4453213303DA61F20BD67FC233AA33262".to_string(), // Test key, safe to publish 458 | ), 459 | fee_rates_to_avg: Some(2), // small to speed up tests 460 | }; 461 | 462 | BitcoinService::new( 463 | runtime_config, 464 | RollupParams { 465 | rollup_name: "sov-btc".to_string(), 466 | reveal_tx_id_prefix: vec![], 467 | }, 468 | ) 469 | .await 470 | } 471 | 472 | #[tokio::test] 473 | async fn get_finalized_header() { 474 | let da_service = get_service().await; 475 | 476 | let get_curr_header = da_service 477 | .get_last_finalized_block_header() 478 | .await 479 | .expect("Failed to get block"); 480 | 481 | let get_head_header = da_service 482 | .get_head_block_header() 483 | .await 484 | .expect("Failed to get block"); 485 | 486 | assert_ne!(get_curr_header, get_head_header); 487 | 488 | let _new_block_hashes = da_service 489 | .client 490 | .generate_to_address( 491 | Address::from_str("bcrt1qxuds94z3pqwqea2p4f4ev4f25s6uu7y3avljrl") 492 | .unwrap() 493 | .require_network(bitcoin::Network::Regtest) 494 | .unwrap(), 495 | FINALITY_DEPTH as u32, 496 | ) 497 | .await 498 | .unwrap(); 499 | 500 | let new_finalized_header = da_service 501 | .get_last_finalized_block_header() 502 | .await 503 | .expect("Failed to get block"); 504 | 505 | assert_eq!(get_head_header, new_finalized_header); 506 | } 507 | 508 | #[tokio::test] 509 | async fn get_block_at() { 510 | let da_service = get_service().await; 511 | 512 | da_service 513 | .get_block_at(132) 514 | .await 515 | .expect("Failed to get block"); 516 | } 517 | 518 | #[tokio::test] 519 | async fn extract_relevant_blobs() { 520 | let da_service = get_service().await; 521 | 522 | let block = da_service 523 | .get_block_at(132) 524 | .await 525 | .expect("Failed to get block"); 526 | // panic!(); 527 | 528 | let txs = da_service.extract_relevant_blobs(&block); 529 | 530 | for tx in txs { 531 | println!("blob: {:?}", tx.blob); 532 | } 533 | } 534 | 535 | #[tokio::test] 536 | async fn extract_relevant_blobs_with_proof() { 537 | let da_service = get_service().await; 538 | 539 | let block = da_service 540 | .get_block_at(142) 541 | .await 542 | .expect("Failed to get block"); 543 | 544 | let (txs, inclusion_proof, completeness_proof) = 545 | da_service.extract_relevant_blobs_with_proof(&block).await; 546 | 547 | // completeness proof 548 | 549 | // create hash set of txs 550 | let mut txs_to_check = txs.iter().map(|blob| blob.hash).collect::>(); 551 | 552 | // Check every 00 bytes tx that parsed correctly is in txs 553 | let mut completeness_tx_hashes = completeness_proof 554 | .iter() 555 | .map(|tx| { 556 | let tx_hash = tx.txid().to_raw_hash().to_byte_array(); 557 | 558 | // it must parsed correctly 559 | if let Ok(parsed_tx) = parse_transaction(tx, &da_service.rollup_name) { 560 | let blob = parsed_tx.body; 561 | let blob_hash: [u8; 32] = sha256d::Hash::hash(&blob).to_byte_array(); 562 | // it must be in txs 563 | assert!(txs_to_check.remove(&blob_hash)); 564 | } 565 | 566 | tx_hash 567 | }) 568 | .collect::>(); 569 | 570 | // assert no extra txs than the ones in the completeness proof are left 571 | assert!(txs_to_check.is_empty()); 572 | 573 | // no 00 bytes left behind completeness proof 574 | inclusion_proof.txs.iter().for_each(|tx_hash| { 575 | if tx_hash[0..2] == [0, 0] { 576 | assert!(completeness_tx_hashes.remove(tx_hash)); 577 | } 578 | }); 579 | 580 | // assert all transactions are included in block 581 | assert!(completeness_tx_hashes.is_empty()); 582 | 583 | println!("\n--- Completeness proof verified ---\n"); 584 | 585 | let tx_root = block.header.merkle_root().to_raw_hash().to_byte_array(); 586 | 587 | // Inclusion proof is all the txs in the block. 588 | let tx_hashes = inclusion_proof 589 | .txs 590 | .iter() 591 | .map(|tx| Txid::from_slice(tx).unwrap()) 592 | .collect::>(); 593 | 594 | let root_from_inclusion = merkle_tree::calculate_root(tx_hashes.into_iter()) 595 | .unwrap() 596 | .to_raw_hash() 597 | .to_byte_array(); 598 | 599 | // Check that the tx root in the block header matches the tx root in the inclusion proof. 600 | assert_eq!(root_from_inclusion, tx_root); 601 | 602 | println!("\n--- Inclusion proof verified ---\n"); 603 | 604 | println!("\n--- Extracted #{:?} txs ---\n", txs.len()); 605 | } 606 | 607 | #[tokio::test] 608 | async fn send_transaction() { 609 | let da_service = get_service().await; 610 | 611 | let blob = "01000000b60000002adbd76606f2bd4125080e6f44df7ba2d728409955c80b8438eb1828ddf23e3c12188eeac7ecf6323be0ed5668e21cc354fca90d8bca513d6c0a240c26afa7007b758bf2e7670fafaf6bf0015ce0ff5aa802306fc7e3f45762853ffc37180fe64a0000000001fea6ac5b8751120fb62fff67b54d2eac66aef307c7dde1d394dea1e09e43dd44c800000000000000135d23aee8cb15c890831ff36db170157acaac31df9bba6cd40e7329e608eabd0000000000000000"; 612 | da_service 613 | .send_transaction(blob.as_bytes()) 614 | .await 615 | .expect("Failed to send transaction"); 616 | } 617 | 618 | #[tokio::test] 619 | async fn send_transaction_with_fee_rate() { 620 | let da_service = get_service().await; 621 | let fee_rate = da_service 622 | .client 623 | .estimate_smart_fee() 624 | .await 625 | .expect("Failed to get fee"); 626 | 627 | let blob = "01000000b60000002adbd76606f2bd4125080e6f44df7ba2d728409955c80b8438eb1828ddf23e3c12188eeac7ecf6323be0ed5668e21cc354fca90d8bca513d6c0a240c26afa7007b758bf2e7670fafaf6bf0015ce0ff5aa802306fc7e3f45762853ffc37180fe64a0000000001fea6ac5b8751120fb62fff67b54d2eac66aef307c7dde1d394dea1e09e43dd44c800000000000000135d23aee8cb15c890831ff36db170157acaac31df9bba6cd40e7329e608eabd0000000000000000"; 628 | 629 | for i in 0..3 { 630 | println!("Sending tx #{}", i); 631 | da_service 632 | .send_transaction_with_fee_rate(blob.as_bytes(), fee_rate) 633 | .await 634 | .expect("Failed to send transaction"); 635 | } 636 | } 637 | 638 | #[tokio::test] 639 | async fn check_signature() { 640 | let rpc = BitcoinNode::new( 641 | "http://localhost:38332".to_string(), 642 | "chainway".to_string(), 643 | "topsecret".to_string(), 644 | bitcoin::Network::Regtest, 645 | ); 646 | 647 | let da_service = get_service().await; 648 | let secp = bitcoin::secp256k1::Secp256k1::new(); 649 | let da_pubkey = KeyPair::from_secret_key(&secp, &da_service.sequencer_da_private_key) 650 | .public_key() 651 | .serialize() 652 | .to_vec(); 653 | 654 | // incorrect private key 655 | 656 | let blob = "01000000b60000002adbd76606f2bd4125080e6f44df7ba2d728409955c80b8438eb1828ddf23e3c12188eeac7ecf6323be0ed5668e21cc354fca90d8bca513d6c0a240c26afa7007b758bf2e7670fafaf6bf0015ce0ff5aa802306fc7e3f45762853ffc37180fe64a0000000001fea6ac5b8751120fb62fff67b54d2eac66aef307c7dde1d394dea1e09e43dd44c800000000000000135d23aee8cb15c890831ff36db170157acaac31df9bba6cd40e7329e608eabd0000000000000000"; 657 | da_service 658 | .send_transaction(blob.as_bytes()) 659 | .await 660 | .expect("Failed to send transaction"); 661 | 662 | let hashes = rpc 663 | .generate_to_address( 664 | Address::from_str("bcrt1qxuds94z3pqwqea2p4f4ev4f25s6uu7y3avljrl") 665 | .unwrap() 666 | .require_network(bitcoin::Network::Regtest) 667 | .unwrap(), 668 | 1, 669 | ) 670 | .await 671 | .unwrap(); 672 | 673 | let block_hash = hashes[0]; 674 | 675 | let block = rpc.get_block(block_hash.to_string()).await.unwrap(); 676 | 677 | let block = da_service.get_block_at(block.header.height).await.unwrap(); 678 | 679 | let txs = da_service.extract_relevant_blobs(&block); 680 | 681 | assert_eq!( 682 | txs.first().unwrap().sender.0, 683 | da_pubkey, 684 | "Publickey recovered incorrectly!" 685 | ); 686 | } 687 | } 688 | -------------------------------------------------------------------------------- /src/spec/address.rs: -------------------------------------------------------------------------------- 1 | use core::fmt::{Display, Formatter}; 2 | use core::str::FromStr; 3 | 4 | use borsh::{BorshDeserialize, BorshSerialize}; 5 | use serde::{Deserialize, Serialize}; 6 | use sov_rollup_interface::BasicAddress; 7 | 8 | // AddressWrapper is a wrapper around Vec to implement AddressTrait 9 | #[derive( 10 | Debug, PartialEq, Clone, Eq, Serialize, Deserialize, BorshDeserialize, BorshSerialize, Hash, 11 | )] 12 | pub struct AddressWrapper(pub Vec); 13 | 14 | impl BasicAddress for AddressWrapper {} 15 | 16 | impl FromStr for AddressWrapper { 17 | type Err = anyhow::Error; 18 | 19 | fn from_str(s: &str) -> Result { 20 | Ok(Self(hex::decode(s)?)) 21 | } 22 | } 23 | 24 | impl Display for AddressWrapper { 25 | fn fmt(&self, f: &mut Formatter) -> core::fmt::Result { 26 | let hash = hex::encode(&self.0); 27 | write!(f, "{hash}") 28 | } 29 | } 30 | 31 | impl AsRef<[u8]> for AddressWrapper { 32 | fn as_ref(&self) -> &[u8] { 33 | self.0.as_ref() 34 | } 35 | } 36 | 37 | impl From<[u8; 32]> for AddressWrapper { 38 | fn from(value: [u8; 32]) -> Self { 39 | Self(value.to_vec()) 40 | } 41 | } 42 | 43 | impl<'a> TryFrom<&'a [u8]> for AddressWrapper { 44 | type Error = anyhow::Error; 45 | 46 | fn try_from(value: &'a [u8]) -> Result { 47 | Ok(Self(value.to_vec())) 48 | } 49 | } 50 | 51 | #[cfg(test)] 52 | mod tests { 53 | use core::str::FromStr; 54 | 55 | use super::AddressWrapper; 56 | 57 | #[test] 58 | fn test_from_str() { 59 | let address = "66f68692c03eB9C0656D676f2F4bD13eba40D1B7"; // notice 0x prefix is missing 60 | 61 | let address_wrapper = AddressWrapper::from_str(address).unwrap(); 62 | 63 | assert_eq!( 64 | address_wrapper.0, 65 | [ 66 | 102, 246, 134, 146, 192, 62, 185, 192, 101, 109, 103, 111, 47, 75, 209, 62, 186, 67 | 64, 209, 183 68 | ] 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/spec/blob.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use sov_rollup_interface::da::{BlobReaderTrait, CountedBufReader}; 3 | use sov_rollup_interface::Buf; 4 | 5 | use super::address::AddressWrapper; 6 | 7 | // BlobBuf is a wrapper around Vec to implement Buf 8 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 9 | pub struct BlobBuf { 10 | pub data: Vec, 11 | 12 | pub offset: usize, 13 | } 14 | 15 | impl BlobWithSender { 16 | pub fn new(blob: Vec, sender: Vec, hash: [u8; 32]) -> Self { 17 | Self { 18 | blob: CountedBufReader::new(BlobBuf { 19 | data: blob, 20 | offset: 0, 21 | }), 22 | sender: AddressWrapper(sender), 23 | hash, 24 | } 25 | } 26 | } 27 | 28 | impl Buf for BlobBuf { 29 | fn remaining(&self) -> usize { 30 | self.data.len() - self.offset 31 | } 32 | 33 | fn chunk(&self) -> &[u8] { 34 | &self.data[self.offset..] 35 | } 36 | 37 | fn advance(&mut self, cnt: usize) { 38 | self.offset += cnt; 39 | } 40 | } 41 | 42 | // BlobWithSender is a wrapper around BlobBuf to implement BlobReaderTrait 43 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 44 | pub struct BlobWithSender { 45 | pub hash: [u8; 32], 46 | 47 | pub sender: AddressWrapper, 48 | 49 | pub blob: CountedBufReader, 50 | } 51 | 52 | impl BlobReaderTrait for BlobWithSender { 53 | type Address = AddressWrapper; 54 | 55 | fn sender(&self) -> Self::Address { 56 | self.sender.clone() 57 | } 58 | 59 | fn hash(&self) -> [u8; 32] { 60 | self.hash 61 | } 62 | 63 | fn verified_data(&self) -> &[u8] { 64 | self.blob.accumulator() 65 | } 66 | 67 | fn total_len(&self) -> usize { 68 | self.blob.total_len() 69 | } 70 | 71 | #[cfg(feature = "native")] 72 | fn advance(&mut self, num_bytes: usize) -> &[u8] { 73 | self.blob.advance(num_bytes); 74 | self.verified_data() 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/spec/block.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use sov_rollup_interface::da::BlockHeaderTrait; 3 | use sov_rollup_interface::services::da::SlotData; 4 | 5 | use super::header::HeaderWrapper; 6 | use super::transaction::Transaction; 7 | use crate::verifier::ChainValidityCondition; 8 | 9 | // BitcoinBlock is a wrapper around Block to remove unnecessary fields and implement SlotData 10 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 11 | pub struct BitcoinBlock { 12 | pub header: HeaderWrapper, 13 | pub txdata: Vec, 14 | } 15 | 16 | impl SlotData for BitcoinBlock { 17 | type BlockHeader = HeaderWrapper; 18 | type Cond = ChainValidityCondition; 19 | 20 | fn hash(&self) -> [u8; 32] { 21 | self.header.hash().to_byte_array() 22 | } 23 | 24 | fn header(&self) -> &Self::BlockHeader { 25 | &self.header 26 | } 27 | 28 | fn validity_condition(&self) -> Self::Cond { 29 | ChainValidityCondition { 30 | prev_hash: self.header.prev_hash().to_byte_array(), 31 | block_hash: self.hash(), 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/spec/block_hash.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use bitcoin::hashes::Hash; 4 | use bitcoin::BlockHash; 5 | use serde::{Deserialize, Serialize}; 6 | use sov_rollup_interface::da::BlockHashTrait; 7 | 8 | // BlockHashWrapper is a wrapper around BlockHash to implement BlockHashTrait 9 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)] 10 | pub struct BlockHashWrapper(pub BlockHash); 11 | 12 | impl BlockHashTrait for BlockHashWrapper {} 13 | 14 | impl From for [u8; 32] { 15 | fn from(val: BlockHashWrapper) -> Self { 16 | *val.0.as_ref() 17 | } 18 | } 19 | 20 | impl AsRef<[u8]> for BlockHashWrapper { 21 | fn as_ref(&self) -> &[u8] { 22 | self.0.as_ref() 23 | } 24 | } 25 | 26 | impl BlockHashWrapper { 27 | pub fn to_byte_array(&self) -> [u8; 32] { 28 | self.0.as_raw_hash().to_byte_array() 29 | } 30 | } 31 | 32 | impl Display for BlockHashWrapper { 33 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 34 | write!(f, "{}", self.0) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/spec/header.rs: -------------------------------------------------------------------------------- 1 | use bitcoin::block::Header; 2 | use bitcoin::hash_types::TxMerkleNode; 3 | use bitcoin::BlockHash; 4 | use serde::{Deserialize, Serialize}; 5 | use sov_rollup_interface::da::BlockHeaderTrait; 6 | 7 | use super::block_hash::BlockHashWrapper; 8 | 9 | // BlockHashWrapper is a wrapper around BlockHash to implement BlockHashTrait 10 | #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] 11 | pub struct HeaderWrapper { 12 | header: Header, // not pub to prevent uses like block.header.header.merkle_root 13 | pub tx_count: u32, 14 | pub height: u64, 15 | } 16 | 17 | impl BlockHeaderTrait for HeaderWrapper { 18 | type Hash = BlockHashWrapper; 19 | 20 | fn prev_hash(&self) -> Self::Hash { 21 | BlockHashWrapper(self.header.prev_blockhash) 22 | } 23 | 24 | fn hash(&self) -> Self::Hash { 25 | BlockHashWrapper(self.header.block_hash()) 26 | } 27 | 28 | fn height(&self) -> u64 { 29 | self.height 30 | } 31 | 32 | fn time(&self) -> sov_rollup_interface::da::Time { 33 | sov_rollup_interface::da::Time::from_secs(self.header.time as i64) 34 | } 35 | } 36 | 37 | impl HeaderWrapper { 38 | pub fn new(header: Header, tx_count: u32, height: u64) -> Self { 39 | Self { 40 | header, 41 | tx_count, 42 | height, 43 | } 44 | } 45 | 46 | pub fn block_hash(&self) -> BlockHash { 47 | self.header.block_hash() 48 | } 49 | 50 | pub fn merkle_root(&self) -> TxMerkleNode { 51 | self.header.merkle_root 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/spec/header_stream.rs: -------------------------------------------------------------------------------- 1 | use std::pin::Pin; 2 | use std::sync::Arc; 3 | use std::task::{Context, Poll}; 4 | use std::time::Duration; 5 | 6 | use futures::{FutureExt, Stream}; 7 | use pin_project::pin_project; 8 | use sov_rollup_interface::da::DaSpec; 9 | use sov_rollup_interface::services::da::DaService; 10 | 11 | use crate::service::BitcoinService; 12 | 13 | #[pin_project] 14 | pub struct BitcoinHeaderStream { 15 | #[pin] 16 | service: Arc, 17 | interval: Duration, 18 | timer: tokio::time::Interval, 19 | } 20 | 21 | impl BitcoinHeaderStream { 22 | pub fn new(service: Arc, interval: Duration) -> Self { 23 | Self { 24 | service, 25 | interval, 26 | timer: tokio::time::interval(interval), 27 | } 28 | } 29 | } 30 | 31 | impl Stream for BitcoinHeaderStream { 32 | type Item = Result< 33 | <::Spec as DaSpec>::BlockHeader, 34 | ::Error, 35 | >; 36 | 37 | fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 38 | let this = self.project(); 39 | if this.timer.poll_tick(cx).is_ready() { 40 | let header = futures::ready!(this 41 | .service 42 | .get_last_finalized_block_header() 43 | .boxed() 44 | .poll_unpin(cx)); 45 | return Poll::Ready(Some(header)); 46 | } 47 | Poll::Pending 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/spec/mod.rs: -------------------------------------------------------------------------------- 1 | use bitcoin::Transaction; 2 | use sov_rollup_interface::da::DaSpec; 3 | 4 | use self::address::AddressWrapper; 5 | use self::blob::BlobWithSender; 6 | use self::block_hash::BlockHashWrapper; 7 | use self::header::HeaderWrapper; 8 | use self::proof::InclusionMultiProof; 9 | use crate::verifier::ChainValidityCondition; 10 | 11 | pub mod address; 12 | pub mod blob; 13 | pub mod block; 14 | mod block_hash; 15 | pub mod header; 16 | #[cfg(feature = "native")] 17 | pub mod header_stream; 18 | pub mod proof; 19 | pub mod transaction; 20 | pub mod utxo; 21 | 22 | #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Clone)] 23 | pub struct BitcoinSpec; 24 | 25 | pub struct RollupParams { 26 | pub rollup_name: String, 27 | pub reveal_tx_id_prefix: Vec, 28 | } 29 | 30 | impl DaSpec for BitcoinSpec { 31 | type SlotHash = BlockHashWrapper; 32 | 33 | type ChainParams = RollupParams; 34 | 35 | type BlockHeader = HeaderWrapper; 36 | 37 | type BlobTransaction = BlobWithSender; 38 | 39 | type Address = AddressWrapper; 40 | 41 | type InclusionMultiProof = InclusionMultiProof; 42 | 43 | type CompletenessProof = Vec; 44 | 45 | type ValidityCondition = ChainValidityCondition; 46 | } 47 | -------------------------------------------------------------------------------- /src/spec/proof.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | // Set of proofs for inclusion of a transaction in a block 4 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 5 | pub struct InclusionMultiProof { 6 | pub txs: Vec<[u8; 32]>, 7 | } 8 | -------------------------------------------------------------------------------- /src/spec/transaction.rs: -------------------------------------------------------------------------------- 1 | // pub use bitcoin::Transaction; 2 | pub type Transaction = bitcoin::Transaction; 3 | -------------------------------------------------------------------------------- /src/spec/utxo.rs: -------------------------------------------------------------------------------- 1 | use bitcoin::Txid; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Clone, Debug, PartialEq, Serialize)] 5 | pub struct UTXO { 6 | pub tx_id: Txid, 7 | pub vout: u32, 8 | pub address: String, 9 | pub script_pubkey: String, 10 | pub amount: u64, 11 | pub confirmations: u64, 12 | pub spendable: bool, 13 | pub solvable: bool, 14 | } 15 | 16 | // Temporary struct to deserialize UTXO from JSON 17 | #[derive(Deserialize)] 18 | struct RawUTXO { 19 | txid: String, 20 | vout: u32, 21 | address: String, 22 | #[serde(rename = "scriptPubKey")] 23 | script_pub_key: String, 24 | amount: f64, 25 | confirmations: u64, 26 | spendable: bool, 27 | solvable: bool, 28 | } 29 | 30 | // Deserialize UTXO from JSON 31 | impl<'de> serde::Deserialize<'de> for UTXO { 32 | fn deserialize(deserializer: D) -> Result 33 | where 34 | D: serde::Deserializer<'de>, 35 | { 36 | let raw_utxo = RawUTXO::deserialize(deserializer)?; 37 | Ok(UTXO { 38 | tx_id: raw_utxo.txid.parse().unwrap(), 39 | vout: raw_utxo.vout, 40 | address: raw_utxo.address, 41 | script_pubkey: raw_utxo.script_pub_key, 42 | amount: (raw_utxo.amount * 100_000_000.0) as u64, // satoshis to bitcoin 43 | confirmations: raw_utxo.confirmations, 44 | spendable: raw_utxo.spendable, 45 | solvable: raw_utxo.solvable, 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/verifier.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use bitcoin::hashes::Hash; 4 | use bitcoin::{merkle_tree, Txid}; 5 | use borsh::{BorshDeserialize, BorshSerialize}; 6 | use serde::{Deserialize, Serialize}; 7 | use sov_rollup_interface::da::{BlockHeaderTrait, DaSpec, DaVerifier}; 8 | use sov_rollup_interface::digest::Digest; 9 | use sov_rollup_interface::zk::ValidityCondition; 10 | use thiserror::Error; 11 | 12 | use crate::helpers::builders::decompress_blob; 13 | use crate::helpers::parsers::parse_transaction; 14 | use crate::spec::BitcoinSpec; 15 | 16 | pub struct BitcoinVerifier { 17 | rollup_name: String, 18 | reveal_tx_id_prefix: Vec, 19 | } 20 | 21 | // TODO: custom errors based on our implementation 22 | #[derive(Debug, Copy, Clone, PartialEq)] 23 | pub enum ValidationError { 24 | InvalidTx, 25 | InvalidProof, 26 | InvalidBlock, 27 | } 28 | 29 | #[derive( 30 | Debug, 31 | Clone, 32 | Copy, 33 | PartialEq, 34 | Eq, 35 | Serialize, 36 | Deserialize, 37 | Hash, 38 | BorshDeserialize, 39 | BorshSerialize, 40 | )] 41 | /// A validity condition expressing that a chain of DA layer blocks is contiguous and canonical 42 | pub struct ChainValidityCondition { 43 | pub prev_hash: [u8; 32], 44 | pub block_hash: [u8; 32], 45 | } 46 | #[derive(Error, Debug)] 47 | pub enum ValidityConditionError { 48 | #[error("conditions for validity can only be combined if the blocks are consecutive")] 49 | BlocksNotConsecutive, 50 | } 51 | 52 | impl ValidityCondition for ChainValidityCondition { 53 | type Error = ValidityConditionError; 54 | fn combine(&self, rhs: Self) -> Result { 55 | if self.block_hash != rhs.prev_hash { 56 | return Err(ValidityConditionError::BlocksNotConsecutive); 57 | } 58 | Ok(rhs) 59 | } 60 | } 61 | 62 | impl DaVerifier for BitcoinVerifier { 63 | type Spec = BitcoinSpec; 64 | 65 | type Error = ValidationError; 66 | 67 | fn new(params: ::ChainParams) -> Self { 68 | Self { 69 | rollup_name: params.rollup_name, 70 | reveal_tx_id_prefix: params.reveal_tx_id_prefix, 71 | } 72 | } 73 | 74 | // Verify that the given list of blob transactions is complete and correct. 75 | fn verify_relevant_tx_list( 76 | &self, 77 | block_header: &::BlockHeader, 78 | blobs: &[::BlobTransaction], 79 | inclusion_proof: ::InclusionMultiProof, 80 | completeness_proof: ::CompletenessProof, 81 | ) -> Result<::ValidityCondition, Self::Error> { 82 | let validity_condition = ChainValidityCondition { 83 | prev_hash: block_header.prev_hash().to_byte_array(), 84 | block_hash: block_header.prev_hash().to_byte_array(), 85 | }; 86 | 87 | // completeness proof 88 | 89 | // create hash set of blobs 90 | let mut blobs_iter = blobs.iter(); 91 | 92 | let mut prev_index_in_inclusion = 0; 93 | 94 | let prefix = self.reveal_tx_id_prefix.as_slice(); 95 | // Check starting bytes tx that parsed correctly is in blobs 96 | let mut completeness_tx_hashes = completeness_proof 97 | .iter() 98 | .enumerate() 99 | .map(|(index_completeness, tx)| { 100 | let tx_hash = tx.txid().to_raw_hash().to_byte_array(); 101 | 102 | // make sure it starts with the correct prefix 103 | assert!( 104 | tx_hash.starts_with(prefix), 105 | "non-relevant tx found in completeness proof" 106 | ); 107 | 108 | // make sure completeness txs are ordered same in inclusion proof 109 | // this logic always start seaching from the last found index 110 | // ordering should be preserved naturally 111 | let mut is_found_in_block = false; 112 | for i in prev_index_in_inclusion..inclusion_proof.txs.len() { 113 | if inclusion_proof.txs[i] == tx_hash { 114 | is_found_in_block = true; 115 | prev_index_in_inclusion = i + 1; 116 | break; 117 | } 118 | } 119 | 120 | // assert tx is included in inclusion proof, thus in block 121 | assert!( 122 | is_found_in_block, 123 | "tx in completeness proof is not found in DA block or order was not preserved" 124 | ); 125 | 126 | // it must be parsed correctly 127 | if let Ok(parsed_tx) = parse_transaction(tx, &self.rollup_name) { 128 | if let Some(blob_hash) = parsed_tx.get_sig_verified_hash() { 129 | let blob = blobs_iter.next(); 130 | 131 | assert!(blob.is_some(), "valid blob was not found in blobs"); 132 | 133 | let blob = blob.unwrap(); 134 | 135 | assert_eq!(blob.hash, blob_hash, "blobs was tampered with"); 136 | 137 | assert_eq!( 138 | parsed_tx.public_key, blob.sender.0, 139 | "incorrect sender in blob" 140 | ); 141 | 142 | // decompress the blob 143 | let decompressed_blob = decompress_blob(&parsed_tx.body); 144 | 145 | // read the supplied blob from txs 146 | let mut blob_content = blobs[index_completeness].blob.clone(); 147 | blob_content.advance(blob_content.total_len()); 148 | let blob_content = blob_content.accumulator(); 149 | 150 | // assert tx content is not modified 151 | assert_eq!(blob_content, decompressed_blob, "blob content was modified"); 152 | } 153 | } 154 | 155 | tx_hash 156 | }) 157 | .collect::>(); 158 | 159 | // assert no extra txs than the ones in the completeness proof are left 160 | assert!( 161 | blobs_iter.next().is_none(), 162 | "completeness proof is incorrect" 163 | ); 164 | 165 | // no prefix bytes left behind completeness proof 166 | inclusion_proof.txs.iter().for_each(|tx_hash| { 167 | if tx_hash.starts_with(prefix) { 168 | // assert all prefixed transactions are included in completeness proof 169 | assert!( 170 | completeness_tx_hashes.remove(tx_hash), 171 | "relevant transaction in DA block was not included in completeness proof" 172 | ); 173 | } 174 | }); 175 | 176 | // assert no other (irrelevant) tx is in completeness proof 177 | assert!( 178 | completeness_tx_hashes.is_empty(), 179 | "non-relevant transaction found in completeness proof" 180 | ); 181 | 182 | let tx_root = block_header.merkle_root().to_raw_hash().to_byte_array(); 183 | 184 | // Inclusion proof is all the txs in the block. 185 | let tx_hashes = inclusion_proof 186 | .txs 187 | .iter() 188 | .map(|tx| Txid::from_slice(tx).unwrap()) 189 | .collect::>(); 190 | 191 | if let Some(root_from_inclusion) = merkle_tree::calculate_root(tx_hashes.into_iter()) { 192 | let root_from_inclusion = root_from_inclusion.to_raw_hash().to_byte_array(); 193 | 194 | // Check that the tx root in the block header matches the tx root in the inclusion proof. 195 | assert_eq!(root_from_inclusion, tx_root, "inclusion proof is incorrect"); 196 | 197 | Ok(validity_condition) 198 | } else { 199 | panic!("merkle root couldn't be computed") 200 | } 201 | } 202 | } 203 | 204 | #[cfg(test)] 205 | mod tests { 206 | 207 | // Transactions for testing is prepared with 2 leading zeros 208 | // So verifier takes in [0, 0] 209 | 210 | use core::str::FromStr; 211 | 212 | use bitcoin::block::{Header, Version}; 213 | use bitcoin::hash_types::TxMerkleNode; 214 | use bitcoin::hashes::{sha256d, Hash}; 215 | use bitcoin::string::FromHexStr; 216 | use bitcoin::{BlockHash, CompactTarget}; 217 | use sov_rollup_interface::da::{DaSpec, DaVerifier}; 218 | 219 | use super::BitcoinVerifier; 220 | use crate::helpers::builders::decompress_blob; 221 | use crate::helpers::parsers::{parse_hex_transaction, parse_transaction}; 222 | use crate::spec::blob::BlobWithSender; 223 | use crate::spec::header::HeaderWrapper; 224 | use crate::spec::proof::InclusionMultiProof; 225 | use crate::spec::transaction::Transaction; 226 | use crate::spec::RollupParams; 227 | 228 | fn get_mock_txs() -> Vec { 229 | // relevant txs are on 6, 8, 10, 12 indices 230 | let txs = std::fs::read_to_string("test_data/mock_txs.txt").unwrap(); 231 | 232 | txs.lines() 233 | .map(|tx| parse_hex_transaction(tx).unwrap()) 234 | .collect() 235 | } 236 | 237 | fn get_blob_with_sender(tx: &Transaction) -> BlobWithSender { 238 | let tx = tx.clone(); 239 | 240 | let parsed_inscription = parse_transaction(&tx, "sov-btc").unwrap(); 241 | 242 | let blob = parsed_inscription.body; 243 | 244 | // Decompress the blob 245 | let decompressed_blob = decompress_blob(&blob); 246 | 247 | BlobWithSender::new( 248 | decompressed_blob, 249 | parsed_inscription.public_key, 250 | sha256d::Hash::hash(&blob).to_byte_array(), 251 | ) 252 | } 253 | 254 | #[allow(clippy::type_complexity)] 255 | fn get_mock_data() -> ( 256 | <::Spec as DaSpec>::BlockHeader, // block header 257 | <::Spec as DaSpec>::InclusionMultiProof, // inclusion proof 258 | <::Spec as DaSpec>::CompletenessProof, // completeness proof 259 | Vec<<::Spec as DaSpec>::BlobTransaction>, // txs 260 | ) { 261 | let header = HeaderWrapper::new( 262 | Header { 263 | version: Version::from_consensus(536870912), 264 | prev_blockhash: BlockHash::from_str( 265 | "6b15a2e4b17b0aabbd418634ae9410b46feaabf693eea4c8621ffe71435d24b0", 266 | ) 267 | .unwrap(), 268 | merkle_root: TxMerkleNode::from_str( 269 | "7750076b3b5498aad3e2e7da55618c66394d1368dc08f19f0b13d1e5b83ae056", 270 | ) 271 | .unwrap(), 272 | time: 1694177029, 273 | bits: CompactTarget::from_hex_str_no_prefix("207fffff").unwrap(), 274 | nonce: 0, 275 | }, 276 | 13, 277 | 2, 278 | ); 279 | 280 | let block_txs = get_mock_txs(); 281 | 282 | // relevant txs are on 6, 8, 10, 12 indices 283 | let completeness_proof = vec![ 284 | block_txs[6].clone(), 285 | block_txs[8].clone(), 286 | block_txs[10].clone(), 287 | block_txs[12].clone(), 288 | ]; 289 | 290 | let inclusion_proof = InclusionMultiProof { 291 | txs: block_txs 292 | .iter() 293 | .map(|t| t.txid().to_raw_hash().to_byte_array()) 294 | .collect(), 295 | }; 296 | 297 | let txs: Vec = vec![ 298 | get_blob_with_sender(&block_txs[6]), 299 | get_blob_with_sender(&block_txs[8]), 300 | get_blob_with_sender(&block_txs[10]), 301 | get_blob_with_sender(&block_txs[12]), 302 | ]; 303 | 304 | (header, inclusion_proof, completeness_proof, txs) 305 | } 306 | 307 | #[test] 308 | fn correct() { 309 | let verifier = BitcoinVerifier::new(RollupParams { 310 | rollup_name: "sov-btc".to_string(), 311 | reveal_tx_id_prefix: vec![0, 0], 312 | }); 313 | 314 | let (block_header, inclusion_proof, completeness_proof, txs) = get_mock_data(); 315 | 316 | assert!(verifier 317 | .verify_relevant_tx_list( 318 | &block_header, 319 | txs.as_slice(), 320 | inclusion_proof, 321 | completeness_proof 322 | ) 323 | .is_ok()); 324 | } 325 | 326 | #[test] 327 | #[should_panic(expected = "inclusion proof is incorrect")] 328 | fn extra_tx_in_inclusion() { 329 | let verifier = BitcoinVerifier::new(RollupParams { 330 | rollup_name: "sov-btc".to_string(), 331 | reveal_tx_id_prefix: vec![0, 0], 332 | }); 333 | 334 | let (block_header, mut inclusion_proof, completeness_proof, txs) = get_mock_data(); 335 | 336 | inclusion_proof.txs.push([1; 32]); 337 | 338 | verifier 339 | .verify_relevant_tx_list( 340 | &block_header, 341 | txs.as_slice(), 342 | inclusion_proof, 343 | completeness_proof, 344 | ) 345 | .unwrap(); 346 | } 347 | 348 | #[test] 349 | #[should_panic( 350 | expected = "tx in completeness proof is not found in DA block or order was not preserved" 351 | )] 352 | fn missing_tx_in_inclusion() { 353 | let verifier = BitcoinVerifier::new(RollupParams { 354 | rollup_name: "sov-btc".to_string(), 355 | reveal_tx_id_prefix: vec![0, 0], 356 | }); 357 | 358 | let (block_header, mut inclusion_proof, completeness_proof, txs) = get_mock_data(); 359 | 360 | inclusion_proof.txs.pop(); 361 | 362 | verifier 363 | .verify_relevant_tx_list( 364 | &block_header, 365 | txs.as_slice(), 366 | inclusion_proof, 367 | completeness_proof, 368 | ) 369 | .unwrap(); 370 | } 371 | 372 | #[test] 373 | #[should_panic = "tx in completeness proof is not found in DA block or order was not preserved"] 374 | fn empty_inclusion() { 375 | let verifier = BitcoinVerifier::new(RollupParams { 376 | rollup_name: "sov-btc".to_string(), 377 | reveal_tx_id_prefix: vec![0, 0], 378 | }); 379 | 380 | let (block_header, mut inclusion_proof, completeness_proof, txs) = get_mock_data(); 381 | 382 | inclusion_proof.txs.clear(); 383 | 384 | verifier 385 | .verify_relevant_tx_list( 386 | &block_header, 387 | txs.as_slice(), 388 | inclusion_proof, 389 | completeness_proof, 390 | ) 391 | .unwrap(); 392 | } 393 | 394 | #[test] 395 | #[should_panic = "inclusion proof is incorrect"] 396 | fn break_order_of_inclusion() { 397 | let verifier = BitcoinVerifier::new(RollupParams { 398 | rollup_name: "sov-btc".to_string(), 399 | reveal_tx_id_prefix: vec![0, 0], 400 | }); 401 | 402 | let (block_header, mut inclusion_proof, completeness_proof, txs) = get_mock_data(); 403 | 404 | inclusion_proof.txs.swap(0, 1); 405 | 406 | verifier 407 | .verify_relevant_tx_list( 408 | &block_header, 409 | txs.as_slice(), 410 | inclusion_proof, 411 | completeness_proof, 412 | ) 413 | .unwrap(); 414 | } 415 | 416 | #[test] 417 | #[should_panic(expected = "completeness proof is incorrect")] 418 | fn missing_tx_in_completeness_proof() { 419 | let verifier = BitcoinVerifier::new(RollupParams { 420 | rollup_name: "sov-btc".to_string(), 421 | reveal_tx_id_prefix: vec![0, 0], 422 | }); 423 | 424 | let (block_header, inclusion_proof, mut completeness_proof, txs) = get_mock_data(); 425 | 426 | completeness_proof.pop(); 427 | 428 | verifier 429 | .verify_relevant_tx_list( 430 | &block_header, 431 | txs.as_slice(), 432 | inclusion_proof, 433 | completeness_proof, 434 | ) 435 | .unwrap(); 436 | } 437 | 438 | #[test] 439 | #[should_panic(expected = "completeness proof is incorrect")] 440 | fn empty_completeness_proof() { 441 | let verifier = BitcoinVerifier::new(RollupParams { 442 | rollup_name: "sov-btc".to_string(), 443 | reveal_tx_id_prefix: vec![0, 0], 444 | }); 445 | 446 | let (block_header, inclusion_proof, mut completeness_proof, txs) = get_mock_data(); 447 | 448 | completeness_proof.clear(); 449 | 450 | verifier 451 | .verify_relevant_tx_list( 452 | &block_header, 453 | txs.as_slice(), 454 | inclusion_proof, 455 | completeness_proof, 456 | ) 457 | .unwrap(); 458 | } 459 | 460 | #[test] 461 | #[should_panic(expected = "non-relevant tx found in completeness proof")] 462 | fn non_relevant_tx_in_completeness_proof() { 463 | let verifier = BitcoinVerifier::new(RollupParams { 464 | rollup_name: "sov-btc".to_string(), 465 | reveal_tx_id_prefix: vec![0, 0], 466 | }); 467 | 468 | let (block_header, inclusion_proof, mut completeness_proof, txs) = get_mock_data(); 469 | 470 | completeness_proof.push(get_mock_txs().get(1).unwrap().clone()); 471 | 472 | verifier 473 | .verify_relevant_tx_list( 474 | &block_header, 475 | txs.as_slice(), 476 | inclusion_proof, 477 | completeness_proof, 478 | ) 479 | .unwrap(); 480 | } 481 | 482 | #[test] 483 | #[should_panic( 484 | expected = "tx in completeness proof is not found in DA block or order was not preserved" 485 | )] 486 | fn break_completeness_proof_order() { 487 | let verifier = BitcoinVerifier::new(RollupParams { 488 | rollup_name: "sov-btc".to_string(), 489 | reveal_tx_id_prefix: vec![0, 0], 490 | }); 491 | 492 | let (block_header, inclusion_proof, mut completeness_proof, mut txs) = get_mock_data(); 493 | 494 | completeness_proof.swap(2, 3); 495 | txs.swap(2, 3); 496 | 497 | verifier 498 | .verify_relevant_tx_list( 499 | &block_header, 500 | txs.as_slice(), 501 | inclusion_proof, 502 | completeness_proof, 503 | ) 504 | .unwrap(); 505 | } 506 | 507 | #[test] 508 | #[should_panic(expected = "blobs was tampered with")] 509 | fn break_rel_tx_order() { 510 | let verifier = BitcoinVerifier::new(RollupParams { 511 | rollup_name: "sov-btc".to_string(), 512 | reveal_tx_id_prefix: vec![0, 0], 513 | }); 514 | 515 | let (block_header, inclusion_proof, completeness_proof, mut txs) = get_mock_data(); 516 | 517 | txs.swap(0, 1); 518 | 519 | verifier 520 | .verify_relevant_tx_list( 521 | &block_header, 522 | txs.as_slice(), 523 | inclusion_proof, 524 | completeness_proof, 525 | ) 526 | .unwrap(); 527 | } 528 | 529 | #[test] 530 | #[should_panic = "tx in completeness proof is not found in DA block or order was not preserved"] 531 | fn break_rel_tx_and_completeness_proof_order() { 532 | let verifier = BitcoinVerifier::new(RollupParams { 533 | rollup_name: "sov-btc".to_string(), 534 | reveal_tx_id_prefix: vec![0, 0], 535 | }); 536 | 537 | let (block_header, inclusion_proof, mut completeness_proof, mut txs) = get_mock_data(); 538 | 539 | txs.swap(0, 1); 540 | completeness_proof.swap(0, 1); 541 | 542 | verifier 543 | .verify_relevant_tx_list( 544 | &block_header, 545 | txs.as_slice(), 546 | inclusion_proof, 547 | completeness_proof, 548 | ) 549 | .unwrap(); 550 | } 551 | 552 | #[test] 553 | #[should_panic(expected = "blob content was modified")] 554 | fn tamper_rel_tx_content() { 555 | let verifier = BitcoinVerifier::new(RollupParams { 556 | rollup_name: "sov-btc".to_string(), 557 | reveal_tx_id_prefix: vec![0, 0], 558 | }); 559 | 560 | let (block_header, inclusion_proof, completeness_proof, mut txs) = get_mock_data(); 561 | 562 | let new_blob = vec![2; 152]; 563 | 564 | txs[1] = BlobWithSender::new(new_blob, txs[1].sender.0.clone(), txs[1].hash); 565 | 566 | verifier 567 | .verify_relevant_tx_list( 568 | &block_header, 569 | txs.as_slice(), 570 | inclusion_proof, 571 | completeness_proof, 572 | ) 573 | .unwrap(); 574 | } 575 | 576 | #[test] 577 | #[should_panic(expected = "incorrect sender in blob")] 578 | fn tamper_senders() { 579 | let verifier = BitcoinVerifier::new(RollupParams { 580 | rollup_name: "sov-btc".to_string(), 581 | reveal_tx_id_prefix: vec![0, 0], 582 | }); 583 | 584 | let (block_header, inclusion_proof, completeness_proof, mut txs) = get_mock_data(); 585 | 586 | txs[1] = BlobWithSender::new( 587 | parse_transaction(&completeness_proof[1], "sov-btc") 588 | .unwrap() 589 | .body, 590 | vec![2; 33], 591 | txs[1].hash, 592 | ); 593 | 594 | verifier 595 | .verify_relevant_tx_list( 596 | &block_header, 597 | txs.as_slice(), 598 | inclusion_proof, 599 | completeness_proof, 600 | ) 601 | .unwrap(); 602 | } 603 | 604 | #[test] 605 | #[should_panic(expected = "valid blob was not found in blobs")] 606 | fn missing_rel_tx() { 607 | let verifier = BitcoinVerifier::new(RollupParams { 608 | rollup_name: "sov-btc".to_string(), 609 | reveal_tx_id_prefix: vec![0, 0], 610 | }); 611 | 612 | let (block_header, inclusion_proof, completeness_proof, mut txs) = get_mock_data(); 613 | 614 | txs = vec![txs[0].clone(), txs[1].clone(), txs[2].clone()]; 615 | 616 | verifier 617 | .verify_relevant_tx_list( 618 | &block_header, 619 | txs.as_slice(), 620 | inclusion_proof, 621 | completeness_proof, 622 | ) 623 | .unwrap(); 624 | } 625 | } 626 | -------------------------------------------------------------------------------- /test_data/mock_txs.txt: -------------------------------------------------------------------------------- 1 | 020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff0402820000ffffffff020e01062a010000001600143bbc738d7757b9a6492fd49d9f2fa1814d717bc50000000000000000266a24aa21a9ed68b5f99b15f2d573377b466c0fad0e6af3e7ba804bfbb20918e4c8b190595fb60120000000000000000000000000000000000000000000000000000000000000000000000000 2 | 0200000000010170713bcae15c31f3d48809953b4b20d0866c37694a82a50b46f644a0be5329050100000000fdffffff024cc90f24010000001976a914c217b9aa4873545005bc44c595080a3494325e8788ac00e1f505000000001976a914c32c4039d46711b7440d8112f8f5bf4d65412d9488ac024730440220048067f9e5b54e7548f08e5c092a519bad7f8006ee23d7f130f8f31c5e5991ec02200fb3c8c71aa4e4d7df4a161801a1f877a3e08cc209f156d76c0692893646289e0121038a0b131d6dfa01dcb0187f082f1528b366935528f482af5d0689f691d6c5e69581000000 3 | 02000000000101af7f5e54e73235fd3846653a1c52d5926067d1d1cdb66045cedb7f44be5e24ec0100000000fdffffff0200e1f505000000001976a914f596d6211a7dd83aea67ffa655c5af946816d4c188ace4eb0f24010000001976a91490b2459a12b57b4552246f0e323b98ec46a1262088ac02473044022019330432b977aa806162a822c3a50128ccc250e8a48f55b1230f5e6edcef4fce022019997fc2a7ab977b32610545e9bfd02d713b17f7fe0b1825207da5cb44d1f4510121039d71d4b2d9f101d5f9276359b8a34f14bca6cdccb640a537d19bd5e42a23f81a81000000 4 | 02000000000101552e39636885890915105d98688b63cad7205fc5f87a85e4ac4ed3a6b97ba6680100000000fdffffff0200e1f505000000001976a914f596d6211a7dd83aea67ffa655c5af946816d4c188ac6e001024010000001976a914ed5ff75594d52fc7719e2e16d584f50ef75dc80b88ac0247304402206c168bb377b5b6e22053b741aef59687ff6623793f95130fbe78c73feaa29925022031d48b98ccfa20be1d56c5d56d286662e44396f4789061fbba20aee08490e925012102e1f047dec8bb6fb40e29d71cefbdcbe38465c5a4a1bd938996229c82c258553f81000000 5 | 02000000000101e620464398310766527948e42e69b7d1e088f3caca305f9739523dd30ed061170100000000fdffffff0200e1f505000000001976a914f596d6211a7dd83aea67ffa655c5af946816d4c188ac46d70f24010000001976a91430a1dfc85aca6de8f8b8d22b0256dd314ab56b4388ac02473044022079b82293223442ecd0bc137d50f1bb5d805bbac8f5b6b73ec3e7d28fe64c3e8e022068fe995b48605ace450654917d970db7e5598236e87f6242cf3b58b21db6dfe801210270ab5352c0d049d2941e664902f3810388beac066f929198476fb4d5feb1c8da25000000 6 | 010000000001011cc0886a1d672699b7dca4019ab9d8d3f99827077e51a68ebf9fe0f6119c0a350100000000fdffffff02ac030000000000002251204bb89437f185f38964daa13193831e5c05597c5c63a482fa069bfea120fef822a8d8052a01000000160014bef05d3715fc4d93b73eecfbca13f661f23a05770247304402206f2b2847db5edf95fb473346e5280f61120086fb1e03d36f4a3c4d38e1a86c600220373e4b4e1f24863df9871cbeacb768a12fc390e0425ff9469de8c0d77898dc3e012103269120a4d2d2f308456a9c57ae89529c87fdc28c6ac108ad49838bdf5d0fd8ea00000000 7 | 01000000000101af49f3ced4752417ef844dd486a37df1b0b5eb5b4c6a52ad69caf0fc9cc7ea330000000000fdffffff012202000000000000160014371b02d451081c0cf541aa6b96552aa435ce789103405158341c234d131e629202e78dc1463a18fc5eb8a9fdeadbb0ace07080904686104b0670b64ce61aef9a08db7b66d2cb4abbb92d03235e5f40d1c32242f62feffd63012071a2477d43a5701e5b4f009ef2d920c2969ef9dd47f1524ff36b5dfa087a5a1dac0063010107736f762d627463010240cc4b23d2cb3e22b2c57a59f24088764f39f7b789847e983b9ee9ce7682578c2b7dbdf4384e230c942b91ae5ce6b1ba33587f549fedee4d19e54ff3a8e54601e801032102588d202afcc1ee4ab5254c7847ec25b9a135bbda0f2bc69ee1a714749fd77dc90104038c0403004cc41b7b01f845c786b10e90638b5cd88023081823b06c20b90040401052860738a7c6cd60c7358f581158bbf7e6bc92c7391efe57ed40c593d8a2e09839969526a688dd6cdf3e13965aeca8592c53b7e8bbce8f89ea5492b146f243b3e5a5035eae51c7ebe6b8bc3cab03487b71a7990116d8b5afdc53370e95bb16a7c0adbd8489749b96ad15ae448c2be3bb332f7dc39b6d967b026f9f591af96f3669f1f7c9cc7b1dd047a2c392bbd145daf11142776253e420f5eccc169afb55693d0febc27f0db159036821c171a2477d43a5701e5b4f009ef2d920c2969ef9dd47f1524ff36b5dfa087a5a1d00000000 8 | 01000000000101af49f3ced4752417ef844dd486a37df1b0b5eb5b4c6a52ad69caf0fc9cc7ea330100000000fdffffff02a8030000000000002251202162a8ec86e01d65323fa49b334ab087105d0d50fa7e2539a3ce58df0d714531e4d3052a010000001600143777de944e9b956e8166f0b1e7b03f461e363eda0247304402204d59c447e0be3714ff5d3be0d4207b00424b4281deccdbb907ca52acb67cd7f802205b6e2628d70b77f1b3e657a501beaf157c8f6c46e4b408f4a5a5d52f8e3e99c2012103c7c2e2975232eacbf11f51de7b216baae245aa5c99a907aec760399012abbde800000000 9 | 010000000001013a931993d889efacf67ec6e98b4739c4d02af1a118de31dfab9331b9af7d121b0000000000fdffffff012202000000000000160014371b02d451081c0cf541aa6b96552aa435ce78910340ba74d48b5eee94276fa436baa74d3f6286220f5130db45f67f4990823d8be7453402e72fad80990566d643f4a1d2a0472258d742a95245435b8263493d958c68fd58012010dd634819823f9857759dceb16ed8c3977f8b2949972b0cc83d83bd55fbc98aac0063010107736f762d6274630102405ced8a43ccf97fe5aea00eff641c4ffd38dec00ec7b277bddaf48241e5e55feb69fe5281113209d1708beeba7ce8dcd821e9000674c1c8ef28c79a0e7bbc352b01032102588d202afcc1ee4ab5254c7847ec25b9a135bbda0f2bc69ee1a714749fd77dc9010403458000004cb91b6401f845c786b10e90638b5cd88023081823b06c10b9004040105286ab9c53e366b0e39a47ac08acdd7b735ec9e31c0fffab76a0e2496c5170cc1ccb4a1353c46eb66f9f094b2d76d42c96a95b942d2f1df0728d3a5e37c7e5e5591d40da8b3bcd0cb0c0ae7de59eba71a8dcb538056eed254ca4dbb46cad7025625c19df9d79e91bde6cb3dc1378fbccd2c87fb3498bbf4f66deeb803e121d96dc8d2ed28e8f10ba139b2207a96767b6d0dcaf4aeb795817fe6b88cd1a006821c110dd634819823f9857759dceb16ed8c3977f8b2949972b0cc83d83bd55fbc98a00000000 10 | 010000000001013a931993d889efacf67ec6e98b4739c4d02af1a118de31dfab9331b9af7d121b0100000000fdffffff02a8030000000000002251207110bc42009b69de4c10e56073f123c083010c6df93d9aa93540f8e226775e1620cf052a0100000016001428c5cee2033838cc289460df9eae5a36536620940247304402205a8aecb1b99a583a5b8952d58eed2368a5692a18089b53d58b8e8abe136b346f02200a1c4c84d34403b5fbdee398c53b41e5ecaf2aef9586362f674eb7a4b099bd35012103041cfe6d6b4a848a9f8d4dcd0019ca03cdd7453c7b47bc21f536333560a0ccf100000000 11 | 010000000001012913ec7a7ba8a24c4ef3cea5a355ef11cfc98fac26c6d599b2d1ecc4bcae68600000000000fdffffff012202000000000000160014371b02d451081c0cf541aa6b96552aa435ce78910340d0644c8a26d02fad3139c8b72513c23184577c54a63706deb4946cc51bb62055f0129153763f006147fbd6ac837cacbc10e4cfc8187d0e6983f7621bfad5d24cfd5b012080c217aaa568136042683a4cfce040b51cd5b697eea59caddbb39c5bdc45a432ac0063010107736f762d6274630102407ba452bda72ead6ee34d52d28b4ac9b1a47e0481c0cd426b8d8eff6131e7e5292125e090e5af52e980fd522e787200d164819ec704be693a53a2f28fe69798eb01032102588d202afcc1ee4ab5254c7847ec25b9a135bbda0f2bc69ee1a714749fd77dc9010402d047004cbd1b6a01f845c786b10e90638b5cd880130e9823b06c10b9004040105286cbc48ac0dabd37e7953ccef1f0bf6a072a9ec41605c7ccb1ac343145ec66fbf699b0d46247cd6299ba45df757e4c54a7928c35921f9a2d2f1df0728d3a5e37c7e5e5591d40da8b3bcd0cb0c0ae7de59eba71a8dcb538056eed254ca4dbb46cad7025625c19df9d79e91bde6cb3dc1378fbccd2c87fb3498bbf4f66deeb803e121d96dc8d2ed28e8f10ba139b2207a96767b6d0dcaf4aeb795817fe6b88cd1a006821c180c217aaa568136042683a4cfce040b51cd5b697eea59caddbb39c5bdc45a43200000000 12 | 01000000000101ebab6ede978a6eb12efac83a8f541d1abdd834712afbc55db3d149ec7a0faa2e0100000000fdffffff029203000000000000225120adad0121a22d7ef0f92a5b66c1edc212094e4a9af1bf8325b3c0ce3db0c72b579cd3052a01000000160014a3261864b7c97ad065187bf15e8d8fad60906f33024730440220292714ffc9a5b2410dc84ae910d5e580e8d050d1b077b421a596fc66dda06da70220117010218f516c173069adc17a3518ddc7b07f9e311ef72339a88eb03843a6ea012103e994715280445faf73db4afa9c4d8b00d27173f8ef2ca5a0f2173b5c84f6aec800000000 13 | 01000000000101db75e1e9b22f92e2e03c1305a36290de0f2adee6bd16dd18127e7e793d01a6530000000000fdffffff012202000000000000160014371b02d451081c0cf541aa6b96552aa435ce78910340f64562ba7e3c6c90cfd44aa657d3c5be5d9417e10ead079d7851a9000fa499bfa22dffffbc1e876429668d3865030c6d33b3e31635d40da8d48632952cd1433bfd2c01203348c52c55bf875b73d9fab83ee4af35f09c20eed1ca9cf386e502e891650466ac0063010107736f762d62746301024050bc15fbbad1c3fad49c3abaac9731939428b2b87c19d07e7b60b6fc0c29234166cf0984f5ddddc7b4b8445e69caaf8165eaccf17cb9063e59a05ff60a0e51a201032102588d202afcc1ee4ab5254c7847ec25b9a135bbda0f2bc69ee1a714749fd77dc901040399da00004c8d1b0f01f845c786b18e23608bdcde6013b582540020200829c361cea97133d8718d123f04d64e75ce2b799c43f1ff7b1da850892d0a8e99635969628ad8cdf6ed3361a9c5ce330b0000dda665bf0a7f448c2be3bb33afa7c39b6d967b026f9f591af93a9bb4f8ab32a3da017d246f587237bae8757c84d09dd81439e87976660bcdfdaab41ec5baf05f436cd6006821c13348c52c55bf875b73d9fab83ee4af35f09c20eed1ca9cf386e502e89165046600000000 --------------------------------------------------------------------------------