Skip to content
105 changes: 73 additions & 32 deletions channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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."""
Expand All @@ -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(
Expand All @@ -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())
Expand All @@ -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
Expand Down
7 changes: 7 additions & 0 deletions lightningd.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
Flag options can be turned off by prefixing with 'no' (Ex: -nodaemon).
"""

import logging
import argparse
import config
import os
Expand Down Expand Up @@ -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)
11 changes: 10 additions & 1 deletion test/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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)
Expand Down