diff --git a/bip380/miniscript/fragments.py b/bip380/miniscript/fragments.py
index c08473c..252d38b 100644
--- a/bip380/miniscript/fragments.py
+++ b/bip380/miniscript/fragments.py
@@ -5,7 +5,6 @@
 See the Miniscript website for the specification of the type system: https://bitcoin.sipa.be/miniscript/.
 """
 
-import copy
 import bip380.miniscript.parsing as parsing
 
 from bip380.key import DescriptorKey
@@ -45,7 +44,7 @@
 
 from .errors import MiniscriptNodeCreationError
 from .property import Property
-from .satisfaction import ExecutionInfo, Satisfaction
+from .satisfaction import ExecutionInfo, Satisfaction, ThreshSubsInfo
 
 
 # Threshold for nLockTime: below this value it is interpreted as block number,
@@ -88,7 +87,8 @@ class Node:
     # Whether this node does not contain a mix of timelock or heightlock of different types.
     # That is, not (abs_heightlocks and rel_heightlocks or abs_timelocks and abs_timelocks)
     no_timelock_mix = None
-    # Information about this Miniscript execution (satisfaction cost, etc..)
+    # Information about this Miniscript execution (satisfaction cost, etc..). Note that the
+    # execution information assumes non-malleable satisfaction.
     exec_info = None
 
     def __init__(self, *args, **kwargs):
@@ -511,7 +511,7 @@ def satisfaction(self, sat_material):
         return Satisfaction.from_concat(sat_material, *self.subs)
 
     def dissatisfaction(self):
-        return Satisfaction.unavailable()  # it's V.
+        return (self.subs[1].dissatisfy() + self.subs[0].satisfy()).set_non_canon()
 
     def __repr__(self):
         return f"and_v({','.join(map(str, self.subs))})"
@@ -564,7 +564,15 @@ def satisfaction(self, sat_material):
         return Satisfaction.from_concat(sat_material, self.subs[0], self.subs[1])
 
     def dissatisfaction(self):
-        return self.subs[1].dissatisfaction() + self.subs[0].dissatisfaction()
+        return (
+            (self.subs[1].dissatisfaction() + self.subs[0].dissatisfaction())
+            | (
+                self.subs[1].dissatisfaction() + self.subs[0].satisfaction()
+            ).set_non_canon()
+            | (
+                self.subs[1].satisfaction() + self.subs[0].dissatisfaction()
+            ).set_non_canon()
+        )
 
     def __repr__(self):
         return f"and_b({','.join(map(str, self.subs))})"
@@ -854,8 +862,10 @@ def satisfaction(self, sat_material):
         ) | (self.subs[2].satisfaction(sat_material) + self.subs[0].dissatisfaction())
 
     def dissatisfaction(self):
-        # Dissatisfy X and Z
-        return self.subs[2].dissatisfaction() + self.subs[0].dissatisfaction()
+        # Dissatisfy X and Z [or satisfy X but dissatisfy Y]
+        return (self.subs[2].dissatisfaction() + self.subs[0].dissatisfaction()) | (
+            self.subs[1].dissatisfaction() + self.subs[0].satisfaction()
+        ).set_non_canon()
 
     def __repr__(self):
         return f"andor({','.join(map(str, self.subs))})"
@@ -945,9 +955,10 @@ def satisfaction(self, sat_material):
         return Satisfaction.from_thresh(sat_material, self.k, self.subs)
 
     def dissatisfaction(self):
-        return sum(
+        all_dissat = sum(
             [sub.dissatisfaction() for sub in self.subs], start=Satisfaction(witness=[])
         )
+        
 
     def __repr__(self):
         return f"thresh({self.k},{','.join(map(str, self.subs))})"
@@ -1172,7 +1183,12 @@ def exec_info(self):
         return ExecutionInfo.from_wrap_dissat(self.sub.exec_info, ops_count=4, dissat=1)
 
     def dissatisfaction(self):
-        return Satisfaction(witness=[b""])
+        sub_dissat = self.sub.dissatisfy()
+        return Satisfaction(witness=[b""]) | (
+            sub_dissat.non_canon()
+            if not sub_dissat.is_unavailable() and len(sub_dissat.witness[-1]) > 0
+            else Satisfaction.unavailable()
+        )
 
     def __repr__(self):
         # Avoid duplicating colons
diff --git a/bip380/miniscript/satisfaction.py b/bip380/miniscript/satisfaction.py
index 67e8780..7c88fcb 100644
--- a/bip380/miniscript/satisfaction.py
+++ b/bip380/miniscript/satisfaction.py
@@ -25,6 +25,35 @@ def max_optional(a, b):
     return max(a, b)
 
 
+class ThreshSubsInfo:
+    """Information about a thresh fragment's sub-fragments."""
+
+    def __init__(self, subs, sat_material):
+        # Fragments that cannot be satisfied with the given material.
+        self.unsatisfiable = []
+        # Fragments that cannot be dis-satisfied with the given material.
+        self.undissatisfiable = []
+        # The fragments that are both satisfiable and dis-satisfiable with
+        # the given material, along with whether they require a signature and
+        # the difference in size between their satisfaction and dis-satisfaction.
+        # These are stored inside the list so that it may be sorted to prefer
+        # satisfying fragments that don't require a signature and have a smaller
+        # satisfaction.
+        self.available = []
+
+        self.available, self.unsatisfiable, self.undissatisfiable = [], [], []
+        for sub in subs:
+            sat, dissat = sub.satisfaction(sat_material), sub.dissatisfaction()
+            if sat.witness is None:
+                self.unsatisfiable.append(sub)
+            elif dissat.witness is None:
+                self.undissatisfiable.append(sub)
+            else:
+                self.available.append(
+                    (int(sat.has_sig), len(sat.witness) - len(dissat.witness), sub)
+                )
+
+
 class SatisfactionMaterial:
     """Data that may be needed in order to satisfy a Minsicript fragment."""
 
