Example #1
0
    def test_make_proof(self):
        wrapper = Mixer(NATIVE_LIB_PATH, VK_PATH, PK_PATH)
        tree_depth = wrapper.tree_depth

        n_items = 2 << (tree_depth - 1)
        tree = MerkleTree(n_items)
        for n in range(0, 2):
            tree.append(int(FQ.random()))

        wallet_address = int(FQ.random())
        nullifier_secret = int(FQ.random())
        nullifier_hash = mimc_hash([nullifier_secret, nullifier_secret])
        leaf_hash = int(
            get_sha256_hash(to_hex(nullifier_secret), to_hex(wallet_address)),
            16)

        leaf_idx = tree.append(leaf_hash)
        self.assertEqual(leaf_idx, tree.index(leaf_hash))

        # Verify it exists in true
        leaf_proof = tree.proof(leaf_idx)
        self.assertTrue(leaf_proof.verify(tree.root))

        # Generate proof
        snark_proof = wrapper.prove(
            tree.root,
            wallet_address,
            nullifier_hash,
            nullifier_secret,
            # (index)_2 bits reversed, i.e. [LSB, ... , MSB]
            leaf_proof.address,
            leaf_proof.path)

        self.assertTrue(wrapper.verify(snark_proof))
Example #2
0
    def test_make_proof(self):
        n_items = 2 << 28
        tree = MerkleTree(n_items)
        for n in range(0, 2):
            tree.append(int(FQ.random()))

            exthash = int(FQ.random())
            prehash = int(FQ.random())
            secret = int(FQ.random())
            msg = int(FQ.random())
            leaf_hash = mimc_hash([secret])
            sig_msg_hash = mimc_hash([msg])
            leaf_idx = tree.append(leaf_hash)
            self.assertEqual(leaf_idx, tree.index(leaf_hash))

        # Verify it exists in true
        leaf_proof = tree.proof(leaf_idx)
        self.assertTrue(leaf_proof.verify(tree.root))
        self.assertTrue(prehash, sig_msg_hash)

        # Generate proof
        wrapper = Ssles(NATIVE_LIB_PATH, VK_PATH, PK_PATH)
        tree_depth = wrapper.tree_depth
        snark_proof = wrapper.prove(tree.root, secret, msg, exthash, prehash,
                                    leaf_proof.address, leaf_proof.path)

        self.assertTrue(wrapper.verify(snark_proof))

        if __name__ == "__main__":
            unittest.main()
	def test_make_proof(self):
		n_items = 2<<28
		tree = MerkleTree(n_items)
		for n in range(0, 2):
			tree.append(int(FQ.random()))

		exthash = int(FQ.random())
		secret = int(FQ.random())
		leaf_hash = mimc_hash([secret])
		leaf_idx = tree.append(leaf_hash)
		self.assertEqual(leaf_idx, tree.index(leaf_hash))

		# Verify it exists in true
		leaf_proof = tree.proof(leaf_idx)
		self.assertTrue(leaf_proof.verify(tree.root))

		# Generate proof		
		wrapper = Miximus(NATIVE_LIB_PATH, VK_PATH, PK_PATH)
		tree_depth = wrapper.tree_depth
		snark_proof = wrapper.prove(
			tree.root,
			secret,
			exthash,
			leaf_proof.address,
			leaf_proof.path)

		self.assertTrue(wrapper.verify(snark_proof))
Example #4
0
    def test_update(self):
        """
        Verify that items in the tree can be updated
        """
        tree = MerkleTree(2)
        tree.append(FQ.random())
        tree.append(FQ.random())
        proof_0_before = tree.proof(0)
        proof_1_before = tree.proof(1)
        root_before = tree.root
        self.assertTrue(proof_0_before.verify(tree.root))
        self.assertTrue(proof_1_before.verify(tree.root))

        leaf_0_after = FQ.random()
        tree.update(0, leaf_0_after)
        root_after_0 = tree.root
        proof_0_after = tree.proof(0)
        self.assertTrue(proof_0_after.verify(tree.root))
        self.assertNotEqual(root_before, root_after_0)

        leaf_1_after = FQ.random()
        tree.update(1, leaf_1_after)
        root_after_1 = tree.root
        proof_1_after = tree.proof(1)
        self.assertTrue(proof_1_after.verify(tree.root))
        self.assertNotEqual(root_before, root_after_1)
        self.assertNotEqual(root_after_0, root_after_1)
