aggstam 4 лет назад
Родитель
Сommit
a3fe19f0d1

+ 66 - 0
script/research/streamlet/streamlet.py

@@ -0,0 +1,66 @@
+from tinysmpc import VirtualMachine, PrivateScalar, SharedScalar
+
+# Generating generals
+general0 = VirtualMachine('general0')
+general1 = VirtualMachine('general1')
+general2 = VirtualMachine('general2')
+general3 = VirtualMachine('general3')
+
+# Using a simple number to represent the block for testing purposes
+print('General 0 is the leader and shares block 42...')
+block = PrivateScalar(42, general0)
+shared_block = block.share([general0, general1, general2, general3])
+print(general0)
+print(general1)
+print(general2)
+print(general3)
+print()
+
+# 1 Stands for vote for, 0 for vote against
+print('Generals vote on the block...')
+general0_vote = PrivateScalar(1, general0)
+general1_vote = PrivateScalar(1, general1)
+general2_vote = PrivateScalar(1, general2)
+general3_vote = PrivateScalar(0, general3)
+shared_general0_vote = general0_vote.share([general0, general1, general2, general3])
+shared_general1_vote = general1_vote.share([general0, general1, general2, general3])
+shared_general2_vote = general2_vote.share([general0, general1, general2, general3])
+shared_general3_vote = general3_vote.share([general0, general1, general2, general3])
+print(general0)
+print(general1)
+print(general2)
+print(general3)
+print()
+
+# Each general sums votes to notarize block if votes exceed 2n/3
+print('Generals check votes...')
+votes_thresshold = (2*4)/3
+generals_votes_sum = shared_general0_vote + shared_general1_vote + shared_general2_vote + shared_general3_vote
+
+general0_votes_sum = generals_votes_sum.reconstruct(general0)
+print('General 0 votes sum: {0}'.format(general0_votes_sum.value))
+if (general0_votes_sum.value > votes_thresshold):
+    print('General 0 will notarize block')
+else:
+    print('General 0 will not notarize block')
+       
+general1_votes_sum = generals_votes_sum.reconstruct(general1)
+print('General 1 votes sum: {0}'.format(general1_votes_sum.value))
+if (general1_votes_sum.value > votes_thresshold):
+    print('General 1 will notarize block')
+else:
+    print('General 1 will not notarize block')
+
+general2_votes_sum = generals_votes_sum.reconstruct(general2)
+print('General 2 votes sum: {0}'.format(general2_votes_sum.value))
+if (general2_votes_sum.value > votes_thresshold):
+    print('General 2 will notarize block')
+else:
+    print('General 2 will not notarize block')
+
+general3_votes_sum = generals_votes_sum.reconstruct(general3)
+print('General 3 votes sum: {0}'.format(general3_votes_sum.value))
+if (general3_votes_sum.value > votes_thresshold):
+    print('General 3 will notarize block')
+else:
+    print('General 3 will not notarize block')

+ 4 - 0
script/research/streamlet/tinysmpc/__init__.py

@@ -0,0 +1,4 @@
+from .tinysmpc import VirtualMachine, PrivateScalar, SharedScalar
+
+__all__ = ['VirtualMachine', 'PrivateScalar', 'SharedScalar']
+__title__ = 'tinysmpc'

+ 53 - 0
script/research/streamlet/tinysmpc/finite_ring.py

