X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=blobdiff_plain;f=lightning%2Fsrc%2Fln%2Ffunctional_test_utils.rs;h=dce4e6289b9536352270bcbbe9eb33268192571a;hb=99e2283aee3fb7ab512ad5688d2eb5e926d6d9f9;hp=d5d8566696740df15542d82c9271c709e51fbd3e;hpb=7c8e740b6e82feeb60938083dcf677ea7e21f7bd;p=rust-lightning diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index d5d85666..dce4e628 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -10,10 +10,10 @@ //! A bunch of useful utilities for building networks of nodes and exchanging messages between //! nodes for functional tests. -use chain::Watch; +use chain::{Confirm, Listen, Watch}; use chain::channelmonitor::ChannelMonitor; use chain::transaction::OutPoint; -use ln::channelmanager::{ChannelManager, ChannelManagerReadArgs, RAACommitmentOrder, PaymentPreimage, PaymentHash, PaymentSecret, PaymentSendFailure}; +use ln::channelmanager::{BestBlock, ChainParameters, ChannelManager, ChannelManagerReadArgs, RAACommitmentOrder, PaymentPreimage, PaymentHash, PaymentSecret, PaymentSendFailure}; use routing::router::{Route, get_route}; use routing::network_graph::{NetGraphMsgHandler, NetworkGraph}; use ln::features::InitFeatures; @@ -28,6 +28,7 @@ use util::config::UserConfig; use util::ser::{ReadableArgs, Writeable, Readable}; use bitcoin::blockdata::block::{Block, BlockHeader}; +use bitcoin::blockdata::constants::genesis_block; use bitcoin::blockdata::transaction::{Transaction, TxOut}; use bitcoin::network::constants::Network; @@ -43,51 +44,139 @@ use std::sync::Mutex; use std::mem; use std::collections::HashMap; -pub const CHAN_CONFIRM_DEPTH: u32 = 100; +pub const CHAN_CONFIRM_DEPTH: u32 = 10; +/// Mine the given transaction in the next block and then mine CHAN_CONFIRM_DEPTH - 1 blocks on +/// top, giving the given transaction CHAN_CONFIRM_DEPTH confirmations. pub fn confirm_transaction<'a, 'b, 'c, 'd>(node: &'a Node<'b, 'c, 'd>, tx: &Transaction) { - let dummy_tx = Transaction { version: 0, lock_time: 0, input: Vec::new(), output: Vec::new() }; - let dummy_tx_count = tx.version as usize; + confirm_transaction_at(node, tx, node.best_block_info().1 + 1); + connect_blocks(node, CHAN_CONFIRM_DEPTH - 1); +} +/// Mine a signle block containing the given transaction +pub fn mine_transaction<'a, 'b, 'c, 'd>(node: &'a Node<'b, 'c, 'd>, tx: &Transaction) { + let height = node.best_block_info().1 + 1; + confirm_transaction_at(node, tx, height); +} +/// Mine the given transaction at the given height, mining blocks as required to build to that +/// height +pub fn confirm_transaction_at<'a, 'b, 'c, 'd>(node: &'a Node<'b, 'c, 'd>, tx: &Transaction, conf_height: u32) { + let first_connect_height = node.best_block_info().1 + 1; + assert!(first_connect_height <= conf_height); + if conf_height > first_connect_height { + connect_blocks(node, conf_height - first_connect_height); + } let mut block = Block { - header: BlockHeader { version: 0x20000000, prev_blockhash: Default::default(), merkle_root: Default::default(), time: 42, bits: 42, nonce: 42 }, - txdata: vec![dummy_tx; dummy_tx_count], + header: BlockHeader { version: 0x20000000, prev_blockhash: node.best_block_hash(), merkle_root: Default::default(), time: 42, bits: 42, nonce: 42 }, + txdata: Vec::new(), }; - block.txdata.push(tx.clone()); - connect_block(node, &block, 1); - for i in 2..CHAN_CONFIRM_DEPTH { - block = Block { - header: BlockHeader { version: 0x20000000, prev_blockhash: block.header.block_hash(), merkle_root: Default::default(), time: 42, bits: 42, nonce: 42 }, - txdata: vec![], - }; - connect_block(node, &block, i); + for _ in 0..*node.network_chan_count.borrow() { // Make sure we don't end up with channels at the same short id by offsetting by chan_count + block.txdata.push(Transaction { version: 0, lock_time: 0, input: Vec::new(), output: Vec::new() }); } -} + block.txdata.push(tx.clone()); + connect_block(node, &block); +} + +/// The possible ways we may notify a ChannelManager of a new block +#[derive(Clone, Copy, PartialEq)] +pub enum ConnectStyle { + /// Calls best_block_updated first, detecting transactions in the block only after receiving the + /// header and height information. + BestBlockFirst, + /// The same as BestBlockFirst, however when we have multiple blocks to connect, we only + /// make a single best_block_updated call. + BestBlockFirstSkippingBlocks, + /// Calls transactions_confirmed first, detecting transactions in the block before updating the + /// header and height information. + TransactionsFirst, + /// The same as TransactionsFirst, however when we have multiple blocks to connect, we only + /// make a single best_block_updated call. + TransactionsFirstSkippingBlocks, + /// Provides the full block via the chain::Listen interface. In the current code this is + /// equivalent to TransactionsFirst with some additional assertions. + FullBlockViaListen, +} + +pub fn connect_blocks<'a, 'b, 'c, 'd>(node: &'a Node<'b, 'c, 'd>, depth: u32) -> BlockHash { + let skip_intermediaries = match *node.connect_style.borrow() { + ConnectStyle::BestBlockFirstSkippingBlocks|ConnectStyle::TransactionsFirstSkippingBlocks => true, + _ => false, + }; -pub fn connect_blocks<'a, 'b, 'c, 'd>(node: &'a Node<'b, 'c, 'd>, depth: u32, height: u32, parent: bool, prev_blockhash: BlockHash) -> BlockHash { let mut block = Block { - header: BlockHeader { version: 0x2000000, prev_blockhash: if parent { prev_blockhash } else { Default::default() }, merkle_root: Default::default(), time: 42, bits: 42, nonce: 42 }, + header: BlockHeader { version: 0x2000000, prev_blockhash: node.best_block_hash(), merkle_root: Default::default(), time: 42, bits: 42, nonce: 42 }, txdata: vec![], }; - connect_block(node, &block, height + 1); - for i in 2..depth + 1 { + assert!(depth >= 1); + for _ in 0..depth - 1 { + do_connect_block(node, &block, skip_intermediaries); block = Block { header: BlockHeader { version: 0x20000000, prev_blockhash: block.header.block_hash(), merkle_root: Default::default(), time: 42, bits: 42, nonce: 42 }, txdata: vec![], }; - connect_block(node, &block, height + i); } + connect_block(node, &block); block.header.block_hash() } -pub fn connect_block<'a, 'b, 'c, 'd>(node: &'a Node<'b, 'c, 'd>, block: &Block, height: u32) { - let txdata: Vec<_> = block.txdata.iter().enumerate().collect(); - node.chain_monitor.chain_monitor.block_connected(&block.header, &txdata, height); - node.node.block_connected(&block.header, &txdata, height); +pub fn connect_block<'a, 'b, 'c, 'd>(node: &'a Node<'b, 'c, 'd>, block: &Block) { + do_connect_block(node, block, false); } -pub fn disconnect_block<'a, 'b, 'c, 'd>(node: &'a Node<'b, 'c, 'd>, header: &BlockHeader, height: u32) { - node.chain_monitor.chain_monitor.block_disconnected(header, height); - node.node.block_disconnected(header); +fn do_connect_block<'a, 'b, 'c, 'd>(node: &'a Node<'b, 'c, 'd>, block: &Block, skip_intermediaries: bool) { + let height = node.best_block_info().1 + 1; + if !skip_intermediaries { + let txdata: Vec<_> = block.txdata.iter().enumerate().collect(); + match *node.connect_style.borrow() { + ConnectStyle::BestBlockFirst|ConnectStyle::BestBlockFirstSkippingBlocks => { + node.chain_monitor.chain_monitor.best_block_updated(&block.header, height); + node.chain_monitor.chain_monitor.transactions_confirmed(&block.header, &txdata, height); + node.node.best_block_updated(&block.header, height); + node.node.transactions_confirmed(&block.header, &txdata, height); + }, + ConnectStyle::TransactionsFirst|ConnectStyle::TransactionsFirstSkippingBlocks => { + node.chain_monitor.chain_monitor.transactions_confirmed(&block.header, &txdata, height); + node.chain_monitor.chain_monitor.best_block_updated(&block.header, height); + node.node.transactions_confirmed(&block.header, &txdata, height); + node.node.best_block_updated(&block.header, height); + }, + ConnectStyle::FullBlockViaListen => { + node.chain_monitor.chain_monitor.block_connected(&block, height); + node.node.block_connected(&block, height); + } + } + } + node.node.test_process_background_events(); + node.blocks.borrow_mut().push((block.header, height)); +} + +pub fn disconnect_blocks<'a, 'b, 'c, 'd>(node: &'a Node<'b, 'c, 'd>, count: u32) { + for i in 0..count { + let orig_header = node.blocks.borrow_mut().pop().unwrap(); + assert!(orig_header.1 > 0); // Cannot disconnect genesis + let prev_header = node.blocks.borrow().last().unwrap().clone(); + + match *node.connect_style.borrow() { + ConnectStyle::FullBlockViaListen => { + node.chain_monitor.chain_monitor.block_disconnected(&orig_header.0, orig_header.1); + Listen::block_disconnected(node.node, &orig_header.0, orig_header.1); + }, + ConnectStyle::BestBlockFirstSkippingBlocks|ConnectStyle::TransactionsFirstSkippingBlocks => { + if i == count - 1 { + node.chain_monitor.chain_monitor.best_block_updated(&prev_header.0, prev_header.1); + node.node.best_block_updated(&prev_header.0, prev_header.1); + } + }, + _ => { + node.chain_monitor.chain_monitor.best_block_updated(&prev_header.0, prev_header.1); + node.node.best_block_updated(&prev_header.0, prev_header.1); + }, + } + } +} + +pub fn disconnect_all_blocks<'a, 'b, 'c, 'd>(node: &'a Node<'b, 'c, 'd>) { + let count = node.blocks.borrow_mut().len() as u32 - 1; + disconnect_blocks(node, count); } pub struct TestChanMonCfg { @@ -120,6 +209,16 @@ pub struct Node<'a, 'b: 'a, 'c: 'b> { pub network_payment_count: Rc>, pub network_chan_count: Rc>, pub logger: &'c test_utils::TestLogger, + pub blocks: RefCell>, + pub connect_style: Rc>, +} +impl<'a, 'b, 'c> Node<'a, 'b, 'c> { + pub fn best_block_hash(&self) -> BlockHash { + self.blocks.borrow_mut().last().unwrap().0.block_hash() + } + pub fn best_block_info(&self) -> (BlockHash, u32) { + self.blocks.borrow_mut().last().map(|(a, b)| (a.block_hash(), *b)).unwrap() + } } impl<'a, 'b, 'c> Drop for Node<'a, 'b, 'c> { @@ -172,7 +271,7 @@ impl<'a, 'b, 'c> Drop for Node<'a, 'b, 'c> { for (_, old_monitor) in old_monitors.iter() { let mut w = test_utils::TestVecWriter(Vec::new()); old_monitor.write(&mut w).unwrap(); - let (_, deserialized_monitor) = <(Option, ChannelMonitor)>::read( + let (_, deserialized_monitor) = <(BlockHash, ChannelMonitor)>::read( &mut ::std::io::Cursor::new(&w.0), self.keys_manager).unwrap(); deserialized_monitors.push(deserialized_monitor); } @@ -188,8 +287,8 @@ impl<'a, 'b, 'c> Drop for Node<'a, 'b, 'c> { let mut w = test_utils::TestVecWriter(Vec::new()); self.node.write(&mut w).unwrap(); - <(Option, ChannelManager)>::read(&mut ::std::io::Cursor::new(w.0), ChannelManagerReadArgs { - default_config: UserConfig::default(), + <(BlockHash, ChannelManager)>::read(&mut ::std::io::Cursor::new(w.0), ChannelManagerReadArgs { + default_config: *self.node.get_current_default_configuration(), keys_manager: self.keys_manager, fee_estimator: &test_utils::TestFeeEstimator { sat_per_kw: 253 }, chain_monitor: self.chain_monitor, @@ -273,6 +372,24 @@ macro_rules! get_event_msg { } } +/// Get a specific event from the pending events queue. +#[macro_export] +macro_rules! get_event { + ($node: expr, $event_type: path) => { + { + let mut events = $node.node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + let ev = events.pop().unwrap(); + match ev { + $event_type { .. } => { + ev + }, + _ => panic!("Unexpected event"), + } + } + } +} + #[cfg(test)] macro_rules! get_htlc_update_msgs { ($node: expr, $node_id: expr) => { @@ -301,7 +418,8 @@ macro_rules! get_feerate { } } -#[cfg(test)] +/// Returns any local commitment transactions for the channel. +#[macro_export] macro_rules! get_local_commitment_txn { ($node: expr, $channel_id: expr) => { { @@ -381,7 +499,7 @@ pub fn create_chan_between_nodes_with_value_init<'a, 'b, 'c>(node_a: &Node<'a, ' let (temporary_channel_id, tx, funding_output) = create_funding_transaction(node_a, channel_value, 42); - node_a.node.funding_transaction_generated(&temporary_channel_id, funding_output); + node_a.node.funding_transaction_generated(&temporary_channel_id, tx.clone()).unwrap(); check_added_monitors!(node_a, 0); node_b.node.handle_funding_created(&node_a.node.get_our_node_id(), &get_event_msg!(node_a, MessageSendEvent::SendFundingCreated, node_b.node.get_our_node_id())); @@ -401,20 +519,18 @@ pub fn create_chan_between_nodes_with_value_init<'a, 'b, 'c>(node_a: &Node<'a, ' } let events_4 = node_a.node.get_and_clear_pending_events(); - assert_eq!(events_4.len(), 1); - match events_4[0] { - Event::FundingBroadcastSafe { ref funding_txo, user_channel_id } => { - assert_eq!(user_channel_id, 42); - assert_eq!(*funding_txo, funding_output); - }, - _ => panic!("Unexpected event"), - }; + assert_eq!(events_4.len(), 0); + + assert_eq!(node_a.tx_broadcaster.txn_broadcasted.lock().unwrap().len(), 1); + assert_eq!(node_a.tx_broadcaster.txn_broadcasted.lock().unwrap()[0], tx); + node_a.tx_broadcaster.txn_broadcasted.lock().unwrap().clear(); tx } -pub fn create_chan_between_nodes_with_value_confirm_first<'a, 'b, 'c, 'd>(node_recv: &'a Node<'b, 'c, 'c>, node_conf: &'a Node<'b, 'c, 'd>, tx: &Transaction) { - confirm_transaction(node_conf, tx); +pub fn create_chan_between_nodes_with_value_confirm_first<'a, 'b, 'c, 'd>(node_recv: &'a Node<'b, 'c, 'c>, node_conf: &'a Node<'b, 'c, 'd>, tx: &Transaction, conf_height: u32) { + confirm_transaction_at(node_conf, tx, conf_height); + connect_blocks(node_conf, CHAN_CONFIRM_DEPTH - 1); node_recv.node.handle_funding_locked(&node_conf.node.get_our_node_id(), &get_event_msg!(node_conf, MessageSendEvent::SendFundingLocked, node_recv.node.get_our_node_id())); } @@ -439,8 +555,10 @@ pub fn create_chan_between_nodes_with_value_confirm_second<'a, 'b, 'c>(node_recv } pub fn create_chan_between_nodes_with_value_confirm<'a, 'b, 'c, 'd>(node_a: &'a Node<'b, 'c, 'd>, node_b: &'a Node<'b, 'c, 'd>, tx: &Transaction) -> ((msgs::FundingLocked, msgs::AnnouncementSignatures), [u8; 32]) { - create_chan_between_nodes_with_value_confirm_first(node_a, node_b, tx); - confirm_transaction(node_a, tx); + let conf_height = std::cmp::max(node_a.best_block_info().1 + 1, node_b.best_block_info().1 + 1); + create_chan_between_nodes_with_value_confirm_first(node_a, node_b, tx, conf_height); + confirm_transaction_at(node_a, tx, conf_height); + connect_blocks(node_a, CHAN_CONFIRM_DEPTH - 1); create_chan_between_nodes_with_value_confirm_second(node_b, node_a) } @@ -804,7 +922,7 @@ macro_rules! expect_pending_htlcs_forwardable { }} } -#[cfg(test)] +#[cfg(any(test, feature = "unstable"))] macro_rules! expect_payment_received { ($node: expr, $expected_payment_hash: expr, $expected_recv_value: expr) => { let events = $node.node.get_and_clear_pending_events(); @@ -840,13 +958,13 @@ macro_rules! expect_payment_failed { assert_eq!(events.len(), 1); match events[0] { Event::PaymentFailed { ref payment_hash, rejected_by_dest, ref error_code, ref error_data } => { - assert_eq!(*payment_hash, $expected_payment_hash); - assert_eq!(rejected_by_dest, $rejected_by_dest); - assert!(error_code.is_some()); - assert!(error_data.is_some()); + assert_eq!(*payment_hash, $expected_payment_hash, "unexpected payment_hash"); + assert_eq!(rejected_by_dest, $rejected_by_dest, "unexpected rejected_by_dest value"); + assert!(error_code.is_some(), "expected error_code.is_some() = true"); + assert!(error_data.is_some(), "expected error_data.is_some() = true"); $( - assert_eq!(error_code.unwrap(), $expected_error_code); - assert_eq!(&error_data.as_ref().unwrap()[..], $expected_error_data); + assert_eq!(error_code.unwrap(), $expected_error_code, "unexpected error code"); + assert_eq!(&error_data.as_ref().unwrap()[..], $expected_error_data, "unexpected error data"); )* }, _ => panic!("Unexpected event"), @@ -1017,12 +1135,12 @@ pub fn claim_payment<'a, 'b, 'c>(origin_node: &Node<'a, 'b, 'c>, expected_route: claim_payment_along_route(origin_node, expected_route, false, our_payment_preimage, expected_amount); } -pub const TEST_FINAL_CLTV: u32 = 32; +pub const TEST_FINAL_CLTV: u32 = 50; pub fn route_payment<'a, 'b, 'c>(origin_node: &Node<'a, 'b, 'c>, expected_route: &[&Node<'a, 'b, 'c>], recv_value: u64) -> (PaymentPreimage, PaymentHash) { let net_graph_msg_handler = &origin_node.net_graph_msg_handler; let logger = test_utils::TestLogger::new(); - let route = get_route(&origin_node.node.get_our_node_id(), &net_graph_msg_handler.network_graph.read().unwrap(), &expected_route.last().unwrap().node.get_our_node_id(), None, &Vec::new(), recv_value, TEST_FINAL_CLTV, &logger).unwrap(); + let route = get_route(&origin_node.node.get_our_node_id(), &net_graph_msg_handler.network_graph.read().unwrap(), &expected_route.last().unwrap().node.get_our_node_id(), None, None, &Vec::new(), recv_value, TEST_FINAL_CLTV, &logger).unwrap(); assert_eq!(route.paths.len(), 1); assert_eq!(route.paths[0].len(), expected_route.len()); for (node, hop) in expected_route.iter().zip(route.paths[0].iter()) { @@ -1035,7 +1153,7 @@ pub fn route_payment<'a, 'b, 'c>(origin_node: &Node<'a, 'b, 'c>, expected_route: pub fn route_over_limit<'a, 'b, 'c>(origin_node: &Node<'a, 'b, 'c>, expected_route: &[&Node<'a, 'b, 'c>], recv_value: u64) { let logger = test_utils::TestLogger::new(); let net_graph_msg_handler = &origin_node.net_graph_msg_handler; - let route = get_route(&origin_node.node.get_our_node_id(), &net_graph_msg_handler.network_graph.read().unwrap(), &expected_route.last().unwrap().node.get_our_node_id(), None, &Vec::new(), recv_value, TEST_FINAL_CLTV, &logger).unwrap(); + let route = get_route(&origin_node.node.get_our_node_id(), &net_graph_msg_handler.network_graph.read().unwrap(), &expected_route.last().unwrap().node.get_our_node_id(), None, None, &Vec::new(), recv_value, TEST_FINAL_CLTV, &logger).unwrap(); assert_eq!(route.paths.len(), 1); assert_eq!(route.paths[0].len(), expected_route.len()); for (node, hop) in expected_route.iter().zip(route.paths[0].iter()) { @@ -1158,10 +1276,18 @@ pub fn create_node_chanmgrs<'a, 'b>(node_count: usize, cfgs: &'a Vec let mut chanmgrs = Vec::new(); for i in 0..node_count { let mut default_config = UserConfig::default(); + // Set cltv_expiry_delta slightly lower to keep the final CLTV values inside one byte in our + // tests so that our script-length checks don't fail (see ACCEPTED_HTLC_SCRIPT_WEIGHT). + default_config.channel_options.cltv_expiry_delta = 6*6; default_config.channel_options.announced_channel = true; default_config.peer_channel_config_limits.force_announced_channel_preference = false; default_config.own_channel_config.our_htlc_minimum_msat = 1000; // sanitization being done by the sender, to exerce receiver logic we need to lift of limit - let node = ChannelManager::new(Network::Testnet, cfgs[i].fee_estimator, &cfgs[i].chain_monitor, cfgs[i].tx_broadcaster, cfgs[i].logger, cfgs[i].keys_manager, if node_config[i].is_some() { node_config[i].clone().unwrap() } else { default_config }, 0); + let network = Network::Testnet; + let params = ChainParameters { + network, + best_block: BestBlock::from_genesis(network), + }; + let node = ChannelManager::new(cfgs[i].fee_estimator, &cfgs[i].chain_monitor, cfgs[i].tx_broadcaster, cfgs[i].logger, cfgs[i].keys_manager, if node_config[i].is_some() { node_config[i].clone().unwrap() } else { default_config }, params); chanmgrs.push(node); } @@ -1172,6 +1298,7 @@ pub fn create_network<'a, 'b: 'a, 'c: 'b>(node_count: usize, cfgs: &'b Vec(node_count: usize, cfgs: &'b Vec(node: &Node<'a, 'b, 'c>, prev_txn: &Vec< res } -pub fn get_announce_close_broadcast_events<'a, 'b, 'c>(nodes: &Vec>, a: usize, b: usize) { +pub fn handle_announce_close_broadcast_events<'a, 'b, 'c>(nodes: &Vec>, a: usize, b: usize, needs_err_handle: bool, expected_error: &str) { let events_1 = nodes[a].node.get_and_clear_pending_msg_events(); - assert_eq!(events_1.len(), 1); + assert_eq!(events_1.len(), 2); let as_update = match events_1[0] { MessageSendEvent::BroadcastChannelUpdate { ref msg } => { msg.clone() }, _ => panic!("Unexpected event"), }; + match events_1[1] { + MessageSendEvent::HandleError { node_id, action: msgs::ErrorAction::SendErrorMessage { ref msg } } => { + assert_eq!(node_id, nodes[b].node.get_our_node_id()); + assert_eq!(msg.data, expected_error); + if needs_err_handle { + nodes[b].node.handle_error(&nodes[a].node.get_our_node_id(), msg); + } + }, + _ => panic!("Unexpected event"), + } let events_2 = nodes[b].node.get_and_clear_pending_msg_events(); - assert_eq!(events_2.len(), 1); + assert_eq!(events_2.len(), if needs_err_handle { 1 } else { 2 }); let bs_update = match events_2[0] { MessageSendEvent::BroadcastChannelUpdate { ref msg } => { msg.clone() }, _ => panic!("Unexpected event"), }; + if !needs_err_handle { + match events_2[1] { + MessageSendEvent::HandleError { node_id, action: msgs::ErrorAction::SendErrorMessage { ref msg } } => { + assert_eq!(node_id, nodes[a].node.get_our_node_id()); + assert_eq!(msg.data, expected_error); + }, + _ => panic!("Unexpected event"), + } + } for node in nodes { node.net_graph_msg_handler.handle_channel_update(&as_update).unwrap(); @@ -1314,6 +1463,10 @@ pub fn get_announce_close_broadcast_events<'a, 'b, 'c>(nodes: &Vec(nodes: &Vec>, a: usize, b: usize) { + handle_announce_close_broadcast_events(nodes, a, b, false, "Commitment or closing transaction was confirmed on chain."); +} + #[cfg(test)] macro_rules! get_channel_value_stat { ($node: expr, $channel_id: expr) => {{