Example #5
0
	def test_6_recover_y(self):
		"""
		There are two y points for every x
		"""
		for _ in range(0, 10):
			p = self._point_r()
			q = Point.from_x(p.x)
			self.assertEqual(p.x, q.x)
			self.assertTrue(p.y in [q.y, -q.y])

		# These confirm compatibility across implementations
		known_test_cases = [
			(20616554786359396897066290204264220576319536076538991133935783866206841138898,
			 10592275084648178561464128859907688344447649297734555224341876545305639835999),

			(11610117029953798428826613242669939481045605849364609771767823351326159443609,
			 3722409228507723418678713896319610332389736117851027921973860155000856891140),

			(21680045038775759642189425577922609025982451102460978847266452551495203884482,
			 6168854640927408084732268325506202000962285527703379133980054444068219727690),

			(18879782252170350866370777185563748782908354718484814019474117245310535071541,
			 2946855428411022359321514310392164228862398839132752152798293872913224129374)
		]
		for x, y in known_test_cases:
			x, y = FQ(x), FQ(y)
			q = Point.from_y(y)
			self.assertEqual(q.x, x)
Example #6
0
 def test_random(self):
     # Randomized tests
     for _ in range(0, 10):
         alpha = [FQ.random() for _ in range(0, 4)]
         points = [(FQ(i), shamirs_poly(FQ(i), alpha))
                   for i in range(0, len(alpha))]
         assert alpha[0] == lagrange(points, 0)
         assert alpha[0] != lagrange(points[1:], 0)
         assert alpha[0] != lagrange(points[2:], 0)
Example #7
0
	def test_multiplicative(self):
		G = self._point_r()
		a = FQ.random()
		A = G*a
		b = FQ.random()
		B = G*b

		ab = (a.n * b.n) % JUBJUB_E
		AB = G*ab
		self.assertEqual(A*b, AB)
		self.assertEqual(B*a, AB)
Example #8
0
 def __init__(self, var, coeff=None):
     assert isinstance(var, Variable)
     self.var = var
     if coeff is None:
         coeff = FQ(1)
     elif isinstance(coeff, int_types):
         coeff = FQ(coeff)
     if not isinstance(coeff, FQ):
         raise TypeError(
             'Coefficient expected to be field element, but got %r' %
             (type(coeff), ))
     self.coeff = coeff
Example #9
0
 def message(self):
     msg_parts = [
         FQ(int(self.realmID), 1 << 32),
         FQ(int(self.accountID), 1 << 20),
         FQ(int(self.tokenID), 1 << 8),
         FQ(int(self.amountRequested), 1 << 96),
         FQ(int(self.walletAccountID), 1 << 20),
         FQ(int(self.feeTokenID), 1 << 8),
         FQ(int(self.fee), 1 << 96),
         FQ(int(self.walletSplitPercentage), 1 << 7),
         FQ(int(self.nonce), 1 << 32),
         FQ(int(0), 1 << 1)
     ]
     return PureEdDSA.to_bits(*msg_parts)
Example #10
0
 def message(self):
     msg_parts = [
         FQ(int(self.realmID), 1 << 32),
         FQ(int(self.accountID), 1 << 20),
         FQ(int(self.orderTokenID), 1 << 8),
         FQ(int(self.orderID), 1 << 32),
         FQ(int(self.dualAuthorAccountID), 1 << 20),
         FQ(int(self.feeTokenID), 1 << 8),
         FQ(int(self.fee), 1 << 96),
         FQ(int(self.walletSplitPercentage), 1 << 7),
         FQ(int(self.nonce), 1 << 32),
         FQ(int(0), 1 << 2)
     ]
     return PureEdDSA.to_bits(*msg_parts)
Example #11
0
    def test_random_small(self):
        q = 100003
        for _ in range(0, 10):
            alpha = [FQ.random(q) for _ in range(0, 4)]
            points = [(FQ(i, q), shamirs_poly(FQ(i, q), alpha))
                      for i in range(0, len(alpha))]
            assert alpha[0] == lagrange(points, 0)
            assert alpha[0] != lagrange(points[1:], 0)
            assert alpha[0] != lagrange(points[2:], 0)

            # XXX: scipy's lagrange has floating point precision for large numbers
            points_x, points_y = unzip(points)
            interpolation = scipy_lagrange([_.n for _ in points_x], [_.n for _ in points_y])
            assert int(interpolation.c[-1]) == alpha[0]