@@ -0,0 +1,53 @@
+# This module provides useful functions for operating on integers in a finite ring.
+#
+# (Any integer that is *shared* in TinySMPC must be an element of a finite ring.
+#  By default, this is the int64 ring, but we also support modulus prime rings.)
+
+# Mathematical note:
+#
+# For additive secret sharing to work, we need all of the numbers we're working with
+# to be in a finite abelian group under addition. [1] 
+# 
+# Technically, for SMPC over additive secret sharing, we'd probably like to be able to 
+# multiply integers as well, so we're actually operating in a ring.
+# 
+# This is not a problem, because int64 is a finite ring! [2]
+# 
+# Another popular choice of a finite abelian ring is the integers modulo a prime [3], 
+# with the caveat that this doesn't support negative numbers. Thus, this implementation
+# defaults to using the int64 ring. We support prime rings as well, which are explicitly
+# used in the PrivateCompare algorithm.
+#
+# [1] 6.1 in https://cs.nyu.edu/courses/spring07/G22.3033-013/scribe/lecture01.pdf
+# [2] https://math.stackexchange.com/q/3692052/28855
+# [3] https://mortendahl.github.io/2017/09/03/the-spdz-protocol-part1/
+
+from random import randint, randrange
+
+# Anywhere in the codebase, if Q is None, that means we're computing with int64s!
+# This is the default behavior. (See the mathematical note above for why.)
+MAX_INT64 =  9223372036854775807
+MIN_INT64 = -9223372036854775808
+
+def mod(n, Q=None):
+    '''Keeps n inside the finite ring. That is:
+         - If we're in a prime ring (Q is the prime size), modulo it by Q
+         - If we're in the int64 ring, do the normal int64 overflow behavior
+           (we need to explicitly overflow since Python3 ints are unbounded)
+    '''
+    if Q is not None: return n % Q
+    return (n + MAX_INT64 + 1) % 2**64 - (MAX_INT64 + 1)  # https://stackoverflow.com/a/7771499/908744
+    
+def rand_element(Q=None):
+    '''Generates a random int64, or a random integer [0, Q) if Q is specified.
+       i.e. an element of the int64 ring, or the size-Q prime ring.'''
+    if Q is not None: return randrange(Q)
+    return randint(MIN_INT64, MAX_INT64)
+
+def assert_is_element(n, Q=None):
+    '''Assert that n is a valid int64, or a valid integer mod Q, if Q is provided.'''
+    val = n if isinstance(n, int) else n.value
+    if Q is None: 
+        assert MIN_INT64 <= val <= MAX_INT64, f'{n} is not an int64 and cannot be reconstructed. Use a smaller value.'
+    else:
+        assert 0 <= val < Q, f'{n} does not fit inside a size-{Q} prime ring, so it cannot be split into shares that can be reconstructed. Use a larger Q or a smaller value.'

+ 20 - 0
script/research/streamlet/tinysmpc/fixed_point.py

@@ -0,0 +1,20 @@
+# This module defines the conversion functions from float <> int, 
+# so that we can use floats in TinySMPC.
+
+from .finite_ring import MAX_INT64, MIN_INT64
+
+PRECISION = 8
+MAX_FLOAT = MAX_INT64 / 10**PRECISION  # 92233720368.54776 (floats must be <, not <= this value, due to precision issues)
+MIN_FLOAT = MIN_INT64 / 10**PRECISION  # -92233720368.54776 (floats must be >, not >= this value, due to precision issues)
+
+def fixed_point(fl):
+    '''Converts a float to an fixed point int, with PRECISION decimal points of precision.'''
+    assert MIN_FLOAT < fl < MAX_FLOAT
+    return int(fl * 10**PRECISION)
+
+def float_point(n, n_mults=0):
+    '''Converts a fixed point integer to a float.
+       n_mults is the number of multiplications that generated the int, since multiplications
+       of fixed point integers will accumulate extra scaling factors.'''
+    scale_factor = (10**PRECISION)**n_mults
+    return n / 10**PRECISION / scale_factor

+ 95 - 0
script/research/streamlet/tinysmpc/secret_sharing.py

