diff --git a/Cargo.lock b/Cargo.lock index 5361d3a51c..785a231332 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -874,7 +874,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f" dependencies = [ - "bitcoin-internals 0.3.0", + "bitcoin-internals", "bitcoin_hashes 0.14.0", ] @@ -927,12 +927,6 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" -[[package]] -name = "bech32" -version = "0.10.0-beta" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98f7eed2b2781a6f0b5c903471d48e15f56fb4e1165df8a9a2337fd1a59d45ea" - [[package]] name = "bech32" version = "0.11.0" @@ -1039,17 +1033,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "bip39" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f2635620bf0b9d4576eb7bb9a38a55df78bd1205d26fa994b25911a69f212f" -dependencies = [ - "bitcoin_hashes 0.11.0", - "serde 1.0.210", - "unicode-normalization", -] - [[package]] name = "bit-set" version = "0.5.3" @@ -1078,21 +1061,6 @@ dependencies = [ "secp256k1 0.27.0", ] -[[package]] -name = "bitcoin" -version = "0.31.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c85783c2fe40083ea54a33aa2f0ba58831d90fcd190f5bdc47e74e84d2a96ae" -dependencies = [ - "bech32 0.10.0-beta", - "bitcoin-internals 0.2.0", - "bitcoin_hashes 0.13.0", - "hex-conservative 0.1.2", - "hex_lit", - "secp256k1 0.28.2", - "serde 1.0.210", -] - [[package]] name = "bitcoin" version = "0.32.2" @@ -1101,21 +1069,14 @@ checksum = "ea507acc1cd80fc084ace38544bbcf7ced7c2aa65b653b102de0ce718df668f6" dependencies = [ "base58ck", "bech32 0.11.0", - "bitcoin-internals 0.3.0", + "bitcoin-internals", "bitcoin-io", "bitcoin-units", "bitcoin_hashes 0.14.0", - "hex-conservative 0.2.1", + "bitcoinconsensus", + "hex-conservative", "hex_lit", "secp256k1 0.29.0", -] - -[[package]] -name = "bitcoin-internals" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb" -dependencies = [ "serde 1.0.210", ] @@ -1124,6 +1085,9 @@ name = "bitcoin-internals" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2" +dependencies = [ + "serde 1.0.210", +] [[package]] name = "bitcoin-io" @@ -1137,7 +1101,7 @@ version = "0.7.0" dependencies = [ "anyhow", "axum 0.7.5", - "bitcoin 0.31.2", + "bitcoin 0.32.2", "brotli 3.5.0", "hex", "move-binary-format", @@ -1166,15 +1130,10 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5285c8bcaa25876d07f37e3d30c303f2609179716e11d688f51e8f1fe70063e2" dependencies = [ - "bitcoin-internals 0.3.0", + "bitcoin-internals", + "serde 1.0.210", ] -[[package]] -name = "bitcoin_hashes" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90064b8dee6815a6470d60bad07bbbaee885c0e12d04177138fa3291a01b7bc4" - [[package]] name = "bitcoin_hashes" version = "0.12.0" @@ -1182,35 +1141,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d7066118b13d4b20b23645932dfb3a81ce7e29f95726c2036fa33cd7b092501" dependencies = [ "bitcoin-private", - "serde 1.0.210", ] [[package]] name = "bitcoin_hashes" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b" +checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" dependencies = [ - "bitcoin-internals 0.2.0", - "hex-conservative 0.1.2", + "bitcoin-io", + "hex-conservative", "serde 1.0.210", ] [[package]] -name = "bitcoin_hashes" -version = "0.14.0" +name = "bitcoinconsensus" +version = "0.105.0+25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" +checksum = "f260ac8fb2c621329013fc0ed371c940fcc512552dcbcb9095ed0179098c9e18" dependencies = [ - "bitcoin-io", - "hex-conservative 0.2.1", + "cc", ] [[package]] name = "bitcoincore-rpc" -version = "0.18.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eb70725a621848c83b3809913d5314c0d20ca84877d99dd909504b564edab00" +checksum = "aedd23ae0fd321affb4bbbc36126c6f49a32818dc6b979395d24da8c9d4e80ee" dependencies = [ "bitcoincore-rpc-json", "jsonrpc", @@ -1221,11 +1178,11 @@ dependencies = [ [[package]] name = "bitcoincore-rpc-json" -version = "0.18.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "856ffbee2e492c23bca715d72ea34aae80d58400f2bda26a82015d6bc2ec3662" +checksum = "d8909583c5fab98508e80ef73e5592a651c954993dc6b7739963257d19f0e71a" dependencies = [ - "bitcoin 0.31.2", + "bitcoin 0.32.2", "serde 1.0.210", "serde_json", ] @@ -4626,12 +4583,6 @@ dependencies = [ "serde 1.0.210", ] -[[package]] -name = "hex-conservative" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212ab92002354b4819390025006c897e8140934349e8635c9b077f47b4dcbd20" - [[package]] name = "hex-conservative" version = "0.2.1" @@ -5144,9 +5095,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", ] [[package]] @@ -5328,11 +5276,12 @@ dependencies = [ [[package]] name = "jsonrpc" -version = "0.14.1" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8128f36b47411cd3f044be8c1f5cc0c9e24d1d1bfdc45f0a57897b32513053f2" +checksum = "3662a38d341d77efecb73caf01420cfa5aa63c0253fd7bc05289ef9f6616e1bf" dependencies = [ "base64 0.13.1", + "minreq", "serde 1.0.210", "serde_json", ] @@ -6091,22 +6040,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] -name = "miniscript" -version = "12.2.0" +name = "miniz_oxide" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "add2d4aee30e4291ce5cffa3a322e441ff4d4bc57b38c8d9bf0e94faa50ab626" +checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" dependencies = [ - "bech32 0.11.0", - "bitcoin 0.32.2", + "adler", ] [[package]] -name = "miniz_oxide" -version = "0.7.3" +name = "minreq" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" +checksum = "763d142cdff44aaadd9268bebddb156ef6c65a0e13486bb81673cf2d8739f9b0" dependencies = [ - "adler", + "log", + "serde 1.0.210", + "serde_json", ] [[package]] @@ -7086,21 +7036,6 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" -[[package]] -name = "musig2" -version = "0.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bed08befaac75bfb31ca5e87678c4e8490bcd21d0c98ccb4f12f4065a7567e83" -dependencies = [ - "base16ct 0.2.0", - "hmac", - "once_cell", - "secp", - "secp256k1 0.28.2", - "sha2 0.10.8", - "subtle", -] - [[package]] name = "named-lock" version = "0.2.0" @@ -7229,29 +7164,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" -[[package]] -name = "nostr" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a38ff6fec17be97b6ea1e30a4913618c8e02b2e555b5188d01668f2e4fa09507" -dependencies = [ - "aes", - "base64 0.21.7", - "bech32 0.9.1", - "bip39", - "bitcoin 0.30.2", - "bitcoin_hashes 0.12.0", - "cbc", - "getrandom 0.2.15", - "instant", - "log", - "reqwest 0.11.27", - "secp256k1 0.27.0", - "serde 1.0.210", - "serde_json", - "url", -] - [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -8997,7 +8909,6 @@ dependencies = [ "tokio", "tokio-native-tls", "tokio-rustls 0.24.1", - "tokio-socks", "tokio-util", "tower-service", "url", @@ -9235,7 +9146,7 @@ dependencies = [ "anyhow", "async-trait", "bcs", - "bitcoin 0.31.2", + "bitcoin 0.32.2", "bitcoin-move", "chrono", "clap 4.5.13", @@ -9324,7 +9235,7 @@ version = "0.7.0" dependencies = [ "anyhow", "bcs", - "bitcoin 0.31.2", + "bitcoin 0.32.2", "bitcoincore-rpc", "bitcoincore-rpc-json", "clap 4.5.13", @@ -9500,7 +9411,7 @@ name = "rooch-framework" version = "0.7.0" dependencies = [ "bcs", - "bitcoin 0.31.2", + "bitcoin 0.32.2", "fastcrypto 0.1.8 (git+https://github.com/MystenLabs/fastcrypto?rev=56f6223b84ada922b6cb2c672c69db2ea3dc6a13)", "hex", "move-binary-format", @@ -9511,7 +9422,6 @@ dependencies = [ "moveos", "moveos-stdlib", "moveos-types", - "musig2", "rooch-types", "smallvec", "tracing", @@ -9523,7 +9433,7 @@ version = "0.7.0" dependencies = [ "anyhow", "bcs", - "bitcoin 0.31.2", + "bitcoin 0.32.2", "bitcoin-move", "clap 4.5.13", "coerce", @@ -9533,13 +9443,11 @@ dependencies = [ "hex", "include_dir", "metrics", - "miniscript", "move-core-types", "moveos-config", "moveos-eventbus", "moveos-store", "moveos-types", - "musig2", "rand 0.8.5", "rooch-config", "rooch-db", @@ -9677,7 +9585,7 @@ name = "rooch-nursery" version = "0.7.0" dependencies = [ "bcs", - "bitcoin 0.31.2", + "bitcoin 0.32.2", "ciborium", "fastcrypto 0.1.8 (git+https://github.com/MystenLabs/fastcrypto?rev=56f6223b84ada922b6cb2c672c69db2ea3dc6a13)", "hex", @@ -9692,7 +9600,6 @@ dependencies = [ "moveos-stdlib", "moveos-types", "moveos-wasm", - "musig2", "rooch-framework", "rooch-types", "serde_json", @@ -9759,7 +9666,7 @@ version = "0.7.0" dependencies = [ "anyhow", "async-trait", - "bitcoin 0.31.2", + "bitcoin 0.32.2", "bitcoincore-rpc", "chrono", "coerce", @@ -9829,7 +9736,7 @@ version = "0.7.0" dependencies = [ "anyhow", "async-trait", - "bitcoin 0.31.2", + "bitcoin 0.32.2", "bitcoincore-rpc", "chrono", "coerce", @@ -9858,7 +9765,7 @@ version = "0.7.0" dependencies = [ "anyhow", "bcs", - "bitcoin 0.31.2", + "bitcoin 0.32.2", "ethers", "hex", "jsonrpsee 0.23.2", @@ -9866,7 +9773,6 @@ dependencies = [ "move-core-types", "move-resource-viewer", "moveos-types", - "nostr", "rooch-open-rpc", "rooch-open-rpc-macros", "rooch-types", @@ -9883,6 +9789,7 @@ version = "0.7.0" dependencies = [ "anyhow", "bcs", + "bitcoin 0.32.2", "futures", "hex", "jsonrpsee 0.23.2", @@ -9896,6 +9803,7 @@ dependencies = [ "serde 1.0.210", "serde_json", "tokio", + "tracing", ] [[package]] @@ -10011,7 +9919,7 @@ dependencies = [ "argon2", "bcs", "bech32 0.11.0", - "bitcoin 0.31.2", + "bitcoin 0.32.2", "bitcoincore-rpc", "bs58 0.5.1", "chacha20poly1305", @@ -10024,14 +9932,12 @@ dependencies = [ "framework-builder", "framework-types", "hex", - "miniscript", "move-binary-format", "move-command-line-common", "move-core-types", "move-resource-viewer", "move-vm-types", "moveos-types", - "nostr", "once_cell", "proptest", "proptest-derive", @@ -10045,6 +9951,7 @@ dependencies = [ "strum", "strum_macros", "thiserror", + "tracing", ] [[package]] @@ -10540,18 +10447,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "secp" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c4754628ff9006f80c6abd1cd1e88c5ca6f5a60eab151ad2e16268aab3514d0" -dependencies = [ - "base16ct 0.2.0", - "once_cell", - "secp256k1 0.28.2", - "subtle", -] - [[package]] name = "secp256k1" version = "0.27.0" @@ -10561,19 +10456,6 @@ dependencies = [ "bitcoin_hashes 0.12.0", "rand 0.8.5", "secp256k1-sys 0.8.1", - "serde 1.0.210", -] - -[[package]] -name = "secp256k1" -version = "0.28.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" -dependencies = [ - "bitcoin_hashes 0.13.0", - "rand 0.8.5", - "secp256k1-sys 0.9.2", - "serde 1.0.210", ] [[package]] @@ -10585,6 +10467,7 @@ dependencies = [ "bitcoin_hashes 0.14.0", "rand 0.8.5", "secp256k1-sys 0.10.0", + "serde 1.0.210", ] [[package]] @@ -10596,15 +10479,6 @@ dependencies = [ "cc", ] -[[package]] -name = "secp256k1-sys" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d1746aae42c19d583c3c1a8c646bfad910498e2051c551a7f2e3c0c9fbb7eb" -dependencies = [ - "cc", -] - [[package]] name = "secp256k1-sys" version = "0.10.0" @@ -11248,7 +11122,7 @@ dependencies = [ "anyhow", "backtrace", "bcs", - "bitcoin_hashes 0.13.0", + "bitcoin_hashes 0.14.0", "byteorder", "bytes", "function_name", @@ -12024,18 +11898,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-socks" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51165dfa029d2a65969413a6cc96f354b86b464498702f174a4efa13608fd8c0" -dependencies = [ - "either", - "futures-util", - "thiserror", - "tokio", -] - [[package]] name = "tokio-stream" version = "0.1.15" diff --git a/Cargo.toml b/Cargo.toml index 26c2cb1e57..044eb7cdfe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -149,9 +149,10 @@ bcs = "0.1.3" bytes = "1.7.1" bech32 = "0.11.0" better_any = "0.1.1" -bitcoin = { version = "0.31.0", features = ["rand-std"] } -bitcoin_hashes = { version = "0.13.0", features = ["serde-std"]} -bitcoincore-rpc = "0.18.0" +bitcoin = { version = "0.32.2", features = ["rand-std", "bitcoinconsensus"] } +bitcoin_hashes = { version = "0.14.0", features = ["serde"] } +bitcoincore-rpc = "0.19.0" +bitcoincore-rpc-json = "0.19.0" bip32 = "0.4.0" byteorder = "1.4.3" clap = { version = "4.5.13", features = ["derive", "env"] } @@ -242,7 +243,6 @@ hyper = { version = "1.0.0", features = ["full"] } num_enum = "0.7.3" libc = "^0.2" include_dir = { version = "0.6.2" } -nostr = "0.22" serde-reflection = "0.3.6" serde-generate = "0.25.1" bcs-ext = { path = "moveos/moveos-commons/bcs_ext" } @@ -316,7 +316,6 @@ pprof = { version = "0.13.0", features = ["flamegraph", "criterion", "cpp", "fra celestia-rpc = { git = "https://github.com/eigerco/celestia-node-rs.git", rev = "129272e8d926b4c7badf27a26dea915323dd6489" } celestia-types = { git = "https://github.com/eigerco/celestia-node-rs.git", rev = "129272e8d926b4c7badf27a26dea915323dd6489" } opendal = { version = "0.47.3", features = ["services-fs", "services-gcs"] } -bitcoincore-rpc-json = "0.18.0" toml = "0.8.19" csv = "1.2.1" revm-precompile = "7.0.0" @@ -336,8 +335,6 @@ rustc-hash = { version = "2.0.0" } xorf = { version = "0.11.0" } vergen-git2 = { version = "1.0.0", features = ["build", "cargo", "rustc"] } vergen-pretty = "0.3.4" -musig2 = { version = "0.0.11" } -miniscript = "12.2.0" crossbeam-channel = "0.5.13" # Note: the BEGIN and END comments below are required for external tooling. Do not remove. diff --git a/crates/rooch-framework-tests/Cargo.toml b/crates/rooch-framework-tests/Cargo.toml index 59460cfb0d..582bc5a55e 100644 --- a/crates/rooch-framework-tests/Cargo.toml +++ b/crates/rooch-framework-tests/Cargo.toml @@ -25,8 +25,6 @@ tracing = { workspace = true } tracing-subscriber = { workspace = true } tempfile = { workspace = true } include_dir = { workspace = true } -musig2 = { workspace = true } -miniscript = { workspace = true } coerce = { workspace = true } tokio = { workspace = true } clap = { features = ["derive", ], workspace = true } diff --git a/crates/rooch-framework-tests/src/bitcoin_block_tester.rs b/crates/rooch-framework-tests/src/bitcoin_block_tester.rs index f83bdc0814..b305e87dab 100644 --- a/crates/rooch-framework-tests/src/bitcoin_block_tester.rs +++ b/crates/rooch-framework-tests/src/bitcoin_block_tester.rs @@ -141,7 +141,7 @@ impl BitcoinBlockTester { let mut utxo_set = HashMap::::new(); let block_data = self.executed_block.as_ref().unwrap(); for tx in block_data.block.txdata.as_slice() { - let txid = tx.txid(); + let txid = tx.compute_txid(); for (index, tx_out) in tx.output.iter().enumerate() { let vout = index as u32; let out_point = OutPoint::new(txid, vout); @@ -486,7 +486,7 @@ impl TesterGenesisBuilder { ); for tx in &block.txdata { - self.block_txids.insert(tx.txid()); + self.block_txids.insert(tx.compute_txid()); } let depdent_txids = block diff --git a/crates/rooch-framework-tests/src/tests/bitcoin_test.rs b/crates/rooch-framework-tests/src/tests/bitcoin_test.rs index 7648b2ce6e..68ecfe44df 100644 --- a/crates/rooch-framework-tests/src/tests/bitcoin_test.rs +++ b/crates/rooch-framework-tests/src/tests/bitcoin_test.rs @@ -100,7 +100,8 @@ fn check_utxo(txs: Vec, binding_test: &binding_test::RustBindingTes for tx in txs.as_slice() { for (index, tx_out) in tx.output.iter().enumerate() { let vout = index as u32; - let out_point = OutPoint::new(tx.txid(), vout); + let txid = tx.compute_txid(); + let out_point = OutPoint::new(txid, vout); utxo_set.insert(out_point, tx_out.clone()); } for tx_in in tx.input.iter() { @@ -147,7 +148,7 @@ fn check_utxo(txs: Vec, binding_test: &binding_test::RustBindingTes let inscriptions = txs .iter() .flat_map(|tx| { - let txid = tx.txid(); + let txid = tx.compute_txid(); let rooch_btc_tx = rooch_types::bitcoin::types::Transaction::from(tx.clone()); ord_module .parse_inscription_from_tx(&rooch_btc_tx) diff --git a/crates/rooch-framework-tests/src/tests/brc20_test.rs b/crates/rooch-framework-tests/src/tests/brc20_test.rs index 2fe050a499..dfb9b1215f 100644 --- a/crates/rooch-framework-tests/src/tests/brc20_test.rs +++ b/crates/rooch-framework-tests/src/tests/brc20_test.rs @@ -9,7 +9,8 @@ fn decode_tx(btx_tx_hex: &str) { let btc_tx_bytes = Vec::from_hex(btx_tx_hex).unwrap(); let btc_tx: bitcoin::Transaction = Decodable::consensus_decode(&mut btc_tx_bytes.as_slice()).unwrap(); - debug!("tx_id: {}", btc_tx.txid()); + let txid = btc_tx.compute_txid(); + debug!("tx_id: {}", txid); for (i, input) in btc_tx.input.iter().enumerate() { debug!("{}. input: {:?}", i, input.previous_output); } diff --git a/crates/rooch-framework-tests/src/tests/ord_test.rs b/crates/rooch-framework-tests/src/tests/ord_test.rs index ee92678fd2..cd70bda836 100644 --- a/crates/rooch-framework-tests/src/tests/ord_test.rs +++ b/crates/rooch-framework-tests/src/tests/ord_test.rs @@ -17,7 +17,8 @@ fn decode_inscription( binding_test: &mut binding_test::RustBindingTest, btc_tx: Transaction, ) -> Vec> { - debug!("tx_id: {}", btc_tx.txid()); + let txid = btc_tx.compute_txid(); + debug!("tx_id: {}", txid); for (i, input) in btc_tx.input.iter().enumerate() { debug!("{}. input: {:?}", i, input.previous_output); } diff --git a/crates/rooch-key/src/keystore/account_keystore.rs b/crates/rooch-key/src/keystore/account_keystore.rs index 8207b48e71..fcf6a45bc3 100644 --- a/crates/rooch-key/src/keystore/account_keystore.rs +++ b/crates/rooch-key/src/keystore/account_keystore.rs @@ -90,8 +90,21 @@ pub trait AccountKeystore { Ok(()) } + /// Get all local accounts + //TODO refactor the keystore, save the public key out of the encryption data, so that we don't need to require password to get the public key fn get_accounts(&self, password: Option) -> Result, anyhow::Error>; + /// Get local account by address + fn get_account( + &self, + address: &RoochAddress, + password: Option, + ) -> Result, anyhow::Error> { + let accounts = self.get_accounts(password)?; + let account = accounts.iter().find(|account| account.address == *address); + Ok(account.cloned()) + } + fn contains_address(&self, address: &RoochAddress) -> bool; fn add_address_encryption_data_to_keys( diff --git a/crates/rooch-key/src/keystore/base_keystore.rs b/crates/rooch-key/src/keystore/base_keystore.rs index 78f3b2ba6e..b544cb60af 100644 --- a/crates/rooch-key/src/keystore/base_keystore.rs +++ b/crates/rooch-key/src/keystore/base_keystore.rs @@ -6,6 +6,7 @@ use crate::keystore::account_keystore::AccountKeystore; use anyhow::{ensure, Ok}; use rooch_types::framework::session_key::SessionKey; use rooch_types::key_struct::{MnemonicData, MnemonicResult}; +use rooch_types::to_bech32::ToBech32; use rooch_types::{ address::RoochAddress, authentication_key::AuthenticationKey, @@ -74,7 +75,7 @@ impl AccountKeystore for BaseKeyStore { let keypair: RoochKeyPair = encryption.decrypt_with_type(password.clone())?; let public_key = keypair.public(); let bitcoin_address = public_key.bitcoin_address()?; - let nostr_bech32_public_key = public_key.nostr_bech32_public_key()?; + let nostr_bech32_public_key = public_key.xonly_public_key()?.to_bech32()?; let has_session_key = self.session_keys.contains_key(address); let local_account = LocalAccount { address: *address, @@ -102,10 +103,9 @@ impl AccountKeystore for BaseKeyStore { let keypair: RoochKeyPair = encryption.decrypt_with_type::(password)?; Ok(keypair) } else { - Err(anyhow::Error::new(RoochError::SignMessageError(format!( - "Cannot find key for address: [{:?}]", - address - )))) + Err(anyhow::Error::new(RoochError::CommandArgumentError( + format!("Cannot find key for address: [{:?}]", address), + ))) } } diff --git a/crates/rooch-open-rpc-spec/schemas/openrpc.json b/crates/rooch-open-rpc-spec/schemas/openrpc.json index 2a05520dc7..4c18bd9041 100644 --- a/crates/rooch-open-rpc-spec/schemas/openrpc.json +++ b/crates/rooch-open-rpc-spec/schemas/openrpc.json @@ -910,7 +910,7 @@ ], "properties": { "txid": { - "$ref": "#/components/schemas/bitcoin::hash_types::newtypes::Txid" + "$ref": "#/components/schemas/bitcoin::blockdata::transaction::Txid" }, "vout": { "type": "integer", @@ -3104,7 +3104,7 @@ "description": "The txid of the UTXO", "allOf": [ { - "$ref": "#/components/schemas/bitcoin::hash_types::newtypes::Txid" + "$ref": "#/components/schemas/bitcoin::blockdata::transaction::Txid" } ] }, @@ -3248,7 +3248,7 @@ "alloc::vec::Vec": { "type": "string" }, - "bitcoin::hash_types::newtypes::Txid": { + "bitcoin::blockdata::transaction::Txid": { "type": "string" }, "move_binary_format::file_format::Ability": { diff --git a/crates/rooch-rpc-api/Cargo.toml b/crates/rooch-rpc-api/Cargo.toml index 36abde2f73..f82230e608 100644 --- a/crates/rooch-rpc-api/Cargo.toml +++ b/crates/rooch-rpc-api/Cargo.toml @@ -23,7 +23,6 @@ serde_json = { workspace = true } thiserror = { workspace = true } schemars = { workspace = true } bitcoin = { workspace = true } -nostr = { workspace = true } move-core-types = { workspace = true } move-resource-viewer = { workspace = true } diff --git a/crates/rooch-rpc-api/src/jsonrpc_types/address.rs b/crates/rooch-rpc-api/src/jsonrpc_types/address.rs index 019b294e17..22f9577ba8 100644 --- a/crates/rooch-rpc-api/src/jsonrpc_types/address.rs +++ b/crates/rooch-rpc-api/src/jsonrpc_types/address.rs @@ -3,11 +3,12 @@ use crate::jsonrpc_types::StrView; use anyhow::Result; +use bitcoin::XOnlyPublicKey; use move_core_types::account_address::AccountAddress; -use nostr::{key::XOnlyPublicKey, prelude::FromBech32}; use rooch_types::{ address::{BitcoinAddress, NostrPublicKey, RoochAddress}, bitcoin::network::Network, + to_bech32::FromBech32, }; use std::str::FromStr; @@ -66,6 +67,7 @@ impl From for AccountAddress { } } +//TODO directly use UnitedAddress and remove UnitedAddressView #[derive(Debug, Clone)] pub struct UnitedAddress { pub rooch_address: RoochAddress, @@ -90,6 +92,7 @@ impl std::fmt::Display for UnitedAddressView { impl FromStr for UnitedAddressView { type Err = anyhow::Error; fn from_str(s: &str) -> Result { + //TODO use the prefix to determine the type of address match RoochAddress::from_str(s) { Ok(rooch_address) => Ok(StrView(UnitedAddress { rooch_address, @@ -165,3 +168,34 @@ impl TryFrom for NostrPublicKey { } } } + +impl From for UnitedAddressView { + fn from(value: BitcoinAddress) -> Self { + StrView(UnitedAddress { + rooch_address: value.to_rooch_address(), + bitcoin_address: Some(value), + nostr_public_key: None, + }) + } +} + +impl From for UnitedAddressView { + fn from(value: RoochAddress) -> Self { + StrView(UnitedAddress { + rooch_address: value, + bitcoin_address: None, + nostr_public_key: None, + }) + } +} + +impl From for UnitedAddressView { + fn from(value: bitcoin::Address) -> Self { + let value = BitcoinAddress::from(value); + StrView(UnitedAddress { + rooch_address: value.to_rooch_address(), + bitcoin_address: Some(value), + nostr_public_key: None, + }) + } +} diff --git a/crates/rooch-rpc-api/src/jsonrpc_types/btc/transaction.rs b/crates/rooch-rpc-api/src/jsonrpc_types/btc/transaction.rs index 6780401487..228cea11ae 100644 --- a/crates/rooch-rpc-api/src/jsonrpc_types/btc/transaction.rs +++ b/crates/rooch-rpc-api/src/jsonrpc_types/btc/transaction.rs @@ -3,7 +3,6 @@ use crate::jsonrpc_types::StrView; use anyhow::Result; -use bitcoin::hashes::Hash; use bitcoin::Txid; use std::fmt; use std::str::FromStr; @@ -11,9 +10,8 @@ use std::str::FromStr; pub type TxidView = StrView; impl fmt::Display for TxidView { - //TODO check display format fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{:x}", self.0) + write!(f, "{}", self.0) } } @@ -30,12 +28,6 @@ impl From for Txid { } } -pub fn hex_to_txid(hex_string: &str) -> Result { - let mut bytes = hex::decode(hex_string)?; - bytes.reverse(); - Ok(Txid::from_slice(&bytes)?) -} - #[cfg(test)] mod test { use super::*; @@ -44,8 +36,10 @@ mod test { #[test] fn test_txid() -> Result<()> { let txid_str = "5fddcbdc3eb21a93e8dd1dd3f9087c3677f422b82d5ba39a6b1ec37338154af6"; - let txid = hex_to_txid(txid_str)?; + let txid_view = TxidView::from_str(txid_str)?; + let txid = Txid::from_str(txid_str)?; let txid_str2 = txid.to_string(); + assert!(txid_view.0 == txid); assert!(txid_str == txid_str2); Ok(()) diff --git a/crates/rooch-rpc-api/src/jsonrpc_types/btc/utxo.rs b/crates/rooch-rpc-api/src/jsonrpc_types/btc/utxo.rs index ffc7be8228..73e611f6a1 100644 --- a/crates/rooch-rpc-api/src/jsonrpc_types/btc/utxo.rs +++ b/crates/rooch-rpc-api/src/jsonrpc_types/btc/utxo.rs @@ -1,15 +1,16 @@ // Copyright (c) RoochNetwork // SPDX-License-Identifier: Apache-2.0 -use crate::jsonrpc_types::btc::transaction::{hex_to_txid, TxidView}; +use crate::jsonrpc_types::btc::transaction::TxidView; use crate::jsonrpc_types::{ H256View, IndexerObjectStateView, IndexerStateIDView, ObjectIDVecView, ObjectIDView, ObjectMetaView, StrView, UnitedAddressView, }; use anyhow::Result; use bitcoin::hashes::Hash; -use bitcoin::Txid; +use bitcoin::{Amount, Txid}; +use moveos_types::move_std::string::MoveString; use moveos_types::state::{MoveState, MoveStructType}; use rooch_types::bitcoin::types::OutPoint; use rooch_types::bitcoin::utxo::{self, UTXO}; @@ -18,6 +19,7 @@ use rooch_types::into_address::{FromAddress, IntoAddress}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use std::str::FromStr; #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Eq, JsonSchema)] pub struct BitcoinOutPointView { @@ -54,6 +56,16 @@ pub enum UTXOFilterView { } impl UTXOFilterView { + pub fn owner>(owner: A) -> Self { + let addr: UnitedAddressView = owner.into(); + UTXOFilterView::Owner(addr) + } + + pub fn object_ids>(object_ids: A) -> Self { + let object_ids: ObjectIDVecView = object_ids.into(); + UTXOFilterView::ObjectId(object_ids) + } + pub fn into_global_state_filter(filter_opt: UTXOFilterView) -> Result { Ok(match filter_opt { UTXOFilterView::Owner(owner) => ObjectStateFilter::ObjectTypeWithOwner { @@ -62,7 +74,7 @@ impl UTXOFilterView { owner: owner.0.rooch_address.into(), }, UTXOFilterView::OutPoint { txid, vout } => { - let txid = hex_to_txid(txid.as_str())?; + let txid = Txid::from_str(&txid)?; let outpoint = rooch_types::bitcoin::types::OutPoint::new(txid.into_address(), vout); let utxo_id = utxo::derive_utxo_id(&outpoint); @@ -79,15 +91,15 @@ impl UTXOFilterView { #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct UTXOView { /// The txid of the UTXO - txid: H256View, + pub txid: H256View, /// The txid of the UTXO - bitcoin_txid: TxidView, + pub bitcoin_txid: TxidView, /// The vout of the UTXO - vout: u32, + pub vout: u32, /// The value of the UTXO - value: StrView, + pub value: StrView, /// Protocol seals - seals: HashMap>, + pub seals: HashMap>, } impl UTXOView { @@ -116,11 +128,46 @@ impl UTXOView { }) } - pub fn get_value(&self) -> u64 { + pub fn value(&self) -> u64 { self.value.0 } + + pub fn txid(&self) -> Txid { + self.bitcoin_txid.0 + } + + pub fn amount(&self) -> Amount { + Amount::from_sat(self.value()) + } + + pub fn outpoint(&self) -> OutPoint { + OutPoint { + txid: self.txid.0.into_address(), + vout: self.vout, + } + } } +impl From for UTXO { + fn from(view: UTXOView) -> Self { + UTXO { + txid: view.txid.0.into_address(), + vout: view.vout, + value: view.value.0, + seals: view + .seals + .into_iter() + .map(|(k, v)| { + ( + MoveString::from(k), + v.into_iter().map(|id| id.0).collect::>(), + ) + }) + .collect::>() + .into(), + } + } +} #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct UTXOStateView { #[serde(flatten)] diff --git a/crates/rooch-rpc-client/Cargo.toml b/crates/rooch-rpc-client/Cargo.toml index b2d7526b24..6ed3b56f70 100644 --- a/crates/rooch-rpc-client/Cargo.toml +++ b/crates/rooch-rpc-client/Cargo.toml @@ -23,6 +23,8 @@ jsonrpsee = { workspace = true } serde_json = { workspace = true } log = { workspace = true } hex = { workspace = true } +bitcoin = { workspace = true } +tracing = { workspace = true } move-core-types = { workspace = true } diff --git a/crates/rooch-rpc-client/src/rooch_client.rs b/crates/rooch-rpc-client/src/rooch_client.rs index ca9d1ab73a..f943d0a965 100644 --- a/crates/rooch-rpc-client/src/rooch_client.rs +++ b/crates/rooch-rpc-client/src/rooch_client.rs @@ -3,8 +3,12 @@ use anyhow::{Ok, Result}; use jsonrpsee::http_client::HttpClient; +use move_core_types::account_address::AccountAddress; use moveos_types::h256::H256; +use moveos_types::move_std::string::MoveString; use moveos_types::moveos_std::account::Account; +use moveos_types::moveos_std::object::ObjectID; +use moveos_types::state::{FieldKey, MoveStructState}; use moveos_types::{access_path::AccessPath, state::ObjectState, transaction::FunctionCall}; use rooch_rpc_api::api::btc_api::BtcAPIClient; use rooch_rpc_api::api::rooch_api::RoochAPIClient; @@ -16,14 +20,18 @@ use rooch_rpc_api::jsonrpc_types::{ DryRunTransactionResponseView, InscriptionPageView, UTXOPageView, }; use rooch_rpc_api::jsonrpc_types::{ - AccessPathView, AnnotatedFunctionResultView, BalanceInfoPageView, EventOptions, EventPageView, - FieldKeyView, ObjectIDView, RoochAddressView, StateOptions, StatePageView, StructTagView, + AccessPathView, AnnotatedFunctionResultView, BalanceInfoPageView, BytesView, EventOptions, + EventPageView, FieldKeyView, ObjectIDVecView, ObjectIDView, RoochAddressView, StateOptions, + StatePageView, StructTagView, }; use rooch_rpc_api::jsonrpc_types::{ExecuteTransactionResponseView, ObjectStateView}; use rooch_rpc_api::jsonrpc_types::{ IndexerObjectStatePageView, ObjectStateFilterView, QueryOptions, }; use rooch_rpc_api::jsonrpc_types::{TransactionWithInfoPageView, TxOptions}; +use rooch_types::address::BitcoinAddress; +use rooch_types::bitcoin::multisign_account::MultisignAccountInfo; +use rooch_types::framework::address_mapping::RoochToBitcoinAddressMapping; use rooch_types::indexer::state::IndexerStateID; use rooch_types::transaction::RoochTransactionData; use rooch_types::{address::RoochAddress, transaction::rooch::RoochTransaction}; @@ -217,6 +225,24 @@ impl RoochRpcClient { .await?) } + pub async fn resolve_bitcoin_address( + &self, + address: RoochAddress, + ) -> Result> { + let object_id = RoochToBitcoinAddressMapping::object_id(); + let field_key = FieldKey::derive_from_address(&address.into()); + let mut field = self + .get_field_states(object_id.into(), vec![field_key.into()], None) + .await?; + let field_obj = field.pop().flatten(); + let bitcoin_address = field_obj.map(|state_view| { + let state = ObjectState::from(state_view); + let df = state.value_as_df::()?; + Ok(df.value) + }); + bitcoin_address.transpose() + } + pub async fn list_field_states( &self, object_id: ObjectIDView, @@ -274,6 +300,20 @@ impl RoochRpcClient { .await?) } + pub async fn get_object_states( + &self, + object_ids: Vec, + state_option: Option, + ) -> Result>> { + if object_ids.is_empty() { + return Ok(vec![]); + } + Ok(self + .http + .get_object_states(ObjectIDVecView::from(object_ids), state_option) + .await?) + } + pub async fn query_object_states( &self, filter: ObjectStateFilterView, @@ -297,7 +337,7 @@ impl RoochRpcClient { filter: UTXOFilterView, cursor: Option, limit: Option, - query_options: Option, + descending_order: Option, ) -> Result { Ok(self .http @@ -305,7 +345,7 @@ impl RoochRpcClient { filter, cursor.map(Into::into), limit.map(Into::into), - query_options.map(|v| v.descending), + descending_order, ) .await?) } @@ -327,4 +367,43 @@ impl RoochRpcClient { ) .await?) } + + pub async fn get_resource( + &self, + account: RoochAddress, + ) -> Result> { + let access_path = AccessPath::resource(account.into(), T::struct_tag()); + let mut states = self.get_states(access_path).await?; + let state = states.pop().flatten(); + if let Some(state) = state { + let state = ObjectState::from(state); + let resource = state.value_as_df::()?; + Ok(Some(resource.value)) + } else { + Ok(None) + } + } + + pub async fn get_multisign_account_info( + &self, + address: RoochAddress, + ) -> Result { + Ok(self + .get_resource::(address) + .await? + .ok_or_else(|| { + anyhow::anyhow!( + "Can not find multisign account info for address {}", + address + ) + })?) + } + + pub async fn broadcast_bitcoin_tx( + &self, + raw_tx: BytesView, + maxfeerate: Option, + ) -> Result { + Ok(self.http.broadcast_tx(raw_tx, maxfeerate, None).await?) + } } diff --git a/crates/rooch-rpc-client/src/wallet_context.rs b/crates/rooch-rpc-client/src/wallet_context.rs index 1a2fae6202..fd55c7e925 100644 --- a/crates/rooch-rpc-client/src/wallet_context.rs +++ b/crates/rooch-rpc-client/src/wallet_context.rs @@ -4,6 +4,10 @@ use crate::client_config::{ClientConfig, DEFAULT_EXPIRATION_SECS}; use crate::Client; use anyhow::{anyhow, Result}; +use bitcoin::key::Secp256k1; +use bitcoin::psbt::{GetKey, KeyRequest}; +use bitcoin::secp256k1::Signing; +use bitcoin::PrivateKey; use move_core_types::account_address::AccountAddress; use moveos_types::moveos_std::gas_schedule::GasScheduleConfig; use moveos_types::transaction::MoveAction; @@ -15,18 +19,22 @@ use rooch_key::keystore::Keystore; use rooch_rpc_api::jsonrpc_types::{ DryRunTransactionResponseView, ExecuteTransactionResponseView, KeptVMStatusView, TxOptions, }; -use rooch_types::address::ParsedAddress; use rooch_types::address::RoochAddress; -use rooch_types::addresses; +use rooch_types::address::{BitcoinAddress, ParsedAddress}; +use rooch_types::bitcoin::network::Network; use rooch_types::crypto::RoochKeyPair; use rooch_types::error::{RoochError, RoochResult}; +use rooch_types::rooch_network::{BuiltinChainID, RoochNetwork}; use rooch_types::transaction::rooch::{RoochTransaction, RoochTransactionData}; +use rooch_types::{addresses, crypto}; use std::collections::BTreeMap; use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; use tokio::sync::RwLock; +use tracing::{debug, info}; +#[derive(Debug)] pub struct WalletContext { client: Arc>>, pub client_config: PersistedConfig, @@ -48,7 +56,7 @@ impl WalletContext { ) })?; - let client_config = client_config.persisted(&client_config_path); + let mut client_config = client_config.persisted(&client_config_path); let keystore_result = FileBasedKeystore::load(&client_config.keystore_path); let keystore = match keystore_result { @@ -60,7 +68,21 @@ impl WalletContext { address_mapping.extend(addresses::rooch_framework_named_addresses()); //TODO support account name alias name. - if let Some(active_address) = client_config.active_address { + if let Some(active_address) = &client_config.active_address { + let active_address = if !keystore.contains_address(active_address) { + //The active address is not in the keystore, maybe the user reset the keystore. + //We auto change the active address to the first address in the keystore. + let first_address = keystore + .addresses() + .pop() + .ok_or_else(|| anyhow!("No address in the keystore"))?; + info!("The active address {} is not in the keystore, auto change the active address to the first address in the keystore: {}", active_address, first_address); + client_config.active_address = Some(first_address); + client_config.save()?; + first_address + } else { + *active_address + }; address_mapping.insert("default".to_string(), active_address.into()); } @@ -83,12 +105,50 @@ impl WalletContext { } pub fn resolve_address(&self, parsed_address: ParsedAddress) -> RoochResult { + self.resolve_rooch_address(parsed_address) + .map(|address| address.into()) + } + + pub fn resolve_rooch_address( + &self, + parsed_address: ParsedAddress, + ) -> RoochResult { match parsed_address { - ParsedAddress::Numerical(address) => Ok(address.into()), - ParsedAddress::Named(name) => { - self.address_mapping.get(&name).cloned().ok_or_else(|| { + ParsedAddress::Numerical(address) => Ok(address), + ParsedAddress::Named(name) => self + .address_mapping + .get(&name) + .cloned() + .map(|address| address.into()) + .ok_or_else(|| { RoochError::CommandArgumentError(format!("Unknown named address: {}", name)) - }) + }), + ParsedAddress::Bitcoin(address) => Ok(address.to_rooch_address()), + } + } + + pub async fn resolve_bitcoin_address( + &self, + parsed_address: ParsedAddress, + ) -> RoochResult { + match parsed_address { + ParsedAddress::Bitcoin(address) => Ok(address), + _ => { + let address = self.resolve_rooch_address(parsed_address)?; + let account = self.keystore.get_account(&address, self.password.clone())?; + if let Some(account) = account { + let bitcoin_address = account.bitcoin_address; + Ok(bitcoin_address) + } else { + let client = self.get_client().await?; + let bitcoin_address = client.rooch.resolve_bitcoin_address(address).await?; + bitcoin_address.ok_or_else(|| { + RoochError::CommandArgumentError(format!( + "Cannot resolve bitcoin address from {}", + address + )) + }) + } } } } @@ -242,4 +302,50 @@ impl WalletContext { pub fn get_password(&self) -> Option { self.password.clone() } + + pub async fn get_rooch_network(&self) -> Result { + let client = self.get_client().await?; + let chain_id = client.rooch.get_chain_id().await?; + //TODO support custom chain id + let builtin_chain_id = BuiltinChainID::try_from(chain_id)?; + Ok(builtin_chain_id.into()) + } + + pub async fn get_bitcoin_network(&self) -> Result { + let rooch_network = self.get_rooch_network().await?; + let bitcoin_network = rooch_types::bitcoin::network::Network::from( + rooch_network.genesis_config.bitcoin_network, + ); + Ok(bitcoin_network) + } +} + +impl GetKey for WalletContext { + type Error = anyhow::Error; + + fn get_key( + &self, + key_request: KeyRequest, + _secp: &Secp256k1, + ) -> Result, Self::Error> { + debug!("Get key for key_request: {:?}", key_request); + let address = match key_request { + KeyRequest::Pubkey(pubkey) => { + let rooch_public_key = crypto::PublicKey::from_bitcoin_pubkey(&pubkey)?; + rooch_public_key.rooch_address()? + } + KeyRequest::Bip32(_key_source) => { + anyhow::bail!("BIP32 key source is not supported"); + } + _ => anyhow::bail!("Unsupported key request: {:?}", key_request), + }; + debug!("Get key for address: {:?}", address); + let kp = self + .keystore + .get_key_pair(&address, self.password.clone())?; + Ok(Some(PrivateKey::from_slice( + kp.private(), + bitcoin::Network::Bitcoin, + )?)) + } } diff --git a/crates/rooch-rpc-server/src/lib.rs b/crates/rooch-rpc-server/src/lib.rs index aec6811db6..50993de417 100644 --- a/crates/rooch-rpc-server/src/lib.rs +++ b/crates/rooch-rpc-server/src/lib.rs @@ -225,6 +225,7 @@ pub async fn run_start_server(opt: RoochOpt, server_opt: ServerOpt) -> Result = Lazy::new(|| Hrp::parse("rooch").expect("rooch is a valid HRP")); +pub const ROOCH_HRP: Hrp = Hrp::parse_unchecked("rooch"); /// Rooch address type #[derive(Copy, Clone, Ord, PartialOrd, PartialEq, Eq, Hash)] @@ -230,7 +230,7 @@ impl RoochAddress { pub fn to_bech32(&self) -> String { let data = self.0.as_bytes(); - bech32::encode::(*ROOCH_HRP, data).expect("bech32 encode should success") + bech32::encode::(ROOCH_HRP, data).expect("bech32 encode should success") } pub fn to_vec(&self) -> Vec { @@ -243,7 +243,7 @@ impl RoochAddress { pub fn from_bech32(bech32: &str) -> Result { let (hrp, data) = bech32::decode(bech32)?; - anyhow::ensure!(hrp == *ROOCH_HRP, "invalid rooch hrp"); + anyhow::ensure!(hrp == ROOCH_HRP, "invalid rooch hrp"); anyhow::ensure!(data.len() == Self::LENGTH, "invalid rooch address length"); let hash = H256::from_slice(data.as_slice()); Ok(Self(hash)) @@ -575,7 +575,7 @@ impl TryFrom for BitcoinAddressPayloadType { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Clone, PartialEq, Eq, Hash)] #[serde_as] pub struct BitcoinAddress { bytes: Vec, @@ -609,6 +609,12 @@ impl<'de> Deserialize<'de> for BitcoinAddress { } } +impl fmt::Debug for BitcoinAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self) + } +} + impl fmt::Display for BitcoinAddress { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { // Write the Bitcoin address as a hexadecimal string @@ -631,16 +637,16 @@ impl BitcoinAddress { Self { bytes } } - pub fn get_pubkey_address_prefix(network: u8) -> u8 { - if network::Network::Bitcoin.to_num() == network { + pub fn get_pubkey_address_prefix(network: network::Network) -> u8 { + if network::Network::Bitcoin == network { bitcoin::constants::PUBKEY_ADDRESS_PREFIX_MAIN } else { bitcoin::constants::PUBKEY_ADDRESS_PREFIX_TEST } } - pub fn get_script_address_prefix(network: u8) -> u8 { - if network::Network::Bitcoin.to_num() == network { + pub fn get_script_address_prefix(network: network::Network) -> u8 { + if network::Network::Bitcoin == network { bitcoin::constants::SCRIPT_ADDRESS_PREFIX_MAIN } else { bitcoin::constants::SCRIPT_ADDRESS_PREFIX_TEST @@ -688,11 +694,37 @@ impl BitcoinAddress { RoochAddress(H256(g_arr.digest)) } + pub fn to_bitcoin_address>( + &self, + network: N, + ) -> Result { + let network: network::Network = network.into(); + let network = bitcoin::network::Network::from(network); + let payload_type = BitcoinAddressPayloadType::try_from(self.bytes[0])?; + let addr = match payload_type { + BitcoinAddressPayloadType::PubkeyHash => { + let pubkey_hash = bitcoin::PubkeyHash::from_slice(&self.bytes[1..])?; + bitcoin::address::Address::p2pkh(pubkey_hash, network) + } + BitcoinAddressPayloadType::ScriptHash => { + let script_hash = bitcoin::ScriptHash::from_slice(&self.bytes[1..])?; + bitcoin::address::Address::p2sh_from_hash(script_hash, network) + } + BitcoinAddressPayloadType::WitnessProgram => { + let version = WitnessVersion::try_from(self.bytes[1])?; + let witness_program = WitnessProgram::new(version, &self.bytes[2..])?; + bitcoin::address::Address::from_witness_program(witness_program, network) + } + }; + Ok(addr) + } + /// Format the base58 as a hexadecimal string - pub fn format(&self, network: u8) -> Result { + pub fn format>(&self, network: N) -> Result { if self.bytes.is_empty() { anyhow::bail!("bitcoin address is empty"); } + let network: network::Network = network.into(); let payload_type = BitcoinAddressPayloadType::try_from(self.bytes[0])?; match payload_type { BitcoinAddressPayloadType::PubkeyHash => { @@ -708,14 +740,14 @@ impl BitcoinAddress { Ok(bs58::encode(&prefixed[..]).with_check().into_string()) } BitcoinAddressPayloadType::WitnessProgram => { - let hrp = network::Network::try_from(network)?.bech32_hrp(); + let hrp = network.bech32_hrp(); let version = WitnessVersion::try_from(self.bytes[1])?; - let buf = PushBytesBuf::try_from(self.bytes[2..].to_vec())?; - let witness_program = WitnessProgram::new(version, buf)?; + + let witness_program = WitnessProgram::new(version, &self.bytes[2..])?; let program: &[u8] = witness_program.program().as_ref(); let mut address_formatter = String::new(); - encode_to_fmt_unchecked(&mut address_formatter, &hrp, version.to_fe(), program)?; + encode_to_fmt_unchecked(&mut address_formatter, hrp, version.to_fe(), program)?; Ok(address_formatter) } } @@ -740,7 +772,7 @@ impl RoochSupportedAddress for BitcoinAddress { let secp = Secp256k1::new(); let p2pkh_address = Address::p2pkh( - &PrivateKey::generate(bitcoin_network).public_key(&secp), + PrivateKey::generate(bitcoin_network).public_key(&secp), bitcoin_network, ); let p2sh_address = Address::p2sh( @@ -748,11 +780,12 @@ impl RoochSupportedAddress for BitcoinAddress { bitcoin_network, ) .unwrap(); + + let sk = PrivateKey::generate(bitcoin_network); let segwit_address = Address::p2wpkh( - &PrivateKey::generate(bitcoin_network).public_key(&secp), + &CompressedPublicKey::from_private_key(&secp, &sk).unwrap(), bitcoin_network, - ) - .unwrap(); + ); // Create an array of addresses bitcoin protocols let addresses = [p2pkh_address, p2sh_address, segwit_address]; @@ -775,25 +808,33 @@ impl FromStr for BitcoinAddress { impl From for BitcoinAddress { fn from(address: bitcoin::Address) -> Self { - address.payload().into() + address.to_address_data().into() } } -impl From<&bitcoin::address::Payload> for BitcoinAddress { - fn from(payload: &bitcoin::address::Payload) -> Self { +impl TryFrom for bitcoin::Address { + type Error = anyhow::Error; + + fn try_from(value: BitcoinAddress) -> Result { + value.to_bitcoin_address(network::Network::Bitcoin) + } +} + +impl From<&bitcoin::address::AddressData> for BitcoinAddress { + fn from(payload: &bitcoin::address::AddressData) -> Self { match payload { - bitcoin::address::Payload::PubkeyHash(pubkey_hash) => Self::new_p2pkh(pubkey_hash), - bitcoin::address::Payload::ScriptHash(bytes) => Self::new_p2sh(bytes), - bitcoin::address::Payload::WitnessProgram(program) => { - Self::new_witness_program(program) + bitcoin::address::AddressData::P2pkh { pubkey_hash } => Self::new_p2pkh(pubkey_hash), + bitcoin::address::AddressData::P2sh { script_hash } => Self::new_p2sh(script_hash), + bitcoin::address::AddressData::Segwit { witness_program } => { + Self::new_witness_program(witness_program) } _ => BitcoinAddress::default(), } } } -impl From for BitcoinAddress { - fn from(payload: bitcoin::address::Payload) -> Self { +impl From for BitcoinAddress { + fn from(payload: bitcoin::address::AddressData) -> Self { Self::from(&payload) } } @@ -806,18 +847,19 @@ impl From for BitcoinAddress { impl From<&bitcoin::ScriptBuf> for BitcoinAddress { fn from(script: &bitcoin::ScriptBuf) -> Self { - let address_opt = bitcoin::address::Payload::from_script(script).ok(); - let payload: Option = match address_opt { + let address_opt = bitcoin::address::Address::from_script(script, &Params::MAINNET).ok(); + let payload: Option = match address_opt { None => { if script.is_p2pk() { let p2pk_pubkey = script.p2pk_public_key(); - p2pk_pubkey - .map(|pubkey| bitcoin::address::Payload::PubkeyHash(pubkey.pubkey_hash())) + p2pk_pubkey.map(|pubkey| bitcoin::address::AddressData::P2pkh { + pubkey_hash: pubkey.pubkey_hash(), + }) } else { None } } - Some(address_opt) => Some(address_opt), + Some(address) => Some(address.to_address_data()), }; payload.map(|payload| payload.into()).unwrap_or_default() } @@ -854,9 +896,12 @@ impl NostrPublicKey { } /// Convert from the Nostr XOnlyPublicKey to Bitcoin Taproot address. BIP-086. - pub fn to_bitcoin_address(&self, network: u8) -> Result { + pub fn to_bitcoin_address>( + &self, + network: N, + ) -> Result { // get the network - let network = network::Network::try_from(network)?; + let network: network::Network = network.into(); // change use of XOnlyPublicKey from nostr to bitcoin lib let internal_key = bitcoin::XOnlyPublicKey::from_slice(&self.0.serialize())?; // new verification crypto @@ -875,7 +920,12 @@ impl NostrPublicKey { impl RoochSupportedAddress for NostrPublicKey { fn random() -> Self { - Self(Keys::generate().public_key()) + Self( + RoochKeyPair::generate_secp256k1() + .public() + .xonly_public_key() + .unwrap(), + ) } } @@ -916,11 +966,12 @@ impl fmt::Display for NostrPublicKey { } } -// Parsed Address, either a name or a numerical address +// Parsed Address, either a name or a numerical address, or Bitcoin Address #[derive(Eq, PartialEq, Debug, Clone)] pub enum ParsedAddress { Named(String), Numerical(RoochAddress), + Bitcoin(BitcoinAddress), } impl ParsedAddress { @@ -933,6 +984,7 @@ impl ParsedAddress { .map(Into::into) .ok_or_else(|| anyhow::anyhow!("Unbound named address: '{}'", n)), Self::Numerical(a) => Ok(a), + Self::Bitcoin(a) => Ok(a.to_rooch_address()), } } @@ -949,21 +1001,27 @@ impl ParsedAddress { } else if s.starts_with(ROOCH_HRP.as_str()) && s.len() == RoochAddress::LENGTH_BECH32 { Ok(Self::Numerical(RoochAddress::from_bech32(s)?)) } else if s.starts_with(PREFIX_BECH32_PUBLIC_KEY) { - Ok(Self::Numerical(BitcoinAddress::to_rooch_address( - &NostrPublicKey::to_bitcoin_address( - &NostrPublicKey::from_str(s)?, - network::Network::Bitcoin.to_num(), - )?, - ))) + Ok(Self::Bitcoin(NostrPublicKey::to_bitcoin_address( + &NostrPublicKey::from_str(s)?, + network::Network::Bitcoin.to_num(), + )?)) } else { match BitcoinAddress::from_str(s) { - Ok(a) => Ok(Self::Numerical(a.to_rooch_address())), + Ok(a) => Ok(Self::Bitcoin(a)), Err(_) => Ok(Self::Named(s.to_string())), } } } } +impl FromStr for ParsedAddress { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + ParsedAddress::parse(s) + } +} + // TODO: Need a testcase to use the nostr address. #[cfg(test)] mod test { @@ -1151,7 +1209,7 @@ mod test { let bitcoin_address = BitcoinAddress { bytes: bytes.clone(), }; - let address_str = bitcoin_address.format(network::Network::Bitcoin.to_num())?; + let address_str = bitcoin_address.format(network::Network::Bitcoin)?; println!("test_bitcoin_address bitcoin address {} ", address_str); let maddress = MultiChainAddress::new(RoochMultiChainID::Bitcoin, bytes.clone()); @@ -1164,6 +1222,12 @@ mod test { assert_eq!(maddress, new_maddress); assert_eq!(bitcoin_address, new_bitcoin_address); assert_eq!(address_str, "bc1qjlxl7n7na4hcsh25554hn4azzsg89t3lcty7gp"); + + let btc_address = bitcoin::Address::from_str(&address_str).unwrap(); + let btc_address = btc_address.assume_checked(); + let btc_address2 = bitcoin_address.to_bitcoin_address(bitcoin::Network::Bitcoin)?; + assert_eq!(btc_address, btc_address2); + Ok(()) } @@ -1176,7 +1240,11 @@ mod test { let bitcoin_address = BitcoinAddress { bytes: bytes.clone(), }; - let address_str = bitcoin_address.format(network::Network::Bitcoin.to_num())?; + let address_str = bitcoin_address.format(network::Network::Bitcoin)?; + let btc_address = bitcoin::Address::from_str(&address_str).unwrap(); + let btc_address = btc_address.assume_checked(); + let btc_address2 = bitcoin_address.to_bitcoin_address(network::Network::Bitcoin)?; + assert_eq!(btc_address, btc_address2); println!( "test_convert_bitcoin_address bitcoin address {} ", address_str diff --git a/crates/rooch-types/src/bitcoin/multisign_account.rs b/crates/rooch-types/src/bitcoin/multisign_account.rs index 2c8dd4be17..2dea7cc9a6 100644 --- a/crates/rooch-types/src/bitcoin/multisign_account.rs +++ b/crates/rooch-types/src/bitcoin/multisign_account.rs @@ -4,10 +4,11 @@ use crate::address::BitcoinAddress; use crate::addresses::BITCOIN_MOVE_ADDRESS; use anyhow::Result; +use bitcoin::bip32::{DerivationPath, Fingerprint}; use bitcoin::key::constants::SCHNORR_PUBLIC_KEY_SIZE; use bitcoin::key::Secp256k1; -use bitcoin::taproot::TaprootBuilder; -use bitcoin::{ScriptBuf, XOnlyPublicKey}; +use bitcoin::taproot::{LeafVersion, TaprootBuilder}; +use bitcoin::{PublicKey, ScriptBuf, TapLeafHash, XOnlyPublicKey}; use move_core_types::{account_address::AccountAddress, ident_str, identifier::IdentStr}; use moveos_types::moveos_std::simple_map::SimpleMap; use moveos_types::moveos_std::tx_context::TxContext; @@ -18,6 +19,8 @@ use moveos_types::{ transaction::MoveAction, }; use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use tracing::debug; pub const MODULE_NAME: &IdentStr = ident_str!("multisign_account"); @@ -57,6 +60,23 @@ pub struct ParticipantInfo { pub public_key: Vec, } +impl ParticipantInfo { + pub fn public_key(&self) -> Result { + PublicKey::from_slice(&self.public_key).map_err(|e| { + anyhow::anyhow!( + "Failed to parse public key: {}, hex: {}", + e, + hex::encode(&self.public_key) + ) + }) + } + + pub fn x_only_public_key(&self) -> Result { + let pubkey = self.public_key()?; + Ok(XOnlyPublicKey::from(pubkey)) + } +} + impl MoveStructType for ParticipantInfo { const ADDRESS: AccountAddress = BITCOIN_MOVE_ADDRESS; const MODULE_NAME: &'static IdentStr = MODULE_NAME; @@ -81,23 +101,18 @@ pub fn generate_multisign_address( .into_iter() .map(|pk| { let x_only_pk = if pk.len() == SCHNORR_PUBLIC_KEY_SIZE { - pk + XOnlyPublicKey::from_slice(&pk)? } else { let pubkey = bitcoin::PublicKey::from_slice(&pk)?; - XOnlyPublicKey::from(pubkey).serialize().to_vec() + XOnlyPublicKey::from(pubkey) }; Ok(x_only_pk) }) .collect::>>()?; // Sort public keys to ensure the same script is generated for the same set of keys - // Note: we sort on the x-only public key bytes x_only_public_keys.sort(); - let x_only_public_keys = x_only_public_keys - .into_iter() - .map(|pk| XOnlyPublicKey::from_slice(&pk)) - .collect::, bitcoin::secp256k1::Error>>()?; let multisig_script = create_multisig_script(threshold, &x_only_public_keys); let builder = TaprootBuilder::new().add_leaf(0, multisig_script)?; @@ -134,6 +149,73 @@ fn create_multisig_script(threshold: usize, public_keys: &Vec) - builder.into_script() } +pub fn update_multisig_psbt( + psbt_input: &mut bitcoin::psbt::Input, + account_info: &MultisignAccountInfo, +) -> Result<()> { + let secp = Secp256k1::new(); + + let threshold = account_info.threshold as usize; + let mut participant_pubkeys = account_info + .participants + .values() + .into_iter() + .map(|info| info.x_only_public_key()) + .collect::>>()?; + + // Sort public keys to ensure the same script is generated for the same set of keys + participant_pubkeys.sort(); + + debug!( + "Ordered public keys when build psbt sign: {:?}", + participant_pubkeys + ); + + let multisig_script = create_multisig_script(threshold, &participant_pubkeys); + + let mut builder = TaprootBuilder::new(); + builder = builder.add_leaf(0, multisig_script.clone())?; + + let internal_key = participant_pubkeys[0]; + let tap_tree = builder + .finalize(&secp, internal_key) + .map_err(|_| anyhow::anyhow!("Failed to finalize taproot tree"))?; + + let tap_leaf_hash = TapLeafHash::from_script(&multisig_script, LeafVersion::TapScript); + + let mut tap_key_origins = BTreeMap::new(); + for pubkey in &participant_pubkeys { + let default_key_source = (Fingerprint::default(), DerivationPath::default()); + tap_key_origins.insert(*pubkey, (vec![tap_leaf_hash], default_key_source)); + } + + psbt_input.tap_internal_key = Some(internal_key); + psbt_input.tap_key_origins = tap_key_origins.clone(); + psbt_input.tap_merkle_root = tap_tree.merkle_root(); + + let control_block = tap_tree + .control_block(&(multisig_script.clone(), LeafVersion::TapScript)) + .unwrap(); + psbt_input.tap_scripts.insert( + control_block, + (multisig_script.clone(), LeafVersion::TapScript), + ); + + let address = bitcoin::Address::p2tr( + &secp, + internal_key, + tap_tree.merkle_root(), + bitcoin::Network::Bitcoin, + ); + let rebuild_address = BitcoinAddress::from(address); + if rebuild_address != account_info.multisign_bitcoin_address { + anyhow::bail!( + "The multisign address in the psbt is not equal to the on-chain multisign address" + ); + } + Ok(()) +} + /// Rust bindings for multisign_acount module pub struct MultisignAccountModule<'a> { caller: &'a dyn MoveFunctionCaller, diff --git a/crates/rooch-types/src/bitcoin/network.rs b/crates/rooch-types/src/bitcoin/network.rs index d68c6f6ef5..2c0998a7e7 100644 --- a/crates/rooch-types/src/bitcoin/network.rs +++ b/crates/rooch-types/src/bitcoin/network.rs @@ -23,16 +23,14 @@ pub enum Network { Regtest = 4, } -impl TryFrom for Network { - type Error = anyhow::Error; - - fn try_from(value: u8) -> Result { +impl From for Network { + fn from(value: u8) -> Self { match value { - 1 => Ok(Network::Bitcoin), - 2 => Ok(Network::Testnet), - 3 => Ok(Network::Signet), - 4 => Ok(Network::Regtest), - _ => Err(anyhow::anyhow!("Bitcoin network {} is invalid", value)), + 1 => Network::Bitcoin, + 2 => Network::Testnet, + 3 => Network::Signet, + 4 => Network::Regtest, + _ => Network::Bitcoin, } } } @@ -64,12 +62,12 @@ impl std::fmt::Display for Network { } impl Network { - pub fn bech32_hrp(&self) -> bitcoin::bech32::Hrp { + pub fn bech32_hrp(&self) -> bech32::Hrp { match self { - Network::Bitcoin => bitcoin::bech32::hrp::BC, - Network::Testnet => bitcoin::bech32::hrp::TB, - Network::Signet => bitcoin::bech32::hrp::TB, - Network::Regtest => bitcoin::bech32::hrp::BCRT, + Network::Bitcoin => bech32::hrp::BC, + Network::Testnet => bech32::hrp::TB, + Network::Signet => bech32::hrp::TB, + Network::Regtest => bech32::hrp::BCRT, } } diff --git a/crates/rooch-types/src/bitcoin/types.rs b/crates/rooch-types/src/bitcoin/types.rs index 9392758c2e..738995f6b4 100644 --- a/crates/rooch-types/src/bitcoin/types.rs +++ b/crates/rooch-types/src/bitcoin/types.rs @@ -169,7 +169,7 @@ pub struct Transaction { impl From for Transaction { fn from(tx: bitcoin::Transaction) -> Self { Self { - id: tx.txid().into_address(), + id: tx.compute_txid().into_address(), version: tx.version.0 as u32, lock_time: tx.lock_time.to_consensus_u32(), input: tx.input.into_iter().map(|tx_in| tx_in.into()).collect(), @@ -312,6 +312,15 @@ impl From for OutPoint { } } +impl From for bitcoin::OutPoint { + fn from(out_point: OutPoint) -> Self { + Self { + txid: Txid::from_address(out_point.txid), + vout: out_point.vout, + } + } +} + impl MoveStructType for OutPoint { const MODULE_NAME: &'static IdentStr = MODULE_NAME; const STRUCT_NAME: &'static IdentStr = ident_str!("OutPoint"); diff --git a/crates/rooch-types/src/bitcoin/utxo.rs b/crates/rooch-types/src/bitcoin/utxo.rs index 4ca6073bc5..2d5256bb0a 100644 --- a/crates/rooch-types/src/bitcoin/utxo.rs +++ b/crates/rooch-types/src/bitcoin/utxo.rs @@ -3,7 +3,9 @@ use super::types; use crate::addresses::BITCOIN_MOVE_ADDRESS; +use crate::into_address::FromAddress; use anyhow::Result; +use bitcoin::{Amount, Txid}; use move_core_types::{account_address::AccountAddress, ident_str, identifier::IdentStr}; use moveos_types::h256::H256; use moveos_types::moveos_std::object::{self, ObjectMeta}; @@ -104,6 +106,18 @@ impl UTXO { pub fn object_id(&self) -> ObjectID { derive_utxo_id(&types::OutPoint::new(self.txid, self.vout)) } + + pub fn amount(&self) -> Amount { + Amount::from_sat(self.value) + } + + pub fn txid(&self) -> Txid { + Txid::from_address(self.txid) + } + + pub fn outpoint(&self) -> types::OutPoint { + types::OutPoint::new(self.txid, self.vout) + } } pub fn derive_utxo_id(outpoint: &types::OutPoint) -> ObjectID { diff --git a/crates/rooch-types/src/crypto.rs b/crates/rooch-types/src/crypto.rs index 165cad36a0..a2d5f73afd 100644 --- a/crates/rooch-types/src/crypto.rs +++ b/crates/rooch-types/src/crypto.rs @@ -12,6 +12,7 @@ use crate::{ }; use anyhow::{anyhow, bail}; use bech32::{encode, Bech32, EncodeError}; +use bitcoin::secp256k1::SecretKey; use derive_more::{AsMut, AsRef, From}; pub use enum_dispatch::enum_dispatch; use eyre::eyre; @@ -37,7 +38,6 @@ use fastcrypto::{ secp256k1::{Secp256k1PublicKey, Secp256k1Signature, Secp256k1SignatureAsBytes}, }; use moveos_types::serde::Readable; -use nostr::prelude::ToBech32; use schemars::JsonSchema; use serde::ser::Serializer; use serde::{Deserialize, Deserializer, Serialize}; @@ -135,6 +135,33 @@ impl RoochKeyPair { } } + /// Get the secp256k1 keypair + pub fn secp256k1_keypair(&self) -> Option { + match self.secp256k1_secret_key() { + Some(sk) => { + let keypair = bitcoin::key::Keypair::from_secret_key( + &bitcoin::secp256k1::Secp256k1::new(), + &sk, + ); + Some(keypair) + } + None => None, + } + } + + /// Get the secp256k1 private key + pub fn secp256k1_secret_key(&self) -> Option { + match self { + RoochKeyPair::Secp256k1(kp) => { + SecretKey::from_slice(kp.secret.as_bytes()).ok() + //The bitcoin and fastcrypto dependent on different version secp256k1 library + //So we cannot directly return the private key + //Some(&kp.secret.privkey) + } + _ => None, + } + } + /// Authentication key is the hash of the public key pub fn authentication_key(&self) -> AuthenticationKey { self.public().authentication_key() @@ -345,15 +372,14 @@ impl PublicKey { } } - pub fn nostr_bech32_public_key(&self) -> Result { + pub fn xonly_public_key(&self) -> Result { match self { PublicKey::Secp256k1(pk) => { - let xonly_pubkey = nostr::secp256k1::XOnlyPublicKey::from( - nostr::secp256k1::PublicKey::from_slice(&pk.0)?, - ); - Ok(xonly_pubkey.to_bech32()?) + let xonly_pubkey = + bitcoin::XOnlyPublicKey::from(bitcoin::PublicKey::from_slice(&pk.0)?); + Ok(xonly_pubkey) } - _ => bail!("Only secp256k1 public key can be converted to nostr bech32 public key"), + _ => bail!("Only secp256k1 public key can be converted to xonly public key"), } } @@ -367,6 +393,10 @@ impl PublicKey { pub fn from_hex(hex: &str) -> Result { let bytes = hex::decode(hex.strip_prefix("0x").unwrap_or(hex))?; + Self::from_bytes(&bytes) + } + + pub fn from_bytes(bytes: &[u8]) -> Result { match SignatureScheme::from_flag_byte( *bytes .first() @@ -393,6 +423,12 @@ impl PublicKey { Err(e) => Err(anyhow!("Invalid bytes :{}", e)), } } + + pub fn from_bitcoin_pubkey(pk: &bitcoin::PublicKey) -> Result { + let bytes = pk.to_bytes(); + let pk = Secp256k1PublicKey::from_bytes(&bytes)?; + Ok(PublicKey::Secp256k1((&pk).into())) + } } impl std::fmt::Display for PublicKey { diff --git a/crates/rooch-types/src/error.rs b/crates/rooch-types/src/error.rs index 0a3e605232..b728b4c37c 100644 --- a/crates/rooch-types/src/error.rs +++ b/crates/rooch-types/src/error.rs @@ -156,6 +156,12 @@ impl From for RoochError { } } +impl From for RoochError { + fn from(e: bitcoin::io::Error) -> Self { + RoochError::IOError(e.to_string()) + } +} + impl From for RoochError { fn from(e: VMError) -> Self { RoochError::VMError(e) @@ -168,6 +174,18 @@ impl From for RoochError { } } +impl From for RoochError { + fn from(e: bitcoin::psbt::Error) -> Self { + RoochError::CommandArgumentError(e.to_string()) + } +} + +impl From for RoochError { + fn from(e: hex::FromHexError) -> Self { + RoochError::CommandArgumentError(e.to_string()) + } +} + #[derive(Debug, Error, Eq, PartialEq)] pub enum GenesisError { #[error("Genesis version mismatch: from store({from_store}), from binary({from_binary}).")] diff --git a/crates/rooch-types/src/framework/address_mapping.rs b/crates/rooch-types/src/framework/address_mapping.rs index debc99eed4..8a05d2d4ac 100644 --- a/crates/rooch-types/src/framework/address_mapping.rs +++ b/crates/rooch-types/src/framework/address_mapping.rs @@ -8,7 +8,8 @@ use move_core_types::value::MoveTypeLayout; use move_core_types::{account_address::AccountAddress, ident_str, identifier::IdentStr}; use moveos_types::moveos_std::object::ObjectID; use moveos_types::moveos_std::object::{self, ObjectMeta}; -use moveos_types::state::{MoveStructState, MoveStructType, ObjectState}; +use moveos_types::state::{FieldKey, MoveStructState, MoveStructType, ObjectState}; +use moveos_types::state_resolver::StateResolver; use moveos_types::{ h256::H256, module_binding::{ModuleBinding, MoveFunctionCaller}, @@ -84,6 +85,23 @@ impl RoochToBitcoinAddressMapping { object.metadata.size = size; object } + + pub fn resolve_bitcoin_address( + state_resolver: &impl StateResolver, + address: AccountAddress, + ) -> Result> { + let address_mapping_object_id = RoochToBitcoinAddressMapping::object_id(); + let object_state = state_resolver.get_field( + &address_mapping_object_id, + &FieldKey::derive_from_address(&address), + )?; + if let Some(object_state) = object_state { + let df = object_state.value_as_df::()?; + Ok(Some(df.value)) + } else { + Ok(None) + } + } } /// Rust bindings for RoochFramework address_mapping module diff --git a/crates/rooch-types/src/framework/auth_payload.rs b/crates/rooch-types/src/framework/auth_payload.rs index 3bd6ad1c0e..52cfb282b2 100644 --- a/crates/rooch-types/src/framework/auth_payload.rs +++ b/crates/rooch-types/src/framework/auth_payload.rs @@ -6,7 +6,10 @@ use crate::{ transaction::RoochTransactionData, }; use anyhow::{ensure, Result}; -use bitcoin::consensus::{Decodable, Encodable}; +use bitcoin::{ + consensus::{Decodable, Encodable}, + io::BufRead, +}; use fastcrypto::{ hash::Sha256, secp256k1::{Secp256k1PublicKey, Secp256k1Signature}, @@ -20,7 +23,6 @@ use moveos_types::{ }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use std::io; pub const MODULE_NAME: &IdentStr = ident_str!("auth_payload"); @@ -92,14 +94,17 @@ impl SignData { } impl Encodable for SignData { - fn consensus_encode(&self, s: &mut S) -> Result { + fn consensus_encode( + &self, + s: &mut S, + ) -> Result { let len = self.message_prefix.consensus_encode(s)?; Ok(len + self.message_info.consensus_encode(s)?) } } impl Decodable for SignData { - fn consensus_decode( + fn consensus_decode( d: &mut D, ) -> Result { Ok(SignData { diff --git a/crates/rooch-types/src/lib.rs b/crates/rooch-types/src/lib.rs index 24aba0bae8..300f2f66a4 100644 --- a/crates/rooch-types/src/lib.rs +++ b/crates/rooch-types/src/lib.rs @@ -24,4 +24,5 @@ pub mod rooch_signature; pub mod sequencer; pub mod service_status; pub mod test_utils; +pub mod to_bech32; pub mod transaction; diff --git a/crates/rooch-types/src/to_bech32.rs b/crates/rooch-types/src/to_bech32.rs new file mode 100644 index 0000000000..dec0c0c141 --- /dev/null +++ b/crates/rooch-types/src/to_bech32.rs @@ -0,0 +1,45 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::Error; +use bech32::Hrp; +use bitcoin::XOnlyPublicKey; + +pub const PREFIX_BECH32_PUBLIC_KEY: &str = "npub"; +pub const NPUB: Hrp = Hrp::parse_unchecked(PREFIX_BECH32_PUBLIC_KEY); + +pub trait ToBech32 { + type Err; + fn to_bech32(&self) -> Result; +} + +impl ToBech32 for XOnlyPublicKey { + type Err = Error; + + fn to_bech32(&self) -> Result { + let data = self.serialize(); + Ok(bech32::encode::(NPUB, &data)?) + } +} + +pub trait FromBech32: Sized { + type Err; + fn from_bech32(s: S) -> Result + where + S: Into; +} + +impl FromBech32 for XOnlyPublicKey { + type Err = Error; + + fn from_bech32(s: S) -> Result + where + S: Into, + { + let (hrp, data) = bech32::decode(&s.into())?; + if hrp != NPUB { + return Err(Error::msg("Invalid HRP")); + } + Ok(XOnlyPublicKey::from_slice(&data)?) + } +} diff --git a/crates/rooch/Cargo.toml b/crates/rooch/Cargo.toml index 7f0fdd5fd9..f9b894b26b 100644 --- a/crates/rooch/Cargo.toml +++ b/crates/rooch/Cargo.toml @@ -51,6 +51,7 @@ vergen-pretty = { workspace = true } prometheus = { workspace = true } lazy_static = { workspace = true } schemars = { workspace = true } +bitcoin = { workspace = true } move-bytecode-utils = { workspace = true } move-binary-format = { workspace = true } @@ -86,7 +87,6 @@ framework-builder = { workspace = true } framework-types = { workspace = true } raw-store = { workspace = true } smt = { workspace = true } -bitcoin = { workspace = true } bitcoin-move = { workspace = true } rooch-key = { workspace = true } diff --git a/crates/rooch/src/cli_types.rs b/crates/rooch/src/cli_types.rs index 24a5ad1b24..52a0c5814f 100644 --- a/crates/rooch/src/cli_types.rs +++ b/crates/rooch/src/cli_types.rs @@ -13,6 +13,8 @@ use rooch_types::error::{RoochError, RoochResult}; use rooch_types::transaction::authenticator::Authenticator; use rpassword::prompt_password; use serde::Serialize; +use std::fs::File; +use std::io::Read; use std::path::PathBuf; use std::str::FromStr; @@ -162,3 +164,33 @@ impl WalletContextOptions { } } } + +#[derive(Debug, Clone)] +pub struct FileOrHexInput { + /// The data decode from file or hex string + pub data: Vec, +} + +impl FromStr for FileOrHexInput { + type Err = anyhow::Error; + fn from_str(s: &str) -> Result { + let data_hex = if is_file_path(s) { + //load hex from file + let mut file = File::open(s) + .map_err(|e| anyhow::anyhow!("Failed to open file: {}, err:{:?}", s, e))?; + let mut hex_str = String::new(); + file.read_to_string(&mut hex_str) + .map_err(|e| anyhow::anyhow!("Failed to read file: {}, err:{:?}", s, e))?; + hex_str.strip_prefix("0x").unwrap_or(&hex_str).to_string() + } else { + s.strip_prefix("0x").unwrap_or(s).to_string() + }; + let data = hex::decode(&data_hex) + .map_err(|e| anyhow::anyhow!("Failed to decode hex: {}, err:{:?}", data_hex, e))?; + Ok(FileOrHexInput { data }) + } +} + +pub(crate) fn is_file_path(s: &str) -> bool { + s.contains('/') || s.contains('\\') || s.contains('.') +} diff --git a/crates/rooch/src/commands/account/commands/balance.rs b/crates/rooch/src/commands/account/commands/balance.rs index 2e5e0d7445..af337879c3 100644 --- a/crates/rooch/src/commands/account/commands/balance.rs +++ b/crates/rooch/src/commands/account/commands/balance.rs @@ -199,7 +199,7 @@ async fn get_total_utxo_value( for utxo in page.data { total_value = total_value - .checked_add(utxo.value.get_value()) + .checked_add(utxo.value.value()) .ok_or_else(|| anyhow::anyhow!("UTXO value overflow"))?; } diff --git a/crates/rooch/src/commands/account/commands/create_multisign.rs b/crates/rooch/src/commands/account/commands/create_multisign.rs index a0b2697084..4a8aa20486 100644 --- a/crates/rooch/src/commands/account/commands/create_multisign.rs +++ b/crates/rooch/src/commands/account/commands/create_multisign.rs @@ -8,8 +8,8 @@ use clap::Parser; use moveos_types::module_binding::MoveFunctionCaller; use rooch_rpc_api::jsonrpc_types::BytesView; use rooch_types::{ - address::{BitcoinAddress, RoochAddress}, - bitcoin::multisign_account::{self, MultisignAccountModule, ParticipantInfo}, + address::RoochAddress, + bitcoin::multisign_account::{self, MultisignAccountModule}, error::RoochResult, }; use serde::{Deserialize, Serialize}; @@ -39,24 +39,14 @@ pub struct CreateMultisignCommand { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ParticipantInfoView { pub participant_address: RoochAddress, - pub participant_bitcoin_address: BitcoinAddress, + pub participant_bitcoin_address: String, pub public_key: BytesView, } -impl From for ParticipantInfoView { - fn from(participant: ParticipantInfo) -> Self { - ParticipantInfoView { - participant_address: participant.participant_address.into(), - participant_bitcoin_address: participant.participant_bitcoin_address, - public_key: participant.public_key.into(), - } - } -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MultisignAccountOutput { pub multisign_address: RoochAddress, - pub multisign_bitcoin_address: BitcoinAddress, + pub multisign_bitcoin_address: String, pub participants: Vec, } @@ -66,6 +56,7 @@ impl CommandAction> for CreateMultisignCommand { let context = self.context_options.build_require_password()?; let sender: RoochAddress = context.resolve_address(self.tx_options.sender)?.into(); + let bitcoin_network = context.get_bitcoin_network().await?; let client = context.get_client().await?; let multisign_account_module = client.as_module_binding::(); @@ -101,8 +92,20 @@ impl CommandAction> for CreateMultisignCommand { let output: MultisignAccountOutput = MultisignAccountOutput { multisign_address, - multisign_bitcoin_address, - participants: participants.into_iter().map(|p| p.into()).collect(), + multisign_bitcoin_address: multisign_bitcoin_address + .format(bitcoin_network) + .expect("format multisign address should success"), + participants: participants + .into_iter() + .map(|p| ParticipantInfoView { + participant_address: p.participant_address.into(), + participant_bitcoin_address: p + .participant_bitcoin_address + .format(bitcoin_network) + .expect("format participant address should success"), + public_key: p.public_key.into(), + }) + .collect(), }; if self.json { Ok(Some(output)) diff --git a/crates/rooch/src/commands/bitcoin/broadcast_tx.rs b/crates/rooch/src/commands/bitcoin/broadcast_tx.rs new file mode 100644 index 0000000000..eeccd8fc2a --- /dev/null +++ b/crates/rooch/src/commands/bitcoin/broadcast_tx.rs @@ -0,0 +1,28 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli_types::{CommandAction, FileOrHexInput, WalletContextOptions}; +use async_trait::async_trait; +use clap::Parser; +use rooch_types::error::RoochResult; + +#[derive(Debug, Parser)] +pub struct BroadcastTx { + /// The input tx file path or hex string + input: FileOrHexInput, + #[clap(flatten)] + pub(crate) context_options: WalletContextOptions, +} + +#[async_trait] +impl CommandAction for BroadcastTx { + async fn execute(self) -> RoochResult { + let context = self.context_options.build()?; + let client = context.get_client().await?; + + Ok(client + .rooch + .broadcast_bitcoin_tx(self.input.data.into(), None) + .await?) + } +} diff --git a/crates/rooch/src/commands/bitcoin/build_tx.rs b/crates/rooch/src/commands/bitcoin/build_tx.rs new file mode 100644 index 0000000000..f39612c50d --- /dev/null +++ b/crates/rooch/src/commands/bitcoin/build_tx.rs @@ -0,0 +1,167 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +use super::transaction_builder::TransactionBuilder; +use super::FileOutput; +use crate::cli_types::{CommandAction, WalletContextOptions}; +use crate::commands::bitcoin::FileOutputData; +use async_trait::async_trait; +use bitcoin::absolute::LockTime; +use bitcoin::{Amount, FeeRate, OutPoint}; +use clap::Parser; +use moveos_types::moveos_std::object::ObjectID; +use rooch_types::address::ParsedAddress; +use rooch_types::bitcoin::utxo::derive_utxo_id; +use rooch_types::error::{RoochError, RoochResult}; +use std::str::FromStr; +use tracing::debug; + +#[derive(Debug, Clone)] +pub enum ParsedInput { + /// Input is an UTXO ObjectID + ObjectID(ObjectID), + /// Input is an OutPoint + OutPoint(OutPoint), +} + +impl FromStr for ParsedInput { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + if s.contains(':') { + let outpoint = OutPoint::from_str(s)?; + Ok(ParsedInput::OutPoint(outpoint)) + } else { + let object_id = ObjectID::from_str(s)?; + Ok(ParsedInput::ObjectID(object_id)) + } + } +} + +impl ParsedInput { + pub fn into_object_id(self) -> ObjectID { + match self { + ParsedInput::ObjectID(object_id) => object_id, + ParsedInput::OutPoint(outpoint) => derive_utxo_id(&outpoint.into()), + } + } +} + +#[derive(Debug, Clone)] +pub struct ParsedOutput { + pub address: ParsedAddress, + pub amount: Amount, +} + +impl FromStr for ParsedOutput { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let (addr_part, amount_part) = s + .split_once(':') + .ok_or_else(|| RoochError::CommandArgumentError("Invalid output format".to_string()))?; + let address = ParsedAddress::parse(addr_part)?; + let amount = u64::from_str(amount_part)?; + Ok(ParsedOutput { + address, + amount: Amount::from_sat(amount), + }) + } +} + +#[derive(Debug, Parser)] +pub struct BuildTx { + /// The sender address of the transaction, if not specified, the active address will be used + #[clap(long, short = 's', value_parser=ParsedAddress::parse, default_value = "default")] + sender: ParsedAddress, + + /// The inputs of the transaction, if not specified, the UTXOs of the sender will be used + /// The format of the input is or : + #[clap(long, short = 'i')] + inputs: Vec, + + // /// The to address of the transaction, if not specified, the outputs will be used + // #[clap(long, short = 't', conflicts_with = "outputs", group = "to", value_parser=ParsedAddress::parse)] + // to: Option, + + // /// The amount of the transaction, if not specified, the amount will be calculated automatically + // #[clap(long, short = 'a', conflicts_with = "outputs", group = "to")] + // amount: Option, + #[clap(long, short = 'o', required = true, num_args = 1..)] + outputs: Vec, + + /// The fee rate of the transaction, if not specified, the fee will be calculated automatically + #[clap(long)] + fee_rate: Option, + + /// The lock time of the transaction, if not specified, the lock time will be 0 + #[clap(long)] + lock_time: Option, + + /// The change address of the transaction, if not specified, the change address will be the sender's address + #[clap(long, value_parser=ParsedAddress::parse)] + change_address: Option, + + /// Skip check seal of the UTXOs, default is false + /// If set to true, some UTXO which carries other asserts, such as Inscription, maybe unexpected spent. + #[clap(long)] + skip_check_seal: bool, + + /// The output file path for the psbt + /// If not specified, the output will write to temp directory. + #[clap(long)] + output_file: Option, + + #[clap(flatten)] + pub(crate) context_options: WalletContextOptions, +} + +#[async_trait] +impl CommandAction for BuildTx { + async fn execute(self) -> RoochResult { + let context = self.context_options.build_require_password()?; + let client = context.get_client().await?; + + let bitcoin_network = context.get_bitcoin_network().await?; + + let sender = context.resolve_bitcoin_address(self.sender).await?; + + let inputs = self + .inputs + .into_iter() + .map(|input| input.into_object_id()) + .collect(); + let mut tx_builder = TransactionBuilder::new( + &context, + client, + sender.to_bitcoin_address(bitcoin_network)?, + inputs, + self.skip_check_seal, + ) + .await?; + + if let Some(fee_rate) = self.fee_rate { + tx_builder = tx_builder.with_fee_rate(fee_rate); + } + if let Some(lock_time) = self.lock_time { + tx_builder = tx_builder.with_lock_time(lock_time); + } + if let Some(change_address) = self.change_address { + let change_address = context.resolve_bitcoin_address(change_address).await?; + tx_builder = + tx_builder.with_change_address(change_address.to_bitcoin_address(bitcoin_network)?); + } + + let mut outputs = Vec::new(); + for output in self.outputs.iter() { + let address = context + .resolve_bitcoin_address(output.address.clone()) + .await?; + outputs.push((address.to_bitcoin_address(bitcoin_network)?, output.amount)); + } + let psbt = tx_builder.build(outputs).await?; + debug!("PSBT: {}", serde_json::to_string_pretty(&psbt).unwrap()); + let fileout = FileOutput::write_to_file(FileOutputData::Psbt(psbt), self.output_file)?; + Ok(fileout) + } +} diff --git a/crates/rooch/src/commands/bitcoin/mod.rs b/crates/rooch/src/commands/bitcoin/mod.rs new file mode 100644 index 0000000000..40ce735bdc --- /dev/null +++ b/crates/rooch/src/commands/bitcoin/mod.rs @@ -0,0 +1,115 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli_types::CommandAction; +use anyhow::Result; +use async_trait::async_trait; +use bitcoin::{consensus::Encodable, Psbt, Transaction, Txid}; +use broadcast_tx::BroadcastTx; +use build_tx::BuildTx; +use clap::{Parser, Subcommand}; +use rooch_types::error::RoochResult; +use serde::{Deserialize, Serialize}; +use sign_tx::SignTx; +use std::{env, fs::File, io::Write, path::PathBuf}; +use transfer::Transfer; + +pub mod broadcast_tx; +pub mod build_tx; +pub mod sign_tx; +pub mod transaction_builder; +pub mod transfer; +pub mod utxo_selector; + +#[derive(Debug, Parser)] +pub struct Bitcoin { + #[clap(subcommand)] + cmd: BitcoinCommands, +} + +#[allow(clippy::large_enum_variant)] +#[derive(Debug, Subcommand)] +pub enum BitcoinCommands { + BuildTx(BuildTx), + SignTx(SignTx), + BroadcastTx(BroadcastTx), + Transfer(Transfer), +} + +#[async_trait] +impl CommandAction for Bitcoin { + async fn execute(self) -> RoochResult { + match self.cmd { + BitcoinCommands::BuildTx(build_tx) => build_tx.execute_serialized().await, + BitcoinCommands::SignTx(sign_tx) => sign_tx.execute_serialized().await, + BitcoinCommands::BroadcastTx(broadcast_tx) => broadcast_tx.execute_serialized().await, + BitcoinCommands::Transfer(transfer) => transfer.execute_serialized().await, + } + } +} + +pub(crate) enum FileOutputData { + Psbt(Psbt), + Tx(Transaction), +} + +impl FileOutputData { + pub fn txid(&self) -> Txid { + match self { + FileOutputData::Psbt(psbt) => psbt.unsigned_tx.compute_txid(), + FileOutputData::Tx(tx) => tx.compute_txid(), + } + } + + pub fn file_suffix(&self) -> &str { + match self { + FileOutputData::Psbt(_) => "psbt", + FileOutputData::Tx(_) => "tx", + } + } + + pub fn encode(&self) -> Vec { + match self { + FileOutputData::Psbt(psbt) => psbt.serialize(), + FileOutputData::Tx(tx) => { + let mut buf = Vec::new(); + tx.consensus_encode(&mut buf) + .expect("encode tx should success"); + buf + } + } + } + + pub fn default_output_file_path(&self) -> Result { + let temp_dir = env::temp_dir(); + let tx_hash = self.txid(); + let file_name = format!("{}.{}", hex::encode(&tx_hash[..8]), self.file_suffix()); + Ok(temp_dir.join(file_name)) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct FileOutput { + pub content: String, + pub output_type: String, + pub path: String, +} + +impl FileOutput { + pub fn write_to_file(data: FileOutputData, output_path: Option) -> Result { + let path = match output_path { + Some(path) => PathBuf::from(path), + None => data.default_output_file_path()?, + }; + let mut file = File::create(&path)?; + // we write the hex encoded data to the file + // not the binary data, for better readability + let hex = hex::encode(data.encode()); + file.write_all(hex.as_bytes())?; + Ok(FileOutput { + content: hex, + output_type: data.file_suffix().to_string(), + path: path.to_string_lossy().to_string(), + }) + } +} diff --git a/crates/rooch/src/commands/bitcoin/sign_tx.rs b/crates/rooch/src/commands/bitcoin/sign_tx.rs new file mode 100644 index 0000000000..8ea686685a --- /dev/null +++ b/crates/rooch/src/commands/bitcoin/sign_tx.rs @@ -0,0 +1,239 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + cli_types::{CommandAction, FileOrHexInput, WalletContextOptions}, + commands::bitcoin::{FileOutput, FileOutputData}, +}; +use anyhow::bail; +use anyhow::Result; +use async_trait::async_trait; +use bitcoin::{ + key::{Keypair, Secp256k1, TapTweak}, + sighash::{Prevouts, SighashCache}, + Psbt, TapLeafHash, TapSighashType, Witness, +}; +use clap::Parser; +use moveos_types::module_binding::MoveFunctionCaller; +use rooch_key::keystore::account_keystore::AccountKeystore; +use rooch_rpc_client::{wallet_context::WalletContext, Client}; +use rooch_types::{ + address::{BitcoinAddress, ParsedAddress, RoochAddress}, + bitcoin::multisign_account::MultisignAccountModule, + error::{RoochError, RoochResult}, +}; +use tracing::debug; + +#[derive(Debug, Parser)] +pub struct SignTx { + /// The input psbt file path or hex string + input: FileOrHexInput, + + /// The address of the signer when the transaction is a multisign account transaction + /// If not specified, we will auto find the existing participants in the multisign account from the keystore + #[clap(short = 's', long)] + signer: Option, + + /// The output file path + /// If not provided, the file will be written to temp directory + #[clap(long)] + output_file: Option, + + #[clap(flatten)] + pub(crate) context_options: WalletContextOptions, +} + +#[derive(Debug, Clone)] +pub enum SignOutput { + Psbt(Psbt), + Tx(bitcoin::Transaction), +} + +#[async_trait] +impl CommandAction for SignTx { + async fn execute(self) -> RoochResult { + let context = self.context_options.build_require_password()?; + let client = context.get_client().await?; + + let psbt = Psbt::deserialize(&self.input.data)?; + debug!("psbt before sign: {:?}", psbt); + let output = sign_psbt(psbt, self.signer, &context, &client).await?; + debug!("sign output: {:?}", output); + + let file_output_data = match output { + SignOutput::Psbt(psbt) => FileOutputData::Psbt(psbt), + SignOutput::Tx(tx) => FileOutputData::Tx(tx), + }; + let output = FileOutput::write_to_file(file_output_data, self.output_file)?; + Ok(output) + } +} + +pub(crate) async fn sign_psbt( + mut psbt: Psbt, + signer: Option, + context: &WalletContext, + client: &Client, +) -> Result { + let secp = Secp256k1::new(); + + let signer = match signer { + Some(signer) => Some(context.resolve_bitcoin_address(signer).await?), + None => None, + }; + + let multisign_account_module = client.as_module_binding::(); + + let spend_utxos = (0..psbt.inputs.len()) + .map(|i| psbt.spend_utxo(i).ok().cloned()) + .collect::>(); + + if !spend_utxos.iter().all(Option::is_some) { + bail!("Missing spend utxo"); + } + + let all_spend_utxos = spend_utxos.into_iter().flatten().collect::>(); + let prevouts = Prevouts::All(&all_spend_utxos); + + let mut sighash_cache = SighashCache::new(&psbt.unsigned_tx); + + for (idx, input) in psbt.inputs.iter_mut().enumerate() { + if let Some(utxo) = input.witness_utxo.as_ref() { + let addr = BitcoinAddress::from(&utxo.script_pubkey); + let rooch_addr = addr.to_rooch_address(); + if multisign_account_module.is_multisign_account(rooch_addr.into())? { + let account_info = client.rooch.get_multisign_account_info(rooch_addr).await?; + debug!("Account info: {:?}", account_info); + let (control_block, (multisig_script, leaf_version)) = input + .tap_scripts + .iter() + .next() + .ok_or_else(|| anyhow::anyhow!("No tap script found for input {}", idx))?; + + let tap_leaf_hash = TapLeafHash::from_script(multisig_script, *leaf_version); + debug!("Tap leaf hash: {:?}", tap_leaf_hash); + + let hash_ty = TapSighashType::Default; + + let sighash = sighash_cache.taproot_script_spend_signature_hash( + idx, + &prevouts, + tap_leaf_hash, + hash_ty, + )?; + debug!("Calculated sighash: {:?}", sighash); + for participant in account_info.participants.values() { + if let Some(signer) = &signer { + if signer != &participant.participant_bitcoin_address { + continue; + } + } + let participant_addr: RoochAddress = participant.participant_address.into(); + if context.keystore.contains_address(&participant_addr) { + debug!("Signing for participant: {}", participant_addr); + let kp = context.get_key_pair(&participant_addr)?; + let our_pubkey = kp.public().xonly_public_key()?; + + let sk = kp.secp256k1_secret_key().expect("should have secret key"); + let key_pair = Keypair::from_secret_key(&secp, &sk); + + let signature = secp.sign_schnorr(&sighash.into(), &key_pair); + + input.tap_script_sigs.insert( + (our_pubkey, tap_leaf_hash), + bitcoin::taproot::Signature { + signature, + sighash_type: hash_ty, + }, + ); + } + } + + //Try to finalize the psbt + if input.tap_script_sigs.len() >= account_info.threshold as usize { + //TODO handle multiple tap_leaf case + + //make sure the signature order same as the public key order + let mut ordered_signatures = vec![]; + let mut x_only_public_keys = account_info + .participants + .values() + .iter() + .map(|p| p.x_only_public_key()) + .collect::>>()?; + x_only_public_keys.sort(); + //Becase the stack is LIFO, we need to reverse the order + x_only_public_keys.reverse(); + + debug!("Ordered public keys before sign: {:?}", x_only_public_keys); + + for xonly_pubkey in x_only_public_keys { + if let Some(sig) = + input.tap_script_sigs.remove(&(xonly_pubkey, tap_leaf_hash)) + { + ordered_signatures.push(sig.to_vec()); + } else { + //insert empty signature to ensure the order + ordered_signatures.push(vec![]); + } + } + + debug!("Collected signatures: {:?}", ordered_signatures); + + let mut witness = Witness::new(); + for sig in ordered_signatures { + witness.push(sig); + } + + witness.push(multisig_script.as_bytes()); + witness.push(control_block.serialize()); + + debug!("Final witness: {:?}", witness); + input.final_script_witness = Some(witness); + } + } else { + let kp = context.get_key_pair(&rooch_addr)?; + let sk = kp.secp256k1_secret_key().expect("should have secret key"); + + let key_pair = Keypair::from_secret_key(&secp, &sk) + .tap_tweak(&secp, input.tap_merkle_root) + .to_inner(); + + let sighash = sighash_cache.taproot_key_spend_signature_hash( + idx, + &prevouts, + TapSighashType::Default, + )?; + debug!("Calculated sighash: {:?}", sighash); + + let signature = secp.sign_schnorr(&sighash.into(), &key_pair); + debug!("Created signature: {:?}", signature); + let tap_key_sig = bitcoin::taproot::Signature { + signature, + sighash_type: TapSighashType::Default, + }; + + let witness = Witness::from_slice(&[tap_key_sig.to_vec()]); + input.tap_key_sig = Some(tap_key_sig); + input.final_script_witness = Some(witness); + } + } + } + + let sign_output = if is_psbt_finalized(&psbt) { + let tx = psbt.extract_tx().map_err(|e| { + RoochError::CommandArgumentError(format!("Failed to extract tx from psbt: {}", e)) + })?; + SignOutput::Tx(tx) + } else { + SignOutput::Psbt(psbt) + }; + + Ok(sign_output) +} + +fn is_psbt_finalized(psbt: &Psbt) -> bool { + psbt.inputs + .iter() + .all(|input| input.final_script_sig.is_some() || input.final_script_witness.is_some()) +} diff --git a/crates/rooch/src/commands/bitcoin/transaction_builder.rs b/crates/rooch/src/commands/bitcoin/transaction_builder.rs new file mode 100644 index 0000000000..fc6366a843 --- /dev/null +++ b/crates/rooch/src/commands/bitcoin/transaction_builder.rs @@ -0,0 +1,246 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +use std::str::FromStr; + +use super::utxo_selector::UTXOSelector; +use anyhow::{anyhow, bail, Result}; +use bitcoin::{ + absolute::LockTime, bip32::Fingerprint, transaction::Version, Address, Amount, FeeRate, + OutPoint, Psbt, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness, +}; +use moveos_types::{module_binding::MoveFunctionCaller, moveos_std::object::ObjectID}; +use rooch_rpc_api::jsonrpc_types::{btc::utxo::UTXOView, ObjectMetaView}; +use rooch_rpc_client::{wallet_context::WalletContext, Client}; +use rooch_types::{ + address::BitcoinAddress, + bitcoin::multisign_account::{self}, +}; +use tracing::debug; + +#[derive(Debug)] +pub struct TransactionBuilder<'a> { + wallet_context: &'a WalletContext, + client: Client, + utxo_selector: UTXOSelector, + fee_rate: FeeRate, + change_address: Address, + lock_time: Option, +} + +impl<'a> TransactionBuilder<'a> { + const ADDITIONAL_INPUT_VBYTES: usize = 58; + const ADDITIONAL_OUTPUT_VBYTES: usize = 43; + const SCHNORR_SIGNATURE_SIZE: usize = 64; + + pub async fn new( + wallet_context: &'a WalletContext, + client: Client, + sender: Address, + inputs: Vec, + skip_seal_check: bool, + ) -> Result { + let utxo_selector = + UTXOSelector::new(client.clone(), sender.clone(), inputs, skip_seal_check).await?; + Ok(Self { + wallet_context, + client, + utxo_selector, + fee_rate: FeeRate::from_sat_per_vb(10).unwrap(), + change_address: sender, + lock_time: None, + }) + } + + pub fn with_fee_rate(mut self, fee_rate: FeeRate) -> Self { + self.fee_rate = fee_rate; + self + } + + pub fn with_lock_time(mut self, locktime: LockTime) -> Self { + self.lock_time = Some(locktime); + self + } + + pub fn with_change_address(mut self, change_address: Address) -> Self { + self.change_address = change_address; + self + } + + fn estimate_vbytes_with(inputs: usize, outputs: Vec
) -> usize { + Transaction { + version: Version::TWO, + lock_time: LockTime::ZERO, + input: (0..inputs) + .map(|_| TxIn { + previous_output: OutPoint::null(), + script_sig: ScriptBuf::new(), + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + witness: Witness::from_slice(&[&[0; Self::SCHNORR_SIGNATURE_SIZE]]), + }) + .collect(), + output: outputs + .into_iter() + .map(|address| TxOut { + value: Amount::from_sat(0), + script_pubkey: address.script_pubkey(), + }) + .collect(), + } + .vsize() + } + + pub async fn build_transfer(self, receipient: Address, amount: Amount) -> Result { + self.build(vec![(receipient, amount)]).await + } + + pub async fn build(mut self, outputs: Vec<(Address, Amount)>) -> Result { + let total_output = outputs.iter().map(|(_, amount)| *amount).sum::(); + let output_address = outputs + .iter() + .map(|(address, _)| address.clone()) + .collect::>(); + let estimate_inputs = if self.utxo_selector.specific_utxos().is_empty() { + 1 + } else { + self.utxo_selector.specific_utxos().len() + }; + let estimate_fee = self + .fee_rate + .fee_vb( + (Self::estimate_vbytes_with(estimate_inputs, output_address) + + Self::ADDITIONAL_INPUT_VBYTES + + Self::ADDITIONAL_OUTPUT_VBYTES) as u64, + ) + .ok_or_else(|| anyhow!("Failed to estimate fee: {}", self.fee_rate))?; + let mut utxos = self.select_utxos(total_output + estimate_fee).await?; + let mut tx_inputs = vec![]; + let mut total_input = Amount::from_sat(0); + for (_, utxo) in utxos.iter() { + tx_inputs.push(Self::utxo_to_txin(utxo)); + total_input += utxo.amount(); + } + + let tx_outputs = outputs + .into_iter() + .map(|(address, amount)| TxOut { + value: amount, + script_pubkey: address.script_pubkey(), + }) + .collect::>(); + + let mut tx = Transaction { + version: Version::TWO, + lock_time: self.lock_time.unwrap_or(LockTime::ZERO), + input: tx_inputs, + output: tx_outputs, + }; + let fee = self + .fee_rate + .fee_vb(tx.vsize() as u64) + .ok_or_else(|| anyhow!("Failed to estimate fee: {}", self.fee_rate))?; + if fee > estimate_fee && total_input < total_output + fee { + //we need to add more inputs + let additional_utxos = self.select_utxos(total_output + fee - total_input).await?; + tx.input.extend( + additional_utxos + .iter() + .map(|(_, utxo)| Self::utxo_to_txin(utxo)), + ); + total_input += additional_utxos + .iter() + .map(|(_, utxo)| utxo.amount()) + .sum::(); + utxos.extend(additional_utxos); + } + + let change = total_input - total_output - fee; + if change > Amount::from_sat(0) { + tx.output.push(TxOut { + value: change, + script_pubkey: self.change_address.script_pubkey(), + }); + } + let mut psbt = Psbt::from_unsigned_tx(tx)?; + + let multisign_account_module = self + .client + .as_module_binding::(); + for (idx, (utxo_obj_meta, utxo)) in utxos.iter().enumerate() { + let input = &mut psbt.inputs[idx]; + let bitcoin_addr_str = + utxo_obj_meta + .owner_bitcoin_address + .as_ref() + .ok_or_else(|| { + anyhow!("Can not recognize the owner of UTXO {}", utxo.outpoint()) + })?; + let bitcoin_addr = Address::from_str(bitcoin_addr_str)?; + let bitcoin_addr = bitcoin_addr.assume_checked(); + + let is_witness = match bitcoin_addr.address_type() { + Some(addr_type) => !matches!( + addr_type, + bitcoin::AddressType::P2pkh | bitcoin::AddressType::P2sh + ), + None => true, + }; + if is_witness { + input.witness_utxo = Some(TxOut { + value: utxo.amount(), + script_pubkey: bitcoin_addr.script_pubkey(), + }); + } else { + //TODO add non-witness utxo + } + + let rooch_addr = BitcoinAddress::from(bitcoin_addr.clone()).to_rooch_address(); + + if multisign_account_module.is_multisign_account(rooch_addr.into())? { + let account_info = self + .client + .rooch + .get_multisign_account_info(rooch_addr) + .await?; + debug!("Multisign account: {:?}", account_info); + multisign_account::update_multisig_psbt(input, &account_info)?; + } else { + let kp = self.wallet_context.get_key_pair(&rooch_addr)?; + + input.bip32_derivation.insert( + kp.bitcoin_public_key()?.inner, + (Fingerprint::default(), Default::default()), + ); + } + } + + Ok(psbt) + } + + async fn select_utxos( + &mut self, + expected_amount: Amount, + ) -> Result> { + let mut utxos = vec![]; + let mut total_input = Amount::from_sat(0); + while total_input < expected_amount { + let utxo = self.utxo_selector.next_utxo().await?; + if utxo.is_none() { + bail!("not enough BTC funds"); + } + let utxo = utxo.unwrap(); + total_input += utxo.1.amount(); + utxos.push(utxo); + } + Ok(utxos) + } + + fn utxo_to_txin(utxo: &UTXOView) -> TxIn { + TxIn { + previous_output: utxo.outpoint().into(), + script_sig: ScriptBuf::default(), + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + witness: Witness::default(), + } + } +} diff --git a/crates/rooch/src/commands/bitcoin/transfer.rs b/crates/rooch/src/commands/bitcoin/transfer.rs new file mode 100644 index 0000000000..1b64c52386 --- /dev/null +++ b/crates/rooch/src/commands/bitcoin/transfer.rs @@ -0,0 +1,89 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +use super::sign_tx::SignOutput; +use super::transaction_builder::TransactionBuilder; +use crate::cli_types::{CommandAction, WalletContextOptions}; +use crate::commands::bitcoin::sign_tx::sign_psbt; +use async_trait::async_trait; +use bitcoin::consensus::Encodable; +use bitcoin::{Amount, FeeRate}; +use clap::Parser; +use rooch_types::address::ParsedAddress; +use rooch_types::error::{RoochError, RoochResult}; +use tracing::debug; + +#[derive(Debug, Parser)] +pub struct Transfer { + /// The sender address of the transaction, if not specified, the active address will be used + #[clap(long, short = 's', default_value = "default")] + sender: ParsedAddress, + + /// The receiver address of the BTC + #[clap(long, short = 't')] + to: ParsedAddress, + + /// The BTC amount in satoshi to transfer + #[clap(long, short = 'a')] + amount: u64, + + /// The fee rate of the transaction, if not specified, the fee will be calculated automatically + #[clap(long)] + fee_rate: Option, + + /// Skip check seal of the UTXOs, default is false + /// If set to true, some UTXO which carries other asserts, such as Inscription, maybe unexpected spent. + #[clap(long)] + skip_check_seal: bool, + + #[clap(flatten)] + pub(crate) context_options: WalletContextOptions, +} + +#[async_trait] +impl CommandAction for Transfer { + async fn execute(self) -> RoochResult { + let context = self.context_options.build_require_password()?; + let client = context.get_client().await?; + + let bitcoin_network = context.get_bitcoin_network().await?; + + let sender = context.resolve_bitcoin_address(self.sender).await?; + let to = context.resolve_bitcoin_address(self.to).await?; + let amount = Amount::from_sat(self.amount); + + let mut tx_builder = TransactionBuilder::new( + &context, + client.clone(), + sender.to_bitcoin_address(bitcoin_network)?, + vec![], + self.skip_check_seal, + ) + .await?; + + if let Some(fee_rate) = self.fee_rate { + tx_builder = tx_builder.with_fee_rate(fee_rate); + } + + let psbt = tx_builder + .build_transfer(to.to_bitcoin_address(bitcoin_network)?, amount) + .await?; + debug!("PSBT: {}", serde_json::to_string_pretty(&psbt).unwrap()); + let sign_out = sign_psbt(psbt, None, &context, &client).await?; + match sign_out { + SignOutput::Psbt(_psbt) => { + return Err(RoochError::CommandArgumentError( + "The sender address should not be a multisig address".to_string(), + )) + } + SignOutput::Tx(tx) => { + let mut raw_tx = vec![]; + tx.consensus_encode(&mut raw_tx)?; + Ok(client + .rooch + .broadcast_bitcoin_tx(raw_tx.into(), None) + .await?) + } + } + } +} diff --git a/crates/rooch/src/commands/bitcoin/utxo_selector.rs b/crates/rooch/src/commands/bitcoin/utxo_selector.rs new file mode 100644 index 0000000000..39ca21c8fa --- /dev/null +++ b/crates/rooch/src/commands/bitcoin/utxo_selector.rs @@ -0,0 +1,152 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::{bail, Result}; +use bitcoin::{Address, Amount}; +use moveos_types::moveos_std::object::{ObjectID, GENESIS_STATE_ROOT}; +use rooch_rpc_api::jsonrpc_types::{ + btc::utxo::{UTXOFilterView, UTXOStateView, UTXOView}, + IndexerStateIDView, ObjectMetaView, +}; +use rooch_rpc_client::Client; +use tracing::debug; + +#[derive(Debug)] +pub struct UTXOSelector { + client: Client, + sender: Address, + specific_utxos: Vec, + loaded_page: Option<(Option, bool)>, + candidate_utxos: Vec<(ObjectMetaView, UTXOView)>, + skip_seal_check: bool, +} + +impl UTXOSelector { + pub async fn new( + client: Client, + sender: Address, + specific_utxos: Vec, + skip_seal_check: bool, + ) -> Result { + let mut selector = Self { + client, + sender, + specific_utxos, + loaded_page: None, + candidate_utxos: vec![], + skip_seal_check, + }; + selector.load_specific_utxos().await?; + Ok(selector) + } + + async fn load_specific_utxos(&mut self) -> Result<()> { + if self.specific_utxos.is_empty() { + return Ok(()); + } + let utxos_objs = self + .client + .rooch + .query_utxos( + UTXOFilterView::object_ids(self.specific_utxos.clone()), + None, + None, + None, + ) + .await?; + for utxo_state_view in utxos_objs.data { + let utxo = &utxo_state_view.value; + if !self.skip_seal_check { + let minimal_non_dust = self.sender.script_pubkey().minimal_non_dust(); + if skip_utxo(&utxo_state_view, minimal_non_dust) { + bail!("UTXO {} has seal or tempstate attachment: {:?}, please use --skip-seal-check to skip this check", utxo_state_view.value.outpoint(), utxo_state_view); + } + } + if utxo_state_view.metadata.owner_bitcoin_address.is_none() { + bail!( + "Can not recognize the owner of UTXO {}, metadata: {:?}", + utxo.outpoint(), + utxo_state_view.metadata + ); + } + self.candidate_utxos + .push((utxo_state_view.metadata, utxo_state_view.value)); + } + Ok(()) + } + + async fn load_utxos(&mut self) -> Result<()> { + let (next_cursor, has_next_page) = self.loaded_page.unwrap_or((None, true)); + if !has_next_page { + return Ok(()); + } + let utxo_page = self + .client + .rooch + .query_utxos( + UTXOFilterView::owner(self.sender.clone()), + next_cursor.map(Into::into), + None, + Some(false), + ) + .await?; + debug!("loaded utxos: {:?}", utxo_page.data.len()); + let minimal_non_dust = self.sender.script_pubkey().minimal_non_dust(); + for utxo_view in utxo_page.data { + let utxo = &utxo_view.value; + if !self.skip_seal_check && skip_utxo(&utxo_view, minimal_non_dust) { + continue; + } + if utxo_view.metadata.owner_bitcoin_address.is_none() { + debug!( + "Can not recognize the owner of UTXO {}, metadata: {:?}, skip.", + utxo.outpoint(), + utxo_view.metadata + ); + continue; + } + self.candidate_utxos + .push((utxo_view.metadata, utxo_view.value)); + } + self.loaded_page = Some((utxo_page.next_cursor, utxo_page.has_next_page)); + Ok(()) + } + /// Get the next utxo from the candidate utxos + pub async fn next_utxo(&mut self) -> Result> { + if self.candidate_utxos.is_empty() { + self.load_utxos().await?; + } + Ok(self.candidate_utxos.pop()) + } + + pub fn specific_utxos(&self) -> &[ObjectID] { + &self.specific_utxos + } +} + +fn skip_utxo(utxo_state_view: &UTXOStateView, minimal_non_dust: Amount) -> bool { + let utxo = &utxo_state_view.value; + if !utxo.seals.is_empty() { + debug!( + "UTXO {} is has seals: {:?}, skip.", + utxo.outpoint(), + utxo.seals + ); + return true; + } + if utxo.amount() <= minimal_non_dust { + debug!( + "UTXO {} is less than dust value: {}, skip.", + utxo.outpoint(), + minimal_non_dust + ); + return true; + } + if utxo_state_view.metadata.state_root.is_some() + && utxo_state_view.metadata.state_root.as_ref().unwrap().0 != *GENESIS_STATE_ROOT + { + debug!("UTXO {} is contains tempstate, skip.", utxo.outpoint()); + return true; + } + false +} diff --git a/crates/rooch/src/commands/mod.rs b/crates/rooch/src/commands/mod.rs index 6d2c3ab0c5..dde19c7665 100644 --- a/crates/rooch/src/commands/mod.rs +++ b/crates/rooch/src/commands/mod.rs @@ -3,6 +3,7 @@ pub mod abi; pub mod account; +pub mod bitcoin; pub mod db; pub mod dynamic_field; pub mod env; diff --git a/crates/rooch/src/commands/object.rs b/crates/rooch/src/commands/object.rs index ccc6d2a0d2..297f9706d1 100644 --- a/crates/rooch/src/commands/object.rs +++ b/crates/rooch/src/commands/object.rs @@ -133,7 +133,12 @@ impl CommandAction for ObjectCommand { }; let result = client .rooch - .query_utxos(utxo_fitler, None, self.limit, Some(query_options)) + .query_utxos( + utxo_fitler, + None, + self.limit, + Some(query_options.descending), + ) .await?; serde_json::to_string_pretty(&result).unwrap() } diff --git a/crates/rooch/src/commands/transaction/commands/mod.rs b/crates/rooch/src/commands/transaction/commands/mod.rs index 9a58668aa2..a1b3891a03 100644 --- a/crates/rooch/src/commands/transaction/commands/mod.rs +++ b/crates/rooch/src/commands/transaction/commands/mod.rs @@ -16,10 +16,6 @@ pub mod query; pub mod sign; pub mod submit; -pub(crate) fn is_file_path(s: &str) -> bool { - s.contains('/') || s.contains('\\') || s.contains('.') -} - pub(crate) enum FileOutputData { RoochTransactionData(RoochTransactionData), SignedRoochTransaction(RoochTransaction), diff --git a/crates/rooch/src/commands/transaction/commands/sign.rs b/crates/rooch/src/commands/transaction/commands/sign.rs index 141b54d3e8..0f8c5e1216 100644 --- a/crates/rooch/src/commands/transaction/commands/sign.rs +++ b/crates/rooch/src/commands/transaction/commands/sign.rs @@ -1,21 +1,20 @@ // Copyright (c) RoochNetwork // SPDX-License-Identifier: Apache-2.0 -use super::{is_file_path, FileOutput, FileOutputData}; -use crate::cli_types::{CommandAction, WalletContextOptions}; +use super::{FileOutput, FileOutputData}; +use crate::cli_types::{CommandAction, FileOrHexInput, WalletContextOptions}; use async_trait::async_trait; use moveos_types::module_binding::MoveFunctionCaller; use rooch_key::keystore::account_keystore::AccountKeystore; use rooch_types::{ address::{ParsedAddress, RoochAddress}, bitcoin::multisign_account::MultisignAccountModule, - error::{RoochError, RoochResult}, + error::RoochResult, transaction::{ authenticator::BitcoinAuthenticator, rooch::PartiallySignedRoochTransaction, RoochTransaction, RoochTransactionData, }, }; -use std::{fs::File, io::Read, str::FromStr}; #[derive(Debug, Clone)] pub enum SignInput { @@ -23,33 +22,14 @@ pub enum SignInput { PartiallySignedRoochTransaction(PartiallySignedRoochTransaction), } -impl FromStr for SignInput { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - let data_hex = if is_file_path(s) { - //load hex from file - let mut file = File::open(s).map_err(|e| { - RoochError::CommandArgumentError(format!("Failed to open file: {}, err:{:?}", s, e)) - })?; - let mut hex_str = String::new(); - file.read_to_string(&mut hex_str).map_err(|e| { - RoochError::CommandArgumentError(format!("Failed to read file: {}, err:{:?}", s, e)) - })?; - hex_str.strip_prefix("0x").unwrap_or(&hex_str).to_string() - } else { - s.strip_prefix("0x").unwrap_or(s).to_string() - }; - let data_bytes = hex::decode(&data_hex).map_err(|e| { - RoochError::CommandArgumentError(format!( - "Failed to decode hex: {}, err:{:?}", - data_hex, e - )) - })?; - let input = match bcs::from_bytes(&data_bytes) { +impl TryFrom for SignInput { + type Error = anyhow::Error; + + fn try_from(value: FileOrHexInput) -> Result { + let input = match bcs::from_bytes::(&value.data) { Ok(tx_data) => SignInput::RoochTransactionData(tx_data), Err(_) => { - let psrt: PartiallySignedRoochTransaction = match bcs::from_bytes(&data_bytes) { + let psrt: PartiallySignedRoochTransaction = match bcs::from_bytes(&value.data) { Ok(psrt) => psrt, Err(_) => { return Err(anyhow::anyhow!("Invalid tx data or psrt data")); @@ -99,7 +79,7 @@ pub struct SignCommand { /// Input data to be used for signing /// Input can be a transaction data hex or a partially signed transaction data hex /// or a file path which contains transaction data or partially signed transaction data - input: SignInput, + input: FileOrHexInput, /// The address of the signer when the transaction is a multisign account transaction /// If not specified, we will auto find the existing participants in the multisign account from the keystore @@ -107,7 +87,7 @@ pub struct SignCommand { signer: Option, /// The output file path for the signed transaction - /// If not specified, the signed output will write to current directory. + /// If not specified, the signed output will write to temp directory. #[clap(long, short = 'o')] output: Option, @@ -124,10 +104,12 @@ impl SignCommand { let context = self.context.build_require_password()?; let client = context.get_client().await?; let multisign_account_module = client.as_module_binding::(); - let sender = self.input.sender(); + let sign_input = SignInput::try_from(self.input)?; + let sender = sign_input.sender(); let output = if multisign_account_module.is_multisign_account(sender.into())? { let threshold = multisign_account_module.threshold(sender.into())?; - let mut psrt = match self.input { + + let mut psrt = match sign_input { SignInput::RoochTransactionData(tx_data) => { PartiallySignedRoochTransaction::new(tx_data, threshold) } @@ -181,7 +163,7 @@ impl SignCommand { SignOutput::PartiallySignedRoochTransaction(psrt) } } else { - let tx_data = match self.input { + let tx_data = match sign_input { SignInput::RoochTransactionData(tx_data) => tx_data, SignInput::PartiallySignedRoochTransaction(_) => { return Err(anyhow::anyhow!( diff --git a/crates/rooch/src/commands/transaction/commands/submit.rs b/crates/rooch/src/commands/transaction/commands/submit.rs index 73337d9244..b0de6f57a5 100644 --- a/crates/rooch/src/commands/transaction/commands/submit.rs +++ b/crates/rooch/src/commands/transaction/commands/submit.rs @@ -1,21 +1,19 @@ // Copyright (c) RoochNetwork // SPDX-License-Identifier: Apache-2.0 -use super::is_file_path; -use crate::cli_types::{CommandAction, WalletContextOptions}; +use crate::cli_types::{CommandAction, FileOrHexInput, WalletContextOptions}; use async_trait::async_trait; use rooch_rpc_api::jsonrpc_types::ExecuteTransactionResponseView; use rooch_types::{ error::{RoochError, RoochResult}, transaction::RoochTransaction, }; -use std::{fs::File, io::Read}; /// Get transactions by order #[derive(Debug, clap::Parser)] pub struct SubmitCommand { /// Transaction data hex or file location to be used for submitting - input: String, + input: FileOrHexInput, #[clap(flatten)] context: WalletContextOptions, @@ -30,34 +28,11 @@ impl CommandAction for SubmitCommand { async fn execute(self) -> RoochResult { let context = self.context.build()?; - let tx_hex = if is_file_path(&self.input) { - let mut file = File::open(&self.input).map_err(|e| { - RoochError::CommandArgumentError(format!( - "Failed to open file: {}, err:{:?}", - self.input, e - )) - })?; - let mut hex_str = String::new(); - file.read_to_string(&mut hex_str).map_err(|e| { - RoochError::CommandArgumentError(format!( - "Failed to read file: {}, err:{:?}", - self.input, e - )) - })?; - hex_str - } else { - self.input - }; - let tx_bytes = hex::decode(tx_hex.strip_prefix("0x").unwrap_or(&tx_hex)).map_err(|e| { + let signed_tx = bcs::from_bytes::(&self.input.data).map_err(|e| { RoochError::CommandArgumentError(format!( "Invalid signed transaction hex, err: {:?}, hex: {}", - e, tx_hex - )) - })?; - let signed_tx = bcs::from_bytes::(&tx_bytes).map_err(|e| { - RoochError::CommandArgumentError(format!( - "Invalid signed transaction hex, err: {:?}, hex: {}", - e, tx_hex + e, + hex::encode(&self.input.data) )) })?; diff --git a/crates/rooch/src/lib.rs b/crates/rooch/src/lib.rs index dabc6f1287..6f62b26c4e 100644 --- a/crates/rooch/src/lib.rs +++ b/crates/rooch/src/lib.rs @@ -11,10 +11,10 @@ use clap::builder::{ }; use cli_types::CommandAction; use commands::{ - abi::ABI, account::Account, dynamic_field::DynamicField, env::Env, genesis::Genesis, - init::Init, move_cli::MoveCli, object::ObjectCommand, resource::ResourceCommand, rpc::Rpc, - server::Server, session_key::SessionKey, state::StateCommand, transaction::Transaction, - upgrade::Upgrade, util::Util, version::Version, + abi::ABI, account::Account, bitcoin::Bitcoin, dynamic_field::DynamicField, env::Env, + genesis::Genesis, init::Init, move_cli::MoveCli, object::ObjectCommand, + resource::ResourceCommand, rpc::Rpc, server::Server, session_key::SessionKey, + state::StateCommand, transaction::Transaction, upgrade::Upgrade, util::Util, version::Version, }; use once_cell::sync::Lazy; use rooch_types::error::RoochResult; @@ -46,6 +46,7 @@ static LONG_VERSION: Lazy = Lazy::new(|| { pub enum Command { Version(Version), Account(Account), + Bitcoin(Bitcoin), Init(Init), Move(MoveCli), Server(Server), @@ -72,6 +73,7 @@ pub async fn run_cli(opt: RoochCli) -> RoochResult { match opt.cmd { Command::Version(version) => version.execute().await, Command::Account(account) => account.execute().await, + Command::Bitcoin(bitcoin) => bitcoin.execute().await, Command::Move(move_cli) => move_cli.execute().await, Command::Server(server) => server.execute().await, Command::Init(init) => init.execute_serialized().await, diff --git a/crates/testsuite/features/multisign.feature b/crates/testsuite/features/multisign.feature index 2486d27ecf..943f0d8c09 100644 --- a/crates/testsuite/features/multisign.feature +++ b/crates/testsuite/features/multisign.feature @@ -3,6 +3,7 @@ Feature: Rooch CLI multisign integration tests @serial Scenario: multisign_account + Given a bitcoind server for multisign_account Given a server for multisign_account Then cmd: "account create" @@ -14,19 +15,58 @@ Feature: Rooch CLI multisign integration tests Then cmd: "account create-multisign -t 2 -p {{$.account[-1].account0.public_key}} -p {{$.account[-1].account1.public_key}} -p {{$.account[-1].account2.public_key}} --json" Then assert: "'{{$.account[-1]}}' not_contains error" + # Create and load a wallet + Then cmd bitcoin-cli: "createwallet \"test_wallet\"" + Then cmd bitcoin-cli: "loadwallet \"test_wallet\"" + + # Prepare funds + Then cmd bitcoin-cli: "getnewaddress" + + # mint btc + Then cmd bitcoin-cli: "generatetoaddress 101 {{$.getnewaddress[-1]}}" + + # Get UTXO for transaction input + Then cmd bitcoin-cli: "listunspent 1 9999999 [\"{{$.getnewaddress[-1]}}\"] true" + + # Create a Bitcoin transaction transfer to multisign account + Then cmd bitcoin-cli: "createrawtransaction [{\"txid\":\"{{$.listunspent[-1][0].txid}}\",\"vout\":{{$.listunspent[-1][0].vout}}}] {\"{{$.account[-1].multisign_bitcoin_address}}\":49.999}" + Then cmd bitcoin-cli: "signrawtransactionwithwallet {{$.createrawtransaction[-1]}}" + Then cmd: "bitcoin broadcast-tx {{$.signrawtransactionwithwallet[-1].hex}}" + Then assert: "'{{$.bitcoin[-1]}}' not_contains error" + + Then cmd bitcoin-cli: "generatetoaddress 1 {{$.getnewaddress[-1]}}" + Then sleep: "20" # wait for the transaction to be confirmed + + # l1 transaction + Then cmd: "bitcoin build-tx --sender {{$.account[-1].multisign_bitcoin_address}} -o {{$.account[-2].account0.bitcoin_address}}:100000000" + Then assert: "'{{$.bitcoin[-1]}}' not_contains error" + Then cmd: "bitcoin sign-tx -s {{$.account[-1].participants[0].participant_address}} {{$.bitcoin[-1].path}}" + Then assert: "'{{$.bitcoin[-1]}}' not_contains error" + Then cmd: "bitcoin sign-tx -s {{$.account[-1].participants[2].participant_address}} {{$.bitcoin[-1].path}}" + Then assert: "'{{$.bitcoin[-1]}}' not_contains error" + Then cmd: "bitcoin broadcast-tx {{$.bitcoin[-1].path}}" + Then assert: "'{{$.bitcoin[-1]}}' not_contains error" + + Then cmd bitcoin-cli: "generatetoaddress 1 {{$.getnewaddress[-1]}}" + Then sleep: "10" # wait for the transaction to be confirmed + + Then cmd: "account balance -a {{$.account[-2].account0.address}} --json" + Then assert: "{{$.account[-1].Bitcoin.balance}} == 100000000" + #transfer some gas to multisign account - Then cmd: "account transfer --to {{$.account[-1].multisign_address}} --amount 10000000000 --coin-type rooch_framework::gas_coin::RGas" - Then assert: "{{$.account[-1].execution_info.status.type}} == executed" + Then cmd: "account transfer --to {{$.account[-2].multisign_address}} --amount 10000000000 --coin-type rooch_framework::gas_coin::RGas" + Then assert: "{{$.account[-1].execution_info.status.type}} == executed" - # transaction - Then cmd: "tx build --sender {{$.account[-2].multisign_address}} --function rooch_framework::empty::empty --json" + # l2 transaction + Then cmd: "tx build --sender {{$.account[-3].multisign_address}} --function rooch_framework::empty::empty --json" Then assert: "'{{$.tx[-1]}}' not_contains error" - Then cmd: "tx sign {{$.tx[-1].path}} -s {{$.account[-2].participants[0].participant_address}} --json" + Then cmd: "tx sign {{$.tx[-1].path}} -s {{$.account[-3].participants[0].participant_address}} --json" Then assert: "'{{$.tx[-1]}}' not_contains error" - Then cmd: "tx sign {{$.tx[-1].path}} -s {{$.account[-2].participants[1].participant_address}} --json" + Then cmd: "tx sign {{$.tx[-1].path}} -s {{$.account[-3].participants[1].participant_address}} --json" Then assert: "'{{$.tx[-1]}}' not_contains error" Then cmd: "tx submit {{$.tx[-1].path}} --json" Then assert: "{{$.tx[-1].execution_info.status.type}} == executed" Then stop the server + Then stop the bitcoind server diff --git a/frameworks/rooch-framework/Cargo.toml b/frameworks/rooch-framework/Cargo.toml index 8b6d0e5316..0760835502 100644 --- a/frameworks/rooch-framework/Cargo.toml +++ b/frameworks/rooch-framework/Cargo.toml @@ -20,7 +20,6 @@ smallvec = { workspace = true } hex = { workspace = true } tracing = { workspace = true } bitcoin = { workspace = true } -musig2 = { workspace = true } move-binary-format = { workspace = true } move-core-types = { workspace = true } diff --git a/frameworks/rooch-framework/src/natives/rooch_framework/bitcoin_address.rs b/frameworks/rooch-framework/src/natives/rooch_framework/bitcoin_address.rs index 8552f824f5..5af04b9a55 100644 --- a/frameworks/rooch-framework/src/natives/rooch_framework/bitcoin_address.rs +++ b/frameworks/rooch-framework/src/natives/rooch_framework/bitcoin_address.rs @@ -6,7 +6,7 @@ use bitcoin::{ hashes::Hash, hex::DisplayHex, secp256k1::Secp256k1, - PublicKey, TapNodeHash, XOnlyPublicKey, + KnownHrp, PublicKey, TapNodeHash, XOnlyPublicKey, }; use move_binary_format::errors::{PartialVMError, PartialVMResult}; use move_core_types::{ @@ -101,14 +101,12 @@ pub fn verify_bitcoin_address_with_public_key( + gas_params.per_byte.unwrap() * NumBytes::new((pk_ref.len() + bitcoin_addr.to_bytes().len()) as u64); - // TODO: convert to internal rooch public key and to bitcoin address? let Ok(pk) = PublicKey::from_slice(&pk_ref) else { return Ok(NativeResult::ok(cost, smallvec![Value::bool(false)])); }; - // TODO: compare the input bitcoin address with the converted bitcoin address - let addr = match Address::from_str(&bitcoin_addr.to_string()) { - Ok(addr) => addr.assume_checked(), + let addr = match Address::try_from(bitcoin_addr) { + Ok(addr) => addr, Err(_) => { return Ok(NativeResult::ok(cost, smallvec![Value::bool(false)])); } @@ -118,7 +116,7 @@ pub fn verify_bitcoin_address_with_public_key( Some(AddressType::P2tr) => { let xonly_pubkey = XOnlyPublicKey::from(pk.inner); let secp = Secp256k1::verification_only(); - let trust_addr = Address::p2tr(&secp, xonly_pubkey, None, *addr.network()); + let trust_addr = Address::p2tr(&secp, xonly_pubkey, None, KnownHrp::Mainnet); addr.is_related_to_pubkey(&pk) || trust_addr.to_string() == addr.to_string() } _ => addr.is_related_to_pubkey(&pk), diff --git a/frameworks/rooch-nursery/Cargo.toml b/frameworks/rooch-nursery/Cargo.toml index 4a921d0674..27b3af2951 100644 --- a/frameworks/rooch-nursery/Cargo.toml +++ b/frameworks/rooch-nursery/Cargo.toml @@ -20,7 +20,6 @@ smallvec = { workspace = true } hex = { workspace = true } tracing = { workspace = true } bitcoin = { workspace = true } -musig2 = { workspace = true } log = { workspace = true } serde_json = { workspace = true } ciborium = { workspace = true } diff --git a/moveos/moveos-types/src/moveos_std/simple_map.rs b/moveos/moveos-types/src/moveos_std/simple_map.rs index 0ee78bb1eb..b0c51440c9 100644 --- a/moveos/moveos-types/src/moveos_std/simple_map.rs +++ b/moveos/moveos-types/src/moveos_std/simple_map.rs @@ -30,6 +30,14 @@ impl SimpleMap { pub fn create() -> Self { Self { data: vec![] } } + + pub fn keys(&self) -> Vec<&Key> { + self.data.iter().map(|element| &element.key).collect() + } + + pub fn values(&self) -> Vec<&Value> { + self.data.iter().map(|element| &element.value).collect() + } } impl SimpleMap diff --git a/moveos/moveos-types/src/moveos_std/simple_multimap.rs b/moveos/moveos-types/src/moveos_std/simple_multimap.rs index efb3596f99..0bf685305f 100644 --- a/moveos/moveos-types/src/moveos_std/simple_multimap.rs +++ b/moveos/moveos-types/src/moveos_std/simple_multimap.rs @@ -117,3 +117,20 @@ where ))]) } } + +impl From> for Vec<(Key, Vec)> { + fn from(map: SimpleMultiMap) -> Self { + map.data.into_iter().map(|e| (e.key, e.value)).collect() + } +} + +impl From)>> for SimpleMultiMap { + fn from(data: Vec<(Key, Vec)>) -> Self { + SimpleMultiMap { + data: data + .into_iter() + .map(|(key, value)| Element { key, value }) + .collect(), + } + } +} diff --git a/moveos/smt/src/jellyfish_merkle/hash.rs b/moveos/smt/src/jellyfish_merkle/hash.rs index 4e2481a02b..c980d30da0 100644 --- a/moveos/smt/src/jellyfish_merkle/hash.rs +++ b/moveos/smt/src/jellyfish_merkle/hash.rs @@ -1,9 +1,8 @@ // Copyright (c) RoochNetwork // SPDX-License-Identifier: Apache-2.0 -use bitcoin_hashes::{sha256t_hash_newtype, HashEngine}; +use bitcoin_hashes::{hex::FromHex, sha256t_hash_newtype, HashEngine}; use bytes::Bytes; -use hex::FromHex; use more_asserts::debug_assert_lt; use once_cell::sync::Lazy; use primitive_types::H256; @@ -145,7 +144,7 @@ impl SMTNodeHash { } /// Parse a given hex string to a hash value. - pub fn from_hex>(hex: T) -> Result { + pub fn from_hex(hex: &str) -> Result { <[u8; Self::LEN]>::from_hex(hex) .map_err(|_| HashParseError) .map(Self::new) @@ -169,18 +168,7 @@ impl SMTNodeHash { return Err(HashParseError); } let literal = literal.strip_prefix("0x").unwrap_or(literal); - let hex_len = literal.len(); - // If the string is too short, pad it - if hex_len < Self::LEN * 2 { - let mut hex_str = String::with_capacity(Self::LEN * 2); - for _ in 0..Self::LEN * 2 - hex_len { - hex_str.push('0'); - } - hex_str.push_str(literal); - Self::from_hex(hex_str) - } else { - Self::from_hex(literal) - } + Self::from_hex(literal) } } diff --git a/scripts/bitcoin/README.md b/scripts/bitcoin/README.md index ec46009020..0835f1eb8c 100644 --- a/scripts/bitcoin/README.md +++ b/scripts/bitcoin/README.md @@ -13,11 +13,11 @@ This directory contains scripts for setting up a local development environment f ## Development on rooch -1. Run `rooch server start --btc-rpc-url http://127.0.0.1:18443 --btc-rpc-username roochuser --btc-rpc-password roochpass` +1. Run `rooch server start -n local --btc-sync-block-interval 1 --btc-rpc-url http://127.0.0.1:18443 --btc-rpc-username roochuser --btc-rpc-password roochpass` 2. Run `rooch account list --json` to get the `bitcoin_address` 3. Run `bitcoin-cli generatetoaddress 101 ` to generate 101 blocks to the address -2. Run `rooch rpc request --method rooch_queryObjectStates --params '[{"object_type":"0x4::utxo::UTXO"}, null, "2", {"descending": true,"showDisplay":false}]'` to query the UTXO set -3. Run `rooch rpc request --method rooch_queryObjectStates --params '[{"object_type":"0x4::ord::Inscription"}, null, "2", {"descending": true,"showDisplay":false}]'` to query the Inscription set +2. Run `rooch rpc request --method btc_queryUTXOs --params '["all", null, "2", true]'` to query the UTXO set +3. Run `rooch rpc request --method btc_queryInscriptions --params '["all", null, "2", true]'` to query the Inscription set ## Usage