diff --git a/channel.py b/channel.py index 0a6127b..fc2af5f 100644 --- a/channel.py +++ b/channel.py @@ -8,9 +8,9 @@ - address -- the url of the counterparty init(conf) - Set up the database -create(url, mymoney, theirmoney) +create(url, my_money, their_money) - Open a channel with the node identified by url, - where you can send mymoney satoshis, and recieve theirmoney satoshis. + where you can send my_money satoshis, and recieve their_money satoshis. send(url, amount) - Update a channel by sending amount satoshis to the node at url. getbalance(url) @@ -38,6 +38,7 @@ from bitcoin.core.scripteval import VerifyScript, SCRIPT_VERIFY_P2SH from bitcoin.core.script import CScript, SignatureHash, SIGHASH_ALL from bitcoin.core.script import OP_CHECKMULTISIG, OP_PUBKEY +from bitcoin.core.key import CPubKey from bitcoin.wallet import CBitcoinAddress import jsonrpcproxy from serverutil import api_factory @@ -101,6 +102,8 @@ class Channel(Model): our_addr = Column(Base58DataType(CBitcoinAddress)) their_balance = Column(Integer) their_addr = Column(Base58DataType(CBitcoinAddress)) + their_pubkey = Column(LargeBinary) + my_pubkey = Column(LargeBinary) def signature(self, transaction): """Signature for a transaction.""" @@ -147,6 +150,14 @@ def settlement(self): return CMutableTransaction([CMutableTxIn(self.anchor_point)], [first, second]) + def commitmentsighash(self, ours=True): + """Generate the sighash for the most recent comitment""" + commit_tx = self.commitment(ours) + # should just be one anchor redeem -- redeem script the same for everyone + sighash = SignatureHash(CScript(self.anchor_redeem), + commit_tx, 0, SIGHASH_ALL) + return sighash + def select_coins(amount): """Get a txin set and change to spend amount.""" coins = g.bit.listunspent() @@ -175,55 +186,69 @@ def get_pubkey(): def update_db(address, amount, sig): """Update the db for a payment.""" - channel = Channel.query.get(address) + channel = Channel.query.get(address) # address is lightning address, address is primary key + # need to make sure we update balances prior to checking signatures + # (so that we get the right sighash) channel.our_balance += amount channel.their_balance -= amount + # make sure we have a valid signature from our counterparty before updating accounts + verify_commitment_signature(CPubKey(channel.their_pubkey), + channel.commitmentsighash(), sig) channel.their_sig = sig database.session.commit() - return channel.signature(channel.commitment()) + return channel.signature(channel.commitment()) # this is our signature of the comittment -def create(url, mymoney, theirmoney, fees=10000): +def create(their_url, my_money, their_money, fees=10000): """Open a payment channel. After this method returns, a payment channel will have been established - with the node identified by url, in which you can send mymoney satoshis - and recieve theirmoney satoshis. Any blockchain fees involved in the + with the node identified by their_url, in which you can send my_money satoshis + and recieve their_money satoshis. Any blockchain fees involved in the setup and teardown of the channel should be collected at this time. """ - bob = jsonrpcproxy.Proxy(url+'channel/') + bob = jsonrpcproxy.Proxy(their_url+'channel/') + # g.logger.debug("### creating channel with bob: " + their_url + "channel/") # Choose inputs and change output - coins, change = select_coins(mymoney + 2 * fees) - pubkey = get_pubkey() + my_coins, my_change = select_coins(my_money + 2 * fees) + my_pubkey = get_pubkey() my_out_addr = g.bit.getnewaddress() # Tell Bob we want to open a channel - transaction, redeem, their_out_addr = bob.open_channel( - g.addr, theirmoney, mymoney, fees, - coins, change, - pubkey, my_out_addr) + transaction, redeem, their_out_addr, their_pubkey = bob.open_channel( + g.addr, their_money, my_money, fees, + my_coins, my_change, + my_pubkey, my_out_addr) # Sign and send the anchor transaction = g.bit.signrawtransaction(transaction) + assert transaction['complete'] transaction = transaction['tx'] g.bit.sendrawtransaction(transaction) # Set up the channel in the DB - channel = Channel(address=url, + channel = Channel(address=their_url, anchor_point=COutPoint(transaction.GetHash(), 0), anchor_index=1, their_sig=b'', anchor_redeem=redeem, - our_balance=mymoney, + our_balance=my_money, our_addr=my_out_addr, - their_balance=theirmoney, + their_balance=their_money, their_addr=their_out_addr, + their_pubkey=their_pubkey, + my_pubkey=my_pubkey, ) # Exchange signatures for the inital commitment transaction - channel.their_sig = \ - bob.update_anchor(g.addr, transaction.GetHash(), - channel.signature(channel.commitment())) + # get the hash of everything including scripsigs (which are nullified in sighash) + # g.addr (our lightning addr), transaction.GetHash() (the TXID) + their_sig = bob.update_anchor(g.addr, transaction.GetHash(), + channel.signature(channel.commitment()), my_pubkey) + # Verify Bob's signature + verify_commitment_signature(CPubKey(their_pubkey), + channel.commitmentsighash(), their_sig) + channel.their_sig = their_sig database.session.add(channel) database.session.commit() # Event: channel opened - CHANNEL_OPENED.send('channel', address=url) + CHANNEL_OPENED.send('channel', address=their_url) def send(url, amount): """Send coin in the channel. @@ -265,6 +290,16 @@ def close(url): database.session.delete(channel) database.session.commit() +def verify_commitment_signature(pubkey, sighash, signature): + """Verify that an updated commitment has been signed by our counterpaty""" + # recovered_pubkey = CPubKey.recover_compact(sighash, signature) # need updated bitcoin lib + pubkey = CPubKey(pubkey) + if not pubkey.verify(sighash, signature): + raise Exception("invalid comitment signature for transaction: " + str(sighash)) + else: + # g.logger.debug("comitment signature verified for comitment with sighash: " + str(sighash)) + return True + @REMOTE def info(): """Get bitcoind info.""" @@ -276,14 +311,15 @@ def get_address(): return str(g.bit.getnewaddress()) @REMOTE -def open_channel(address, mymoney, theirmoney, fees, their_coins, their_change, their_pubkey, their_out_addr): # pylint: disable=too-many-arguments, line-too-long +def open_channel(address, my_money, their_money, fees, their_coins, their_change, their_pubkey, their_out_addr): # pylint: disable=too-many-arguments, line-too-long, too-many-locals """Open a payment channel.""" # Get inputs and change output - coins, change = select_coins(mymoney + 2 * fees) + coins, change = select_coins(my_money + 2 * fees) # Make the anchor script - anchor_output_script = anchor_script(get_pubkey(), their_pubkey) + my_pubkey = get_pubkey() + anchor_output_script = anchor_script(my_pubkey, their_pubkey) # Construct the anchor utxo - payment = CMutableTxOut(mymoney + theirmoney + 2 * fees, + payment = CMutableTxOut(my_money + their_money + 2 * fees, anchor_output_script.to_p2sh_scriptPubKey()) # Anchor tx transaction = CMutableTransaction( @@ -292,28 +328,32 @@ def open_channel(address, mymoney, theirmoney, fees, their_coins, their_change, # Half-sign transaction = g.bit.signrawtransaction(transaction)['tx'] # Create channel in DB - our_addr = g.bit.getnewaddress() + our_btc_addr = g.bit.getnewaddress() channel = Channel(address=address, anchor_point=COutPoint(transaction.GetHash(), 0), anchor_index=0, their_sig=b'', anchor_redeem=anchor_output_script, - our_balance=mymoney, - our_addr=our_addr, - their_balance=theirmoney, + our_balance=my_money, + our_addr=our_btc_addr, + their_balance=their_money, their_addr=their_out_addr, + my_pubkey=my_pubkey, + their_pubkey=their_pubkey, ) database.session.add(channel) database.session.commit() # Event: channel opened CHANNEL_OPENED.send('channel', address=address) - return (transaction, anchor_output_script, our_addr) + return (transaction, anchor_output_script, our_btc_addr, my_pubkey) @REMOTE -def update_anchor(address, new_anchor, their_sig): +def update_anchor(their_lightning_address, new_anchor, their_sig, their_pubkey): """Update the anchor txid after both have signed.""" - channel = Channel.query.get(address) + channel = Channel.query.get(their_lightning_address) + # COoutPoint = The combination of a transaction hash and an index n into its vout ['hash', 'n'] channel.anchor_point = COutPoint(new_anchor, channel.anchor_point.n) + verify_commitment_signature(their_pubkey, channel.commitmentsighash(), their_sig) channel.their_sig = their_sig database.session.commit() return channel.signature(channel.commitment()) @@ -323,6 +363,7 @@ def propose_update(address, amount): """Sign commitment transactions.""" channel = Channel.query.get(address) assert amount > 0 + # need to decrement to generate commitment channel.our_balance += amount channel.their_balance -= amount # don't persist yet diff --git a/lightningd.py b/lightningd.py index 068d05e..be6d91a 100755 --- a/lightningd.py +++ b/lightningd.py @@ -16,6 +16,7 @@ Flag options can be turned off by prefixing with 'no' (Ex: -nodaemon). """ +import logging import argparse import config import os @@ -108,5 +109,11 @@ def add_switch(name): app.register_blueprint(lightning.API) app.register_blueprint(local.API) + handler = logging.StreamHandler() + handler.setLevel(logging.DEBUG) + app.logger.addHandler(handler) + app.logger.setLevel(logging.DEBUG) + # app.logger.debug("i'm logging!") + app.run(port=port, debug=conf.getboolean('debug'), use_reloader=False, processes=3) diff --git a/test/test_integration.py b/test/test_integration.py index a3d2628..fee762e 100644 --- a/test/test_integration.py +++ b/test/test_integration.py @@ -39,7 +39,14 @@ def test_setup(self): def test_basic(self): """Test basic operation of a payment channel.""" # Open a channel between Alice and Bob - self.alice.lit.create(self.bob.lurl, 50000000, 25000000) + self.alice.lit.create(self.bob.lurl, 50000000, 25000000) + + # try: + # self.alice.lit.create(self.bob.lurl, 50000000, 25000000) + # finally: + # self.alice.lightning.print_log() + # self.bob.lightning.print_log() + self.propagate() # There are some fees associated with opening a channel afee = 50000000 - self.alice.bit.getbalance() @@ -49,8 +56,10 @@ def test_basic(self): # (Balance) Alice: 0.50 BTC, Bob: 0.25 BTC self.assertEqual(self.alice.lit.getbalance(self.bob.lurl), 50000000) self.assertEqual(self.bob.lit.getbalance(self.alice.lurl), 25000000) + # Bob sends Alice 0.05 BTC self.bob.lit.send(self.alice.lurl, 5000000) + # (Balance) Alice: 0.55 BTC, Bob: 0.20 BTC self.assertEqual(self.alice.lit.getbalance(self.bob.lurl), 55000000) self.assertEqual(self.bob.lit.getbalance(self.alice.lurl), 20000000)