@@ -0,0 +1,95 @@
+# This module defines how additive secret sharing works in TinySMPC:
+#  - how to create secret shares from a number
+#  - how to reconstruct the number from the shares
+#  - the internal Share class that represents a single secret share
+#
+# We use the simple additive secret sharing scheme that's compatible
+# with SPDZ. This is sort of a well-known "obvious" scheme, so has 
+# no canonical citation [1].
+#
+# However, you can read more about it in [2] and [3].
+#
+# [1] https://crypto.stackexchange.com/questions/68666/reference-for-additive-secret-sharing
+# [2] https://mortendahl.github.io/2017/06/04/secret-sharing-part1/
+# [3] https://cs.nyu.edu/courses/spring07/G22.3033-013/scribe/lecture01.pdf
+
+from .fixed_point import fixed_point, float_point
+from .finite_ring import assert_is_element, mod, rand_element
+
+class Share():
+    '''A class that represents a secret share that belongs to a machine.
+       It supports ring arithmetic with other Shares or integers (+, -, *).'''
+    def __init__(self, value, owner, Q=None):
+        assert_is_element(value, Q)
+        self.value = value
+        self.owner = owner
+        self.Q = Q
+        owner.objects.append(self)
+        
+    def send_to(self, owner):
+        '''Send a copy of a Share to a different owner/machine.'''
+        return Share(self.value, owner, self.Q)
+    
+    def __add__(self, other):
+        '''Called by: self + other.'''
+        self._assert_can_operate(other)
+        other_value = other if isinstance(other, int) else other.value 
+        sum_value = mod(self.value + other_value, self.Q)
+        return Share(sum_value, self.owner, self.Q)
+    
+    def __radd__(self, other):
+        '''Called by: other + self (when other is not a Share).'''
+        return self.__add__(other)
+    
+    def __sub__(self, other):
+        '''Called by: self - other.'''
+        return self.__add__(-1*other)
+    
+    def __rsub__(self, other):
+        '''Called by: other - self (when other is not a Share).'''
+        return (-1*self).__add__(other)
+    
+    def __mul__(self, other):
+        '''Called by: self * other.'''
+        self._assert_can_operate(other)
+        other_value = other if isinstance(other, int) else other.value
+        prod_value = mod(self.value * other_value, self.Q)
+        return Share(prod_value, self.owner, self.Q)
+    
+    def __rmul__(self, other):
+        '''Called by: other * self (when other is not a Share).'''
+        return self.__mul__(other)
+
+    def __repr__(self):
+        return f'Share({self.value}, \'{self.owner.name}\', Q={self.Q})'
+    
+    def _assert_can_operate(self, other):
+        '''Assert that two Shares have the same owners and rings.'''
+        if isinstance(other, int): return  # It's okay to do operations with any public integers
+        assert self.owner == other.owner, f'{self} and {other} do not have the same owners.'
+        assert self.Q == other.Q, f'{self} and {other} are not over the same rings.'
+
+def n_to_shares(n, owners, Q=None):  
+    '''Create additive secret Shares for an integer n, split across a group of machines.'''
+    # Make sure there are no duplicate owners (technically this is okay, but let's keep it simple)
+    assert len(owners) == len(set(owners))
+
+    # Make sure the number actually fits into the finite ring, so we can reconstruct it!
+    assert_is_element(n, Q)
+
+    # Generate the value of each secret share using additive secret sharing
+    values = [rand_element(Q) for _ in owners[:-1]]
+    values.append(mod(n - sum(values), Q))
+    
+    # Give one secret Share to each machine
+    shares = [Share(value, owner, Q) for value, owner in zip(values, owners)]
+    
+    return shares
+
+def n_from_shares(shares, owner, Q=None):
+    '''Given a list of additive secret Shares, reconstruct the integer value they're hiding.'''
+    # First, move all shares onto one machine
+    local_shares = [share.send_to(owner) for share in shares]
+    
+    # Now, reconstruct the original value (we just add the shares!)
+    return sum(local_shares).value

+ 31 - 0
script/research/streamlet/tinysmpc/shared_addition.py

@@ -0,0 +1,31 @@
+# This module defines addition on SharedScalars, using the SPDZ algorithm 
+# for addition [1].
+#
+# Technically, this method is extremely simple as it follows directly 
+# from additive secret sharing, and likely predates SPDZ.
+#
+# [1] "Computations" on pg 6 of https://eprint.iacr.org/2011/535.pdf
+
+# Small hack:
+# 
+# We can't import the SharedScalar class in this module as that would
+# create a circular dependency. 
+# 
+# However, we'd obviously still like to be able to construct new 
+# SharedScalars here when doing arithmetic. To be able to do so, 
+# we can use `type(sh)` to get access to the SharedScalar class &
+# constructor.
+
+def add_2sh(sh1, sh2):
+    '''Implements addition on two SharedScalars.'''
+    # To do the addition, we add each machine's shares together
+    sh1._assert_can_operate(sh2)
+    sum_shares = [sh1.share_of[owner] + sh2.share_of[owner]
+                  for owner in sh1.owners]
+    return type(sh1)(sum_shares, Q=sh1.Q)
+
+def add_sh_pub(sh, pub):
+    '''Implements addition on a SharedScalar and a public integer.'''
+    # To do the addition, we add the integer to one (random) share only
+    new_shares = [sh.shares[0] + pub] + sh.shares[1:]
+    return type(sh)(new_shares, sh.Q)

+ 148 - 0
script/research/streamlet/tinysmpc/shared_comparison.py