Example #12
0
 def test_mult_all_known(self):
     rp = self._point_a()
     all_points = [rp, rp.as_proj(), rp.as_etec(), rp.as_mont()]
     expected = Point(
         FQ(6317123931401941284657971611369077243307682877199795030160588338302336995127
            ),
         FQ(17705894757276775630165779951991641206660307982595100429224895554788146104270
            ))
     for p in all_points:
         q = p.mult(
             6890855772600357754907169075114257697580319025794532037257385534741338397365
         )
         r = q.as_point()
         self.assertEqual(r.x, expected.x)
         self.assertEqual(r.y, expected.y)
Example #13
0
 def test_fromdocs2(self):
     p = 100003
     k = 4
     a = [FQ.random(p) for _ in range(0, k)] # [6257, 85026, 44499, 14701]
     F = lambda i, x: a[i] * (x**i)
     X = lambda x: a[0] + F(1, x) + F(2, x) + F(3, x)
     # Create the shares
     Sx = range(1, 5)
     Sy = [X(_) for _ in Sx]
     for x, y in zip(Sx, Sy):
         z = shamirs_poly(FQ(x, p), a)
         assert z == y
     # Then recover secret
     result = int(scipy_lagrange(Sx, [_.n for _ in Sy]).c[-1]) % p
     assert a[0] == result
Example #14
0
def orderFromJSON(jOrder, state):
    realmID = int(jOrder["realmID"])
    orderID = int(jOrder["orderID"])
    accountID = int(jOrder["accountID"])
    walletAccountID = int(jOrder["walletAccountID"])
    dualAuthPublicKeyX = int(jOrder["dualAuthPublicKeyX"])
    dualAuthPublicKeyY = int(jOrder["dualAuthPublicKeyY"])
    dualAuthSecretKey = int(jOrder["dualAuthSecretKey"])
    tokenS = int(jOrder["tokenIdS"])
    tokenB = int(jOrder["tokenIdB"])
    tokenF = int(jOrder["tokenIdF"])
    amountS = int(jOrder["amountS"])
    amountB = int(jOrder["amountB"])
    amountF = int(jOrder["amountF"])
    allOrNone = int(jOrder["allOrNone"])
    validSince = int(jOrder["validSince"])
    validUntil = int(jOrder["validUntil"])
    walletSplitPercentage = int(jOrder["walletSplitPercentage"])
    waiveFeePercentage = int(jOrder["waiveFeePercentage"])

    account = state.getAccount(accountID)
    walletAccount = state.getAccount(walletAccountID)

    order = Order(Point(account.publicKeyX, account.publicKeyY),
                  Point(walletAccount.publicKeyX, walletAccount.publicKeyY),
                  Point(dualAuthPublicKeyX,
                        dualAuthPublicKeyY), dualAuthSecretKey, realmID,
                  orderID, accountID, walletAccountID, tokenS, tokenB, tokenF,
                  amountS, amountB, amountF, allOrNone, validSince, validUntil,
                  walletSplitPercentage, waiveFeePercentage)

    order.sign(FQ(int(account.secretKey)))

    return order
Example #15
0
    def offchainWithdraw(self, realmID, accountID, tokenID, amountRequested,
                         operatorAccountID, walletAccountID, feeTokenID, fee,
                         walletSplitPercentage):

        feeToWallet = int(fee) * walletSplitPercentage // 100
        feeToOperator = int(fee) - feeToWallet

        # Update account
        rootBefore = self._accountsTree._root
        accountBefore = copyAccountInfo(self.getAccount(accountID))
        nonce = accountBefore.nonce
        proof = self._accountsTree.createProof(accountID)

        balanceUpdateF_A = self.getAccount(accountID).updateBalance(
            feeTokenID, -fee)

        balance = int(self.getAccount(accountID).getBalance(tokenID))
        amountWithdrawn = int(amountRequested) if (
            int(amountRequested) < balance) else balance

        balanceUpdateW_A = self.getAccount(accountID).updateBalance(
            tokenID, -amountWithdrawn)
        self.getAccount(accountID).nonce += 1

        self.updateAccountTree(accountID)
        accountAfter = copyAccountInfo(self.getAccount(accountID))
        rootAfter = self._accountsTree._root
        accountUpdate_A = AccountUpdateData(accountID, proof, rootBefore,
                                            rootAfter, accountBefore,
                                            accountAfter)
        ###

        # Update wallet
        rootBefore = self._accountsTree._root
        accountBefore = copyAccountInfo(self.getAccount(walletAccountID))
        proof = self._accountsTree.createProof(walletAccountID)

        balanceUpdateF_W = self.getAccount(walletAccountID).updateBalance(
            feeTokenID, feeToWallet)

        self.updateAccountTree(walletAccountID)
        accountAfter = copyAccountInfo(self.getAccount(walletAccountID))
        rootAfter = self._accountsTree._root
        accountUpdate_W = AccountUpdateData(walletAccountID, proof, rootBefore,
                                            rootAfter, accountBefore,
                                            accountAfter)
        ###

        # Operator payment
        balanceUpdateF_O = self.getAccount(operatorAccountID).updateBalance(
            feeTokenID, feeToOperator)

        account = self.getAccount(accountID)
        withdrawal = OffchainWithdrawal(
            realmID, accountID, tokenID, amountRequested, amountWithdrawn,
            walletAccountID, feeTokenID, fee, walletSplitPercentage,
            balanceUpdateF_A, balanceUpdateW_A, accountUpdate_A,
            balanceUpdateF_W, accountUpdate_W, balanceUpdateF_O, nonce)
        withdrawal.sign(FQ(int(account.secretKey)))
        return withdrawal
