diff --git a/coordinator/src/main.rs b/coordinator/src/main.rs
index 19208cb3..ca150656 100644
--- a/coordinator/src/main.rs
+++ b/coordinator/src/main.rs
@@ -12,7 +12,11 @@ use std::{
 use zeroize::{Zeroize, Zeroizing};
 use rand_core::OsRng;
 
-use ciphersuite::{group::ff::PrimeField, Ciphersuite, Ristretto};
+use ciphersuite::{
+  group::ff::{Field, PrimeField},
+  Ciphersuite, Ristretto,
+};
+use schnorr::SchnorrSignature;
 
 use serai_db::{DbTxn, Db};
 use serai_env as env;
@@ -476,6 +480,10 @@ pub async fn handle_processors<D: Db, Pro: Processors, P: P2p>(
   loop {
     // TODO: Dispatch this message to a task dedicated to handling this processor, preventing one
     // processor from holding up all the others
+    // In order to do so, we need to locally track if a message was handled, even if its ID is
+    // greater than what the message-queue tracks as ack'd
+    // TODO: Shouldn't the processor also perform such checks? That its ack doesn't exceed the
+    // queue's?
     let msg = processors.recv().await;
 
     // TODO2: This is slow, and only works as long as a network only has a single Tributary
@@ -565,7 +573,23 @@ pub async fn handle_processors<D: Db, Pro: Processors, P: P2p>(
           signed: Transaction::empty_signed(),
         })),
         sign::ProcessorMessage::Completed { key: _, id, tx } => {
-          Some(Transaction::SignCompleted(id, tx, Transaction::empty_signed()))
+          let r = Zeroizing::new(<Ristretto as Ciphersuite>::F::random(&mut OsRng));
+          #[allow(non_snake_case)]
+          let R = <Ristretto as Ciphersuite>::generator() * r.deref();
+          let mut tx = Transaction::SignCompleted {
+            plan: id,
+            tx_hash: tx,
+            first_signer: pub_key,
+            signature: SchnorrSignature { R, s: <Ristretto as Ciphersuite>::F::ZERO },
+          };
+          let signed = SchnorrSignature::sign(&key, r, tx.sign_completed_challenge());
+          match &mut tx {
+            Transaction::SignCompleted { signature, .. } => {
+              *signature = signed;
+            }
+            _ => unreachable!(),
+          }
+          Some(tx)
         }
       },
       ProcessorMessage::Coordinator(inner_msg) => match inner_msg {
diff --git a/coordinator/src/substrate/mod.rs b/coordinator/src/substrate/mod.rs
index b6bb3608..d114b4a5 100644
--- a/coordinator/src/substrate/mod.rs
+++ b/coordinator/src/substrate/mod.rs
@@ -101,7 +101,6 @@ async fn handle_key_gen<Pro: Processors>(
   key_pair: KeyPair,
 ) -> Result<(), SeraiError> {
   if in_set(key, serai, set).await?.expect("KeyGen occurred for a set which doesn't exist") {
-    // TODO: Check how the processor handles this being fired multiple times
     processors
       .send(
         set.network,
@@ -196,7 +195,6 @@ async fn handle_batch_and_burns<Pro: Processors>(
         .expect("network had a batch/burn yet never set a latest block")
     };
 
-    // TODO: Check how the processor handles this being fired multiple times
     processors
       .send(
         network,
diff --git a/coordinator/src/tests/tributary/mod.rs b/coordinator/src/tests/tributary/mod.rs
index 62ea82e7..449fc3bc 100644
--- a/coordinator/src/tests/tributary/mod.rs
+++ b/coordinator/src/tests/tributary/mod.rs
@@ -116,4 +116,17 @@ fn serialize_transaction() {
 
   test_read_write(Transaction::SignPreprocess(random_sign_data(&mut OsRng)));
   test_read_write(Transaction::SignShare(random_sign_data(&mut OsRng)));
+
+  {
+    let mut plan = [0; 32];
+    OsRng.fill_bytes(&mut plan);
+    let mut tx_hash = vec![0; (OsRng.next_u64() % 64).try_into().unwrap()];
+    OsRng.fill_bytes(&mut tx_hash);
+    test_read_write(Transaction::SignCompleted {
+      plan,
+      tx_hash,
+      first_signer: random_signed(&mut OsRng).signer,
+      signature: random_signed(&mut OsRng).signature,
+    });
+  }
 }
diff --git a/coordinator/src/tributary/handle.rs b/coordinator/src/tributary/handle.rs
index 3c223867..1fd60b05 100644
--- a/coordinator/src/tributary/handle.rs
+++ b/coordinator/src/tributary/handle.rs
@@ -585,8 +585,8 @@ pub async fn handle_application_tx<
         None => {}
       }
     }
-    Transaction::SignCompleted(id, tx, signed) => {
-      // TODO: Confirm this is a valid ID
+    Transaction::SignCompleted { plan, tx_hash, .. } => {
+      // TODO: Confirm this is a valid plan ID
       // TODO: Confirm this signer hasn't prior published a completion
       let Some(key_pair) = TributaryDb::<D>::key_pair(txn, spec.set()) else { todo!() };
       processors
@@ -594,8 +594,8 @@ pub async fn handle_application_tx<
           spec.set().network,
           CoordinatorMessage::Sign(sign::CoordinatorMessage::Completed {
             key: key_pair.1.to_vec(),
-            id,
-            tx,
+            id: plan,
+            tx: tx_hash,
           }),
         )
         .await;
diff --git a/coordinator/src/tributary/mod.rs b/coordinator/src/tributary/mod.rs
index f7858ff0..ae7a6f7b 100644
--- a/coordinator/src/tributary/mod.rs
+++ b/coordinator/src/tributary/mod.rs
@@ -232,6 +232,10 @@ pub enum Transaction {
   DkgConfirmed(u32, [u8; 32], Signed),
 
   // When we have synchrony on a batch, we can allow signing it
+  // TODO (never?): This is less efficient compared to an ExternalBlock provided transaction,
+  // which would be binding over the block hash and automatically achieve synchrony on all
+  // relevant batches. ExternalBlock was removed for this due to complexity around the pipeline
+  // with the current processor, yet it would still be an improvement.
   Batch([u8; 32], [u8; 32]),
   // When a Serai block is finalized, with the contained batches, we can allow the associated plan
   // IDs
@@ -242,10 +246,18 @@ pub enum Transaction {
 
   SignPreprocess(SignData),
   SignShare(SignData),
-  // TODO: We can't make this an Unsigned as we need to prevent spam, which requires a max of 1
-  // claim per sender
-  // Can we de-duplicate across senders though, if they claim the same hash completes?
-  SignCompleted([u8; 32], Vec<u8>, Signed),
+  // This is defined as an Unsigned transaction in order to de-duplicate SignCompleted amongst
+  // reporters (who should all report the same thing)
+  // We do still track the signer in order to prevent a single signer from publishing arbitrarily
+  // many TXs without penalty
+  // Here, they're denoted as the first_signer, as only the signer of the first TX to be included
+  // with this pairing will be remembered on-chain
+  SignCompleted {
+    plan: [u8; 32],
+    tx_hash: Vec<u8>,
+    first_signer: <Ristretto as Ciphersuite>::G,
+    signature: SchnorrSignature<Ristretto>,
+  },
 }
 
 impl ReadWrite for Transaction {
@@ -354,13 +366,15 @@ impl ReadWrite for Transaction {
         let mut plan = [0; 32];
         reader.read_exact(&mut plan)?;
 
-        let mut tx_len = [0];
-        reader.read_exact(&mut tx_len)?;
-        let mut tx = vec![0; usize::from(tx_len[0])];
-        reader.read_exact(&mut tx)?;
+        let mut tx_hash_len = [0];
+        reader.read_exact(&mut tx_hash_len)?;
+        let mut tx_hash = vec![0; usize::from(tx_hash_len[0])];
+        reader.read_exact(&mut tx_hash)?;
 
-        let signed = Signed::read(reader)?;
-        Ok(Transaction::SignCompleted(plan, tx, signed))
+        let first_signer = Ristretto::read_G(reader)?;
+        let signature = SchnorrSignature::<Ristretto>::read(reader)?;
+
+        Ok(Transaction::SignCompleted { plan, tx_hash, first_signer, signature })
       }
 
       _ => Err(io::Error::new(io::ErrorKind::Other, "invalid transaction type")),
@@ -460,12 +474,14 @@ impl ReadWrite for Transaction {
         writer.write_all(&[8])?;
         data.write(writer)
       }
-      Transaction::SignCompleted(plan, tx, signed) => {
+      Transaction::SignCompleted { plan, tx_hash, first_signer, signature } => {
         writer.write_all(&[9])?;
         writer.write_all(plan)?;
-        writer.write_all(&[u8::try_from(tx.len()).expect("tx hash length exceed 255 bytes")])?;
-        writer.write_all(tx)?;
-        signed.write(writer)
+        writer
+          .write_all(&[u8::try_from(tx_hash.len()).expect("tx hash length exceed 255 bytes")])?;
+        writer.write_all(tx_hash)?;
+        writer.write_all(&first_signer.to_bytes())?;
+        signature.write(writer)
       }
     }
   }
@@ -486,7 +502,7 @@ impl TransactionTrait for Transaction {
 
       Transaction::SignPreprocess(data) => TransactionKind::Signed(&data.signed),
       Transaction::SignShare(data) => TransactionKind::Signed(&data.signed),
-      Transaction::SignCompleted(_, _, signed) => TransactionKind::Signed(signed),
+      Transaction::SignCompleted { .. } => TransactionKind::Unsigned,
     }
   }
 
@@ -506,6 +522,12 @@ impl TransactionTrait for Transaction {
       }
     }
 
+    if let Transaction::SignCompleted { plan, tx_hash, first_signer, signature } = self {
+      if !signature.verify(*first_signer, self.sign_completed_challenge()) {
+        Err(TransactionError::InvalidContent)?;
+      }
+    }
+
     Ok(())
   }
 }
@@ -545,7 +567,7 @@ impl Transaction {
 
         Transaction::SignPreprocess(ref mut data) => &mut data.signed,
         Transaction::SignShare(ref mut data) => &mut data.signed,
-        Transaction::SignCompleted(_, _, ref mut signed) => signed,
+        Transaction::SignCompleted { .. } => panic!("signing SignCompleted"),
       }
     }
 