@@ -0,0 +1,148 @@
+# This module defines comparison between a SharedScalar and public integer,
+# using the PrivateCompare algorithm in SecureNN [1]. 
+#
+# The notation used here is as close to the paper's as possible.
+#
+# [1] Algorithm 3 in https://eprint.iacr.org/2018/442.pdf
+
+# Security note:
+#
+# The PrivateCompare algorithm [1] requires a bitwise share representation.
+# However, this is not the share representation of SharedScalars, so we use
+# the workaround of reconstructing the private value on a temporarily created
+# VirtualMachine, and then resharing with the bitwise representation.
+# 
+# Technically speaking, this isn't really secure. However, it's still useful
+# for educational purposes, and enables a nice high-level API like `x > 10`,
+# where x is any normal SharedScalar (even the output of an arithmetic op). 
+# 
+# I'd like to implement a better solution, eventually. Here are the options:
+#   1) Be able to convert from SharedScalar's Shares -> bitwise Shares directly.
+#      ^I don't know if this is possible.
+#   2) Make SharedScalar have two share representations. The normal/current one,
+#      and a bitwise one. And update all arithmetic operations to support the 
+#      bitwise sharing scheme.
+#      ^This would add too much complexity.
+#
+# Alternatively, you can also directly use _share_bitwise() and _private_compare()
+# from this module on unshared integers to generate fresh bitwise shares.
+
+# Small hack:
+#
+# In the other shared_* modules, we use the `type(sh)` hack. However,
+# PrivateCompare requires fairly heavy operations on Shares, SharedScalars, 
+# etc, so we instead import these classes at function runtime.
+# 
+# Personally, I don't like this style, but it's the price to pay for modularity.
+# (Dependency-wise, these functions should really be part of tinysmpc.py, 
+#  but it's so much cleaner to split them out.)
+
+from .finite_ring import MIN_INT64
+from .secret_sharing import Share
+from random import random, randint, shuffle
+
+P = 67  # Smaller prime field size to encode bit values
+L = 64  # Number of bits of the integers we're using
+
+def greater_than(x_sh, pub):
+    '''Provides the high-level API for comparing x_sh (SharedScalar) > pub (int).
+       This basically does some TinySMPC-specific setup before calling PrivateCompare.'''
+    assert len(x_sh.owners) == 2, 'PrivateCompare only works for 2-party shares'
+    
+    # Reconstruct the private value on a temporary VM (see the Security Note above)
+    from .tinysmpc import VirtualMachine
+    tmp_vm = VirtualMachine('tmp_vm')
+    x = x_sh.reconstruct(tmp_vm).value
+    
+    # The paper's implementation only works on positive numbers, but we want negatives too!
+    # So, just shift TinySMPC's int64s into the positive range (int64 + -MIN_INT64).
+    if pub < 0 or x < 0: pub += -MIN_INT64; x += -MIN_INT64
+    
+    # Decompose x into its bit representation, and share each bit independently
+    x_sh = _share_bitwise(x, list(x_sh.owners))
+    
+    return _private_compare(x_sh, pub)
+
+def _private_compare(x_sh, r, β=None):
+    '''Compares x_sh > r, where x_sh is bitwise shared and r is a public integer.
+       Returns 0 or 1 as a PrivateScalar on a temporary VirtualMachine.
+       This is the PrivateCompare algorithm in [1].'''
+    # A necessary evil; see the "small hack" note above
+    from .tinysmpc import PrivateScalar, SharedScalar, VirtualMachine
+
+    # Decompose r into its bit representation (public)
+    rb = _get_bits(r)
+    
+    # Common randomness (public)
+    β = randint(0, 1) if β is None else β
+    s = _randlist()
+    u = _randlist()
+    π = _fixed_shuffle()
+
+    # Line 1
+    t = (r + 1) % 2**L
+    tb = _get_bits(t)
+    
+    # Line 2
+    p0, p1 = tuple(x_sh[0].owners)
+    w_c = {p0: {'w': [None] * L, 'c': [None] * L}, 
+           p1: {'w': [None] * L, 'c': [None] * L}}
+    for j, machine in enumerate([p0, p1]):  
+        w, c = w_c[machine]['w'], w_c[machine]['c']
+        
+        # Line 3
+        for i in range(L-1, -1, -1):
+            sh = x_sh[i].share_of[machine]
+            
+            # Line 4
+            if β == 0:
+                w[i] = sh + j*rb[i] - 2*rb[i]*sh
+                c[i] = j*rb[i] - sh + j + sum(w[i+1:])
+
+            # Line 7
+            elif (β == 1) and (r != 2**L - 1):
+                w[i] = sh + j*tb[i] - 2*tb[i]*sh
+                c[i] = -1*j*tb[i] + sh + j + sum(w[i+1:])
+
+            # Line 10
+            else:  
+                if i != 1:  c_val = ((1 - j)*(u[i] + 1) - j*u[i]) % P
+                else: c_val = ((-1)**j * u[i]) % P
+                c[i] = Share(c_val, machine, Q=P)
+
+    # Line 14
+    d_p0 = [s[i] * w_c[p0]['c'][i] for i in range(L)]
+    d_p1 = [s[i] * w_c[p1]['c'][i] for i in range(L)]
+    π(d_p0); π(d_p1)
+    d_shared = [SharedScalar([d0, d1], Q=P) for d0, d1 in zip(d_p0, d_p1)]
+    
+    # Line 15
+    p2 = VirtualMachine('p2')
+    d = [d_sh.reconstruct(p2) for d_sh in d_shared]
+    β_prime = any(ps.value == 0 for ps in d)  # (we break the abstraction of only operating on PrivateScalars a bit)
+        
+    # Return x > r
+    return PrivateScalar(β ^ β_prime, p2)    
+    
+def _share_bitwise(n, machines):
+    '''Split integer n into bitwise secret shares, returns a list of SharedScalars (one per bit).'''
+    from .tinysmpc import PrivateScalar
+    bits = _get_bits(n)
+    ps_bits = [PrivateScalar(bit, machines[0]) for bit in bits]
+    sh_bits = [ps_bit.share(machines, P) for ps_bit in ps_bits]
+    return sh_bits
+
+def _get_bits(n):
+    '''Returns the (reverse) binary representation of n as an L-sized list.'''
+    bits = bin(n).replace('0b', '')
+    bits = '0' * (L - len(bits)) + bits
+    return list(map(int, reversed(bits)))  # FYI: the paper requires reversed binary, but doesn't say this!
+
+def _randlist():
+    '''Returns a list of L random integers in [1, P-1].'''
+    return [randint(1, P-1) for _ in range(L)]
+
+def _fixed_shuffle():
+    '''Returns a deterministic shuffle function that always permutes a list in the same way.'''
+    seed = random()
+    return lambda x: shuffle(x, lambda: seed)

