diff --git a/node/uhpo/src/payout_settlement.rs b/node/uhpo/src/payout_settlement.rs
index 835fe26..53d91d6 100644
--- a/node/uhpo/src/payout_settlement.rs
+++ b/node/uhpo/src/payout_settlement.rs
@@ -1,3 +1,228 @@
-pub struct PayoutSettlement {}
+use std::collections::HashMap;
-impl PayoutSettlement {}
+use bitcoin::{Address, Amount, Transaction, TxOut};
+use secp256k1::Scalar;
+
+use crate::{
+ crypto::{
+ add_signature,
+ signature::{KeypairBehavior, Secp256k1Behavior, SecretKeyBehavior},
+ },
+ transaction::TransactionBuilder,
+ UhpoError,
+};
+
+// `PayoutSettlement` represents an cashout of an Eltoo style payout.
+pub struct PayoutSettlement {
+ transaction: Transaction,
+
+ // `prev_update_txout` is the output of the latest Eltoo style `PayoutUpdate` it is stored and later used to sign inputs.
+ prev_update_txout: TxOut,
+}
+
+impl PayoutSettlement {
+ /// Creates a new `PayoutSettlement`
+ ///
+ /// # Arguments
+ /// * `latest_eltoo_out` - The output of the latest `PayoutUpdate`
+ /// * `payout_map` - A map of payout addresses to payout amounts
+ ///
+ /// # Returns
+ /// Result containing a new `PayoutSettlement` or an `UhpoError`
+ pub fn new(latest_eltoo_out: Transaction, payout_map: HashMap
) -> Self {
+ // payout_update_tx would always have vout set to 0
+ let builder = TransactionBuilder::new().add_input(latest_eltoo_out.compute_txid(), 0);
+
+ let builder = payout_map
+ .into_iter()
+ .fold(builder, |builder, (address, amount)| {
+ builder.add_output(address, amount)
+ });
+
+ PayoutSettlement {
+ transaction: builder.build(),
+ prev_update_txout: latest_eltoo_out.output[0].clone(),
+ }
+ }
+
+ /// Adds a signature to the latest `PayoutUpdate`
+ ///
+ /// # Arguments
+ /// * `private_key` - The private key to sign with
+ /// * `tweak` - An optional tweak
+ /// * `secp` - The secp256k1 context
+ ///
+ /// # Returns
+ /// Result indicating success or an `UhpoError`
+ pub fn add_prev_update_sig(
+ &mut self,
+ private_key: S,
+ tweak: Option<&Scalar>,
+ secp: &E,
+ ) -> Result<(), UhpoError>
+ where
+ S: SecretKeyBehavior,
+ K: KeypairBehavior,
+ E: Secp256k1Behavior + 'static,
+ {
+ let prevouts = vec![&self.prev_update_txout];
+
+ add_signature(
+ &mut self.transaction,
+ 0,
+ &prevouts,
+ private_key,
+ tweak,
+ secp,
+ )
+ }
+
+ pub fn build(self) -> Transaction {
+ self.transaction
+ }
+}
+
+// unit tests
+#[cfg(test)]
+mod tests {
+ use bitcoin::{absolute::LockTime, transaction::Version, Network, ScriptBuf};
+ use rand::Rng;
+ use secp256k1::{All, Keypair, Secp256k1, SecretKey};
+
+ use crate::crypto::signature::{
+ MockKeypairBehavior, MockSecp256k1Behavior, MockSecretKeyBehavior,
+ };
+
+ use super::*;
+
+ fn create_dummy_transaction(amount_out: Amount) -> Transaction {
+ Transaction {
+ version: Version::TWO,
+ lock_time: LockTime::ZERO,
+ input: vec![],
+ output: vec![TxOut {
+ value: amount_out,
+ script_pubkey: ScriptBuf::default(),
+ }],
+ }
+ }
+
+ fn setup(miner_count: u64) -> (Secp256k1, Keypair, HashMap, u64) {
+ let secp = Secp256k1::new();
+ let mut rng = rand::thread_rng();
+
+ let data: [u8; 32] = rng.gen();
+ let keypair = SecretKey::from_slice(&data).unwrap().keypair(&secp);
+
+ let mut total_amount = 0;
+ let cashout_map: HashMap = (0..miner_count)
+ .map(|_| {
+ let data: [u8; 32] = rng.gen();
+ let keypair = SecretKey::from_slice(&data).unwrap().keypair(&secp);
+ let new_user_address: Address =
+ Address::p2tr(&secp, keypair.x_only_public_key().0, None, Network::Regtest);
+
+ let amount = rng.gen::();
+ total_amount += amount as u64;
+ (new_user_address, Amount::from_sat(amount as u64))
+ })
+ .collect();
+
+ (secp, keypair, cashout_map, total_amount)
+ }
+
+ #[test]
+ fn test_payout_settlement() {
+ let (_, _, cashout_map, total_amount) = setup(3);
+ let latest_payout_update = create_dummy_transaction(Amount::from_sat(total_amount + 1000));
+
+ let settlement_tx =
+ PayoutSettlement::new(latest_payout_update, cashout_map.clone()).build();
+
+ assert!(settlement_tx.output.len() == 3);
+ assert!(settlement_tx.input.len() == 1);
+
+ settlement_tx.output.iter().for_each(|o| {
+ let address = Address::from_script(&o.script_pubkey, Network::Regtest).unwrap();
+ assert!(
+ cashout_map
+ .get(&address)
+ .expect("Address not found in cashout_map")
+ == &o.value
+ );
+ });
+ }
+
+ #[test]
+ fn test_add_signature_with_no_tweak() {
+ let (secp, keypair, cashout_map, _) = setup(3);
+ let latest_payout_update = create_dummy_transaction(Amount::from_sat(1000));
+
+ let mut payout_settlement = PayoutSettlement::new(latest_payout_update, cashout_map);
+
+ payout_settlement
+ .add_prev_update_sig(keypair.secret_key(), None, &secp)
+ .expect("Failed to add signature");
+
+ assert_eq!(payout_settlement.transaction.input[0].witness.len(), 1);
+ assert_eq!(payout_settlement.transaction.input[0].witness[0].len(), 65);
+ }
+
+ #[test]
+ fn test_add_signature_with_tweak() {
+ let (secp, keypair, cashout_map, _) = setup(3);
+ let latest_payout_update = create_dummy_transaction(Amount::from_sat(1000));
+
+ let mut payout_settlement = PayoutSettlement::new(latest_payout_update, cashout_map);
+
+ payout_settlement
+ .add_prev_update_sig(
+ keypair.secret_key(),
+ Some(&Scalar::random_custom(&mut rand::thread_rng())),
+ &secp,
+ )
+ .expect("Failed to add signature");
+
+ assert_eq!(payout_settlement.transaction.input[0].witness.len(), 1);
+ assert_eq!(payout_settlement.transaction.input[0].witness[0].len(), 65);
+ }
+
+ #[test]
+ fn test_add_signature_mock_error_should_fail() {
+ let (_, _, cashout_map, _) = setup(3);
+ let latest_payout_update = create_dummy_transaction(Amount::from_sat(1000));
+
+ let mut payout_settlement = PayoutSettlement::new(latest_payout_update, cashout_map);
+
+ // setup mocks
+ let mock_secp = MockSecp256k1Behavior::new();
+
+ let create_mock_secret_key = || {
+ let mut mock_keypair = MockKeypairBehavior::new();
+ let mut mock_secret_key = MockSecretKeyBehavior::<
+ MockKeypairBehavior,
+ MockSecp256k1Behavior,
+ >::new();
+
+ // add_xonly_tweak on keypair, returns an error
+ mock_keypair
+ .expect_add_xonly_tweak()
+ .return_once(|_: &MockSecp256k1Behavior, _| Err(secp256k1::Error::InvalidTweak));
+
+ // secretKey.keypair returns an MockKeypair
+ mock_secret_key
+ .expect_keypair()
+ .return_once(move |_| mock_keypair);
+
+ mock_secret_key
+ };
+
+ let result = payout_settlement.add_prev_update_sig(
+ create_mock_secret_key(),
+ Some(&Scalar::random_custom(&mut rand::thread_rng())),
+ &mock_secp,
+ );
+
+ assert!(matches!(result, Err(UhpoError::KeypairCreationError(_))));
+ }
+}