@@ -558,4 +580,18 @@ impl Transaction {
     let sig_hash = self.sig_hash(genesis);
     signed(self).signature = SchnorrSignature::<Ristretto>::sign(key, sig_nonce, sig_hash);
   }
+
+  pub fn sign_completed_challenge(&self) -> <Ristretto as Ciphersuite>::F {
+    if let Transaction::SignCompleted { plan, tx_hash, first_signer, signature } = self {
+      let mut transcript =
+        RecommendedTranscript::new(b"Coordinator Tributary Transaction SignCompleted");
+      transcript.append_message(b"plan", plan);
+      transcript.append_message(b"tx_hash", tx_hash);
+      transcript.append_message(b"signer", first_signer.to_bytes());
+      transcript.append_message(b"nonce", signature.R.to_bytes());
+      Ristretto::hash_to_F(b"SignCompleted signature", &transcript.challenge(b"challenge"))
+    } else {
+      panic!("sign_completed_challenge called on transaction which wasn't SignCompleted")
+    }
+  }
 }
diff --git a/coordinator/src/tributary/scanner.rs b/coordinator/src/tributary/scanner.rs
index a8e9588e..05df7395 100644
--- a/coordinator/src/tributary/scanner.rs
+++ b/coordinator/src/tributary/scanner.rs
@@ -76,7 +76,7 @@ async fn handle_block<
         // mark the node as fatally slashed
         TributaryDb::<D>::set_fatally_slashed(&mut txn, genesis, msgs.0.msg.sender);
 
-        // TODO: disconnect the node from network/ban from further participation in Tributary
+        // TODO2: disconnect the node from network/ban from further participation in Tributary
       }
       TributaryTransaction::Application(tx) => {
         handle_application_tx::<D, _, _, _, _, _>(
@@ -99,7 +99,7 @@ async fn handle_block<
     event_id += 1;
   }
 
-  // TODO: Trigger any necessary re-attempts
+  // TODO2: Trigger any necessary re-attempts
 }
 
 pub async fn handle_new_blocks<