aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--pyecsca/ec/countermeasures.py8
-rw-r--r--pyecsca/ec/mult/base.py6
-rw-r--r--pyecsca/ec/mult/binary.py4
-rw-r--r--pyecsca/ec/mult/comb.py14
-rw-r--r--pyecsca/ec/mult/fake.py2
-rw-r--r--pyecsca/ec/mult/fixed.py12
-rw-r--r--pyecsca/ec/mult/ladder.py7
-rw-r--r--pyecsca/ec/mult/naf.py8
-rw-r--r--pyecsca/ec/mult/window.py14
-rw-r--r--test/ec/test_countermeasures.py199
-rw-r--r--test/ec/test_mult.py6
11 files changed, 205 insertions, 75 deletions
diff --git a/pyecsca/ec/countermeasures.py b/pyecsca/ec/countermeasures.py
index 8a479b6..19afb06 100644
--- a/pyecsca/ec/countermeasures.py
+++ b/pyecsca/ec/countermeasures.py
@@ -70,6 +70,11 @@ class GroupScalarRandomization(ScalarMultiplierCountermeasure):
super().__init__(mult)
self.rand_bits = rand_bits
+ def init(self, params: DomainParameters, point: Point):
+ self.params = params
+ self.point = point
+ self.mult.init(self.params, self.point, bits=params.full_order.bit_length() + self.rand_bits)
+
def multiply(self, scalar: int) -> Point:
if self.params is None or self.point is None:
raise ValueError("Not initialized.")
@@ -189,8 +194,7 @@ class EuclideanSplitting(ScalarMultiplierCountermeasure):
if self.params is None or self.point is None:
raise ValueError("Not initialized.")
with ScalarMultiplicationAction(self.point, self.params, scalar) as action:
- order = self.params.order
- half_bits = order.bit_length() // 2
+ half_bits = self.params.order.bit_length() // 2
r = Mod.random(1 << half_bits)
R = self.mult.multiply(int(r))
diff --git a/pyecsca/ec/mult/base.py b/pyecsca/ec/mult/base.py
index f7a8c5b..15f2fd5 100644
--- a/pyecsca/ec/mult/base.py
+++ b/pyecsca/ec/mult/base.py
@@ -87,6 +87,7 @@ class ScalarMultiplier(ABC):
"""All formulas the multiplier was initialized with."""
_params: DomainParameters
_point: Point
+ _bits: int
_initialized: bool = False
def __init__(self, short_circuit: bool = True, **formulas: Optional[Formula]):
@@ -202,7 +203,7 @@ class ScalarMultiplier(ABC):
def __repr__(self):
return f"{self.__class__.__name__}({', '.join(map(str, self.formulas.values()))}, short_circuit={self.short_circuit})"
- def init(self, params: DomainParameters, point: Point):
+ def init(self, params: DomainParameters, point: Point, bits: Optional[int] = None):
"""
Initialize the scalar multiplier with :paramref:`~.init.params` and a :paramref:`~.init.point`.
@@ -211,6 +212,8 @@ class ScalarMultiplier(ABC):
:param params: The domain parameters to initialize the multiplier with.
:param point: The point to initialize the multiplier with.
+ :param bits: The number of bits to use in the scalar multiplication (i.e. no scalar will be larger than 2^bits).
+ The default is the bit length of the full order of the curve (including cofactor).
"""
coord_model = set(self.formulas.values()).pop().coordinate_model
if (
@@ -222,6 +225,7 @@ class ScalarMultiplier(ABC):
)
self._params = params
self._point = point
+ self._bits = bits if bits is not None else params.full_order.bit_length()
self._initialized = True
@abstractmethod
diff --git a/pyecsca/ec/mult/binary.py b/pyecsca/ec/mult/binary.py
index 5b6bed7..f0cb5ac 100644
--- a/pyecsca/ec/mult/binary.py
+++ b/pyecsca/ec/mult/binary.py
@@ -94,7 +94,7 @@ class DoubleAndAddMultiplier(AccumulatorMultiplier, ScalarMultiplier, ABC):
if self.complete:
q = self._point
r = copy(self._params.curve.neutral)
- top = self._params.order.bit_length() - 1
+ top = self._bits - 1
else:
q = copy(self._point)
r = copy(self._point)
@@ -112,7 +112,7 @@ class DoubleAndAddMultiplier(AccumulatorMultiplier, ScalarMultiplier, ABC):
q = self._point
r = copy(self._params.curve.neutral)
if self.complete:
- top = self._params.order.bit_length()
+ top = self._bits
else:
top = scalar.bit_length()
for _ in range(top):
diff --git a/pyecsca/ec/mult/comb.py b/pyecsca/ec/mult/comb.py
index 35d1678..1a6c0c2 100644
--- a/pyecsca/ec/mult/comb.py
+++ b/pyecsca/ec/mult/comb.py
@@ -85,10 +85,10 @@ class BGMWMultiplier(AccumulatorMultiplier, PrecompMultiplier, ScalarMultiplier)
def __repr__(self):
return f"{self.__class__.__name__}({', '.join(map(str, self.formulas.values()))}, short_circuit={self.short_circuit}, width={self.width}, direction={self.direction.name}, accumulation_order={self.accumulation_order.name})"
- def init(self, params: DomainParameters, point: Point):
+ def init(self, params: DomainParameters, point: Point, bits: Optional[int] = None):
with PrecomputationAction(params, point) as action:
- super().init(params, point)
- d = ceil(params.order.bit_length() / self.width)
+ super().init(params, point, bits)
+ d = ceil(self._bits / self.width)
self._points = {}
current_point = point
for i in range(d):
@@ -179,10 +179,10 @@ class CombMultiplier(AccumulatorMultiplier, PrecompMultiplier, ScalarMultiplier)
def __repr__(self):
return f"{self.__class__.__name__}({', '.join(map(str, self.formulas.values()))}, short_circuit={self.short_circuit}, width={self.width}, accumulation_order={self.accumulation_order.name})"
- def init(self, params: DomainParameters, point: Point):
+ def init(self, params: DomainParameters, point: Point, bits: Optional[int] = None):
with PrecomputationAction(params, point) as action:
- super().init(params, point)
- d = ceil(params.order.bit_length() / self.width)
+ super().init(params, point, bits)
+ d = ceil(self._bits / self.width)
base_points = {}
current_point = point
for i in range(self.width):
@@ -208,7 +208,7 @@ class CombMultiplier(AccumulatorMultiplier, PrecompMultiplier, ScalarMultiplier)
if scalar == 0:
return action.exit(copy(self._params.curve.neutral))
q = copy(self._params.curve.neutral)
- d = ceil(self._params.order.bit_length() / self.width)
+ d = ceil(self._bits / self.width)
recoded = convert_base(scalar, 2**d)
if len(recoded) != self.width:
recoded.extend([0] * (self.width - len(recoded)))
diff --git a/pyecsca/ec/mult/fake.py b/pyecsca/ec/mult/fake.py
index f89d622..391696b 100644
--- a/pyecsca/ec/mult/fake.py
+++ b/pyecsca/ec/mult/fake.py
@@ -9,7 +9,7 @@ from pyecsca.ec.params import DomainParameters
def fake_mult(mult_class: Type[ScalarMultiplier], mult_factory: Callable, params: DomainParameters) -> ScalarMultiplier:
"""
- Get a multiplier with FakeFormulas.
+ Get a multiplier with `FakeFormula`s.
:param mult_class: The class of the scalar multiplier to use.
:param mult_factory: A callable that takes the formulas and instantiates the multiplier.
diff --git a/pyecsca/ec/mult/fixed.py b/pyecsca/ec/mult/fixed.py
index 2c01648..070aaec 100644
--- a/pyecsca/ec/mult/fixed.py
+++ b/pyecsca/ec/mult/fixed.py
@@ -90,20 +90,20 @@ class FullPrecompMultiplier(AccumulatorMultiplier, PrecompMultiplier, ScalarMult
def __repr__(self):
return f"{self.__class__.__name__}({', '.join(map(str, self.formulas.values()))}, short_circuit={self.short_circuit}, accumulation_order={self.accumulation_order.name}, always={self.always}, complete={self.complete})"
- def init(self, params: DomainParameters, point: Point):
+ def init(self, params: DomainParameters, point: Point, bits: Optional[int] = None):
with PrecomputationAction(params, point) as action:
- super().init(params, point)
+ super().init(params, point, bits)
self._points = {}
current_point = point
- for i in range(params.order.bit_length() + 1):
+ for i in range(self._bits + 1):
self._points[i] = current_point
- if i != params.order.bit_length():
+ if i != self._bits:
current_point = self._dbl(current_point)
action.exit(self._points)
def _ltr(self, scalar: int) -> Point:
if self.complete:
- top = self._params.order.bit_length() - 1
+ top = self._bits - 1
else:
top = scalar.bit_length() - 1
r = copy(self._params.curve.neutral)
@@ -118,7 +118,7 @@ class FullPrecompMultiplier(AccumulatorMultiplier, PrecompMultiplier, ScalarMult
def _rtl(self, scalar: int) -> Point:
r = copy(self._params.curve.neutral)
if self.complete:
- top = self._params.order.bit_length()
+ top = self._bits
else:
top = scalar.bit_length()
for i in range(top):
diff --git a/pyecsca/ec/mult/ladder.py b/pyecsca/ec/mult/ladder.py
index 5f5d8a2..3635ada 100644
--- a/pyecsca/ec/mult/ladder.py
+++ b/pyecsca/ec/mult/ladder.py
@@ -12,6 +12,7 @@ from pyecsca.ec.formula import (
LadderFormula,
DifferentialAdditionFormula,
)
+from pyecsca.ec.params import DomainParameters
from pyecsca.ec.point import Point
@@ -87,7 +88,7 @@ class LadderMultiplier(ScalarMultiplier):
if self.complete:
p0 = copy(self._params.curve.neutral)
p1 = self._point
- top = self._params.full_order.bit_length() - 1
+ top = self._bits - 1
elif self.full:
p0 = copy(self._params.curve.neutral)
p1 = self._point
@@ -154,7 +155,7 @@ class SimpleLadderMultiplier(ScalarMultiplier):
if scalar == 0:
return action.exit(copy(self._params.curve.neutral))
if self.complete:
- top = self._params.full_order.bit_length() - 1
+ top = self._bits - 1
else:
top = scalar.bit_length() - 1
p0 = copy(self._params.curve.neutral)
@@ -232,7 +233,7 @@ class DifferentialLadderMultiplier(ScalarMultiplier):
if self.complete:
p0 = copy(self._params.curve.neutral)
p1 = copy(q)
- top = self._params.full_order.bit_length() - 1
+ top = self._bits - 1
elif self.full:
p0 = copy(self._params.curve.neutral)
p1 = copy(q)
diff --git a/pyecsca/ec/mult/naf.py b/pyecsca/ec/mult/naf.py
index 83dc0dc..b886552 100644
--- a/pyecsca/ec/mult/naf.py
+++ b/pyecsca/ec/mult/naf.py
@@ -81,9 +81,9 @@ class BinaryNAFMultiplier(AccumulatorMultiplier, PrecompMultiplier, ScalarMultip
def __repr__(self):
return f"{self.__class__.__name__}({', '.join(map(str, self.formulas.values()))}, short_circuit={self.short_circuit}, direction={self.direction.name}, accumulation_order={self.accumulation_order.name})"
- def init(self, params: DomainParameters, point: Point):
+ def init(self, params: DomainParameters, point: Point, bits: Optional[int] = None):
with PrecomputationAction(params, point) as action:
- super().init(params, point)
+ super().init(params, point, bits)
self._point_neg = self._neg(point)
action.exit({-1: self._point_neg})
@@ -195,9 +195,9 @@ class WindowNAFMultiplier(AccumulatorMultiplier, PrecompMultiplier, ScalarMultip
def __repr__(self):
return f"{self.__class__.__name__}({', '.join(map(str, self.formulas.values()))}, short_circuit={self.short_circuit}, width={self.width}, precompute_negation={self.precompute_negation}, accumulation_order={self.accumulation_order.name})"
- def init(self, params: DomainParameters, point: Point):
+ def init(self, params: DomainParameters, point: Point, bits: Optional[int] = None):
with PrecomputationAction(params, point) as action:
- super().init(params, point)
+ super().init(params, point, bits)
self._points = {}
self._points_neg = {}
current_point = point
diff --git a/pyecsca/ec/mult/window.py b/pyecsca/ec/mult/window.py
index 6fbee24..1a0ecec 100644
--- a/pyecsca/ec/mult/window.py
+++ b/pyecsca/ec/mult/window.py
@@ -90,9 +90,9 @@ class SlidingWindowMultiplier(AccumulatorMultiplier, PrecompMultiplier, ScalarMu
def __repr__(self):
return f"{self.__class__.__name__}({', '.join(map(str, self.formulas.values()))}, short_circuit={self.short_circuit}, width={self.width}, recoding_direction={self.recoding_direction.name}, accumulation_order={self.accumulation_order.name})"
- def init(self, params: DomainParameters, point: Point):
+ def init(self, params: DomainParameters, point: Point, bits: Optional[int] = None):
with PrecomputationAction(params, point) as action:
- super().init(params, point)
+ super().init(params, point, bits)
self._points = {}
current_point = point
double_point = self._dbl(point)
@@ -186,9 +186,9 @@ class FixedWindowLTRMultiplier(AccumulatorMultiplier, PrecompMultiplier, ScalarM
def __repr__(self):
return f"{self.__class__.__name__}({', '.join(map(str, self.formulas.values()))}, short_circuit={self.short_circuit}, m={self.m}, accumulation_order={self.accumulation_order.name})"
- def init(self, params: DomainParameters, point: Point):
+ def init(self, params: DomainParameters, point: Point, bits: Optional[int] = None):
with PrecomputationAction(params, point) as action:
- super().init(params, point)
+ super().init(params, point, bits)
double_point = self._dbl(point)
self._points = {1: point, 2: double_point}
current_point = double_point
@@ -298,9 +298,9 @@ class WindowBoothMultiplier(AccumulatorMultiplier, PrecompMultiplier, ScalarMult
def __repr__(self):
return f"{self.__class__.__name__}({', '.join(map(str, self.formulas.values()))}, short_circuit={self.short_circuit}, width={self.width}, precompute_negation={self.precompute_negation}, accumulation_order={self.accumulation_order.name})"
- def init(self, params: DomainParameters, point: Point):
+ def init(self, params: DomainParameters, point: Point, bits: Optional[int] = None):
with PrecomputationAction(params, point) as actions:
- super().init(params, point)
+ super().init(params, point, bits)
double_point = self._dbl(point)
self._points = {1: point, 2: double_point}
if self.precompute_negation:
@@ -323,7 +323,7 @@ class WindowBoothMultiplier(AccumulatorMultiplier, PrecompMultiplier, ScalarMult
if scalar == 0:
return action.exit(copy(self._params.curve.neutral))
scalar_booth = booth_window(
- scalar, self.width, self._params.order.bit_length()
+ scalar, self.width, self._bits
)
q = copy(self._params.curve.neutral)
for val in scalar_booth:
diff --git a/test/ec/test_countermeasures.py b/test/ec/test_countermeasures.py
index 0559d26..8a572e8 100644
--- a/test/ec/test_countermeasures.py
+++ b/test/ec/test_countermeasures.py
@@ -1,3 +1,6 @@
+from itertools import product
+from copy import copy
+
import pytest
from pyecsca.ec.countermeasures import (
@@ -6,7 +9,7 @@ from pyecsca.ec.countermeasures import (
MultiplicativeSplitting,
EuclideanSplitting,
)
-from pyecsca.ec.mult import LTRMultiplier
+from pyecsca.ec.mult import *
@pytest.fixture(params=["add-1998-cmo-2", "add-2015-rcb"])
@@ -20,81 +23,199 @@ def dbl(secp128r1, request):
@pytest.fixture()
-def mult(secp128r1, add, dbl):
- return LTRMultiplier(add, dbl, complete=False)
+def mults(secp128r1, add, dbl):
+ neg = secp128r1.curve.coordinate_model.formulas["neg"]
+ scale = secp128r1.curve.coordinate_model.formulas["z"]
+
+ ltr_options = {
+ "always": (True, False),
+ "complete": (True, False),
+ "accumulation_order": tuple(AccumulationOrder),
+ }
+ ltrs = [
+ LTRMultiplier(add, dbl, scale, **dict(zip(ltr_options.keys(), combination)))
+ for combination in product(*ltr_options.values())
+ ]
+ rtl_options = ltr_options
+ rtls = [
+ RTLMultiplier(add, dbl, scale, **dict(zip(rtl_options.keys(), combination)))
+ for combination in product(*rtl_options.values())
+ ]
+ bnaf_options = {
+ "direction": tuple(ProcessingDirection),
+ "accumulation_order": tuple(AccumulationOrder),
+ }
+ bnafs = [
+ BinaryNAFMultiplier(
+ add, dbl, neg, scale, **dict(zip(bnaf_options.keys(), combination))
+ )
+ for combination in product(*bnaf_options.values())
+ ]
+ wnaf_options = {
+ "precompute_negation": (True, False),
+ "width": (3, 5),
+ "accumulation_order": tuple(AccumulationOrder),
+ }
+ wnafs = [
+ WindowNAFMultiplier(
+ add, dbl, neg, scl=scale, **dict(zip(wnaf_options.keys(), combination))
+ )
+ for combination in product(*wnaf_options.values())
+ ]
+ booth_options = {
+ "precompute_negation": (True, False),
+ "width": (3, 5),
+ "accumulation_order": tuple(AccumulationOrder),
+ }
+ booths = [
+ WindowBoothMultiplier(
+ add, dbl, neg, scl=scale, **dict(zip(booth_options.keys(), combination))
+ )
+ for combination in product(*booth_options.values())
+ ]
+ ladder_options = {"complete": (True, False)}
+ ladders = [
+ SimpleLadderMultiplier(
+ add, dbl, scale, **dict(zip(ladder_options.keys(), combination))
+ )
+ for combination in product(*ladder_options.values())
+ ]
+ fixed_options = {"m": (5, 8), "accumulation_order": tuple(AccumulationOrder)}
+ fixeds = [
+ FixedWindowLTRMultiplier(
+ add, dbl, scl=scale, **dict(zip(fixed_options.keys(), combination))
+ )
+ for combination in product(*fixed_options.values())
+ ]
+ sliding_options = {
+ "width": (3, 5),
+ "recoding_direction": tuple(ProcessingDirection),
+ "accumulation_order": tuple(AccumulationOrder),
+ }
+ slides = [
+ SlidingWindowMultiplier(
+ add, dbl, scl=scale, **dict(zip(sliding_options.keys(), combination))
+ )
+ for combination in product(*sliding_options.values())
+ ]
+ precomp_options = {
+ "always": (True, False),
+ "complete": (True, False),
+ "direction": tuple(ProcessingDirection),
+ "accumulation_order": tuple(AccumulationOrder),
+ }
+ precomps = [
+ FullPrecompMultiplier(
+ add, dbl, scl=scale, **dict(zip(precomp_options.keys(), combination))
+ )
+ for combination in product(*precomp_options.values())
+ ]
+ bgmw_options = {
+ "width": (2, 3, 5),
+ "direction": tuple(ProcessingDirection),
+ "accumulation_order": tuple(AccumulationOrder),
+ }
+ bgmws = [
+ BGMWMultiplier(
+ add, dbl, scl=scale, **dict(zip(bgmw_options.keys(), combination))
+ )
+ for combination in product(*bgmw_options.values())
+ ]
+ comb_options = {"width": (2, 3, 4, 5), "accumulation_order": tuple(AccumulationOrder)}
+ combs = [
+ CombMultiplier(
+ add, dbl, scl=scale, **dict(zip(comb_options.keys(), combination))
+ )
+ for combination in product(*comb_options.values())
+ ]
+
+ return (
+ ltrs
+ + rtls
+ + bnafs
+ + wnafs
+ + booths
+ + [CoronMultiplier(add, dbl, scale)]
+ + ladders
+ + fixeds
+ + slides
+ + precomps
+ + bgmws
+ + combs
+ )
@pytest.mark.parametrize(
"num",
[
- 325385790209017329644351321912443757746,
- 123456789314159265358979323846264338327,
- 987654321314159265358979323846264338327,
- 786877845665557891354654531354008066400,
+ 3253857902090173296443513219124437746,
+ 1234567893141592653589793238464338327,
],
)
-def test_group_scalar_rand(mult, secp128r1, num):
+def test_group_scalar_rand(mults, secp128r1, num):
+ mult = copy(mults[0])
mult.init(secp128r1, secp128r1.generator)
raw = mult.multiply(num)
- gsr = GroupScalarRandomization(mult)
- gsr.init(secp128r1, secp128r1.generator)
- masked = gsr.multiply(num)
- assert raw.equals(masked)
+ for mult in mults:
+ gsr = GroupScalarRandomization(mult)
+ gsr.init(secp128r1, secp128r1.generator)
+ masked = gsr.multiply(num)
+ assert raw.equals(masked)
@pytest.mark.parametrize(
"num",
[
- 325385790209017329644351321912443757746,
- 123456789314159265358979323846264338327,
- 987654321314159265358979323846264338327,
- 786877845665557891354654531354008066400,
+ 3253857902090173296443513219124437746,
+ 1234567893141592653589793238464338327,
],
)
-def test_additive_splitting(mult, secp128r1, num):
+def test_additive_splitting(mults, secp128r1, num):
+ mult = copy(mults[0])
mult.init(secp128r1, secp128r1.generator)
raw = mult.multiply(num)
- asplit = AdditiveSplitting(mult)
- asplit.init(secp128r1, secp128r1.generator)
- masked = asplit.multiply(num)
- assert raw.equals(masked)
+ for mult in mults:
+ asplit = AdditiveSplitting(mult)
+ asplit.init(secp128r1, secp128r1.generator)
+ masked = asplit.multiply(num)
+ assert raw.equals(masked)
@pytest.mark.parametrize(
"num",
[
- 325385790209017329644351321912443757746,
- 123456789314159265358979323846264338327,
- 987654321314159265358979323846264338327,
- 786877845665557891354654531354008066400,
+ 3253857902090173296443513219124437746,
+ 1234567893141592653589793238464338327,
],
)
-def test_multiplicative_splitting(mult, secp128r1, num):
+def test_multiplicative_splitting(mults, secp128r1, num):
+ mult = copy(mults[0])
mult.init(secp128r1, secp128r1.generator)
raw = mult.multiply(num)
- msplit = MultiplicativeSplitting(mult)
- msplit.init(secp128r1, secp128r1.generator)
- masked = msplit.multiply(num)
- assert raw.equals(masked)
+ for mult in mults:
+ msplit = MultiplicativeSplitting(mult)
+ msplit.init(secp128r1, secp128r1.generator)
+ masked = msplit.multiply(num)
+ assert raw.equals(masked)
@pytest.mark.parametrize(
"num",
[
- 325385790209017329644351321912443757746,
- 123456789314159265358979323846264338327,
- 987654321314159265358979323846264338327,
- 786877845665557891354654531354008066400,
+ 3253857902090173296443513219124437746,
+ 1234567893141592653589793238464338327,
],
)
-def test_euclidean_splitting(mult, secp128r1, num):
+def test_euclidean_splitting(mults, secp128r1, num):
+ mult = copy(mults[0])
mult.init(secp128r1, secp128r1.generator)
raw = mult.multiply(num)
- esplit = EuclideanSplitting(mult)
- esplit.init(secp128r1, secp128r1.generator)
- masked = esplit.multiply(num)
- assert raw.equals(masked)
+ for mult in mults:
+ esplit = EuclideanSplitting(mult)
+ esplit.init(secp128r1, secp128r1.generator)
+ masked = esplit.multiply(num)
+ assert raw.equals(masked)
diff --git a/test/ec/test_mult.py b/test/ec/test_mult.py
index 721badc..e915585 100644
--- a/test/ec/test_mult.py
+++ b/test/ec/test_mult.py
@@ -361,7 +361,7 @@ def dbl(secp128r1, request):
@pytest.mark.parametrize(
- "num", [10, 2355498743, 325385790209017329644351321912443757746]
+ "num", [10, 2355498743, 3253857902090173296443513219124437746]
)
def test_basic_multipliers(secp128r1, num, add, dbl):
neg = secp128r1.curve.coordinate_model.formulas["neg"]
@@ -451,7 +451,7 @@ def test_basic_multipliers(secp128r1, num, add, dbl):
for combination in product(*precomp_options.values())
]
bgmw_options = {
- "width": (3, 5),
+ "width": (2, 3, 5),
"direction": tuple(ProcessingDirection),
"accumulation_order": tuple(AccumulationOrder),
}
@@ -461,7 +461,7 @@ def test_basic_multipliers(secp128r1, num, add, dbl):
)
for combination in product(*bgmw_options.values())
]
- comb_options = {"width": (2, 3, 5), "accumulation_order": tuple(AccumulationOrder)}
+ comb_options = {"width": (2, 3, 4, 5), "accumulation_order": tuple(AccumulationOrder)}
combs = [
CombMultiplier(
add, dbl, scl=scale, **dict(zip(comb_options.keys(), combination))