+ 54 - 0
script/research/streamlet/tinysmpc/shared_multiplication.py

@@ -0,0 +1,54 @@
+# This module defines multiplication on SharedScalars, using the SPDZ 
+# algorithm for multiplication [1].
+#
+# [1] https://bristolcrypto.blogspot.com/2016/10/what-is-spdz-part-2-circuit-evaluation.html
+
+# Small hack:
+# 
+# We can't import the SharedScalar class in this module as that would
+# create a circular dependency. 
+# 
+# However, we'd obviously still like to be able to construct new 
+# SharedScalars here when doing arithmetic. To be able to do so, 
+# we can use `type(sh)` to get access to the SharedScalar class &
+# constructor.
+
+from .finite_ring import mod, rand_element
+from .secret_sharing import n_to_shares
+from random import choice
+
+def mult_2sh(sh1, sh2):
+    '''Implements multiplication on two SharedScalars.'''
+    # Make sure that these two SharedScalars are compatible 
+    sh1._assert_can_operate(sh2)
+    
+    # Generate a random multiplication triple (public)
+    a, b = rand_element(sh1.Q), rand_element(sh1.Q)
+    c = mod(a * b, sh1.Q)
+
+    # Share the triple across all machines
+    # (It'd be nicer to use the higher-level PrivateScalar.share() here, 
+    # but we don't have access to PrivateScalar in this module.)
+    machines = list(sh1.owners)
+    shared_a = type(sh1)(n_to_shares(a, machines, sh1.Q), sh1.Q)
+    shared_b = type(sh1)(n_to_shares(b, machines, sh1.Q), sh1.Q)
+    shared_c = type(sh1)(n_to_shares(c, machines, sh1.Q), sh1.Q)
+
+    # Compute sh1 - a, sh2 - b (shared)
+    shared_sh1_m_a = sh1 - shared_a
+    shared_sh2_m_b = sh2 - shared_b
+
+    # Reconstruct sh1 - a, sh2 - b (public)
+    rand_machine = choice(machines)
+    sh1_m_a = shared_sh1_m_a.reconstruct(rand_machine).value
+    sh2_m_b = shared_sh2_m_b.reconstruct(rand_machine).value
+
+    # Magic! Compute each machine's share of the product
+    shared_prod = shared_c + (sh1_m_a * shared_b) + (sh2_m_b * shared_a) + (sh1_m_a * sh2_m_b)
+    return shared_prod
+
+def mult_sh_pub(sh, pub):
+    '''Implements multiplication on a SharedScalar and a public integer.'''
+    # To do the multiplication, we multiply the integer with all shares
+    prod_shares = [share * pub for share in sh.shares]
+    return type(sh)(prod_shares, Q=sh.Q)

