diff --git a/src/envelope.rs b/src/envelope.rs index eb5a5681e0..ddba25e127 100644 --- a/src/envelope.rs +++ b/src/envelope.rs @@ -10,6 +10,7 @@ pub(crate) const PROTOCOL_ID: [u8; 3] = *b"ord"; pub(crate) const BODY_TAG: [u8; 0] = []; pub(crate) const CONTENT_TYPE_TAG: [u8; 1] = [1]; +pub(crate) const POINTER_TAG: [u8; 1] = [2]; pub(crate) const PARENT_TAG: [u8; 1] = [3]; pub(crate) const METADATA_TAG: [u8; 1] = [5]; pub(crate) const METAPROTOCOL_TAG: [u8; 1] = [7]; @@ -27,12 +28,18 @@ pub(crate) struct Envelope { } fn remove_field(fields: &mut BTreeMap<&[u8], Vec<&[u8]>>, field: &[u8]) -> Option> { - let value = fields.get_mut(field)?; + let values = fields.get_mut(field)?; - if value.is_empty() { + if values.is_empty() { None } else { - Some(value.remove(0).to_vec()) + let value = values.remove(0).to_vec(); + + if values.is_empty() { + fields.remove(field); + } + + Some(value) } } @@ -72,6 +79,7 @@ impl From for ParsedEnvelope { let content_type = remove_field(&mut fields, &CONTENT_TYPE_TAG); let parent = remove_field(&mut fields, &PARENT_TAG); + let pointer = remove_field(&mut fields, &POINTER_TAG); let metaprotocol = remove_field(&mut fields, &METAPROTOCOL_TAG); let metadata = remove_and_concatenate_field(&mut fields, &METADATA_TAG); @@ -90,6 +98,7 @@ impl From for ParsedEnvelope { }), content_type, parent, + pointer, unrecognized_even_field, duplicate_field, incomplete_field, @@ -743,6 +752,36 @@ mod tests { ); } + #[test] + fn pointer_field_is_recognized() { + assert_eq!( + parse(&[envelope(&[b"ord", &[2], &[1]])]), + vec![ParsedEnvelope { + payload: Inscription { + pointer: Some(vec![1]), + ..Default::default() + }, + ..Default::default() + }], + ); + } + + #[test] + fn duplicate_pointer_field_makes_inscription_unbound() { + assert_eq!( + parse(&[envelope(&[b"ord", &[2], &[1], &[2], &[0]])]), + vec![ParsedEnvelope { + payload: Inscription { + pointer: Some(vec![1]), + duplicate_field: true, + unrecognized_even_field: true, + ..Default::default() + }, + ..Default::default() + }], + ); + } + #[test] fn incomplete_field() { assert_eq!( diff --git a/src/index.rs b/src/index.rs index af6860c814..c7ca288fa8 100644 --- a/src/index.rs +++ b/src/index.rs @@ -4122,4 +4122,594 @@ mod tests { .is_none()); } } + + #[test] + fn inscription_with_pointer() { + for context in Context::configurations() { + context.mine_blocks(1); + + let inscription = Inscription { + content_type: Some("text/plain".into()), + body: Some("hello".into()), + pointer: Some(100u64.to_le_bytes().to_vec()), + ..Default::default() + }; + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, inscription.to_witness())], + ..Default::default() + }); + + context.mine_blocks(1); + + let inscription_id = InscriptionId { txid, index: 0 }; + + context.index.assert_inscription_location( + inscription_id, + SatPoint { + outpoint: OutPoint { txid, vout: 0 }, + offset: 100, + }, + Some(50 * COIN_VALUE + 100), + ); + } + } + + #[test] + fn inscription_with_pointer_greater_than_output_value_assigned_default() { + for context in Context::configurations() { + context.mine_blocks(1); + + let inscription = Inscription { + content_type: Some("text/plain".into()), + body: Some("hello".into()), + pointer: Some((50 * COIN_VALUE).to_le_bytes().to_vec()), + ..Default::default() + }; + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, inscription.to_witness())], + ..Default::default() + }); + + context.mine_blocks(1); + + let inscription_id = InscriptionId { txid, index: 0 }; + + context.index.assert_inscription_location( + inscription_id, + SatPoint { + outpoint: OutPoint { txid, vout: 0 }, + offset: 0, + }, + Some(50 * COIN_VALUE), + ); + } + } + + #[test] + fn inscription_with_pointer_into_fee_ignored_and_assigned_default_location() { + for context in Context::configurations() { + context.mine_blocks(1); + + let inscription = Inscription { + content_type: Some("text/plain".into()), + body: Some("hello".into()), + pointer: Some((25 * COIN_VALUE).to_le_bytes().to_vec()), + ..Default::default() + }; + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, inscription.to_witness())], + fee: 25 * COIN_VALUE, + ..Default::default() + }); + + context.mine_blocks(1); + + let inscription_id = InscriptionId { txid, index: 0 }; + + context.index.assert_inscription_location( + inscription_id, + SatPoint { + outpoint: OutPoint { txid, vout: 0 }, + offset: 0, + }, + Some(50 * COIN_VALUE), + ); + } + } + + #[test] + fn inscription_with_pointer_to_parent_is_cursed_reinscription() { + for context in Context::configurations() { + context.mine_blocks(1); + + let parent_txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, inscription("text/plain", "parent").to_witness())], + ..Default::default() + }); + + context.mine_blocks(1); + + let parent_inscription_id = InscriptionId { + txid: parent_txid, + index: 0, + }; + + let child_inscription = Inscription { + content_type: Some("text/plain".into()), + body: Some("pointer-child".into()), + parent: Some(parent_inscription_id.parent_value()), + pointer: Some(0u64.to_le_bytes().to_vec()), + ..Default::default() + }; + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(2, 1, 0, child_inscription.to_witness())], + ..Default::default() + }); + + context.mine_blocks(1); + + let child_inscription_id = InscriptionId { txid, index: 0 }; + + context.index.assert_inscription_location( + parent_inscription_id, + SatPoint { + outpoint: OutPoint { txid, vout: 0 }, + offset: 0, + }, + Some(50 * COIN_VALUE), + ); + + context.index.assert_inscription_location( + child_inscription_id, + SatPoint { + outpoint: OutPoint { txid, vout: 0 }, + offset: 0, + }, + Some(50 * COIN_VALUE), + ); + + assert_eq!( + context + .index + .get_inscription_entry(child_inscription_id) + .unwrap() + .unwrap() + .inscription_number, + -1 + ); + + assert_eq!( + context + .index + .get_inscription_entry(child_inscription_id) + .unwrap() + .unwrap() + .parent + .unwrap(), + parent_inscription_id + ); + + assert_eq!( + context + .index + .get_children_by_inscription_id(parent_inscription_id) + .unwrap(), + vec![child_inscription_id] + ); + } + } + + #[test] + fn inscriptions_in_same_input_with_pointers_to_same_output() { + for context in Context::configurations() { + context.mine_blocks(1); + + let builder = script::Builder::new(); + + let builder = Inscription { + pointer: Some(100u64.to_le_bytes().to_vec()), + ..Default::default() + } + .append_reveal_script_to_builder(builder); + + let builder = Inscription { + pointer: Some(300_000u64.to_le_bytes().to_vec()), + ..Default::default() + } + .append_reveal_script_to_builder(builder); + + let builder = Inscription { + pointer: Some(1_000_000u64.to_le_bytes().to_vec()), + ..Default::default() + } + .append_reveal_script_to_builder(builder); + + let witness = Witness::from_slice(&[builder.into_bytes(), Vec::new()]); + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, witness)], + ..Default::default() + }); + + context.mine_blocks(1); + + let first = InscriptionId { txid, index: 0 }; + let second = InscriptionId { txid, index: 1 }; + let third = InscriptionId { txid, index: 2 }; + + context.index.assert_inscription_location( + first, + SatPoint { + outpoint: OutPoint { txid, vout: 0 }, + offset: 100, + }, + Some(50 * COIN_VALUE + 100), + ); + + context.index.assert_inscription_location( + second, + SatPoint { + outpoint: OutPoint { txid, vout: 0 }, + offset: 300_000, + }, + Some(50 * COIN_VALUE + 300_000), + ); + + context.index.assert_inscription_location( + third, + SatPoint { + outpoint: OutPoint { txid, vout: 0 }, + offset: 1_000_000, + }, + Some(50 * COIN_VALUE + 1_000_000), + ); + } + } + + #[test] + fn inscriptions_in_same_input_with_pointers_to_different_outputs() { + for context in Context::configurations() { + context.mine_blocks_with_subsidy(1, 300_000); + + let builder = script::Builder::new(); + + let builder = Inscription { + pointer: Some(100u64.to_le_bytes().to_vec()), + ..Default::default() + } + .append_reveal_script_to_builder(builder); + + let builder = Inscription { + pointer: Some(100_111u64.to_le_bytes().to_vec()), + ..Default::default() + } + .append_reveal_script_to_builder(builder); + + let builder = Inscription { + pointer: Some(299_999u64.to_le_bytes().to_vec()), + ..Default::default() + } + .append_reveal_script_to_builder(builder); + + let witness = Witness::from_slice(&[builder.into_bytes(), Vec::new()]); + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, witness)], + outputs: 3, + ..Default::default() + }); + + context.mine_blocks(1); + + let first = InscriptionId { txid, index: 0 }; + let second = InscriptionId { txid, index: 1 }; + let third = InscriptionId { txid, index: 2 }; + + context.index.assert_inscription_location( + first, + SatPoint { + outpoint: OutPoint { txid, vout: 0 }, + offset: 100, + }, + Some(50 * COIN_VALUE + 100), + ); + + context.index.assert_inscription_location( + second, + SatPoint { + outpoint: OutPoint { txid, vout: 1 }, + offset: 111, + }, + Some(50 * COIN_VALUE + 100_111), + ); + + context.index.assert_inscription_location( + third, + SatPoint { + outpoint: OutPoint { txid, vout: 2 }, + offset: 99_999, + }, + Some(50 * COIN_VALUE + 299_999), + ); + } + } + + #[test] + fn inscriptions_in_different_inputs_with_pointers_to_different_outputs() { + for context in Context::configurations() { + context.mine_blocks(3); + + let inscription_for_second_output = Inscription { + content_type: Some("text/plain".into()), + body: Some("hello jupiter".into()), + pointer: Some((50 * COIN_VALUE).to_le_bytes().to_vec()), + ..Default::default() + }; + + let inscription_for_third_output = Inscription { + content_type: Some("text/plain".into()), + body: Some("hello mars".into()), + pointer: Some((100 * COIN_VALUE).to_le_bytes().to_vec()), + ..Default::default() + }; + + let inscription_for_first_output = Inscription { + content_type: Some("text/plain".into()), + body: Some("hello world".into()), + pointer: Some(0u64.to_le_bytes().to_vec()), + ..Default::default() + }; + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[ + (1, 0, 0, inscription_for_second_output.to_witness()), + (2, 0, 0, inscription_for_third_output.to_witness()), + (3, 0, 0, inscription_for_first_output.to_witness()), + ], + outputs: 3, + ..Default::default() + }); + + context.mine_blocks(1); + + let inscription_for_second_output = InscriptionId { txid, index: 0 }; + let inscription_for_third_output = InscriptionId { txid, index: 1 }; + let inscription_for_first_output = InscriptionId { txid, index: 2 }; + + context.index.assert_inscription_location( + inscription_for_second_output, + SatPoint { + outpoint: OutPoint { txid, vout: 1 }, + offset: 0, + }, + Some(100 * COIN_VALUE), + ); + + context.index.assert_inscription_location( + inscription_for_third_output, + SatPoint { + outpoint: OutPoint { txid, vout: 2 }, + offset: 0, + }, + Some(150 * COIN_VALUE), + ); + + context.index.assert_inscription_location( + inscription_for_first_output, + SatPoint { + outpoint: OutPoint { txid, vout: 0 }, + offset: 0, + }, + Some(50 * COIN_VALUE), + ); + } + } + + #[test] + fn inscriptions_in_different_inputs_with_pointers_to_same_output() { + for context in Context::configurations() { + context.mine_blocks(3); + + let first_inscription = Inscription { + content_type: Some("text/plain".into()), + body: Some("hello jupiter".into()), + ..Default::default() + }; + + let second_inscription = Inscription { + content_type: Some("text/plain".into()), + body: Some("hello mars".into()), + pointer: Some(1u64.to_le_bytes().to_vec()), + ..Default::default() + }; + + let third_inscription = Inscription { + content_type: Some("text/plain".into()), + body: Some("hello world".into()), + pointer: Some(2u64.to_le_bytes().to_vec()), + ..Default::default() + }; + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[ + (1, 0, 0, first_inscription.to_witness()), + (2, 0, 0, second_inscription.to_witness()), + (3, 0, 0, third_inscription.to_witness()), + ], + outputs: 1, + ..Default::default() + }); + + context.mine_blocks(1); + + let first_inscription_id = InscriptionId { txid, index: 0 }; + let second_inscription_id = InscriptionId { txid, index: 1 }; + let third_inscription_id = InscriptionId { txid, index: 2 }; + + context.index.assert_inscription_location( + first_inscription_id, + SatPoint { + outpoint: OutPoint { txid, vout: 0 }, + offset: 0, + }, + Some(50 * COIN_VALUE), + ); + + context.index.assert_inscription_location( + second_inscription_id, + SatPoint { + outpoint: OutPoint { txid, vout: 0 }, + offset: 1, + }, + Some(50 * COIN_VALUE + 1), + ); + + context.index.assert_inscription_location( + third_inscription_id, + SatPoint { + outpoint: OutPoint { txid, vout: 0 }, + offset: 2, + }, + Some(50 * COIN_VALUE + 2), + ); + } + } + + #[test] + fn inscriptions_with_pointers_to_same_sat_one_becomes_cursed_reinscriptions() { + for context in Context::configurations() { + context.mine_blocks(2); + + let inscription = Inscription { + content_type: Some("text/plain".into()), + body: Some("hello jupiter".into()), + ..Default::default() + }; + + let cursed_reinscription = Inscription { + content_type: Some("text/plain".into()), + body: Some("hello mars".into()), + pointer: Some(0u64.to_le_bytes().to_vec()), + ..Default::default() + }; + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[ + (1, 0, 0, inscription.to_witness()), + (2, 0, 0, cursed_reinscription.to_witness()), + ], + outputs: 2, + ..Default::default() + }); + + context.mine_blocks(1); + + let inscription_id = InscriptionId { txid, index: 0 }; + let cursed_reinscription_id = InscriptionId { txid, index: 1 }; + + context.index.assert_inscription_location( + inscription_id, + SatPoint { + outpoint: OutPoint { txid, vout: 0 }, + offset: 0, + }, + Some(50 * COIN_VALUE), + ); + + context.index.assert_inscription_location( + cursed_reinscription_id, + SatPoint { + outpoint: OutPoint { txid, vout: 0 }, + offset: 0, + }, + Some(50 * COIN_VALUE), + ); + + assert_eq!( + context + .index + .get_inscription_entry(inscription_id) + .unwrap() + .unwrap() + .inscription_number, + 0 + ); + + assert_eq!( + context + .index + .get_inscription_entry(cursed_reinscription_id) + .unwrap() + .unwrap() + .inscription_number, + -1 + ); + } + } + + #[test] + fn inscribe_into_fee() { + for context in Context::configurations() { + context.mine_blocks(1); + + let inscription = Inscription::default(); + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, inscription.to_witness())], + fee: 50 * COIN_VALUE, + ..Default::default() + }); + + let blocks = context.mine_blocks(1); + + let inscription_id = InscriptionId { txid, index: 0 }; + + context.index.assert_inscription_location( + inscription_id, + SatPoint { + outpoint: OutPoint { + txid: blocks[0].txdata[0].txid(), + vout: 0, + }, + offset: 50 * COIN_VALUE, + }, + Some(50 * COIN_VALUE), + ); + } + } + + #[test] + fn inscribe_into_fee_with_reduced_subsidy() { + for context in Context::configurations() { + context.mine_blocks(1); + + let inscription = Inscription::default(); + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, inscription.to_witness())], + fee: 50 * COIN_VALUE, + ..Default::default() + }); + + let blocks = context.mine_blocks_with_subsidy(1, 25 * COIN_VALUE); + + let inscription_id = InscriptionId { txid, index: 0 }; + + context.index.assert_inscription_location( + inscription_id, + SatPoint { + outpoint: OutPoint { + txid: blocks[0].txdata[0].txid(), + vout: 0, + }, + offset: 50 * COIN_VALUE, + }, + Some(50 * COIN_VALUE), + ); + } + } } diff --git a/src/index/updater/inscription_updater.rs b/src/index/updater/inscription_updater.rs index dc8448e148..a38ba3d01c 100644 --- a/src/index/updater/inscription_updater.rs +++ b/src/index/updater/inscription_updater.rs @@ -13,6 +13,7 @@ enum Origin { cursed: bool, fee: u64, parent: Option, + pointer: Option, unbound: bool, }, Old { @@ -246,6 +247,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { cursed, fee: 0, parent: inscription.payload.parent(), + pointer: inscription.payload.pointer(), unbound, }, }); @@ -287,6 +289,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { cursed, fee: _, parent, + pointer, unbound, }, } = flotsam @@ -298,6 +301,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { fee: (total_input_value - total_output_value) / u64::from(id_counter), cursed, parent, + pointer, unbound, }, } @@ -320,6 +324,8 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { floating_inscriptions.sort_by_key(|flotsam| flotsam.offset); let mut inscriptions = floating_inscriptions.into_iter().peekable(); + let mut range_to_vout = BTreeMap::new(); + let mut new_locations = Vec::new(); let mut output_value = 0; for (vout, tx_out) in tx.output.iter().enumerate() { let end = output_value + tx_out.value; @@ -337,13 +343,11 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { offset: flotsam.offset - output_value, }; - self.update_inscription_location( - input_sat_ranges, - inscriptions.next().unwrap(), - new_satpoint, - )?; + new_locations.push((new_satpoint, inscriptions.next().unwrap())); } + range_to_vout.insert((output_value, end), vout.try_into().unwrap()); + output_value = end; self.value_cache.insert( @@ -355,6 +359,31 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { ); } + for (new_satpoint, mut flotsam) in new_locations.into_iter() { + let new_satpoint = match flotsam.origin { + Origin::New { + pointer: Some(pointer), + .. + } if pointer < output_value => { + match range_to_vout.iter().find_map(|((start, end), vout)| { + (pointer >= *start && pointer < *end).then(|| (vout, pointer - start)) + }) { + Some((vout, offset)) => { + flotsam.offset = pointer; + SatPoint { + outpoint: OutPoint { txid, vout: *vout }, + offset, + } + } + _ => new_satpoint, + } + } + _ => new_satpoint, + }; + + self.update_inscription_location(input_sat_ranges, flotsam, new_satpoint)?; + } + if is_coinbase { for flotsam in inscriptions { let new_satpoint = SatPoint { @@ -413,6 +442,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { fee, parent, unbound, + .. } => { let inscription_number = if cursed { let number: i64 = self.cursed_inscription_count.try_into().unwrap(); diff --git a/src/inscription.rs b/src/inscription.rs index 8632eda466..f460af8f7f 100644 --- a/src/inscription.rs +++ b/src/inscription.rs @@ -31,6 +31,7 @@ pub struct Inscription { pub metadata: Option>, pub metaprotocol: Option>, pub parent: Option>, + pub pointer: Option>, pub unrecognized_even_field: bool, } @@ -67,12 +68,10 @@ impl Inscription { Ok(Self { body: Some(body), content_type: Some(content_type.into()), - duplicate_field: false, - incomplete_field: false, metadata, metaprotocol: metaprotocol.map(|metaprotocol| metaprotocol.into_bytes()), parent: parent.map(|id| id.parent_value()), - unrecognized_even_field: false, + ..Default::default() }) } @@ -103,6 +102,12 @@ impl Inscription { .push_slice(PushBytesBuf::try_from(parent).unwrap()); } + if let Some(pointer) = self.pointer.clone() { + builder = builder + .push_slice(envelope::POINTER_TAG) + .push_slice(PushBytesBuf::try_from(pointer).unwrap()); + } + if let Some(metadata) = &self.metadata { for chunk in metadata.chunks(520) { builder = builder.push_slice(envelope::METADATA_TAG); @@ -193,6 +198,27 @@ impl Inscription { Some(InscriptionId { txid, index }) } + pub(crate) fn pointer(&self) -> Option { + let value = self.pointer.as_ref()?; + + if value.iter().skip(8).copied().any(|byte| byte != 0) { + return None; + } + + let pointer = [ + value.first().copied().unwrap_or(0), + value.get(1).copied().unwrap_or(0), + value.get(2).copied().unwrap_or(0), + value.get(3).copied().unwrap_or(0), + value.get(4).copied().unwrap_or(0), + value.get(5).copied().unwrap_or(0), + value.get(6).copied().unwrap_or(0), + value.get(7).copied().unwrap_or(0), + ]; + + Some(u64::from_le_bytes(pointer)) + } + #[cfg(test)] pub(crate) fn to_witness(&self) -> Witness { let builder = script::Builder::new(); @@ -507,4 +533,85 @@ mod tests { None, ); } + + #[test] + fn pointer_decode() { + assert_eq!( + Inscription { + pointer: None, + ..Default::default() + } + .pointer(), + None + ); + assert_eq!( + Inscription { + pointer: Some(vec![0]), + ..Default::default() + } + .pointer(), + Some(0), + ); + assert_eq!( + Inscription { + pointer: Some(vec![1, 2, 3, 4, 5, 6, 7, 8]), + ..Default::default() + } + .pointer(), + Some(0x0807060504030201), + ); + assert_eq!( + Inscription { + pointer: Some(vec![1, 2, 3, 4, 5, 6]), + ..Default::default() + } + .pointer(), + Some(0x0000060504030201), + ); + assert_eq!( + Inscription { + pointer: Some(vec![1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0]), + ..Default::default() + } + .pointer(), + Some(0x0807060504030201), + ); + assert_eq!( + Inscription { + pointer: Some(vec![1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 1]), + ..Default::default() + } + .pointer(), + None, + ); + assert_eq!( + Inscription { + pointer: Some(vec![1, 2, 3, 4, 5, 6, 7, 8, 1]), + ..Default::default() + } + .pointer(), + None, + ); + } + + #[test] + fn pointer_encode() { + assert_eq!( + Inscription { + pointer: None, + ..Default::default() + } + .to_witness(), + envelope(&[b"ord"]), + ); + + assert_eq!( + Inscription { + pointer: Some(vec![1, 2, 3]), + ..Default::default() + } + .to_witness(), + envelope(&[b"ord", &[2], &[1, 2, 3]]), + ); + } } diff --git a/test-bitcoincore-rpc/src/state.rs b/test-bitcoincore-rpc/src/state.rs index 9d1a6c8d4d..4cb63e3bbe 100644 --- a/test-bitcoincore-rpc/src/state.rs +++ b/test-bitcoincore-rpc/src/state.rs @@ -143,6 +143,7 @@ impl State { } let value_per_output = (total_value - template.fee) / template.outputs as u64; + assert_eq!( value_per_output * template.outputs as u64 + template.fee, total_value