diff --git a/Makefile b/Makefile index 2e339ec..f4943f5 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ fmt: check: $(CC) build - RUST_LOG=debug $(CC) test --all -- --show-output + $(CC) test --all -- --show-output example: @echo "No example for the moment" diff --git a/flake.nix b/flake.nix index 908491a..e13d672 100644 --- a/flake.nix +++ b/flake.nix @@ -19,8 +19,8 @@ version = "rgb-hooks"; src = pkgs.fetchgit { url = "https://github.com/vincenzopalazzo/lightning"; - rev = "0b39cf0318f04a4c3f3bb59d2c63cd8ba856a0d9"; - sha256 = "sha256-y+4i8srlaiVfNnIKhLhUUSrcdSjD2/WOjLSzffcoX88="; + rev = "e1da6c799f302ea0346e352a07fbc083e4b7d0df"; + sha256 = "sha256-zaTNd0KnokiQdeMG9cE6Yx5FbQBA4F3Lm2vvWiWWjR8="; fetchSubmodules = true; }; configureFlags = [ "--disable-rust" "--disable-valgrind" ]; @@ -54,7 +54,9 @@ shellHook = '' export HOST_CC=gcc export PWD="$(pwd)" - export RUST_LOG=debug + export RUST_LOG=info + + export PLUGIN_NAME=rgb-cln make ''; diff --git a/rgb-cln/Cargo.toml b/rgb-cln/Cargo.toml index a4042ad..430b54a 100644 --- a/rgb-cln/Cargo.toml +++ b/rgb-cln/Cargo.toml @@ -5,8 +5,9 @@ edition = "2021" [dependencies] clightningrpc-common = { git = "https://github.com/laanwj/cln4rust.git" } -clightningrpc-plugin = { git = "https://github.com/laanwj/cln4rust.git " } +clightningrpc-plugin = { git = "https://github.com/laanwj/cln4rust.git", features = [ "log" ] } clightningrpc-plugin-macros = { git = "https://github.com/laanwj/cln4rust.git" } +vls-core = { git = "https://gitlab.com/vincenzopalazzo/validating-lightning-signer.git", branch = "macros/derivation" } log = "0.4.20" anyhow = "1.0.79" serde = "1.0.159" diff --git a/rgb-cln/src/plugin.rs b/rgb-cln/src/plugin.rs index 532528c..b5494a8 100644 --- a/rgb-cln/src/plugin.rs +++ b/rgb-cln/src/plugin.rs @@ -1,24 +1,35 @@ -//! Plugin implementation +//! RGB Plugin implementation //! //! Author: Vincenzo Palazzo -use std::{fmt::Debug, sync::Arc}; +use std::fmt; +use std::fs; +use std::io; +use std::sync::Arc; +use lightning_signer::signer::derive::KeyDerive; +use lightning_signer::signer::derive::NativeKeyDerive; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_json as json; use clightningrpc_common::client::Client; +use clightningrpc_plugin::error; +use clightningrpc_plugin::errors::PluginError; use clightningrpc_plugin::{commands::RPCCommand, plugin::Plugin}; use clightningrpc_plugin_macros::plugin; -use rgb_common::{anyhow, RGBManager}; +use rgb_common::bitcoin::consensus::serialize; +use rgb_common::bitcoin::consensus::Decodable; +use rgb_common::bitcoin::hashes::hex::{FromHex, ToHex}; +use rgb_common::bitcoin::util::bip32::ExtendedPrivKey; +use rgb_common::RGBManager; +use rgb_common::{anyhow, bitcoin}; #[derive(Clone, Debug)] pub(crate) struct State { /// The RGB Manager where we ask to do everything /// related to lightning. rgb_manager: Option>, - /// CLN RPC cln_rpc: Option>, } @@ -31,6 +42,7 @@ impl State { } } + #[allow(dead_code)] pub(crate) fn rpc(&self) -> Arc { self.cln_rpc.clone().unwrap() } @@ -39,7 +51,7 @@ impl State { self.rgb_manager.clone().unwrap() } - pub fn call( + pub fn call( &self, method: &str, payload: T, @@ -70,44 +82,90 @@ pub fn build_plugin() -> anyhow::Result> { Ok(plugin) } -// FIXME: move to another part of the code. -#[derive(Debug, Deserialize)] -pub struct GetInfo { - id: String, +fn read_secret(file: fs::File, network: &str) -> anyhow::Result { + let buffer = io::BufReader::new(file); + let hsmd_derive = NativeKeyDerive::new(network)?; + let xpriv = hsmd_derive.master_key(buffer.buffer()); + Ok(xpriv) } fn on_init(plugin: &mut Plugin) -> json::Value { let config = plugin.configuration.clone().unwrap(); let rpc_file = format!("{}/{}", config.lightning_dir, config.rpc_file); + let hsmd_file = format!("{}/hsm_secret", config.lightning_dir); + + let hsmd_file = fs::File::open(hsmd_file); + if let Err(err) = hsmd_file { + return json::json!({ "disable": format!("{err}") }); + } + // SAFETY: we check if it is an error just before. + let hsmd_file = hsmd_file.unwrap(); + + let hsmd_secret = read_secret(hsmd_file, &config.network); + if let Err(err) = hsmd_secret { + return json::json!({ "disable": format!("{err}") }); + } + // SAFETY: we check if it is an error just error. + let master_xprv = hsmd_secret.unwrap(); let rpc = Client::new(rpc_file); plugin.state.cln_rpc = Some(Arc::new(rpc)); - let getinfo: anyhow::Result = plugin.state.call("getinfo", json::json!({})); - if let Err(err) = getinfo { - return json::json!({ "disable": format!("{err}") }); - } - // SAFETY: Safe to unwrap because we unwrap before. - let getinfo = getinfo.unwrap(); - // FIXME: I can get the public key from the configuration? - let manager = RGBManager::init(&config.lightning_dir, &getinfo.id, &config.network); + let manager = RGBManager::init(&config.lightning_dir, &master_xprv, &config.network); if let Err(err) = manager { return json::json!({ "disable": format!("{err}") }); } - + // SAFETY: we check if it is an error just before. + let manager = manager.unwrap(); + plugin.state.rgb_manager = Some(Arc::new(manager)); json::json!({}) } #[derive(Clone, Debug)] struct OnFundingChannelTx; +#[derive(Clone, Debug, Deserialize)] +struct OnFundingChannelTxHook { + onfunding_channel_tx: OnFundingChannelTxBody, +} + +#[derive(Clone, Debug, Deserialize)] +struct OnFundingChannelTxBody { + tx: String, + txid: String, + psbt: String, + channel_id: String, +} + +#[derive(Clone, Debug, Serialize)] +struct OnFundingChannelTxResponse { + tx: String, + psbt: String, +} + impl RPCCommand for OnFundingChannelTx { fn call<'c>( &self, - _: &mut Plugin, - _: json::Value, + plugin: &mut Plugin, + body: json::Value, ) -> Result { - log::info!("Calling hook `onfunding_channel_tx`"); - Ok(json::json!({ "result": "continue" })) + log::info!("Calling hook `onfunding_channel_tx` with `{body}`",); + let body: OnFundingChannelTxHook = json::from_value(body)?; + let body = body.onfunding_channel_tx; + let raw_tx = Vec::from_hex(&body.tx).unwrap(); + let tx: bitcoin::Transaction = Decodable::consensus_decode(&mut raw_tx.as_slice()).unwrap(); + let txid = bitcoin::Txid::from_hex(&body.txid).unwrap(); + assert_eq!(txid, tx.txid()); + let tx = plugin + .state + .manager() + .handle_onfunding_tx(tx, txid, body.channel_id) + .unwrap(); + let serialized_tx = serialize(&tx); + let result = OnFundingChannelTxResponse { + tx: serialized_tx.to_hex(), + psbt: body.psbt, + }; + Ok(json::json!({ "result": json::to_value(&result)? })) } } diff --git a/rgb-common/src/comm.rs b/rgb-common/src/comm.rs index 339c2ad..79e5564 100644 --- a/rgb-common/src/comm.rs +++ b/rgb-common/src/comm.rs @@ -1,17 +1,12 @@ //! A module to provide RGB functionality use std::fs; use std::path::PathBuf; -use std::str::FromStr; -use bitcoin::OutPoint as BtcOutPoint; use serde::{Deserialize, Serialize}; -use bp::seals::txout::CloseMethod; use rgb_core::ContractId; use crate::ldk; -use crate::proxy; -use crate::std::persistence::ConsignerError::Reveal; use ldk::ln::PaymentHash; diff --git a/rgb-common/src/lib.rs b/rgb-common/src/lib.rs index 4222fa3..0f9a07a 100644 --- a/rgb-common/src/lib.rs +++ b/rgb-common/src/lib.rs @@ -6,6 +6,7 @@ use lightning as ldk; use reqwest::blocking::Client as BlockingClient; pub use anyhow; +pub use bitcoin; // Re-exporting RGB dependencies under a single module. pub use rgb; pub use rgb::interface::rgb20 as asset20; diff --git a/rgb-common/src/rgb_manager.rs b/rgb-common/src/rgb_manager.rs index d2ec538..1d9f76f 100644 --- a/rgb-common/src/rgb_manager.rs +++ b/rgb-common/src/rgb_manager.rs @@ -5,6 +5,8 @@ use std::sync::Mutex; use bitcoin::Network; +use crate::bitcoin::secp256k1; +use crate::bitcoin::util::bip32::{ExtendedPrivKey, ExtendedPubKey}; use crate::lib::wallet::{DatabaseType, Wallet, WalletData}; use crate::lib::BitcoinNetwork; use crate::proxy; @@ -21,14 +23,30 @@ impl std::fmt::Debug for RGBManager { } impl RGBManager { - pub fn init(root_dir: &str, pubkey: &str, network: &str) -> anyhow::Result { + pub fn init( + root_dir: &str, + master_xprv: &ExtendedPrivKey, + network: &str, + ) -> anyhow::Result { let client = proxy::Client::new(network)?; + + // with rgb library tere is a new function for calculate the account key + let ext_pub_key = ExtendedPubKey { + network: master_xprv.network, + depth: master_xprv.depth, + parent_fingerprint: master_xprv.parent_fingerprint, + child_number: master_xprv.child_number, + public_key: master_xprv + .private_key + .public_key(&secp256k1::Secp256k1::new()), + chain_code: master_xprv.chain_code, + }; let mut wallet = Wallet::new(WalletData { data_dir: root_dir.to_owned(), bitcoin_network: BitcoinNetwork::from_str(network)?, database_type: DatabaseType::Sqlite, max_allocations_per_utxo: 11, - pubkey: pubkey.to_owned(), + pubkey: ext_pub_key.to_string().to_owned(), mnemonic: None, vanilla_keychain: None, })?; @@ -56,4 +74,14 @@ impl RGBManager { pub fn proxy_client(&self) -> Arc { self.proxy_client.clone() } + + /// Modify the funding transaction before sign it with the node signer. + pub fn handle_onfunding_tx( + &self, + tx: bitcoin::Transaction, + txid: bitcoin::Txid, + channel_id: String, + ) -> anyhow::Result { + Ok(tx) + } } diff --git a/rgb-testing/Cargo.toml b/rgb-testing/Cargo.toml index a85ca59..650f91c 100644 --- a/rgb-testing/Cargo.toml +++ b/rgb-testing/Cargo.toml @@ -9,4 +9,6 @@ env_logger = "0.11.1" anyhow = "1.0.71" log = "0.4" tokio = { version = "1.36.0", features = ["rt-multi-thread", "macros"] } +serde = "1.0" serde_json = "1.0.1" +ntest = "0.9.0" diff --git a/rgb-testing/src/lib.rs b/rgb-testing/src/lib.rs index cfdbbe7..837144b 100644 --- a/rgb-testing/src/lib.rs +++ b/rgb-testing/src/lib.rs @@ -2,18 +2,22 @@ //! //! Author: Vincenzo Palazzo +#[cfg(test)] mod utils; #[cfg(test)] mod tests { - use std::sync::Once; - use clightning_testing::cln; + use json::Value; + use serde::Deserialize; use serde_json as json; + use clightning_testing::cln; + + use crate::node; #[allow(unused_imports)] - use super::utils::*; + use crate::utils::*; static INIT: Once = Once::new(); @@ -27,12 +31,7 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn test_init_plugin() -> anyhow::Result<()> { init(); - let pwd = std::env!("PWD"); - let cln = cln::Node::with_params( - &format!("--developer --experimental-splicing --plugin={pwd}/target/debug/rgb-cln"), - "regtest", - ) - .await?; + let cln = node!(); let result = cln .rpc() .call::("getinfo", json::json!({})); @@ -40,4 +39,61 @@ mod tests { assert!(result.is_ok(), "{:?}", result); Ok(()) } + + #[tokio::test(flavor = "multi_thread")] + #[ntest::timeout(560000)] + async fn test_simple_open_rgb_channel() -> anyhow::Result<()> { + init(); + + let ocean_ln = node!(); + let btc = ocean_ln.btc(); + let miner_1 = node!(btc.clone()); + if let Err(err) = open_channel(&miner_1, &ocean_ln, false) { + miner_1.print_logs()?; + panic!("{err}"); + } + + ocean_ln.print_logs()?; + + #[derive(Deserialize, Debug)] + struct Invoice { + bolt11: String, + } + + // the miner generate the payout reusable offer + let payout_miner: Invoice = miner_1.rpc().call( + "invoice", + json::json!({ + "amount_msat": "any", + "label": "invoice", + "description": "invoice1", + + }), + )?; + + log::info!("offer invoice: {:?}", payout_miner); + // FIXME: we are not able at the moment to splice the channel to increase the balance, + // so at the moment, so atm we open a new channel but this is not inside our simulation + open_channel(&ocean_ln, &miner_1, false)?; + + let listchannels = ocean_ln.rpc().listchannels(None, None, None)?.channels; + log::debug!( + "channels before paying: {}", + json::to_string(&listchannels)? + ); + let listchannels = ocean_ln.rpc().listfunds()?.channels; + log::debug!( + "channels in list funds before paying: {}", + json::to_string(&listchannels)? + ); + let payout: Value = ocean_ln.rpc().call( + "pay", + json::json!({ + "bolt11": payout_miner.bolt11, + "amount_msat": "10sat", + }), + )?; + log::info!("payment result: {payout}"); + Ok(()) + } } diff --git a/rgb-testing/src/utils.rs b/rgb-testing/src/utils.rs index 6566fa2..02ac7b5 100644 --- a/rgb-testing/src/utils.rs +++ b/rgb-testing/src/utils.rs @@ -1,4 +1,9 @@ //! Test Utils +use std::{str::FromStr, sync::Arc}; + +use clightning_testing::prelude::clightningrpc::requests::AmountOrAll; +use clightning_testing::{btc, cln}; + #[macro_export] macro_rules! wait { ($callback:expr, $timeout:expr) => {{ @@ -19,3 +24,149 @@ macro_rules! wait { $crate::wait!($callback, 100); }; } + +#[macro_export] +macro_rules! node { + ($btc:expr) => {{ + let pwd = std::env!("PWD"); + let plugin_name = std::env!("PLUGIN_NAME"); + log::debug!("plugin path: {pwd}/../{plugin_name}"); + cln::Node::with_btc_and_params( + $btc, + &format!("--developer --experimental-offers --plugin={pwd}/target/debug/{plugin_name}"), + "regtest", + ) + .await? + }}; + () => {{ + let pwd = std::env!("PWD"); + let plugin_name = std::env!("PLUGIN_NAME"); + log::debug!("plugin path: {pwd}/../{plugin_name}"); + cln::Node::with_params( + &format!("--developer --experimental-offers --plugin={pwd}/target/debug/{plugin_name}"), + "regtest", + ) + .await? + }}; +} + +#[macro_export] +macro_rules! check { + ($cln:expr, $value:expr, $($arg:tt)+) => {{ + if $value.is_err() { + let _ = $cln.print_logs(); + } + assert!($value.is_ok()); + }}; +} + +#[macro_export] +macro_rules! wait_sync { + ($cln:expr) => {{ + wait!( + || { + let Ok(cln_info) = $cln.rpc().getinfo() else { + return Err(()); + }; + log::trace!("cln info: {:?}", cln_info); + if cln_info.warning_bitcoind_sync.is_some() { + return Err(()); + } + + if cln_info.warning_lightningd_sync.is_some() { + return Err(()); + } + let mut out = $cln.rpc().listfunds().unwrap().outputs; + log::trace!("{:?}", out); + out.retain(|tx| tx.status == "confirmed"); + if out.is_empty() { + let addr = $cln.rpc().newaddr(None).unwrap().bech32.unwrap(); + let _ = fund_wallet($cln.btc(), &addr, 6); + return Err(()); + } + + Ok(()) + }, + 10000 + ); + }}; +} + +/// Open a channel from node_a -> node_b +pub fn open_channel(node_a: &cln::Node, node_b: &cln::Node, dual_open: bool) -> anyhow::Result<()> { + let addr = node_a.rpc().newaddr(None)?.bech32.unwrap(); + fund_wallet(node_a.btc(), &addr, 8)?; + wait_for_funds(node_a)?; + + wait_sync!(node_a); + + if dual_open { + let addr = node_b.rpc().newaddr(None)?.address.unwrap(); + fund_wallet(node_b.btc(), &addr, 6)?; + } + + let getinfo2 = node_b.rpc().getinfo()?; + node_a + .rpc() + .connect(&getinfo2.id, Some(&format!("127.0.0.1:{}", node_b.port)))?; + let listfunds = node_a.rpc().listfunds()?; + log::debug!("list funds {:?}", listfunds); + node_a + .rpc() + .fundchannel(&getinfo2.id, AmountOrAll::All, None)?; + wait!( + || { + let mut channels = node_a.rpc().listfunds().unwrap().channels; + log::info!("{:?}", channels); + let origin_size = channels.len(); + channels.retain(|chan| chan.state == "CHANNELD_NORMAL"); + if channels.len() == origin_size { + return Ok(()); + } + let addr = node_a.rpc().newaddr(None).unwrap().bech32.unwrap(); + fund_wallet(node_a.btc(), &addr, 6).unwrap(); + wait_sync!(node_a); + Err(()) + }, + 10000 + ); + Ok(()) +} + +pub fn fund_wallet(btc: Arc, addr: &str, blocks: u64) -> anyhow::Result { + use clightning_testing::prelude::bitcoincore_rpc; + use clightning_testing::prelude::bitcoincore_rpc::RpcApi; + // mine some bitcoin inside the lampo address + let address = bitcoincore_rpc::bitcoin::Address::from_str(addr) + .unwrap() + .assume_checked(); + let _ = btc.rpc().generate_to_address(blocks, &address).unwrap(); + + Ok(address.to_string()) +} + +pub fn wait_for_funds(cln: &cln::Node) -> anyhow::Result<()> { + use clightning_testing::prelude::bitcoincore_rpc; + use clightning_testing::prelude::bitcoincore_rpc::RpcApi; + + wait!( + || { + let addr = cln.rpc().newaddr(None).unwrap().bech32.unwrap(); + let address = bitcoincore_rpc::bitcoin::Address::from_str(&addr) + .unwrap() + .assume_checked(); + let _ = cln.btc().rpc().generate_to_address(1, &address).unwrap(); + + let Ok(funds) = cln.rpc().listfunds() else { + return Err(()); + }; + log::trace!("listfunds {:?}", funds); + if funds.outputs.is_empty() { + return Err(()); + } + Ok(()) + }, + 10000 + ); + Ok(()) +}