def get_tx( self, inputs: Optional[List[WalletInputInfo]] = None, outputs: Optional[List[WalletOutputInfo]] = None) -> Transaction: if not outputs: address = self.get_address(0) assert address is not None outputs = [ WalletOutputInfo(address=decode_address(address), value=1, timelock=None), WalletOutputInfo(address=decode_address(address), value=1, timelock=None) ] if inputs: tx = self.manager.wallet.prepare_transaction( Transaction, inputs, outputs) else: tx = self.manager.wallet.prepare_transaction_compute_inputs( Transaction, outputs, self.manager.tx_storage) tx.storage = self.manager.tx_storage tx.weight = 1 max_ts_spent_tx = max( tx.get_spent_tx(txin).timestamp for txin in tx.inputs) tx.timestamp = max(max_ts_spent_tx + 1, int(self.manager.reactor.seconds())) tx.parents = self.manager.get_new_tx_parents(tx.timestamp) tx.resolve() return tx
def gen_custom_tx(manager: HathorManager, tx_inputs: List[Tuple[Transaction, int]], *, n_outputs: int = 1, base_parent: Optional[Transaction] = None, weight: Optional[float] = None) -> Transaction: """Generate a custom tx based on the inputs and outputs. It gives full control to the inputs and can be used to generate conflicts and specific patterns in the DAG.""" inputs = [] value = 0 parents = [] for tx_base, txout_index in tx_inputs: assert tx_base.hash is not None spent_tx = tx_base spent_txout = spent_tx.outputs[txout_index] p2pkh = parse_address_script(spent_txout.script) assert isinstance(p2pkh, P2PKH) from hathor.wallet.base_wallet import WalletInputInfo, WalletOutputInfo value += spent_txout.value wallet = manager.wallet assert wallet is not None assert spent_tx.hash is not None private_key = wallet.get_private_key(p2pkh.address) inputs.append(WalletInputInfo(tx_id=spent_tx.hash, index=txout_index, private_key=private_key)) if not tx_base.is_block: parents.append(tx_base.hash) assert wallet is not None address = wallet.get_unused_address(mark_as_used=True) if n_outputs == 1: outputs = [WalletOutputInfo(address=decode_address(address), value=int(value), timelock=None)] elif n_outputs == 2: assert int(value) > 1 outputs = [ WalletOutputInfo(address=decode_address(address), value=int(value) - 1, timelock=None), WalletOutputInfo(address=decode_address(address), value=1, timelock=None), ] else: raise NotImplementedError tx2 = wallet.prepare_transaction(Transaction, inputs, outputs) tx2.storage = manager.tx_storage tx2.timestamp = max(tx_base.timestamp + 1, int(manager.reactor.seconds())) tx2.parents = parents[:2] if len(tx2.parents) < 2: if base_parent: assert base_parent.hash is not None tx2.parents.append(base_parent.hash) elif not tx_base.is_block: tx2.parents.append(tx_base.parents[0]) else: tx2.parents.extend(manager.get_new_tx_parents(tx2.timestamp)) tx2.parents = tx2.parents[:2] assert len(tx2.parents) == 2 tx2.weight = weight or 25 tx2.update_hash() return tx2
def test_choose_inputs(self): blocks = add_new_blocks(self.manager, 1, advance_clock=15) blocks_tokens = [ sum(txout.value for txout in blk.outputs) for blk in blocks ] add_blocks_unlock_reward(self.manager) address = self.manager.wallet.get_unused_address(mark_as_used=False) outputs = [ WalletOutputInfo(address=decode_address(address), value=blocks_tokens[0], timelock=int(self.clock.seconds()) + 10) ] tx1 = self.manager.wallet.prepare_transaction_compute_inputs( Transaction, outputs, self.manager.tx_storage) tx1.weight = 10 tx1.parents = self.manager.get_new_tx_parents() tx1.timestamp = int(self.clock.seconds()) tx1.resolve() self.manager.propagate_tx(tx1) self.clock.advance(1) self.assertEqual( self.manager.wallet.balance[settings.HATHOR_TOKEN_UID], WalletBalance(blocks_tokens[0], 0)) outputs = [ WalletOutputInfo(address=decode_address(address), value=blocks_tokens[0], timelock=None) ] with self.assertRaises(InsufficientFunds): self.manager.wallet.prepare_transaction_compute_inputs( Transaction, outputs, self.manager.tx_storage) self.clock.advance(10) tx2 = self.manager.wallet.prepare_transaction_compute_inputs( Transaction, outputs, self.manager.tx_storage) tx2.weight = 10 tx2.parents = self.manager.get_new_tx_parents() tx2.timestamp = int(self.clock.seconds()) tx2.resolve() self.manager.propagate_tx(tx2) self.assertEqual( self.manager.wallet.balance[settings.HATHOR_TOKEN_UID], WalletBalance(0, blocks_tokens[0]))
def test_transaction_and_balance(self): # generate a new block and check if we increase balance new_address = self.wallet.get_unused_address() out = WalletOutputInfo(decode_address(new_address), self.TOKENS, timelock=None) block = add_new_block(self.manager) block.verify() utxo = self.wallet.unspent_txs[settings.HATHOR_TOKEN_UID].get((block.hash, 0)) self.assertIsNotNone(utxo) self.assertEqual(self.wallet.balance[settings.HATHOR_TOKEN_UID], WalletBalance(0, self.BLOCK_TOKENS)) # create transaction spending this value, but sending to same wallet new_address2 = self.wallet.get_unused_address() out = WalletOutputInfo(decode_address(new_address2), self.TOKENS, timelock=None) tx1 = self.wallet.prepare_transaction_compute_inputs(Transaction, outputs=[out]) tx1.update_hash() tx1.verify_script(tx1.inputs[0], block) tx1.storage = self.tx_storage self.wallet.on_new_tx(tx1) self.tx_storage.save_transaction(tx1) self.assertEqual(len(self.wallet.spent_txs), 1) utxo = self.wallet.unspent_txs[settings.HATHOR_TOKEN_UID].get((tx1.hash, 0)) self.assertIsNotNone(utxo) self.assertEqual(self.wallet.balance[settings.HATHOR_TOKEN_UID], WalletBalance(0, self.TOKENS)) # pass inputs and outputs to prepare_transaction, but not the input keys # spend output last transaction input_info = WalletInputInfo(tx1.hash, 0, None) new_address3 = self.wallet.get_unused_address() out = WalletOutputInfo(decode_address(new_address3), self.TOKENS, timelock=None) tx2 = self.wallet.prepare_transaction_incomplete_inputs(Transaction, inputs=[input_info], outputs=[out], tx_storage=self.tx_storage) tx2.storage = self.tx_storage tx2.update_hash() tx2.storage = self.tx_storage tx2.verify_script(tx2.inputs[0], tx1) self.tx_storage.save_transaction(tx2) self.wallet.on_new_tx(tx2) self.assertEqual(len(self.wallet.spent_txs), 2) self.assertEqual(self.wallet.balance[settings.HATHOR_TOKEN_UID], WalletBalance(0, self.TOKENS)) # Test getting more unused addresses than the gap limit for i in range(3): kwargs = {'mark_as_used': True} if i == 2: # Last one we dont mark as used kwargs['mark_as_used'] = False self.wallet.get_unused_address(**kwargs)
def gen_new_double_spending(manager: HathorManager, *, use_same_parents: bool = False) -> Transaction: tx_interval = random.choice(list(manager.tx_storage.get_tx_tips())) tx = manager.tx_storage.get_transaction(tx_interval.data) txin = random.choice(tx.inputs) from hathor.transaction.scripts import P2PKH, parse_address_script spent_tx = tx.get_spent_tx(txin) spent_txout = spent_tx.outputs[txin.index] p2pkh = parse_address_script(spent_txout.script) assert isinstance(p2pkh, P2PKH) from hathor.wallet.base_wallet import WalletInputInfo, WalletOutputInfo value = spent_txout.value private_key = manager.wallet.get_private_key(p2pkh.address) inputs = [WalletInputInfo(tx_id=txin.tx_id, index=txin.index, private_key=private_key)] address = manager.wallet.get_unused_address(mark_as_used=True) outputs = [WalletOutputInfo(address=decode_address(address), value=int(value), timelock=None)] tx2 = manager.wallet.prepare_transaction(Transaction, inputs, outputs, manager.tx_storage) tx2.storage = manager.tx_storage tx2.weight = 1 tx2.timestamp = max(tx.timestamp + 1, int(manager.reactor.seconds())) if use_same_parents: tx2.parents = list(tx.parents) else: tx2.parents = manager.get_new_tx_parents(tx2.timestamp) tx2.resolve() return tx2
def setUp(self): super().setUp() self.network = 'testnet' self.manager = self.create_peer(self.network, unlock_wallet=True) self.tx_storage = self.manager.tx_storage data = b'This is a test block.' self.blocks = add_new_blocks(self.manager, 3, advance_clock=15, block_data=data) address = self.get_address(0) value = 100 outputs = [ WalletOutputInfo(address=decode_address(address), value=int(value), timelock=None) ] self.tx1 = self.manager.wallet.prepare_transaction_compute_inputs(Transaction, outputs) self.tx1.weight = 10 self.tx1.parents = self.manager.get_new_tx_parents() self.tx1.timestamp = int(self.clock.seconds()) self.tx1.resolve() self.manager.propagate_tx(self.tx1) # Change of parents only, so it's a twin. # With less weight, so the balance will continue because tx1 will be the winner self.tx2 = Transaction.create_from_struct(self.tx1.get_struct()) self.tx2.parents = [self.tx1.parents[1], self.tx1.parents[0]] self.tx2.weight = 9 self.tx2.resolve() # Propagate a conflicting twin transaction self.manager.propagate_tx(self.tx2)
def test_maybe_spent_txs(self): add_new_block(self.manager, advance_clock=15) blocks = add_blocks_unlock_reward(self.manager) w = self.manager.wallet new_address = w.get_unused_address() out = WalletOutputInfo(decode_address(new_address), 1, timelock=None) tx1 = w.prepare_transaction_compute_inputs(Transaction, outputs=[out]) self.assertEqual(len(tx1.inputs), 1) _input = tx1.inputs[0] key = (_input.tx_id, _input.index) self.assertNotIn(key, w.unspent_txs[settings.HATHOR_TOKEN_UID]) self.assertIn(key, w.maybe_spent_txs[settings.HATHOR_TOKEN_UID]) self.run_to_completion() self.assertIn(key, w.unspent_txs[settings.HATHOR_TOKEN_UID]) self.assertEqual(0, len(w.maybe_spent_txs[settings.HATHOR_TOKEN_UID])) # when we receive the new tx it will remove from maybe_spent tx2 = w.prepare_transaction_compute_inputs(Transaction, outputs=[out]) tx2.storage = self.manager.tx_storage tx2.timestamp = max( tx2.get_spent_tx(txin).timestamp for txin in tx2.inputs) + 1 tx2.parents = self.manager.get_new_tx_parents(tx2.timestamp) tx2.weight = 1 tx2.timestamp = blocks[-1].timestamp + 1 tx2.resolve() self.assertTrue(self.manager.on_new_tx(tx2, fails_silently=False)) self.clock.advance(2) self.assertEqual(0, len(w.maybe_spent_txs[settings.HATHOR_TOKEN_UID]))
def setUp(self): super().setUp() self.network = 'testnet' self.manager = self.create_peer(self.network, unlock_wallet=True) blocks = add_new_blocks(self.manager, 3, advance_clock=15) self.blocks_tokens = [ sum(txout.value for txout in blk.outputs) for blk in blocks ] address = self.get_address(0) value = 100 self.initial_balance = sum(self.blocks_tokens[:3]) - 100 outputs = [ WalletOutputInfo(address=decode_address(address), value=int(value), timelock=None) ] add_blocks_unlock_reward(self.manager) self.tx1 = self.manager.wallet.prepare_transaction_compute_inputs( Transaction, outputs) self.tx1.weight = 10 self.tx1.parents = self.manager.get_new_tx_parents() self.tx1.timestamp = int(self.clock.seconds()) self.tx1.resolve() self.manager.propagate_tx(self.tx1) self.run_to_completion()
def test_invalid_address(self): w = Wallet(directory=self.directory) w.unlock(PASSWORD) # creating valid address valid_address = '15d14K5jMqsN2uwUEFqiPG5SoD7Vr1BfnH' WalletOutputInfo(decode_address(valid_address), 100, None) # creating invalid address invalid_address = '5d14K5jMqsN2uwUEFqiPG5SoD7Vr1BfnH' with self.assertRaises(InvalidAddress): WalletOutputInfo(decode_address(invalid_address), 100, None) # invalid address (checksum invalid) invalid_address2 = '15d14K5jMqsN2uwUEFqiPG5SoD7Vr1Bfnq' with self.assertRaises(InvalidAddress): WalletOutputInfo(decode_address(invalid_address2), 100, None)
def test_create_token_transaction(self): add_new_block(self.manager, advance_clock=5) add_blocks_unlock_reward(self.manager) tx = create_tokens(self.manager) tokens_created = tx.outputs[0].value token_uid = tx.tokens[0] address_b58 = self.manager.wallet.get_unused_address() address = decode_address(address_b58) _, hathor_balance = self.manager.wallet.balance[ settings.HATHOR_TOKEN_UID] # prepare tx with hathors and another token # hathor tx hathor_out = WalletOutputInfo(address, hathor_balance, None) # token tx token_out = WalletOutputInfo(address, tokens_created - 20, None, token_uid.hex()) tx2 = self.manager.wallet.prepare_transaction_compute_inputs( Transaction, [hathor_out, token_out]) tx2.storage = self.manager.tx_storage tx2.timestamp = tx.timestamp + 1 tx2.parents = self.manager.get_new_tx_parents() tx2.resolve() tx2.verify() self.assertNotEqual(len(tx2.inputs), 0) token_dict = defaultdict(int) for _input in tx2.inputs: output_tx = self.manager.tx_storage.get_transaction(_input.tx_id) output = output_tx.outputs[_input.index] token_uid = output_tx.get_token_uid(output.get_token_index()) token_dict[token_uid] += output.value # make sure balance is the same and we've checked both balances did_enter = 0 for token_uid, value in token_dict.items(): if token_uid == settings.HATHOR_TOKEN_UID: self.assertEqual(value, hathor_balance) did_enter += 1 elif token_uid == token_uid: self.assertEqual(value, tokens_created) did_enter += 1 self.assertEqual(did_enter, 2)
def test_twin_tx(self): add_new_blocks(self.manager, 5, advance_clock=15) add_blocks_unlock_reward(self.manager) address = self.get_address(0) value1 = 100 value2 = 101 outputs = [ WalletOutputInfo(address=decode_address(address), value=int(value1), timelock=None), WalletOutputInfo(address=decode_address(address), value=int(value2), timelock=None) ] tx1 = self.manager.wallet.prepare_transaction_compute_inputs( Transaction, outputs, self.manager.tx_storage) tx1.weight = 10 tx1.parents = self.manager.get_new_tx_parents() tx1.timestamp = int(self.clock.seconds()) tx1.resolve() # Change of parents only, so it's a twin tx2 = Transaction.create_from_struct(tx1.get_struct()) tx2.parents = [tx1.parents[1], tx1.parents[0]] tx2.resolve() self.assertNotEqual(tx1.hash, tx2.hash) self.manager.propagate_tx(tx1) self.run_to_completion() wallet_data = self.manager.tx_storage.wallet_index.get_from_address( address) self.assertEqual(len(wallet_data), 1) self.assertEqual(wallet_data, [tx1.hash]) # Propagate a conflicting twin transaction self.manager.propagate_tx(tx2) self.run_to_completion() wallet_data = self.manager.tx_storage.wallet_index.get_from_address( address) self.assertEqual(len(wallet_data), 2) self.assertEqual(set(wallet_data), set([tx1.hash, tx2.hash]))
def test_prepare_transaction(self): block = add_new_block(self.manager, advance_clock=5) w = self.manager.wallet new_address = w.get_unused_address() out = WalletOutputInfo(decode_address(new_address), 1, timelock=None) with self.assertRaises(InsufficientFunds): w.prepare_transaction_compute_inputs(Transaction, outputs=[out], timestamp=block.timestamp)
def test_insuficient_funds(self): w = Wallet(directory=self.directory) w.unlock(PASSWORD) # create transaction spending some value new_address = w.get_unused_address() out = WalletOutputInfo(decode_address(new_address), 100, timelock=None) with self.assertRaises(InsufficientFunds): w.prepare_transaction_compute_inputs(Transaction, outputs=[out])
def test_insuficient_funds(self): add_blocks_unlock_reward(self.manager) # create transaction spending some value new_address = self.wallet.get_unused_address() out = WalletOutputInfo(decode_address(new_address), self.TOKENS, timelock=None) with self.assertRaises(InsufficientFunds): self.wallet.prepare_transaction_compute_inputs( Transaction, [out], self.tx_storage)
def test_balance_update4(self): # Tx2 spends Tx1 output # Tx3 is twin of Tx2 with same acc weight, so both will get voided self.manager.reactor.advance(1) # Start balance self.assertEqual(self.manager.wallet.balance[settings.HATHOR_TOKEN_UID], WalletBalance(0, self.initial_balance)) address = self.manager.wallet.get_unused_address_bytes() value = self.blocks_tokens[0] - 100 inputs = [WalletInputInfo(tx_id=self.tx1.hash, index=0, private_key=None)] outputs = [WalletOutputInfo(address=address, value=int(value), timelock=None)] tx2 = self.manager.wallet.prepare_transaction_incomplete_inputs(Transaction, inputs, outputs, self.manager.tx_storage) tx2.weight = 10 tx2.parents = [self.tx1.hash, self.tx1.parents[0]] tx2.timestamp = int(self.clock.seconds()) tx2.resolve() self.manager.propagate_tx(tx2) self.run_to_completion() # Test create same tx with allow double spending with self.assertRaises(PrivateKeyNotFound): self.manager.wallet.prepare_transaction_incomplete_inputs( Transaction, inputs=inputs, outputs=outputs, tx_storage=self.manager.tx_storage ) self.manager.wallet.prepare_transaction_incomplete_inputs(Transaction, inputs=inputs, outputs=outputs, force=True, tx_storage=self.manager.tx_storage) # Change of parents only, so it's a twin. tx3 = Transaction.create_from_struct(tx2.get_struct()) tx3.parents = [tx2.parents[1], tx2.parents[0]] tx3.resolve() # Propagate a conflicting twin transaction self.manager.propagate_tx(tx3) self.run_to_completion() meta2 = tx2.get_metadata(force_reload=True) self.assertEqual(meta2.twins, [tx3.hash]) self.assertEqual(meta2.voided_by, {tx2.hash}) meta3 = tx3.get_metadata() self.assertEqual(meta3.voided_by, {tx3.hash}) # Balance is the same self.assertEqual(self.manager.wallet.balance[settings.HATHOR_TOKEN_UID], WalletBalance(0, self.initial_balance))
def test_balance_update5(self): # Tx2 spends Tx1 output # Tx3 is twin of Tx1, with less acc weight # So we have conflict between all three txs but tx1 and tx2 are winners and tx3 is voided self.clock.advance(1) # Start balance self.assertEqual( self.manager.wallet.balance[settings.HATHOR_TOKEN_UID], WalletBalance(0, self.initial_balance)) address = self.manager.wallet.get_unused_address_bytes() value = self.blocks_tokens[0] - 100 inputs = [ WalletInputInfo(tx_id=self.tx1.hash, index=0, private_key=None) ] outputs = [ WalletOutputInfo(address=address, value=int(value), timelock=None) ] tx2 = self.manager.wallet.prepare_transaction_incomplete_inputs( Transaction, inputs, outputs, self.manager.tx_storage) tx2.weight = 10 tx2.parents = [self.tx1.hash, self.tx1.parents[0]] tx2.timestamp = int(self.clock.seconds()) tx2.resolve() # Change of parents only, so it's a twin. tx3 = Transaction.create_from_struct(self.tx1.get_struct()) tx3.parents = [self.tx1.parents[1], self.tx1.parents[0]] tx3.resolve() # Propagate a conflicting twin transaction self.manager.propagate_tx(tx2) self.manager.propagate_tx(tx3) self.run_to_completion() meta2 = tx2.get_metadata() self.assertEqual(meta2.twins, []) self.assertEqual(meta2.voided_by, None) meta3 = tx3.get_metadata() self.assertEqual(meta3.voided_by, {tx3.hash}) self.assertEqual(meta3.twins, [self.tx1.hash]) # Balance is the same self.assertEqual( self.manager.wallet.balance[settings.HATHOR_TOKEN_UID], WalletBalance(0, self.initial_balance))
def test_block_increase_balance(self): # generate a new block and check if we increase balance w = Wallet(directory=self.directory) w.unlock(PASSWORD) new_address = w.get_unused_address() key = w.keys[new_address] out = WalletOutputInfo(decode_address(key.address), BLOCK_REWARD, timelock=None) tx = w.prepare_transaction(Transaction, inputs=[], outputs=[out]) tx.update_hash() w.on_new_tx(tx) utxo = w.unspent_txs[settings.HATHOR_TOKEN_UID].get((tx.hash, 0)) self.assertIsNotNone(utxo) self.assertEqual(w.balance[settings.HATHOR_TOKEN_UID], WalletBalance(0, BLOCK_REWARD))
def gen_new_double_spending(manager: HathorManager, *, use_same_parents: bool = False, tx: Optional[Transaction] = None, weight: float = 1) -> Transaction: if tx is None: tx_candidates = manager.get_new_tx_parents() genesis = manager.tx_storage.get_all_genesis() genesis_txs = [tx for tx in genesis if not tx.is_block] # XXX: it isn't possible to double-spend a genesis transaction, thus we remove it from tx_candidates for genesis_tx in genesis_txs: if genesis_tx.hash in tx_candidates: tx_candidates.remove(genesis_tx.hash) if not tx_candidates: raise NoCandidatesError() # assert tx_candidates, 'Must not be empty, otherwise test was wrongly set up' tx_hash = manager.rng.choice(tx_candidates) tx = cast(Transaction, manager.tx_storage.get_transaction(tx_hash)) txin = manager.rng.choice(tx.inputs) from hathor.transaction.scripts import P2PKH, parse_address_script spent_tx = tx.get_spent_tx(txin) spent_txout = spent_tx.outputs[txin.index] p2pkh = parse_address_script(spent_txout.script) assert isinstance(p2pkh, P2PKH) from hathor.wallet.base_wallet import WalletInputInfo, WalletOutputInfo value = spent_txout.value wallet = manager.wallet assert wallet is not None private_key = wallet.get_private_key(p2pkh.address) inputs = [WalletInputInfo(tx_id=txin.tx_id, index=txin.index, private_key=private_key)] address = wallet.get_unused_address(mark_as_used=True) outputs = [WalletOutputInfo(address=decode_address(address), value=int(value), timelock=None)] tx2 = wallet.prepare_transaction(Transaction, inputs, outputs) tx2.storage = manager.tx_storage tx2.weight = weight tx2.timestamp = max(tx.timestamp + 1, int(manager.reactor.seconds())) if use_same_parents: tx2.parents = list(tx.parents) else: tx2.parents = manager.get_new_tx_parents(tx2.timestamp) tx2.resolve() return tx2
def gen_new_tx(manager, address, value, verify=True): from hathor.transaction import Transaction from hathor.wallet.base_wallet import WalletOutputInfo outputs = [] outputs.append(WalletOutputInfo(address=decode_address(address), value=int(value), timelock=None)) tx = manager.wallet.prepare_transaction_compute_inputs(Transaction, outputs) tx.storage = manager.tx_storage max_ts_spent_tx = max(tx.get_spent_tx(txin).timestamp for txin in tx.inputs) tx.timestamp = max(max_ts_spent_tx + 1, int(manager.reactor.seconds())) tx.weight = 1 tx.parents = manager.get_new_tx_parents(tx.timestamp) tx.resolve() if verify: tx.verify() return tx
def _add_new_tx(self, address, value): from hathor.transaction import Transaction from hathor.wallet.base_wallet import WalletOutputInfo outputs = [] outputs.append( WalletOutputInfo(address=decode_address(address), value=int(value), timelock=None)) tx = self.manager1.wallet.prepare_transaction_compute_inputs( Transaction, outputs, self.manager1.tx_storage) tx.timestamp = int(self.clock.seconds()) tx.storage = self.manager1.tx_storage tx.weight = 10 tx.parents = self.manager1.get_new_tx_parents() tx.resolve() tx.verify() self.manager1.propagate_tx(tx) self.clock.advance(10)
def test_balance_update6(self): # Tx2 is twin of tx1, so both voided # Tx3 has tx1 as parent, so increases tx1 acc weight, then tx1 is winner against tx2 self.manager.reactor.advance(1) # Start balance self.assertEqual( self.manager.wallet.balance[settings.HATHOR_TOKEN_UID], WalletBalance(0, self.initial_balance)) # Change of parents only, so it's a twin. tx2 = Transaction.create_from_struct(self.tx1.get_struct()) tx2.parents = [self.tx1.parents[1], self.tx1.parents[0]] tx2.resolve() address = self.get_address(0) value = 100 outputs = [ WalletOutputInfo(address=decode_address(address), value=int(value), timelock=None) ] tx3 = self.manager.wallet.prepare_transaction_compute_inputs( Transaction, outputs) tx3.weight = 10 tx3.parents = [self.tx1.hash, self.tx1.parents[0]] tx3.timestamp = int(self.clock.seconds()) tx3.resolve() # Propagate a conflicting twin transaction self.manager.propagate_tx(tx2) self.manager.propagate_tx(tx3) self.run_to_completion() # Balance is the same self.assertEqual( self.manager.wallet.balance[settings.HATHOR_TOKEN_UID], WalletBalance(0, self.initial_balance - 100))
def test_balance_update_twin_tx(self): # Start balance self.assertEqual( self.manager.wallet.balance[settings.HATHOR_TOKEN_UID], WalletBalance(0, self.initial_balance)) wallet_address = self.manager.wallet.get_unused_address() outputs2 = [ WalletOutputInfo(address=decode_address(wallet_address), value=500, timelock=None) ] tx2 = self.manager.wallet.prepare_transaction_compute_inputs( Transaction, outputs2) tx2.weight = 10 tx2.parents = self.manager.get_new_tx_parents() tx2.timestamp = int(self.clock.seconds()) tx2.resolve() self.manager.propagate_tx(tx2) self.run_to_completion() outputs3 = [ WalletOutputInfo(address=decode_address(wallet_address), value=self.blocks_tokens[0], timelock=None) ] tx3 = self.manager.wallet.prepare_transaction_compute_inputs( Transaction, outputs3) tx3.weight = 10 tx3.parents = self.manager.get_new_tx_parents() tx3.timestamp = int(self.clock.seconds()) tx3.resolve() self.manager.propagate_tx(tx3) self.run_to_completion() self.clock.advance(1) new_address = self.manager.wallet.get_unused_address_bytes() inputs = [WalletInputInfo(tx_id=tx3.hash, index=0, private_key=None)] outputs = [ WalletOutputInfo(address=new_address, value=self.blocks_tokens[0], timelock=None) ] tx4 = self.manager.wallet.prepare_transaction_incomplete_inputs( Transaction, inputs, outputs, self.manager.tx_storage) tx4.weight = 10 tx4.parents = [tx3.hash, tx3.parents[0]] tx4.timestamp = int(self.clock.seconds()) tx4.resolve() self.manager.propagate_tx(tx4) self.run_to_completion() # Change of parents only, so it's a twin. tx5 = Transaction.create_from_struct(tx4.get_struct()) tx5.parents = [tx4.parents[1], tx4.parents[0]] tx5.weight = 10 tx5.resolve() # Propagate a conflicting twin transaction self.manager.propagate_tx(tx5) self.run_to_completion() meta4 = tx4.get_metadata(force_reload=True) self.assertEqual(meta4.twins, [tx5.hash]) meta5 = tx5.get_metadata(force_reload=True) self.assertEqual(meta5.voided_by, {tx5.hash}) # Balance is the same self.assertEqual( self.manager.wallet.balance[settings.HATHOR_TOKEN_UID], WalletBalance(0, self.initial_balance))
def render_POST(self, request): """ POST request for /wallet/send_tokens/ We expect 'data' as request args 'data': stringified json with an array of inputs and array of outputs If inputs array is empty we use 'prepare_compute_inputs', that calculate the inputs We return success (bool) :rtype: string (json) """ request.setHeader(b'content-type', b'application/json; charset=utf-8') set_cors(request, 'POST') post_data = json.loads(request.content.read().decode('utf-8')) data = post_data['data'] outputs = [] for output in data['outputs']: try: address = decode_address(output['address']) # bytes except InvalidAddress: return self.return_POST( False, 'The address {} is invalid'.format(output['address'])) value = int(output['value']) timelock = output.get('timelock') token_uid = output.get('token_uid') if token_uid: outputs.append( WalletOutputInfo(address=address, value=value, timelock=timelock, token_uid=token_uid)) else: outputs.append( WalletOutputInfo(address=address, value=value, timelock=timelock)) timestamp = None if 'timestamp' in data: if data['timestamp'] > 0: timestamp = data['timestamp'] else: timestamp = int(self.manager.reactor.seconds()) if len(data['inputs']) == 0: try: inputs, outputs = self.manager.wallet.prepare_compute_inputs( outputs, self.manager.tx_storage, timestamp) except InsufficientFunds as e: return self.return_POST( False, 'Insufficient funds, {}'.format(str(e))) else: inputs = [] for input_tx in data['inputs']: input_tx['private_key'] = None input_tx['index'] = int(input_tx['index']) input_tx['tx_id'] = bytes.fromhex(input_tx['tx_id']) inputs.append(WalletInputInfo(**input_tx)) try: inputs = self.manager.wallet.prepare_incomplete_inputs( inputs, self.manager.tx_storage) except (PrivateKeyNotFound, InputDuplicated): return self.return_POST(False, 'Invalid input to create transaction') storage = self.manager.tx_storage if timestamp is None: max_ts_spent_tx = max( storage.get_transaction(txin.tx_id).timestamp for txin in inputs) timestamp = max(max_ts_spent_tx + 1, int(self.manager.reactor.seconds())) parents = self.manager.get_new_tx_parents(timestamp) values = { 'inputs': inputs, 'outputs': outputs, 'storage': storage, 'weight': data.get('weight'), 'parents': parents, 'timestamp': timestamp, } deferred = threads.deferToThread(self._render_POST_thread, values, request) deferred.addCallback(self._cb_tx_resolve, request) deferred.addErrback(self._err_tx_resolve, request) from twisted.web.server import NOT_DONE_YET return NOT_DONE_YET
def test_timelock(self): blocks = add_new_blocks(self.manager, 5, advance_clock=15) blocks_tokens = [ sum(txout.value for txout in blk.outputs) for blk in blocks ] add_blocks_unlock_reward(self.manager) address = self.manager.wallet.get_unused_address() outside_address = self.get_address(0) outputs = [ WalletOutputInfo(address=decode_address(address), value=500, timelock=int(self.clock.seconds()) + 10), WalletOutputInfo(address=decode_address(address), value=700, timelock=int(self.clock.seconds()) - 10), WalletOutputInfo(address=decode_address(address), value=sum(blocks_tokens[:2]) - 500 - 700, timelock=None) ] tx1 = self.manager.wallet.prepare_transaction_compute_inputs( Transaction, outputs, self.manager.tx_storage) tx1.weight = 10 tx1.parents = self.manager.get_new_tx_parents() tx1.timestamp = int(self.clock.seconds()) tx1.resolve() self.manager.propagate_tx(tx1) self.assertEqual( self.manager.wallet.balance[settings.HATHOR_TOKEN_UID], WalletBalance(500, sum(blocks_tokens) - 500)) self.clock.advance(1) outputs1 = [ WalletOutputInfo(address=decode_address(outside_address), value=500, timelock=None) ] inputs1 = [WalletInputInfo(tx_id=tx1.hash, index=0, private_key=None)] tx2 = self.manager.wallet.prepare_transaction_incomplete_inputs( Transaction, inputs1, outputs1, self.manager.tx_storage) tx2.weight = 10 tx2.parents = self.manager.get_new_tx_parents() tx2.timestamp = int(self.clock.seconds()) tx2.resolve() propagated = self.manager.propagate_tx(tx2) self.assertEqual( self.manager.wallet.balance[settings.HATHOR_TOKEN_UID], WalletBalance(500, sum(blocks_tokens) - 500)) self.assertFalse(propagated) self.clock.advance(1) outputs2 = [ WalletOutputInfo(address=decode_address(outside_address), value=700, timelock=None) ] inputs2 = [WalletInputInfo(tx_id=tx1.hash, index=1, private_key=None)] tx3 = self.manager.wallet.prepare_transaction_incomplete_inputs( Transaction, inputs2, outputs2, self.manager.tx_storage) tx3.weight = 10 tx3.parents = self.manager.get_new_tx_parents() tx3.timestamp = int(self.clock.seconds()) tx3.resolve() propagated = self.manager.propagate_tx(tx3, False) self.assertEqual( self.manager.wallet.balance[settings.HATHOR_TOKEN_UID], WalletBalance(500, sum(blocks_tokens) - 500 - 700)) self.assertTrue(propagated) self.clock.advance(1) outputs3 = [ WalletOutputInfo(address=decode_address(outside_address), value=sum(blocks_tokens[:2]) - 500 - 700, timelock=None) ] inputs3 = [WalletInputInfo(tx_id=tx1.hash, index=2, private_key=None)] tx4 = self.manager.wallet.prepare_transaction_incomplete_inputs( Transaction, inputs3, outputs3, self.manager.tx_storage) tx4.weight = 10 tx4.parents = self.manager.get_new_tx_parents() tx4.timestamp = int(self.clock.seconds()) tx4.resolve() propagated = self.manager.propagate_tx(tx4, False) self.assertEqual( self.manager.wallet.balance[settings.HATHOR_TOKEN_UID], WalletBalance(500, sum(blocks_tokens[:3]))) self.assertTrue(propagated) self.clock.advance(8) tx2.timestamp = int(self.clock.seconds()) tx2.resolve() propagated = self.manager.propagate_tx(tx2, False) self.assertEqual( self.manager.wallet.balance[settings.HATHOR_TOKEN_UID], WalletBalance(0, sum(blocks_tokens[:3]))) self.assertTrue(propagated)
def test_wallet_create_transaction(self): genesis_private_key_bytes = get_private_key_bytes( self.genesis_private_key, encryption_algorithm=serialization.BestAvailableEncryption( PASSWORD)) genesis_address = get_address_b58_from_public_key( self.genesis_public_key) # create wallet with genesis block key key_pair = KeyPair(private_key_bytes=genesis_private_key_bytes, address=genesis_address, used=True) keys = {} keys[key_pair.address] = key_pair w = Wallet(keys=keys, directory=self.directory) w.unlock(PASSWORD) genesis_blocks = [ tx for tx in get_genesis_transactions(None) if tx.is_block ] genesis_block = genesis_blocks[0] genesis_value = sum([output.value for output in genesis_block.outputs]) # wallet will receive genesis block and store in unspent_tx w.on_new_tx(genesis_block) for index in range(len(genesis_block.outputs)): utxo = w.unspent_txs[settings.HATHOR_TOKEN_UID].get( (genesis_block.hash, index)) self.assertIsNotNone(utxo) self.assertEqual(w.balance[settings.HATHOR_TOKEN_UID], WalletBalance(0, genesis_value)) # create transaction spending this value, but sending to same wallet new_address = w.get_unused_address() out = WalletOutputInfo(decode_address(new_address), 100, timelock=None) tx1 = w.prepare_transaction_compute_inputs(Transaction, outputs=[out]) tx1.storage = self.storage tx1.update_hash() self.storage.save_transaction(tx1) w.on_new_tx(tx1) self.assertEqual(len(w.spent_txs), 1) self.assertEqual(w.balance[settings.HATHOR_TOKEN_UID], WalletBalance(0, genesis_value)) # pass inputs and outputs to prepare_transaction, but not the input keys # spend output last transaction input_info = WalletInputInfo(tx1.hash, 1, None) new_address = w.get_unused_address() key2 = w.keys[new_address] out = WalletOutputInfo(decode_address(key2.address), 100, timelock=None) tx2 = w.prepare_transaction_incomplete_inputs(Transaction, inputs=[input_info], outputs=[out], tx_storage=self.storage) tx2.storage = self.storage tx2.update_hash() self.storage.save_transaction(tx2) w.on_new_tx(tx2) self.assertEqual(len(w.spent_txs), 2) self.assertEqual(w.balance[settings.HATHOR_TOKEN_UID], WalletBalance(0, genesis_value)) # test keypair exception with self.assertRaises(WalletLocked): key_pair.get_private_key(None)
def test_tx_tips_voided(self): from hathor.wallet.base_wallet import WalletOutputInfo add_new_blocks(self.manager, 5, advance_clock=15) add_blocks_unlock_reward(self.manager) address1 = self.get_address(0) address2 = self.get_address(1) address3 = self.get_address(2) output1 = WalletOutputInfo(address=decode_address(address1), value=123, timelock=None) output2 = WalletOutputInfo(address=decode_address(address2), value=234, timelock=None) output3 = WalletOutputInfo(address=decode_address(address3), value=345, timelock=None) outputs = [output1, output2, output3] tx1 = self.manager.wallet.prepare_transaction_compute_inputs( Transaction, outputs, self.manager.tx_storage) tx1.weight = 2.0 tx1.parents = self.manager.get_new_tx_parents() tx1.timestamp = int(self.clock.seconds()) tx1.resolve() self.assertTrue(self.manager.propagate_tx(tx1, False)) self.assertEqual( { tx.hash for tx in self.manager.tx_storage.indexes.mempool_tips.iter( self.manager.tx_storage) }, {tx1.hash}) tx2 = self.manager.wallet.prepare_transaction_compute_inputs( Transaction, outputs, self.manager.tx_storage) tx2.weight = 2.0 tx2.parents = [tx1.hash] + self.manager.get_new_tx_parents()[1:] self.assertIn(tx1.hash, tx2.parents) tx2.timestamp = int(self.clock.seconds()) + 1 tx2.resolve() self.assertTrue(self.manager.propagate_tx(tx2, False)) self.assertEqual( { tx.hash for tx in self.manager.tx_storage.indexes.mempool_tips.iter( self.manager.tx_storage) }, {tx2.hash}) tx3 = Transaction.create_from_struct(tx2.get_struct()) tx3.weight = 3.0 # tx3.timestamp = tx2.timestamp + 1 tx3.parents = tx1.parents # self.assertIn(tx1.hash, tx3.parents) tx3.resolve() self.assertNotEqual(tx2.hash, tx3.hash) self.assertTrue(self.manager.propagate_tx(tx3, False)) # self.assertIn(tx3.hash, tx2.get_metadata().voided_by) self.assertIn(tx3.hash, tx2.get_metadata().conflict_with) self.assertEqual( { tx.hash for tx in self.manager.tx_storage.indexes.mempool_tips.iter( self.manager.tx_storage) }, # XXX: what should we expect here? I don't think we should exclude both tx2 and tx3, but maybe let the # function using the index decide {tx1.hash, tx3.hash})
def test_spending_voided(self) -> Generator: self.manager.wallet.unlock(b'MYPASS') add_new_blocks(self.manager, 5, advance_clock=15) add_blocks_unlock_reward(self.manager) # Push a first tx tx = self.get_tx() tx_hex = tx.get_struct().hex() response = yield self.push_tx({'hex_tx': tx_hex}) data = response.json_value() self.assertTrue(data['success']) wallet = self.manager.wallet # Pushing a tx that spends this first tx works txout = tx.outputs[0] p2pkh = parse_address_script(txout.script) assert p2pkh is not None private_key = wallet.get_private_key(p2pkh.address) assert tx.hash is not None inputs = [ WalletInputInfo(tx_id=tx.hash, index=0, private_key=private_key) ] outputs = [ WalletOutputInfo(address=decode_address(p2pkh.address), value=txout.value, timelock=None), ] tx2 = self.get_tx(inputs, outputs) tx2_hex = tx2.get_struct().hex() response = yield self.push_tx({'hex_tx': tx2_hex}) data = response.json_value() self.assertTrue(data['success']) # Now we set this tx2 as voided and try to push a tx3 that spends tx2 tx_meta = tx2.get_metadata() assert tx2.hash is not None tx_meta.voided_by = {tx2.hash} self.manager.tx_storage.save_transaction(tx2, only_metadata=True) inputs = [ WalletInputInfo(tx_id=tx2.hash, index=0, private_key=private_key) ] outputs = [ WalletOutputInfo(address=decode_address(p2pkh.address), value=txout.value, timelock=None), ] tx3 = self.get_tx(inputs, outputs) tx3_hex = tx3.get_struct().hex() response = yield self.push_tx({'hex_tx': tx3_hex}) data = response.json_value() self.assertFalse(data['success']) # Now we set this tx2 as voided and try to push a tx3 that spends tx2 tx_meta = tx2.get_metadata() tx_meta.voided_by = {settings.SOFT_VOIDED_ID} self.manager.tx_storage.save_transaction(tx2, only_metadata=True) # Try to push again with soft voided id as voided by response = yield self.push_tx({'hex_tx': tx3_hex}) data = response.json_value() self.assertFalse(data['success']) # Now without voided_by the push tx must succeed tx_meta = tx2.get_metadata() tx_meta.voided_by = None self.manager.tx_storage.save_transaction(tx2, only_metadata=True) response = yield self.push_tx({'hex_tx': tx3_hex}) data = response.json_value() self.assertTrue(data['success'])
def test_spend_multisig(self): # Adding funds to the wallet blocks = add_new_blocks(self.manager, 2, advance_clock=15) add_blocks_unlock_reward(self.manager) self.assertEqual( self.manager.wallet.balance[settings.HATHOR_TOKEN_UID], WalletBalance(0, sum(blk.outputs[0].value for blk in blocks))) first_block_amount = blocks[0].outputs[0].value # First we send tokens to a multisig address outputs = [ WalletOutputInfo(address=self.multisig_address, value=first_block_amount, timelock=int(self.clock.seconds()) + 15) ] tx1 = self.manager.wallet.prepare_transaction_compute_inputs( Transaction, outputs) tx1.weight = 10 tx1.parents = self.manager.get_new_tx_parents() tx1.timestamp = int(self.clock.seconds()) tx1.resolve() self.manager.propagate_tx(tx1) self.clock.advance(10) self.assertEqual( self.manager.wallet.balance[settings.HATHOR_TOKEN_UID], WalletBalance(0, first_block_amount)) # Then we create a new tx that spends this tokens from multisig wallet tx = Transaction.create_from_struct(tx1.get_struct()) tx.weight = 10 tx.parents = self.manager.get_new_tx_parents() tx.timestamp = int(self.clock.seconds()) multisig_script = create_output_script(self.multisig_address) multisig_output = TxOutput(200, multisig_script) wallet_output = TxOutput(300, create_output_script(self.address)) outside_output = TxOutput(first_block_amount - 200 - 300, create_output_script(self.outside_address)) tx.outputs = [multisig_output, wallet_output, outside_output] tx_input = TxInput(tx1.hash, 0, b'') tx.inputs = [tx_input] signatures = [] for private_key_hex in self.private_keys: signature = generate_signature(tx, bytes.fromhex(private_key_hex), password=b'1234') signatures.append(signature) input_data = MultiSig.create_input_data(self.redeem_script, signatures) tx.inputs[0].data = input_data tx.resolve() # Transaction is still locked self.assertFalse(self.manager.propagate_tx(tx)) self.clock.advance(6) tx.timestamp = int(self.clock.seconds()) tx.resolve() # First we try to propagate with a P2PKH input private_key_obj = get_private_key_from_bytes(bytes.fromhex( self.private_keys[0]), password=b'1234') pubkey_obj = private_key_obj.public_key() public_key_compressed = get_public_key_bytes_compressed(pubkey_obj) p2pkh_input_data = P2PKH.create_input_data(public_key_compressed, signatures[0]) tx2 = Transaction.create_from_struct(tx.get_struct()) tx2.inputs[0].data = p2pkh_input_data tx2.resolve() self.assertFalse(self.manager.propagate_tx(tx2)) # Now we propagate the correct self.assertTrue(self.manager.propagate_tx(tx)) self.assertEqual( self.manager.wallet.balance[settings.HATHOR_TOKEN_UID], WalletBalance(0, first_block_amount + 300)) # Testing the MultiSig class methods cls_script = parse_address_script(multisig_script) self.assertTrue(isinstance(cls_script, MultiSig)) self.assertEqual(cls_script.address, self.multisig_address_b58) expected_dict = { 'type': 'MultiSig', 'address': self.multisig_address_b58, 'timelock': None } self.assertEqual(cls_script.to_human_readable(), expected_dict) script_eval(tx, tx_input, tx1) # Script error with self.assertRaises(ScriptError): create_output_script( base58.b58decode('55d14K5jMqsN2uwUEFqiPG5SoD7Vr1BfnH'))
def test_spend_multisig(self): # Adding funds to the wallet # XXX: note further down the test, 20.00 HTR will be used, block_count must yield at least that amount block_count = 3 # 3 * 8.00 -> 24.00 HTR is enough blocks = add_new_blocks(self.manager, block_count, advance_clock=15) add_blocks_unlock_reward(self.manager) blocks_tokens = [sum(txout.value for txout in blk.outputs) for blk in blocks] available_tokens = sum(blocks_tokens) self.assertEqual(self.manager.wallet.balance[settings.HATHOR_TOKEN_UID], WalletBalance(0, available_tokens)) # First we send tokens to a multisig address block_reward = blocks_tokens[0] outputs = [WalletOutputInfo(address=self.multisig_address, value=block_reward, timelock=None)] tx1 = self.manager.wallet.prepare_transaction_compute_inputs(Transaction, outputs, self.manager.tx_storage) tx1.weight = 10 tx1.parents = self.manager.get_new_tx_parents() tx1.timestamp = int(self.clock.seconds()) tx1.resolve() self.manager.propagate_tx(tx1) self.clock.advance(10) wallet_balance = WalletBalance(0, available_tokens - block_reward) self.assertEqual(self.manager.wallet.balance[settings.HATHOR_TOKEN_UID], wallet_balance) # Then we create a new tx that spends this tokens from multisig wallet tx = Transaction.create_from_struct(tx1.get_struct()) tx.weight = 10 tx.parents = self.manager.get_new_tx_parents() tx.timestamp = int(self.clock.seconds()) multisig_script = create_output_script(self.multisig_address) multisig_output = TxOutput(200, multisig_script) wallet_output = TxOutput(300, create_output_script(self.address)) outside_output = TxOutput(block_reward - 200 - 300, create_output_script(self.outside_address)) tx.outputs = [multisig_output, wallet_output, outside_output] tx_input = TxInput(tx1.hash, 0, b'') tx.inputs = [tx_input] signatures = [] for private_key_hex in self.private_keys: signature = generate_signature(tx, bytes.fromhex(private_key_hex), password=b'1234') signatures.append(signature) parser = create_parser() # Generate spend tx args = parser.parse_args([ tx.get_struct().hex(), '{},{}'.format(signatures[0].hex(), signatures[1].hex()), self.redeem_script.hex() ]) f = StringIO() with capture_logs(): with redirect_stdout(f): execute(args) # Transforming prints str in array output = f.getvalue().strip().splitlines() tx_raw = output[0].split(':')[1].strip() tx = Transaction.create_from_struct(bytes.fromhex(tx_raw)) self.assertTrue(self.manager.propagate_tx(tx, False))
def test_twin_tx(self): add_new_blocks(self.manager, 5, advance_clock=15) add_blocks_unlock_reward(self.manager) address = self.get_address(0) value1 = 100 value2 = 101 value3 = 102 outputs = [ WalletOutputInfo(address=decode_address(address), value=int(value1), timelock=None), WalletOutputInfo(address=decode_address(address), value=int(value2), timelock=None) ] outputs2 = [ WalletOutputInfo(address=decode_address(address), value=int(value1), timelock=None), WalletOutputInfo(address=decode_address(address), value=int(value3), timelock=None) ] tx1 = self.manager.wallet.prepare_transaction_compute_inputs( Transaction, outputs) tx1.weight = 10 tx1.parents = self.manager.get_new_tx_parents() tx1.timestamp = int(self.clock.seconds()) tx1.resolve() # Change of parents only, so it's a twin tx2 = Transaction.create_from_struct(tx1.get_struct()) tx2.parents = [tx1.parents[1], tx1.parents[0]] tx2.resolve() self.assertNotEqual(tx1.hash, tx2.hash) # The same as tx1 but with one input different, so it's not a twin tx3 = self.manager.wallet.prepare_transaction_compute_inputs( Transaction, outputs2) tx3.inputs = tx1.inputs tx3.weight = tx1.weight tx3.parents = tx1.parents tx3.timestamp = tx1.timestamp tx3.resolve() self.manager.propagate_tx(tx1) meta1 = tx1.get_metadata() self.assertEqual(meta1.conflict_with, None) self.assertEqual(meta1.voided_by, None) self.assertEqual(meta1.twins, []) # Propagate a conflicting twin transaction self.manager.propagate_tx(tx2) meta1 = tx1.get_metadata(force_reload=True) self.assertEqual(meta1.conflict_with, [tx2.hash]) self.assertEqual(meta1.voided_by, {tx1.hash}) self.assertEqual(meta1.twins, [tx2.hash]) meta2 = tx2.get_metadata() self.assertEqual(meta2.conflict_with, [tx1.hash]) self.assertEqual(meta2.voided_by, {tx2.hash}) self.assertEqual(meta2.twins, [tx1.hash]) # Propagate another conflicting transaction but it's not a twin self.manager.propagate_tx(tx3) meta1 = tx1.get_metadata() self.assertEqual(meta1.twins, [tx2.hash]) meta3 = tx3.get_metadata() self.assertEqual(meta3.twins, []) self.assertConsensusValid(self.manager)