Example #16
0
    def __init__(self):
        """
		The state holds all variables and linear combinations used by the program

			- Variables hold a single field element
			- Linear combinations hold a combination of variables

		Linear combinations can be used to combine multiple operations on variables
		into a single statement, whenever a variable is multiplied by a constant
		or two variables (optionally multipled by constants) are added together etc.

		This forms the basis of many optimisations, think of linear combinations as
		temporary variables, where intermediate results which don't require a constraint
		of their own can be calculated, storedĀ and used in the same way as normal variables.

		Each variable or linear combination is addressed by an index, an index can only be
		a linear combination *or* a variable, but none will have the same index as another.

		When writing a function it can accept zero or more inputs, and emit zero or more
		outputs. For example, an `assert` statement takes one or more inputs and emits none,
		an `add` statement takes two or more inputs and emits one output. The inputs can be
		any combination of variables or linear combinations, as can the outputs.

		If a linear combinationĀ or variable is unused by any constraints then it has no purpose.
		"""
        self._vars = OrderedDict()
        self._lcs = dict()
        self._values = dict()
        self.var_new('ONE', value=FQ(1))
Example #17
0
 def message(self):
     msg_parts = [
         FQ(int(self.orderA.hash), 1 << 254),
         FQ(int(self.orderB.hash), 1 << 254),
         FQ(int(self.orderA.waiveFeePercentage), 1 << 7),
         FQ(int(self.orderB.waiveFeePercentage), 1 << 7),
         FQ(int(self.minerAccountID), 1 << 20),
         FQ(int(self.tokenID), 1 << 8),
         FQ(int(self.fee), 1 << 96),
         FQ(int(self.feeRecipientAccountID), 1 << 20),
         FQ(int(self.nonce), 1 << 32)
     ]
     return PureEdDSA.to_bits(*msg_parts)
Example #18
0
	def set_value(self, idx, value):
		if not isinstance(value, FQ):
			if not isinstance(value, int_types):
				raise ProgramError("Value (%r=%r) is of wrong type: %r" % (idx, value, type(value)))
			value = FQ(value)
		if idx not in self.inputs and idx not in self.secrets:
			raise ProgramError("Cannot set a value (%r=%r) that's neither an input nor a secret" % (idx, value))
		self.state.var_value_set(idx, value)
Example #19
0
	def test_mont_double(self):
		"""
		Verified in Sage, using `ejubjub.py`
		Ensure that addition laws remain the same between Montgomery and Edwards coordinates
		"""
		q = Point.from_hash(b'x')
		mq = MontPoint(FQ(4828722366376575650251607168518886976429844446767098803596167689250506416759),
					   FQ(12919092401030192644826086113396919334232812611316996694878363256143428656958))
		self.assertEqual(q.as_mont(), mq)

		q2 = MontPoint(FQ(760569539648116659146730905587051427168718890872716379895718021693339839266),
					   FQ(19523163946365579499783218718995636854804792079073783994015125253921919723342))
		self.assertEqual(q.double().as_mont(), q2)

		for _ in range(0, 10):
			p = Point.from_hash(urandom(32))
			self.assertEqual(p.as_mont().double().as_edwards_yz().as_point().as_edwards_yz(), p.double().as_edwards_yz())
