Merge pull request #1 from vincenzopalazzo/macros/rgb-openchannel

tests: adding the rgb open channel test
This commit is contained in:
Vincenzo Palazzo 2024-03-05 11:55:05 +01:00 committed by GitHub
commit 0d799d5a31
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 337 additions and 43 deletions

View File

@ -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"

View File

@ -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
'';

View File

@ -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"

View File

@ -1,24 +1,35 @@
//! Plugin implementation
//! RGB Plugin implementation
//!
//! Author: Vincenzo Palazzo <vincenzopalazzo@member.fsf.org>
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<Arc<RGBManager>>,
/// CLN RPC
cln_rpc: Option<Arc<Client>>,
}
@ -31,6 +42,7 @@ impl State {
}
}
#[allow(dead_code)]
pub(crate) fn rpc(&self) -> Arc<Client> {
self.cln_rpc.clone().unwrap()
}
@ -39,7 +51,7 @@ impl State {
self.rgb_manager.clone().unwrap()
}
pub fn call<T: Serialize, U: DeserializeOwned + Debug>(
pub fn call<T: Serialize, U: DeserializeOwned + fmt::Debug>(
&self,
method: &str,
payload: T,
@ -70,44 +82,90 @@ pub fn build_plugin() -> anyhow::Result<Plugin<State>> {
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<ExtendedPrivKey> {
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<State>) -> 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<GetInfo> = 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<State> for OnFundingChannelTx {
fn call<'c>(
&self,
_: &mut Plugin<State>,
_: json::Value,
plugin: &mut Plugin<State>,
body: json::Value,
) -> Result<json::Value, clightningrpc_plugin::errors::PluginError> {
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)? }))
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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<Self> {
pub fn init(
root_dir: &str,
master_xprv: &ExtendedPrivKey,
network: &str,
) -> anyhow::Result<Self> {
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<proxy::Client> {
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<bitcoin::Transaction> {
Ok(tx)
}
}

View File

@ -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"

View File

@ -2,18 +2,22 @@
//!
//! Author: Vincenzo Palazzo <vincenzopalazzo@member.fsf.org>
#[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::<json::Value, json::Value>("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(())
}
}

View File

@ -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<btc::BtcNode>, addr: &str, blocks: u64) -> anyhow::Result<String> {
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(())
}