+ 97 - 0
script/research/streamlet/tinysmpc/tinysmpc.py

@@ -0,0 +1,97 @@
+# This is TinySMPC's top-level module that defines its user-facing API:
+# the three classes VirtualMachine, PrivateScalar, and SharedScalar.
+#
+# For modularity, almost all of the behavior of these classes is implemented 
+# in functions imported from the other files here. Check them out!
+
+from .finite_ring import assert_is_element, mod, rand_element
+from .secret_sharing import n_from_shares, n_to_shares
+from .shared_addition import add_2sh, add_sh_pub
+from .shared_comparison import greater_than
+from .shared_multiplication import mult_2sh, mult_sh_pub
+
+class VirtualMachine():
+    '''A very simple class that represents a machine's data. 
+       It just has a name and owns objects (PrivateScalars and Shares).'''
+    def __init__(self, name):
+        self.name = name
+        self.objects = []
+    
+    def __repr__(self):
+        return f'VirtualMachine(\'{self.name}\')\n - ' + '\n - '.join(map(str, self.objects))
+
+class PrivateScalar():
+    '''A class that represents a secret number that belongs to a machine.'''
+    def __init__(self, value, owner):
+        self.value = value
+        self.owner = owner
+        owner.objects.append(self)
+
+    def share(self, machines, Q=None):
+        '''Split self.value into secret shares and distribute them across machines (tracked in a SharedScalar).'''
+        shares = n_to_shares(self.value, machines, Q)
+        return SharedScalar(shares, Q)
+    
+    def __repr__(self):
+        return f'PrivateScalar({self.value}, \'{self.owner.name}\')'
+    
+class SharedScalar():
+    '''A class that tracks all secret shares that corresponds to one PrivateScalar.
+       It supports *secure* arithmetic with other SharedScalars or integers (+, -, *).'''
+    def __init__(self, shares, Q=None):
+        assert all(share.Q == Q for share in shares)
+        self.shares = shares
+        self.share_of = {share.owner: share for share in shares}
+        self.owners = {share.owner for share in shares}
+        self.Q = Q
+        
+    def reconstruct(self, owner):
+        '''Send all shares to one machine, and reconstruct the hidden value as a PrivateScalar.'''
+        value = n_from_shares(self.shares, owner, self.Q)
+        return PrivateScalar(value, owner)
+        
+    def __add__(self, other):
+        '''Called by: self + other.'''
+        if isinstance(other, int):            return add_sh_pub(self, other)
+        elif isinstance(other, SharedScalar): return add_2sh(self, other)
+        
+    def __radd__(self, other):
+        '''Called by: other + self (when other is not a SharedScalar).'''
+        return self.__add__(other)
+    
+    def __sub__(self, other):
+        '''Called by: self - other.'''
+        return self.__add__(-1*other)
+    
+    def __rsub__(self, other):
+        '''Called by: other - self (when other is not a SharedScalar).'''
+        return (-1*self).__add__(other)
+    
+    def __mul__(self, other):
+        '''Called by: self * other.'''
+        if isinstance(other, int):            return mult_sh_pub(self, other)
+        elif isinstance(other, SharedScalar): return mult_2sh(self, other)
+            
+    def __rmul__(self, other):
+        '''Called by: other * self (when other is not a SharedScalar).'''
+        return self.__mul__(other)
+    
+    def __pow__(self, other):
+        '''Called by: self ** other. Only implemented when other is a public integer > 0.'''
+        assert isinstance(other, int) and other > 0
+        res = self
+        for _ in range(other-1): res *= self
+        return res
+    
+    def __gt__(self, other):
+        '''Called by: self > other. Only implemented when other is a public integer.'''
+        assert isinstance(other, int)
+        return greater_than(self, other)
+    
+    def __repr__(self):
+        return 'SharedScalar\n - ' + '\n - '.join(map(str, self.shares))
+    
+    def _assert_can_operate(self, other):
+        '''Assert that two SharedScalars have the same owners and rings.'''
+        assert self.owners == other.owners, f'{self}\nand\n{other}\ndo not have the same owners.'
+        assert self.Q == other.Q, f'{self}\nand\n{other}\nare not over the same rings.'