def loopring_sign(input_message, private_key):
    print(f"loopring sign message {input_message}")
    hasher = hashlib.sha256()
    hasher.update(input_message.encode('utf-8'))
    msgHash = int(hasher.hexdigest(), 16) % SNARK_SCALAR_FIELD
    signed = PoseidonEdDSA.sign(msgHash, FQ(int(private_key)))
    signature = ','.join(
        str(_) for _ in [signed.sig.R.x, signed.sig.R.y, signed.sig.s])
    return signature
Example #21
0
def ringFromJSON(jRing, state):
    orderA = orderFromJSON(jRing["orderA"], state)
    orderB = orderFromJSON(jRing["orderB"], state)
    minerAccountID = int(jRing["minerAccountID"])
    feeRecipientAccountID = int(jRing["feeRecipientAccountID"])
    tokenID = int(jRing["tokenID"])
    fee = int(jRing["fee"])

    minerAccount = state.getAccount(minerAccountID)

    ring = Ring(orderA, orderB, minerAccountID, feeRecipientAccountID, tokenID,
                fee, minerAccount.nonce)

    ring.sign(FQ(int(minerAccount.secretKey)),
              FQ(int(orderA.dualAuthSecretKey)),
              FQ(int(orderB.dualAuthSecretKey)))

    return ring
Example #22
0
    def test_make_proof(self):
        num_blocks = 32

        wrapper = Contingent(NATIVE_LIB_PATH, num_blocks)

        # Generate proving and verification keys
        result = wrapper.genkeys(PK_PATH, VK_PATH)
        self.assertTrue(result == 0)

        print('Keygen done!')

        plaintext = [int(FQ.random()) for n in range(0, num_blocks)]
        plaintext_root = merkle_root(plaintext)
        key = int(FQ.random())
        key_bytes = key.to_bytes(32, 'little')
        sha256 = hashlib.sha256()
        sha256.update(key_bytes)
        key_hash = sha256.digest()
        ciphertext = mimc_encrypt(plaintext, key)

        print('Number of blocks = {}'.format(num_blocks))
        print('Plaintext = {}'.format(plaintext))
        print('Plaintext Root = {}'.format(plaintext_root))
        print('Key = {}'.format(key))
        print('Key Hex = {}'.format(key_bytes.hex()))
        print('Key Hash = {}'.format(key_hash.hex()))
        print('Ciphertext = {}'.format(ciphertext))

        # Generate proof
        proof = wrapper.prove(PK_PATH, key_hash, ciphertext, plaintext_root,
                              key, plaintext)

        with open(PROOF_PATH, 'w') as f:
            f.write(proof)

        print('Prove done!')

        # Verify proof
        self.assertTrue(
            wrapper.verify(VK_PATH, proof, key_hash, ciphertext,
                           plaintext_root))

        print('Verify done!')
Example #23
0
 def test_mult_all_random(self):
     rp = self._point_a()
     x = FQ.random(JUBJUB_L)
     all_points = [rp, rp.as_proj(), rp.as_etec(), rp.as_mont()]
     expected = rp * x
     for p in all_points:
         q = p.mult(x)
         r = q.as_point()
         self.assertEqual(r.x, expected.x)
         self.assertEqual(r.y, expected.y)
Example #24
0
def test_ifft():
    p = 337
    domain = [FQ(i) for i in [1, 85, 148, 111, 336, 252, 189, 226]]
    poly = [3, 1, 4, 1, 5, 9, 2, 6]
    result = []

    p_x = fft(p, domain, poly)

    result = ifft(p, domain, p_x)

    assert result == poly
Example #25
0
def test_mul_poly():
    # (x + 1) **2
    a = [1, 1, 0, 0, 0, 0, 0, 0]
    b = [1, 1, 0, 0, 0, 0, 0, 0]
    domain = [FQ(i) for i in [1, 85, 148, 111, 336, 252, 189, 226]]
    p = 337

    a_fft = fft(p, domain, a)
    b_fft = fft(p, domain, b)
    c = [a * b for a, b in zip(a_fft, b_fft)]

    res = ifft(p, domain, c)
    assert res == [1, 2, 1, 0, 0, 0, 0, 0]
Example #26
0
def test_degree_reduction():
    a = [1, 0, 0, 0, 0, 0, 0, 1]
    b = [0, 1, 0, 0, 0, 0, 0, 0]
    domain = [FQ(i) for i in [1, 85, 148, 111, 336, 252, 189, 226]]
    p = 337

    a_fft = fft(p, domain, a)
    b_fft = fft(p, domain, b)
    c = [a * b for a, b in zip(a_fft, b_fft)]

    res = ifft(p, domain, c)

    assert res == [1, 1, 0, 0, 0, 0, 0, 0]