@@ -59,14 +88,11 @@ def __repr__(self):
 class Satisfaction:
     """All information about a satisfaction."""
 
-    def __init__(self, witness, has_sig=False):
+    def __init__(self, witness, has_sig=False, non_canon=False):
         assert isinstance(witness, list) or witness is None
         self.witness = witness
         self.has_sig = has_sig
-        # TODO: we probably need to take into account non-canon sats, as the algorithm
-        # described on the website mandates it:
-        # > Iterate over all the valid satisfactions/dissatisfactions in the table above
-        # > (including the non-canonical ones),
+        self.non_canon = non_canon
 
     def __add__(self, other):
         """Concatenate two satisfactions together."""
@@ -114,6 +140,10 @@ def unavailable():
     def is_unavailable(self):
         return self.witness is None
 
+    def set_non_canon(self):
+        self.non_canon = True
+        return self
+
     def size(self):
         return len(self.witness) + sum(len(elem) for elem in self.witness)
 
@@ -126,8 +156,12 @@ def from_concat(sat_material, sub_a, sub_b, disjunction=False):
         :param disjunction: Whether this fragment has an 'or()' semantic.
         """
         if disjunction:
-            return (sub_b.dissatisfaction() + sub_a.satisfaction(sat_material)) | (
-                sub_b.satisfaction(sat_material) + sub_a.dissatisfaction()
+            return (
+                (sub_b.dissatisfaction() + sub_a.satisfaction(sat_material))
+                | (sub_b.satisfaction(sat_material) + sub_a.dissatisfaction())
+                | (
+                    sub_b.satisfaction(sat_material) + sub_a.satisfaction(sat_material)
+                ).set_non_canon()
             )
         return sub_b.satisfaction(sat_material) + sub_a.satisfaction(sat_material)
 
@@ -150,29 +184,22 @@ def from_thresh(sat_material, k, subs):
         :param k: The number of subs that need to be satisfied.
         :param subs: The list of all subs of the threshold.
         """
-        # Pick the k sub-fragments to satisfy, prefering (in order):
-        # 1. Fragments that don't require a signature to be satisfied
-        # 2. Fragments whose satisfaction's size is smaller
-        # Record the unavailable (in either way) ones as we go.
-        arbitrage, unsatisfiable, undissatisfiable = [], [], []
-        for sub in subs:
-            sat, dissat = sub.satisfaction(sat_material), sub.dissatisfaction()
-            if sat.witness is None:
-                unsatisfiable.append(sub)
-            elif dissat.witness is None:
-                undissatisfiable.append(sub)
-            else:
-                arbitrage.append(
-                    (int(sat.has_sig), len(sat.witness) - len(dissat.witness), sub)
-                )
+        thresh_info = ThreshSubsInfo(subs, sat_material)
 
         # If not enough (dis)satisfactions are available, fail.
-        if len(unsatisfiable) > len(subs) - k or len(undissatisfiable) > k:
+        if (
+            len(thresh_info.unsatisfiable) > len(subs) - k
+            or len(thresh_info.undissatisfiable) > k
+        ):
             return Satisfaction.unavailable()
 
         # Otherwise, satisfy the k most optimal ones.
-        arbitrage = sorted(arbitrage, key=lambda x: x[:2])
-        optimal_sat = undissatisfiable + [a[2] for a in arbitrage] + unsatisfiable
+        arbitrage = sorted(thresh_info.available, key=lambda x: x[:2])
+        optimal_sat = (
+            thresh_info.undissatisfiable
+            + [a[2] for a in arbitrage]
+            + thresh_info.unsatisfiable
+        )
         to_satisfy = set(optimal_sat[:k])
         return sum(
             [
@@ -343,6 +370,8 @@ def from_thresh(k, subs):
         # All subs are executed, there is no OP_IF branch.
         dyn_ops = sum([sub._dyn_ops_count for sub in subs])
 
+        # FIXME: has_sig should be taken into account. Maybe use ThreshSubsInfo.
+
         # In order to estimate the worst case we simulate to satisfy the k subs whose
         # sat/dissat ratio is the largest, and dissatisfy the others.
         # We do so by iterating through all the subs, recording their sat-dissat "score"