def test_status_close_channel(): """Test ability to get a channel's status and close it.""" channel_server._db = DatabaseSQLite3(':memory:', db_dir='') test_client = _create_client_txs() # Test that channel close fails when no channel exists with pytest.raises(PaymentChannelNotFoundError): channel_server.close('fake', BAD_SIGNATURE) # Open the channel and make a payment deposit_txid = channel_server.open(test_client.deposit_tx, test_client.redeem_script) payment_txid = channel_server.receive_payment(deposit_txid, test_client.payment_tx) channel_server.redeem(payment_txid) # Test that channel close fails without a valid signature with pytest.raises(TransactionVerificationError): closed = channel_server.close(deposit_txid, BAD_SIGNATURE) # Test that channel close succeeds good_signature = codecs.encode( cust_wallet._private_key.sign(deposit_txid).to_der(), 'hex_codec') closed = channel_server.close(deposit_txid, good_signature) assert closed
def test_channel_low_balance_message(): """Test that the channel server returns a useful error when the balance is low.""" channel_server._db = DatabaseSQLite3(':memory:', db_dir='') test_client = _create_client_txs() # Open the channel and make a payment deposit_txid = channel_server.open(test_client.deposit_tx, test_client.redeem_script) payment_txid = channel_server.receive_payment(deposit_txid, test_client.payment_tx) channel_server.redeem(payment_txid) # Create a payment that almost completely drains the channel payment_tx2 = _create_client_payment(test_client, 17) payment_txid2 = channel_server.receive_payment(deposit_txid, payment_tx2) channel_server.redeem(payment_txid2) # Make a payment that spends more than the remaining channel balance payment_tx3 = _create_client_payment(test_client, 18) with pytest.raises(BadTransactionError) as exc: channel_server.receive_payment(deposit_txid, payment_tx3) assert 'Payment channel balance' in str(exc) # Test that channel close succeeds good_signature = codecs.encode( cust_wallet._private_key.sign(deposit_txid).to_der(), 'hex_codec') closed = channel_server.close(deposit_txid, good_signature) assert closed
def test_channel_server_open(): """Test ability to open a payment channel.""" channel_server._db = DatabaseSQLite3(':memory:', db_dir='') test_client = _create_client_txs() # Initialize the handshake and ensure that it returns successfully channel_server.open(test_client.deposit_tx, test_client.redeem_script) # Test for handshake failure when using the same refund twice with pytest.raises(PaymentServerError): channel_server.open(test_client.deposit_tx, test_client.redeem_script)
def test_identify(): """Test ability to identify a payment server.""" channel_server._db = DatabaseSQLite3(':memory:', db_dir='') pc_config = channel_server.identify() merchant_public_key = pc_config['public_key'] test_public_key = codecs.encode( merch_wallet._private_key.public_key.compressed_bytes, 'hex_codec').decode('utf-8') assert merchant_public_key == test_public_key assert pc_config['version'] == channel_server.PROTOCOL_VERSION assert pc_config['zeroconf'] is False
def test_channel_redeem_race_condition(): """Test ability lock multiprocess redeems.""" # Clear test database multiprocess_db = '/tmp/bitserv_test.sqlite3' with open(multiprocess_db, 'w') as f: f.write('') # Initialize test vectors channel_server._db = DatabaseSQLite3(multiprocess_db) test_client = _create_client_txs() deposit_txid = channel_server.open(test_client.deposit_tx, test_client.redeem_script) payment_txid = channel_server.receive_payment(deposit_txid, test_client.payment_tx) # Cache channel result for later channel = channel_server._db.pc.lookup(deposit_txid) # This is a function that takes a long time def delayed_pc_lookup(deposit_txid): time.sleep(0.5) return channel # This is the normal function def normal_pc_lookup(deposit_txid): return channel # This function is called between the first lookup and the final record update # We make sure this function takes extra long the first time its called # in order to expose the race condition channel_server._db.pc.lookup = delayed_pc_lookup # Start the first redeem in its own process and allow time to begin p = multiprocessing.Process(target=channel_server.redeem, args=(payment_txid, )) p.start() time.sleep(0.1) # After starting the first redeem, reset the function to take a normal amount of time channel_server._db.pc.lookup = normal_pc_lookup # To test the race, this redeem is called while the other redeem is still in-process # Because this call makes it to the final database update first, it should be successful channel_server.redeem(payment_txid) # The multiprocess redeem is intentionally made slow, and will finish after the redeem above # Because of this, the multiprocess redeem should throw and exception and exit with an error p.join() assert p.exitcode == 1
def test_receive_payment(): """Test ability to receive a payment within a channel.""" channel_server._db = DatabaseSQLite3(':memory:', db_dir='') test_client = _create_client_txs() # Test that payment receipt fails when no channel exists with pytest.raises(PaymentChannelNotFoundError): channel_server.receive_payment('fake', test_client.payment_tx) # Initiate and complete the payment channel handshake deposit_txid = channel_server.open(test_client.deposit_tx, test_client.redeem_script) # Test that payment receipt succeeds channel_server.receive_payment(deposit_txid, test_client.payment_tx) # Test that payment receipt fails with a duplicate payment with pytest.raises(PaymentServerError): channel_server.receive_payment(deposit_txid, test_client.payment_tx)
def test_redeem_payment(): """Test ability to redeem a payment made within a channel.""" channel_server._db = DatabaseSQLite3(':memory:', db_dir='') test_client = _create_client_txs() # Test that payment redeem fails when no channel exists with pytest.raises(PaymentChannelNotFoundError): channel_server.redeem('fake') # Test that payment redeem succeeds deposit_txid = channel_server.open(test_client.deposit_tx, test_client.redeem_script) payment_txid = channel_server.receive_payment(deposit_txid, test_client.payment_tx) amount = channel_server.redeem(payment_txid) assert amount == TEST_PMT_AMOUNT # Test that payment redeem fails with a duplicate payment with pytest.raises(PaymentServerError): channel_server.redeem(payment_txid)
def test_channel_sync(monkeypatch): """Test ability to sync the status of all channels.""" channel_server._db = DatabaseSQLite3(':memory:', db_dir='') # Seed the database with activity in Channel A test_client_a = _create_client_txs() deposit_txid_a = channel_server.open(test_client_a.deposit_tx, test_client_a.redeem_script) payment_txid = channel_server.receive_payment(deposit_txid_a, test_client_a.payment_tx) amount = channel_server.redeem(payment_txid) assert amount == TEST_PMT_AMOUNT # Seed the database with activity in Channel B cust_wallet._private_key = PrivateKey.from_random() test_client_b = _create_client_txs() deposit_txid_b = channel_server.open(test_client_b.deposit_tx, test_client_b.redeem_script) payment_txid = channel_server.receive_payment(deposit_txid_b, test_client_b.payment_tx) amount = channel_server.redeem(payment_txid) payment_tx1 = _create_client_payment(test_client_b, 2) payment_tx2 = _create_client_payment(test_client_b, 3) payment_tx3 = _create_client_payment(test_client_b, 4) payment_txid1 = channel_server.receive_payment(deposit_txid_b, payment_tx1) payment_txid2 = channel_server.receive_payment(deposit_txid_b, payment_tx2) payment_txid3 = channel_server.receive_payment(deposit_txid_b, payment_tx3) amount1 = channel_server.redeem(payment_txid1) amount2 = channel_server.redeem(payment_txid3) amount3 = channel_server.redeem(payment_txid2) assert amount1 == TEST_PMT_AMOUNT assert amount2 == TEST_PMT_AMOUNT assert amount3 == TEST_PMT_AMOUNT # Both channels should be `ready` since our channel is zeroconf by default channels = channel_server._db.pc.lookup() assert channels, 'Channel lookup with no args should return a list of all channels.' for channel in channels: assert channel.state == ChannelSQLite3.READY, 'Channel should be READY.' # Change Channel A to `confirming` for testing purposes channel_server._db.pc.update_state(deposit_txid_a, ChannelSQLite3.CONFIRMING) test_state = channel_server._db.pc.lookup(deposit_txid_a).state assert test_state == ChannelSQLite3.CONFIRMING, 'Channel should be CONFIRMING' # Change Channel B's expiration to be very close to allowable expiration new_expiry = int(time.time() + 3600) update = 'UPDATE payment_channel SET expires_at=? WHERE deposit_txid=?' channel_server._db.pc.c.execute(update, (new_expiry, deposit_txid_b)) channel_server._db.pc.c.connection.commit() test_expiry = channel_server._db.pc.lookup(deposit_txid_b).expires_at assert test_expiry == new_expiry, 'Channel should closing soon.' # Sync all of the server's payment channels channel_server.sync() # Test that Channel A is `ready` after a sync test_state = channel_server._db.pc.lookup(deposit_txid_a).state assert test_state == ChannelSQLite3.READY, 'Channel should be READY' # Test that Channel B is `closed` after a sync test_state = channel_server._db.pc.lookup(deposit_txid_b).state assert test_state == ChannelSQLite3.CLOSED, 'Channel should be CLOSED' # Test that Channel B payment is fully signed after a sync test_payment = channel_server._db.pc.lookup(deposit_txid_b).payment_tx goodsig_1 = Script.validate_template(test_payment.inputs[0].script, [bytes, bytes, 'OP_1', bytes]) goodsig_true = Script.validate_template(test_payment.inputs[0].script, [bytes, bytes, 'OP_TRUE', bytes]) assert goodsig_1 or goodsig_true, 'Payment should be in a fully signed format' # Test that Channel A remains `ready` after another sync channel_server.sync() test_state = channel_server._db.pc.lookup(deposit_txid_a).state assert test_state == ChannelSQLite3.READY, 'Channel should be READY' # Modify `lookup_spend_txid` to return a txid, as if the tx were spent monkeypatch.setattr(MockBlockchain, 'lookup_spend_txid', mock_lookup_spent_txid) # Test that Channel A is `closed` after a sync where it finds a spent txid channel_server.sync() test_state = channel_server._db.pc.lookup(deposit_txid_a).state assert test_state == ChannelSQLite3.CLOSED, 'Channel should be CLOSED'