# ported from lnd 42de4400bff5105352d0552155f73589166d162b
import unittest
import lib . bitcoin as bitcoin
import lib . lnbase as lnbase
import lib . lnhtlc as lnhtlc
import lib . util as util
import os
import binascii
def create_channel_state ( funding_txid , funding_index , funding_sat , local_feerate , is_initiator , local_amount , remote_amount , privkeys , other_pubkeys , seed , cur , nex , other_node_id ) :
assert local_amount > 0
assert remote_amount > 0
channel_id , _ = lnbase . channel_id_from_funding_tx ( funding_txid , funding_index )
their_revocation_store = lnbase . RevocationStore ( )
local_config = lnbase . ChannelConfig (
payment_basepoint = privkeys [ 0 ] ,
multisig_key = privkeys [ 1 ] ,
htlc_basepoint = privkeys [ 2 ] ,
delayed_basepoint = privkeys [ 3 ] ,
revocation_basepoint = privkeys [ 4 ] ,
to_self_delay = 143 ,
dust_limit_sat = 10 ,
max_htlc_value_in_flight_msat = 500000 * 1000 ,
max_accepted_htlcs = 5
)
remote_config = lnbase . ChannelConfig (
payment_basepoint = other_pubkeys [ 0 ] ,
multisig_key = other_pubkeys [ 1 ] ,
htlc_basepoint = other_pubkeys [ 2 ] ,
delayed_basepoint = other_pubkeys [ 3 ] ,
revocation_basepoint = other_pubkeys [ 4 ] ,
to_self_delay = 143 ,
dust_limit_sat = 10 ,
max_htlc_value_in_flight_msat = 500000 * 1000 ,
max_accepted_htlcs = 5
)
return lnbase . OpenChannel (
channel_id = channel_id ,
short_channel_id = channel_id [ : 8 ] ,
funding_outpoint = lnbase . Outpoint ( funding_txid , funding_index ) ,
local_config = local_config ,
remote_config = remote_config ,
remote_state = lnbase . RemoteState (
ctn = 0 ,
next_per_commitment_point = nex ,
last_per_commitment_point = cur ,
amount_msat = remote_amount ,
revocation_store = their_revocation_store ,
next_htlc_id = 0
) ,
local_state = lnbase . LocalState (
ctn = 0 ,
per_commitment_secret_seed = seed ,
amount_msat = local_amount ,
next_htlc_id = 0 ,
funding_locked_received = True
) ,
constraints = lnbase . ChannelConstraints ( capacity = funding_sat , feerate = local_feerate , is_initiator = is_initiator , funding_txn_minimum_depth = 3 ) ,
node_id = other_node_id
)
def bip32 ( sequence ) :
xprv , xpub = bitcoin . bip32_root ( b " 9dk " , ' standard ' )
xprv , xpub = bitcoin . bip32_private_derivation ( xprv , " m/ " , sequence )
xtype , depth , fingerprint , child_number , c , k = bitcoin . deserialize_xprv ( xprv )
assert len ( k ) == 32
assert type ( k ) is bytes
return k
def create_test_channels ( ) :
funding_txid = binascii . hexlify ( os . urandom ( 32 ) ) . decode ( " ascii " )
funding_index = 0
funding_sat = bitcoin . COIN * 5
local_amount = ( funding_sat * 1000 ) / / 2
remote_amount = ( funding_sat * 1000 ) / / 2
alice_raw = [ bip32 ( " m/ " + str ( i ) ) for i in range ( 5 ) ]
bob_raw = [ bip32 ( " m/ " + str ( i ) ) for i in range ( 5 , 11 ) ]
alice_privkeys = [ lnbase . Keypair ( lnbase . privkey_to_pubkey ( x ) , x ) for x in alice_raw ]
bob_privkeys = [ lnbase . Keypair ( lnbase . privkey_to_pubkey ( x ) , x ) for x in bob_raw ]
alice_pubkeys = [ lnbase . OnlyPubkeyKeypair ( x . pubkey ) for x in alice_privkeys ]
bob_pubkeys = [ lnbase . OnlyPubkeyKeypair ( x . pubkey ) for x in bob_privkeys ]
alice_seed = os . urandom ( 32 )
bob_seed = os . urandom ( 32 )
alice_cur = lnbase . secret_to_pubkey ( int . from_bytes ( lnbase . get_per_commitment_secret_from_seed ( alice_seed , 2 * * 48 - 1 ) , " big " ) )
alice_next = lnbase . secret_to_pubkey ( int . from_bytes ( lnbase . get_per_commitment_secret_from_seed ( alice_seed , 2 * * 48 - 2 ) , " big " ) )
bob_cur = lnbase . secret_to_pubkey ( int . from_bytes ( lnbase . get_per_commitment_secret_from_seed ( bob_seed , 2 * * 48 - 1 ) , " big " ) )
bob_next = lnbase . secret_to_pubkey ( int . from_bytes ( lnbase . get_per_commitment_secret_from_seed ( bob_seed , 2 * * 48 - 2 ) , " big " ) )
return lnhtlc . HTLCStateMachine (
create_channel_state ( funding_txid , funding_index , funding_sat , 20000 , True , local_amount , remote_amount , alice_privkeys , bob_pubkeys , alice_seed , bob_cur , bob_next , b " \x02 " * 33 ) , " alice " ) , lnhtlc . HTLCStateMachine (
create_channel_state ( funding_txid , funding_index , funding_sat , 20000 , False , remote_amount , local_amount , bob_privkeys , alice_pubkeys , bob_seed , alice_cur , alice_next , b " \x01 " * 33 ) , " bob " )
one_bitcoin_in_msat = bitcoin . COIN * 1000
class TestLNBaseHTLCStateMachine ( unittest . TestCase ) :
def assertOutputExistsByValue ( self , tx , amt_sat ) :
for typ , scr , val in tx . outputs ( ) :
if val == amt_sat :
break
else :
self . assertFalse ( )
def test_SimpleAddSettleWorkflow ( self ) :
# Create a test channel which will be used for the duration of this
# unittest. The channel will be funded evenly with Alice having 5 BTC,
# and Bob having 5 BTC.
alice_channel , bob_channel = create_test_channels ( )
paymentPreimage = b " \x01 " * 32
paymentHash = bitcoin . sha256 ( paymentPreimage )
htlcAmt = one_bitcoin_in_msat
htlc = lnhtlc . UpdateAddHtlc (
payment_hash = paymentHash ,
amount_msat = htlcAmt ,
cltv_expiry = 5 ,
total_fee = 0
)
# First Alice adds the outgoing HTLC to her local channel's state
# update log. Then Alice sends this wire message over to Bob who adds
# this htlc to his remote state update log.
aliceHtlcIndex = alice_channel . add_htlc ( htlc )
bobHtlcIndex = bob_channel . receive_htlc ( htlc )
# Next alice commits this change by sending a signature message. Since
# we expect the messages to be ordered, Bob will receive the HTLC we
# just sent before he receives this signature, so the signature will
# cover the HTLC.
aliceSig , aliceHtlcSigs = alice_channel . sign_next_commitment ( )
self . assertEqual ( len ( aliceHtlcSigs ) , 1 , " alice should generate one htlc signature " )
# Bob receives this signature message, and checks that this covers the
# state he has in his remote log. This includes the HTLC just sent
# from Alice.
bob_channel . receive_new_commitment ( aliceSig , aliceHtlcSigs )
# Bob revokes his prior commitment given to him by Alice, since he now
# has a valid signature for a newer commitment.
bobRevocation , _ = bob_channel . revoke_current_commitment ( )
# Bob finally send a signature for Alice's commitment transaction.
# This signature will cover the HTLC, since Bob will first send the
# revocation just created. The revocation also acks every received
# HTLC up to the point where Alice sent here signature.
bobSig , bobHtlcSigs = bob_channel . sign_next_commitment ( )
# Alice then processes this revocation, sending her own revocation for
# her prior commitment transaction. Alice shouldn't have any HTLCs to
# forward since she's sending an outgoing HTLC.
alice_channel . receive_revocation ( bobRevocation )
# Alice then processes bob's signature, and since she just received
# the revocation, she expect this signature to cover everything up to
# the point where she sent her signature, including the HTLC.
alice_channel . receive_new_commitment ( bobSig , bobHtlcSigs )
# Alice then generates a revocation for bob.
aliceRevocation , _ = alice_channel . revoke_current_commitment ( )
# Finally Bob processes Alice's revocation, at this point the new HTLC
# is fully locked in within both commitment transactions. Bob should
# also be able to forward an HTLC now that the HTLC has been locked
# into both commitment transactions.
bob_channel . receive_revocation ( aliceRevocation )
# At this point, both sides should have the proper number of satoshis
# sent, and commitment height updated within their local channel
# state.
aliceSent = 0
bobSent = 0
self . assertEqual ( alice_channel . total_msat_sent , aliceSent , " alice has incorrect milli-satoshis sent: %s vs %s " % ( alice_channel . total_msat_sent , aliceSent ) )
self . assertEqual ( alice_channel . total_msat_received , bobSent , " alice has incorrect milli-satoshis received %s vs %s " % ( alice_channel . total_msat_received , bobSent ) )
self . assertEqual ( bob_channel . total_msat_sent , bobSent , " bob has incorrect milli-satoshis sent %s vs %s " % ( bob_channel . total_msat_sent , bobSent ) )
self . assertEqual ( bob_channel . total_msat_received , aliceSent , " bob has incorrect milli-satoshis received %s vs %s " % ( bob_channel . total_msat_received , aliceSent ) )
self . assertEqual ( bob_channel . state . local_state . ctn , 1 , " bob has incorrect commitment height, %s vs %s " % ( bob_channel . state . local_state . ctn , 1 ) )
self . assertEqual ( alice_channel . state . local_state . ctn , 1 , " alice has incorrect commitment height, %s vs %s " % ( alice_channel . state . local_state . ctn , 1 ) )
# Both commitment transactions should have three outputs, and one of
# them should be exactly the amount of the HTLC.
self . assertEqual ( len ( alice_channel . local_commitment . outputs ( ) ) , 3 , " alice should have three commitment outputs, instead have %s " % len ( alice_channel . local_commitment . outputs ( ) ) )
self . assertEqual ( len ( bob_channel . local_commitment . outputs ( ) ) , 3 , " bob should have three commitment outputs, instead have %s " % len ( bob_channel . local_commitment . outputs ( ) ) )
self . assertOutputExistsByValue ( alice_channel . local_commitment , htlcAmt / / 1000 )
self . assertOutputExistsByValue ( bob_channel . local_commitment , htlcAmt / / 1000 )
# Now we'll repeat a similar exchange, this time with Bob settling the
# HTLC once he learns of the preimage.
preimage = paymentPreimage
bob_channel . settle_htlc ( preimage , bobHtlcIndex , None , None , None )
alice_channel . receive_htlc_settle ( preimage , aliceHtlcIndex )
bobSig2 , bobHtlcSigs2 = bob_channel . sign_next_commitment ( )
alice_channel . receive_new_commitment ( bobSig2 , bobHtlcSigs2 )
aliceRevocation2 , _ = alice_channel . revoke_current_commitment ( )
aliceSig2 , aliceHtlcSigs2 = alice_channel . sign_next_commitment ( )
bob_channel . receive_revocation ( aliceRevocation2 )
bob_channel . receive_new_commitment ( aliceSig2 , aliceHtlcSigs2 )
bobRevocation2 , _ = bob_channel . revoke_current_commitment ( )
alice_channel . receive_revocation ( bobRevocation2 )
# At this point, Bob should have 6 BTC settled, with Alice still having
# 4 BTC. Alice's channel should show 1 BTC sent and Bob's channel
# should show 1 BTC received. They should also be at commitment height
# two, with the revocation window extended by 1 (5).
mSatTransferred = one_bitcoin_in_msat
self . assertEqual ( alice_channel . total_msat_sent , mSatTransferred , " alice satoshis sent incorrect %s vs %s expected " % ( alice_channel . total_msat_sent , mSatTransferred ) )
self . assertEqual ( alice_channel . total_msat_received , 0 , " alice satoshis received incorrect %s vs %s expected " % ( alice_channel . total_msat_received , 0 ) )
self . assertEqual ( bob_channel . total_msat_received , mSatTransferred , " bob satoshis received incorrect %s vs %s expected " % ( bob_channel . total_msat_received , mSatTransferred ) )
self . assertEqual ( bob_channel . total_msat_sent , 0 , " bob satoshis sent incorrect %s vs %s expected " % ( bob_channel . total_msat_sent , 0 ) )
self . assertEqual ( bob_channel . l_current_height , 2 , " bob has incorrect commitment height, %s vs %s " % ( bob_channel . l_current_height , 2 ) )
self . assertEqual ( alice_channel . l_current_height , 2 , " alice has incorrect commitment height, %s vs %s " % ( alice_channel . l_current_height , 2 ) )
# The logs of both sides should now be cleared since the entry adding
# the HTLC should have been removed once both sides receive the
# revocation.
self . assertEqual ( alice_channel . local_update_log , [ ] , " alice ' s local not updated, should be empty, has %s entries instead " % len ( alice_channel . local_update_log ) )
self . assertEqual ( alice_channel . remote_update_log , [ ] , " alice ' s remote not updated, should be empty, has %s entries instead " % len ( alice_channel . remote_update_log ) )