Example #27
0
def test_fft():
    p = 337
    _domain = [1, 85, 148, 111, 336, 252, 189, 226]
    domain = [FQ(i) for i in _domain]
    poly = [3, 1, 4, 1, 5, 9, 2, 6]
    result = []

    p_x = fft(p, domain, poly)

    for x in _domain:
        result.append(polynomial_eval_prime(poly, x, p, 1, 0))

    assert p_x == result
Example #28
0
    def test_12_nonsquares(self):
        """
		If (A+2)*(A-2) is a square (e.g. if `ad` is a square) then there are two more
		points with v=0. These points have order 2
		"""
        try:
            x = (MONT_A + 2) * (MONT_A - 2)
            FQ(int(x)).sqrt()
            self.assertTrue(False)
        except SquareRootError:
            pass
        """
		If (A-2)/B is a square (e.g. if `d` is a square) then there are two points
		with `u=-1`. These points have order 4. These points correspond to two points
		of order 4 at infinity of the desingularization of E_{E,a,d}
		"""
        try:
            x = int((MONT_A - 2) / MONT_B)
            FQ(x).sqrt()
            self.assertTrue(False)
        except SquareRootError:
            pass
    def _create_order(self, base_token, quote_token, buy, price, volume):
        if buy:
            tokenS = self.market_info_map[quote_token]
            tokenB = self.market_info_map[base_token]
            amountS = str(int(10 ** tokenS['decimals'] * price * volume))
            amountB = str(int(10 ** tokenB['decimals'] * volume))
        else:
            tokenS = self.market_info_map[base_token]
            tokenB = self.market_info_map[quote_token]
            amountS = str(int(10 ** tokenS['decimals'] * volume))
            amountB = str(int(10 ** tokenB['decimals'] * price * volume))

        tokenSId = tokenS['tokenId']
        tokenBId = tokenB['tokenId']

        orderId = self.orderId[tokenSId]
        assert orderId < self.MAX_ORDER_ID
        self.orderId[tokenSId] += 1

        # make valid time ahead 1 hour
        validSince = int(time()) - self.time_offset - 3600

        # order base
        order = {
            "exchangeId"    : self.exchangeId,
            "orderId"       : orderId,
            "accountId"     : self.accountId,
            "tokenSId"      : tokenSId,
            "tokenBId"      : tokenBId,
            "amountS"       : amountS,
            "amountB"       : amountB,
            "allOrNone"     : "false",
            "validSince"    : validSince,
            "validUntil"    : validSince + 60 * 24 * 60 * 60,
            "maxFeeBips"    : 50,
            "label"         : 211,
            "buy"           : "true" if buy else "false",
            "clientOrderId" : "SampleOrder" + str(int(time()*1000))
        }

        order_message = self._serialize_order(order)
        msgHash = poseidon(order_message, self.order_sign_param)
        signedMessage = PoseidonEdDSA.sign(msgHash, FQ(int(self.private_key)))
        # update signaure
        order.update({
            "hash"        : str(msgHash),
            "signatureRx" : str(signedMessage.sig.R.x),
            "signatureRy" : str(signedMessage.sig.R.y),
            "signatureS"  : str(signedMessage.sig.s)
        })
        return order
Example #30
0
    def message(self, nonce):
        """
        Return an array of bits representing the on-chain transaction details
        that will be included in the signature.

        This is 104 bits long:

        +-----------+---------+---------+---------+
        | from_idx  | to_idx  | amount  | nonce   |
        +-----------+---------+---------+---------+
        | 24 bits   | 24 bits | 32 bits | 24 bits |
        +-----------+---------+---------+---------+

        Each integer is encoded in little-endian form,
        with the least significant bit first.
        """
        assert self.from_idx < (1<<TREE_SIZE)
        assert self.to_idx < (1<<TREE_SIZE)
        assert self.amount < (1<<AMOUNT_BITS)
        assert nonce < (1<<TREE_SIZE)
        msg_parts = [FQ(self.from_idx, 1<<TREE_SIZE), FQ(self.to_idx, 1<<TREE_SIZE),
                     FQ(self.amount, 1<<AMOUNT_BITS), FQ(nonce, 1<<TREE_SIZE)]
        return eddsa_tobits(*msg_parts)