def test_total_voting_weight(self): deed = Deed() v = 0 expected_total_voting_weight = 0 for signee in self.board: v += 1 expected_total_voting_weight += v deed.set_signee(signee, v) self.assertEqual(deed.total_votes, expected_total_voting_weight)
def test_warn_on_missing_amend_threshold(self): """Checks a warning is printed when creating a deed when amend threshold is specified""" deed = Deed() deed.set_signee(self.board[0], 1) deed.set_operation(Operation.transfer, 1) # Attempting to create un-amendable deed without explicitly requesting it is an error with self.assertRaises(InvalidDeedError): deed.validate() # Attempting to create un-amendable deed without explicitly requesting it is an error with self.assertRaises(InvalidDeedError): deed.to_json()
def create_default_deed(self): deed = Deed() # Add signees with total voting weight of 5 for signee in self.board: deed.set_signee(signee, 1) deed.set_operation(Operation.amend, deed.total_votes) deed.set_operation(Operation.transfer, deed.total_votes - 1) return deed
def test_allow_missing_amend_threshold(self): """Checks successful deed operation if amend threshold is missing, but explicit permission has been granted""" deed = Deed() deed.set_signee(self.board[0], 1) deed.set_operation(Operation.transfer, 1) deed.require_amend = False # With explicit request, this should still be a warning with patch('logging.warning') as mock_warn: deed.validate() self.assertEqual(mock_warn.call_count, 1) # With explicit request, this should still be a warning with patch('logging.warning') as mock_warn: obj = deed.to_json() self.assertEqual(mock_warn.call_count, 1) thresholds = obj['thresholds'] self.assertNotIn(str(Operation.amend), thresholds) self.assertIn(str(Operation.transfer), thresholds)
def test_create(self): """Test correct creation of deed""" deed = Deed() for signee in self.board: deed.set_signee(signee, 1) deed.set_operation(Operation.amend, 3) self.assertEqual(deed.total_votes, 4) self.assertEqual(deed.get_threshold(Operation.amend), 3) for signee in self.board: self.assertEqual(deed.get_signee(signee), 1)
def test_drop_signee(self): deed = Deed() deed.set_signee(self.address, 10) self.assertIn(Address(self.address), deed.signees) deed.remove_signee(self.address) self.assertNotIn(Address(self.address), deed.signees)
def test_deed(self): self.post_test(self.api.deed, 'deed', TokenTxFactory.deed, self.entity, Deed(), 2000)
def main(): # create the APIs api = LedgerApi(HOST, PORT) # we generate an identity from a known key, which contains funds. multi_sig_identity = Entity.from_hex( "6e8339a0c6d51fc58b4365bf2ce18ff2698d2b8c40bb13fcef7e1ba05df18e4b") # generate a board to control multi-sig account, with variable voting weights board = [ Entity.from_hex( "e833c747ee0aeae29e6823e7c825d3001638bc30ffe50363f8adf2693c3286f8" ), Entity.from_hex( "4083a476c4872f25cb40839ac8d994924bcef12d83e2ba4bd3ed6c9705959860" ), Entity.from_hex( "20293422c4b5faefba3422ed436427f2d37f310673681e98ac8637b04e756de3" ), Entity.from_hex( "d5f10ad865fff147ae7fcfdc98b755452a27a345975c8b9b3433ff16f23495fb" ), ] voting_weights = { board[0]: 1, board[1]: 1, board[2]: 1, board[3]: 2, } # generate another entity as a target for transfers other_identity = Entity.from_hex( "7da0e3fa62a916238decd4f54d43301c809595d66dd469f82f29e076752b155c") # submit deed print("\nCreating deed...") deed = Deed() for sig, weight in voting_weights.items(): deed.set_signee(sig, weight) deed.set_operation(Operation.amend, 4) deed.set_operation(Operation.transfer, 3) api.sync(api.tokens.deed(multi_sig_identity, deed, 500)) # display balance before print("\nBefore remote-multisig transfer") print('Balance 1:', api.tokens.balance(multi_sig_identity)) print('Balance 2:', api.tokens.balance(other_identity)) print() # scatter/gather example print("Generating transaction and distributing to signers...") # add intended signers to transaction ref_tx = TokenTxFactory.transfer(multi_sig_identity, other_identity, 250, 20, signatories=board) api.set_validity_period(ref_tx) # make a reference payload that can be used in this script for validation reference_payload = ref_tx.encode_payload() # have signers individually sign transaction signed_txs = [] for signer in board: # signer builds their own transaction to compare to note that each of the signers will need to agree on all # parts of the message including the validity period and the counter signer_tx = TokenTxFactory.transfer(multi_sig_identity, other_identity, 250, 20, signatories=board) signer_tx.counter = ref_tx.counter signer_tx.valid_until = ref_tx.valid_until signer_tx.valid_from = ref_tx.valid_from # sanity check each of the signers payload should match the reference payload assert signer_tx.encode_payload() == reference_payload # signers locally sign there version of the transaction signer_tx.sign(signer) # simulate distribution of signed partial transactions signed_txs.append(signer_tx.encode_partial()) # gather and encode final transaction - this step in theory can be done by all the signers provided they are # received all the signature shares print("Gathering and combining signed transactions...") partial_txs = [Transaction.decode_partial(s)[1] for s in signed_txs] # merge them together into one fully signed transaction success, tx = Transaction.merge(partial_txs) assert success # this indicates that all the signatures have been merged and that the transaction now validates # submit the transaction api.sync(api.submit_signed_tx(tx)) print("\nAfter remote multisig-transfer") print('Balance 1:', api.tokens.balance(multi_sig_identity)) print('Balance 2:', api.tokens.balance(other_identity)) # round-robin example print("\nGenerating transaction and sending down the line of signers...") # create the basis for the transaction tx = TokenTxFactory.transfer(multi_sig_identity, other_identity, 250, 20, signatories=board) api.set_validity_period(tx) # serialize and send to be signed tx_payload = tx.encode_payload() # have signers individually sign transaction and pass on to next signer for signer in board: # build the target transaction signer_tx = Transaction.decode_payload(tx_payload) # Signer decodes payload to inspect transaction signer_tx.sign(signer) # ensure that when we merge the signers signature into the payload that it is correct assert tx.merge_signatures(signer_tx) # once all the partial signatures have been merged then it makes sense print("Collecting final signed transaction...") assert tx.is_valid() api.sync(api.submit_signed_tx(tx)) print("\nAfter remote multisig-transfer") print('Balance 1:', api.tokens.balance(multi_sig_identity)) print('Balance 2:', api.tokens.balance(other_identity))
def test_threshold_sanity(self): """Test that thresholds can be correctly set""" deed = Deed() # Add signees with total voting weight of 5 deed.set_signee(self.board[0], 2) deed.set_signee(self.board[1], 2) deed.set_signee(self.board[2], 1) deed.set_operation(Operation.amend, 5) deed.validate() deed.set_operation(Operation.transfer, 5 + 1) # Setting any threshold higher than the voting weight should fail with self.assertRaises(InvalidDeedError): deed.validate() # Check threshold successfully set deed.set_operation(Operation.transfer, 5 - 1) deed.validate() self.assertEqual(deed.get_threshold(Operation.amend), 5) self.assertEqual(deed.get_threshold(Operation.transfer), 4)
def test_setting_invalid_threshold(self): deed = Deed() with self.assertRaises(ValueError): deed.set_operation(Operation.transfer, -2)
def test_setting_invalid_weight(self): deed = Deed() with self.assertRaises(ValueError): deed.set_signee(self.address, -10)
def test_inequality(self): deed = Deed() other = self.create_default_deed() other.set_operation(Operation.amend, 1) self.assertTrue(deed != other)
def test_from_json_string(self): deed = self.create_default_deed() obj = deed.to_json() deed_reconstituted = Deed.from_json(json.dumps(obj)) self.assertEqual(deed, deed_reconstituted)
def test_from_json(self): """Test creation from obj - promary workflow""" deed = self.create_default_deed() obj = deed.to_json() deed_reconstituted = Deed.from_json(obj) self.assertEqual(deed, deed_reconstituted)
def main(): # create the APIs api = LedgerApi(HOST, PORT) # generate an identity from a known key, which contains funds. multi_sig_identity = Entity.from_hex( "6e8339a0c6d51fc58b4365bf2ce18ff2698d2b8c40bb13fcef7e1ba05df18e4b") # generate a board to control multi-sig account, with variable voting weights. # we use keys for accounts which already have funds. board = [] board.append( Entity.from_hex( "e833c747ee0aeae29e6823e7c825d3001638bc30ffe50363f8adf2693c3286f8") ) board.append( Entity.from_hex( "4083a476c4872f25cb40839ac8d994924bcef12d83e2ba4bd3ed6c9705959860") ) board.append( Entity.from_hex( "20293422c4b5faefba3422ed436427f2d37f310673681e98ac8637b04e756de3") ) board.append( Entity.from_hex( "d5f10ad865fff147ae7fcfdc98b755452a27a345975c8b9b3433ff16f23495fb") ) voting_weights = { board[0]: 1, board[1]: 1, board[2]: 1, board[3]: 2, } # generate another entity as a target for transfers other_identity = Entity.from_hex( "e833c747ee0aeae29e6823e7c825d3001638bc30ffe50363f8adf2693c3286f8") print('Original balance of multi_sig_identity:', api.tokens.balance(multi_sig_identity)) # transfers can happen normally without a deed print('\nSubmitting pre-deed transfer with original signature...') api.sync(api.tokens.transfer(multi_sig_identity, other_identity, 250, 20)) print('Balance 1:', api.tokens.balance(multi_sig_identity)) print('Balance 2:', api.tokens.balance(other_identity)) # submit the original deed print("\nCreating deed...") deed = Deed() for sig, weight in voting_weights.items(): deed.set_signee(sig, weight) # set our initial voting thresholds deed.set_operation(Operation.transfer, 2) deed.set_operation(Operation.amend, 4) api.sync(api.tokens.deed(multi_sig_identity, deed, 6000)) # original address can no longer validate transfers print("\nTransfer with original signature should fail...") try: api.sync( api.tokens.transfer(multi_sig_identity, other_identity, 250, 20)) except RuntimeError as e: print("Transaction failed as expected") else: print("Transaction succeeded, it shouldn't have") # sufficient voting power required to sign transfers print("\nSubmitting transfer with two signatures with total 2 votes...") print_signing_votes(voting_weights, board[:2]) # since we now want to create a transaction which has only been signed by a subset of the board, we must use # the factory interface in order to build out the transaction we are after tx = TokenTxFactory.transfer(multi_sig_identity, other_identity, 250, 20, board[:2]) tx.valid_until = api.tokens.current_block_number() + 100 # the signatories to sign the transaction for signatory in board[:2]: tx.sign(signatory) api.sync(api.submit_signed_tx(tx)) print('Balance 1:', api.tokens.balance(multi_sig_identity)) print('Balance 2:', api.tokens.balance(other_identity)) # some entities may have more voting power print("\nSubmitting transfer with single signature with 2 votes...") print_signing_votes(voting_weights, board[3]) tx = TokenTxFactory.transfer(multi_sig_identity, other_identity, 250, 20, [board[3]]) tx.valid_until = api.tokens.current_block_number() + 100 tx.sign(board[3]) api.sync(api.submit_signed_tx(tx)) print('Balance 1:', api.tokens.balance(multi_sig_identity)) print('Balance 2:', api.tokens.balance(other_identity)) # amend the deed print("\nAmending deed to increase transfer threshold to 3 votes...") deed.set_operation(Operation.transfer, 3) tx = TokenTxFactory.deed(multi_sig_identity, deed, 400, board) tx.valid_until = api.tokens.current_block_number() + 100 for member in board: tx.sign(member) api.sync(api.submit_signed_tx(tx)) # single member no longer has enough voting power print("\nSingle member transfer with 2 votes should no longer succeed...") try: print_signing_votes(voting_weights, board[3]) tx = TokenTxFactory.transfer(multi_sig_identity, other_identity, 250, 20, [board[3]]) tx.valid_until = api.tokens.current_block_number() + 100 tx.sign(board[3]) api.sync(api.submit_signed_tx(tx)) except RuntimeError as e: print("Transaction failed as expected") else: print("Transaction succeeded, it shouldn't have") # correct number of signatory votes print("\nSuccesful transaction with sufficient voting weight...") print_signing_votes(voting_weights, board[1:]) tx = TokenTxFactory.transfer(multi_sig_identity, other_identity, 250, 20, board[1:]) tx.valid_until = api.tokens.current_block_number() + 100 for member in board[1:]: tx.sign(member) api.sync(api.submit_signed_tx(tx)) print('Balance 1:', api.tokens.balance(multi_sig_identity)) print('Balance 2:', api.tokens.balance(other_identity)) # warning: if no amend threshold is set, future amendments are impossible print("\nAmending deed to remove threshold...") deed.remove_operation(Operation.amend) deed.require_amend = False tx = TokenTxFactory.deed(multi_sig_identity, deed, 400, board) tx.valid_until = api.tokens.current_block_number() + 100 for member in board: tx.sign(member) api.sync(api.submit_signed_tx(tx)) deed.set_operation(Operation.amend, 1) print("\nExpecting further amendment to fail...") try: tx = TokenTxFactory.deed(multi_sig_identity, deed, 400, board) tx.valid_until = api.tokens.current_block_number() + 100 for member in board: tx.sign(member) api.sync(api.submit_signed_tx(tx)) except RuntimeError as e: print("Transaction failed as expected") else: print("Transaction succeeded, it shouldn't have")