Add buy request (#39)

* Add buy squeak request

* Add payment of invoice in itest

* Use latest beta release of lnd

* Got connect peer from itest working

* Open channel in itest before trying to pay

* Add list channels and list peers commands

* Got lightning payment working

* Got full itest working with payment and decryption
This commit is contained in:
Jonathan Zernik 2020-06-09 23:44:54 -07:00 committed by GitHub
parent 30034d35ab
commit 6fe55699f4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 324 additions and 5 deletions

View file

@ -7,7 +7,7 @@ RUN apk update && \
apk add git
# Copy in the local repository to build from.
RUN git clone https://github.com/lightningnetwork/lnd.git --branch v0.9.0-beta
RUN git clone https://github.com/lightningnetwork/lnd.git --branch v0.10.1-beta
# Force Go to use the cgo based DNS resolver. This is required to ensure DNS
# queries required to connect to linked containers succeed.

View file

@ -4,6 +4,7 @@ network=simnet
[lnd]
rpc_host=lnd
rpc_port=10009
port=9735
[electrum]
rpc_host=

10
itests/generate_blocks.sh Executable file
View file

@ -0,0 +1,10 @@
#!/bin/bash
while true; do
alice_address=$(docker exec -it lnd_alice lncli --network=simnet newaddress np2wkh | jq .address -r)
MINING_ADDRESS=$alice_address docker-compose up -d btcd
echo "Mining 1 blocks to address: $alice_address ..."
docker-compose run btcctl generate 400
sleep 10
done

View file

@ -1,5 +1,17 @@
#!/bin/bash
trap "exit" INT TERM
trap "kill 0" EXIT
function mine_blocks {
while true; do
printf "Mining 1 block to address: $MINING_ADDRESS ..."
docker-compose run btcctl generate 1
sleep 10
done
}
cd itests
docker-compose down --volumes
docker-compose build
@ -14,6 +26,10 @@ docker-compose run btcctl generate 400
echo "Finished mining blocks."
sleep 10
echo "Continue mining blocks, 1 every 10 seconds."
mine_blocks &
echo "Background mining task is in background..."
echo "Running test.sh...."
docker-compose run test ./test.sh

0
itests/shell.py Normal file
View file

View file

@ -18,10 +18,10 @@ import logging
import random
import time
from bitcoin.core import lx, x
from squeak.params import SelectParams
from squeak.core import CSqueak
from squeak.core import CheckSqueak
from squeak.core import HASH_LENGTH
from squeak.core import MakeSqueakFromStr
from squeak.core.signing import CSigningKey
@ -89,6 +89,14 @@ def load_lightning_client() -> LNDLightningClient:
)
def bxor(b1, b2): # use xor for bytes
result = bytearray()
for b1, b2 in zip(b1, b2):
result.append(b1 ^ b2)
return bytes(result)
def run():
# Set the network to simnet for itest.
SelectParams("mainnet")
@ -102,7 +110,7 @@ def run():
lnd_lightning_client = load_lightning_client()
balance_from_client = lnd_lightning_client.get_wallet_balance()
print("Balance from direct client: %s" % balance_from_client)
assert balance_from_client.total_balance == 1505000000000
assert balance_from_client.total_balance >= 1505000000000
# Make the stubs
server_stub = squeak_server_pb2_grpc.SqueakServerStub(server_channel)
@ -164,6 +172,53 @@ def run():
))
assert get_hash(squeak) not in set(lookup_response.hashes)
# Buy the squeak data key
buy_response = server_stub.BuySqueak(squeak_server_pb2.BuySqueakRequest(hash=post_response.hash))
print("Server buy response: " + str(buy_response))
assert buy_response.offer.payment_request.startswith('ln')
# Connect to the server lightning node
# lightning_host_port = buy_response.offer.host + ':' + buy_response.offer.port
lightning_host_port = buy_response.offer.host
# connect_peer_response = lnd_lightning_client.connect_peer(buy_response.offer.pubkey, 'lnd_sqkserver')
connect_peer_response = lnd_lightning_client.connect_peer(buy_response.offer.pubkey, lightning_host_port)
print("Server connect peer response: " + str(connect_peer_response))
# List peers
list_peers_response = lnd_lightning_client.list_peers()
print("Server list peers response: " + str(list_peers_response))
# Open channel to the server lightning node
# pubkey_bytes = bytes.fromhex(buy_response.offer.pubkey)
open_channel_response = lnd_lightning_client.open_channel_sync(buy_response.offer.pubkey, 1000000)
print("Server open channel response: " + str(open_channel_response))
# List channels
list_channels_response = lnd_lightning_client.list_channels()
print("Server list channels response: " + str(list_channels_response))
# Sleep for 30 seconds to confirm the channel open transaction
time.sleep(60)
# List channels
list_channels_response = lnd_lightning_client.list_channels()
print("Server list channels response: " + str(list_channels_response))
# Pay the invoice
preimage = None
payment = lnd_lightning_client.pay_invoice_sync(buy_response.offer.payment_request)
print("Server pay invoice response: " + str(payment))
preimage = payment.payment_preimage
print("preimage: " + str(preimage))
# Clear the data key from the squeak and verify with the payment preimage
new_data_key = bxor(buy_response.offer.nonce, preimage)
print("new data key: " + str(new_data_key))
squeak.ClearDataKey()
squeak.SetDataKey(new_data_key)
CheckSqueak(squeak)
print("Finished checking squeak.")
if __name__ == '__main__':

View file

@ -52,3 +52,74 @@ class LNDLightningClient():
def get_wallet_balance(self):
# Retrieve and display the wallet balance
return self.stub.WalletBalance(self.ln_module.WalletBalanceRequest(), metadata=[('macaroon', self.macaroon)])
def add_invoice(self, preimage, amount):
""" Create a new invoice with the given hash pre-image.
args:
preimage -- the preimage bytes used to create the invoice
amount -- the value of the invoice
"""
invoice = self.ln_module.Invoice(
r_preimage = preimage,
value=amount,
)
return self.stub.AddInvoice(invoice, metadata=[('macaroon', self.macaroon)])
def pay_invoice_sync(self, payment_request):
""" Pay an invoice with a given payment_request
args:
payment_request -- the payment_request as a string
"""
send_payment_request = self.ln_module.SendRequest(
payment_request=payment_request,
)
return self.stub.SendPaymentSync(send_payment_request, metadata=[('macaroon', self.macaroon)])
def connect_peer(self, pubkey, host):
""" Connect to a lightning node peer.
args:
pubkey -- The identity pubkey of the Lightning node
host -- The network location of the lightning node
"""
lightning_address = self.ln_module.LightningAddress(
pubkey=pubkey,
host=host,
)
connect_peer_request = self.ln_module.ConnectPeerRequest(
addr=lightning_address,
)
return self.stub.ConnectPeer(connect_peer_request, metadata=[('macaroon', self.macaroon)])
def get_info(self):
""" Get info about the lightning network node.
"""
get_info_request = self.ln_module.GetInfoRequest()
return self.stub.GetInfo(get_info_request, metadata=[('macaroon', self.macaroon)])
def open_channel_sync(self, pubkey_str, local_amount):
""" Open a channel with a remote lightning node.
args:
pubkey (str) -- The identity pubkey of the Lightning node
local_amount -- The number of satoshis the wallet should commit to the channel
"""
open_channel_request = self.ln_module.OpenChannelRequest(
node_pubkey_string=pubkey_str,
local_funding_amount=local_amount,
)
return self.stub.OpenChannelSync(open_channel_request, metadata=[('macaroon', self.macaroon)])
def list_channels(self):
""" List the channels
"""
list_channels_request = self.ln_module.ListChannelsRequest()
return self.stub.ListChannels(list_channels_request, metadata=[('macaroon', self.macaroon)])
def list_peers(self):
""" List the peers
"""
list_peers_request = self.ln_module.ListPeersRequest()
return self.stub.ListPeers(list_peers_request, metadata=[('macaroon', self.macaroon)])

View file

@ -36,6 +36,10 @@ service SqueakServer {
*/
rpc LookupSqueaks (LookupSqueaksRequest) returns (LookupSqueaksReply) {}
/** sqk: `buysqueak`
*/
rpc BuySqueak (BuySqueakRequest) returns (BuySqueakReply) {}
}
message PostSqueakRequest {
@ -57,6 +61,11 @@ message LookupSqueaksRequest {
int32 max_block = 3;
}
message BuySqueakRequest {
/// Hash of the squeak to buy.
bytes hash = 1;
}
message PostSqueakReply {
/// The squeak.
bytes hash = 1;
@ -72,6 +81,11 @@ message LookupSqueaksReply {
repeated bytes hashes = 1;
}
message BuySqueakReply {
/// The buy offer
SqueakBuyOffer offer = 1;
}
message Squeak {
/// Hash of the squeak.
bytes hash = 1;
@ -79,3 +93,29 @@ message Squeak {
/// Serialized squeak.
bytes serialized_squeak = 2;
}
message SqueakBuyOffer {
/// The squeak hash.
bytes squeak_hash = 1;
/// The decryption nonce
bytes nonce = 2;
/// The amount
int64 amount = 3;
/// The invoice preimage hash.
bytes preimage_hash = 4;
/// The invoice
string payment_request = 5;
/// The seller node pubkey
string pubkey = 6;
/// The host of the seller lightning node
string host = 7;
/// The port of the seller lightning node
string port = 8;
}

View file

@ -0,0 +1,13 @@
class BuyOffer():
def __init__(self, squeak_hash, nonce, amount, preimage_hash, payment_request, pubkey, host, port):
self.squeak_hash = squeak_hash
self.nonce = nonce
self.amount = amount
self.preimage_hash = preimage_hash
self.payment_request = payment_request
self.pubkey = pubkey
self.host = host
self.port = port

View file

@ -0,0 +1,4 @@
from collections import namedtuple
LightningAddressHostPort = namedtuple('LightningAddress', ['host', 'port'])

View file

@ -1,3 +1,4 @@
import socket
import argparse
import logging
import signal
@ -14,6 +15,7 @@ import squeakserver.common.rpc.lnd_pb2 as ln
import squeakserver.common.rpc.lnd_pb2_grpc as lnrpc
from squeakserver.common.lnd_lightning_client import LNDLightningClient
from squeakserver.server.lightning_address import LightningAddressHostPort
from squeakserver.server.squeak_server_servicer import SqueakServerServicer
from squeakserver.server.squeak_server_handler import SqueakServerHandler
from squeakserver.server.db_params import parse_db_params
@ -30,6 +32,17 @@ def load_lightning_client(config) -> LNDLightningClient:
)
def load_lightning_host_port(config) -> LNDLightningClient:
lnd_ip_address = socket.gethostbyname(
config['lnd']['rpc_host'],
)
lnd_port = config['lnd']['port']
return LightningAddressHostPort(
lnd_ip_address,
lnd_port,
)
def load_rpc_server(config, handler) -> SqueakServerServicer:
return SqueakServerServicer(
config['server']['rpc_host'],
@ -51,8 +64,9 @@ def start_rpc_server(handler):
server.serve()
def load_handler(lightning_client, postgres_db):
def load_handler(lightning_host_port, lightning_client, postgres_db):
return SqueakServerHandler(
lightning_host_port,
lightning_client,
postgres_db
)
@ -142,8 +156,9 @@ def run_server(config):
print('starting lightning client here...', flush=True)
lightning_client = load_lightning_client(config)
lightning_host_port = load_lightning_host_port(config)
# db_factory = load_db_factory(config)
handler = load_handler(lightning_client, postgres_db)
handler = load_handler(lightning_host_port, lightning_client, postgres_db)
# start rpc server
# start_rpc_server(handler)

View file

@ -5,7 +5,11 @@ from squeak.core.signing import CSigningKey
from squeak.core.signing import CSqueakAddress
from squeakserver.common.lnd_lightning_client import LNDLightningClient
from squeakserver.server.buy_offer import BuyOffer
from squeakserver.server.postgres_db import PostgresDb
from squeakserver.server.util import generate_offer_nonce
from squeakserver.server.util import bxor
from squeakserver.server.lightning_address import LightningAddressHostPort
logger = logging.getLogger(__name__)
@ -17,9 +21,11 @@ class SqueakServerHandler(object):
def __init__(
self,
lightning_host_port: LightningAddressHostPort,
lightning_client: LNDLightningClient,
postgres_db: PostgresDb,
) -> None:
self.lightning_host_port = lightning_host_port
self.lightning_client = lightning_client
self.postgres_db = postgres_db
@ -42,3 +48,41 @@ class SqueakServerHandler(object):
hashes = self.postgres_db.lookup_squeaks(addresses, min_block, max_block)
logger.info("Got hashes from db: " + str(hashes))
return hashes
def handle_buy_squeak(self, squeak_hash):
logger.info("Handler buy squeak by hash: " + str(squeak_hash))
# Get the squeak from the database
squeak = self.postgres_db.get_squeak(squeak_hash)
# Get the datakey from the squeak
data_key = squeak.GetDataKey()
# Generate a new random offer nonce
nonce = generate_offer_nonce()
# Get the invoice preimage from the nonce and the squeak data key
logger.info("Handling buy with nonce: " + str(nonce))
logger.info("Handling buy with data_key: " + str(data_key))
preimage = bxor(nonce, data_key)
# TODO: Get the offer price
amount = 100
logger.info("Handling buy with preimage: " + str(preimage))
# Create the lightning invoice
add_invoice_response = self.lightning_client.add_invoice(preimage, amount)
preimage_hash = add_invoice_response.r_hash
invoice_payment_request = add_invoice_response.payment_request
# Get the lightning network node pubkey
get_info_response = self.lightning_client.get_info()
pubkey = get_info_response.identity_pubkey
# Return the buy offer
return BuyOffer(
squeak_hash,
nonce,
amount,
preimage_hash,
invoice_payment_request,
pubkey,
self.lightning_host_port.host,
self.lightning_host_port.port,
)

View file

@ -85,6 +85,41 @@ class SqueakServerServicer(squeak_server_pb2_grpc.SqueakServerServicer):
hashes=hashes,
)
def BuySqueak(self, request, context):
squeak_hash = request.hash
# TODO: check if hash is valid
buy_response = self.handler.handle_buy_squeak(squeak_hash)
if buy_response == None:
context.set_code(grpc.StatusCode.INVALID_ARGUMENT)
return squeak_server_pb2.BuySqueakReply(
offer=None,
)
offer_squeak_hash = buy_response.squeak_hash
amount = buy_response.amount
nonce = buy_response.nonce
if offer_squeak_hash != squeak_hash:
context.set_code(grpc.StatusCode.INVALID_ARGUMENT)
return squeak_server_pb2.BuySqueakReply(
offer=None,
)
return squeak_server_pb2.BuySqueakReply(
offer=squeak_server_pb2.SqueakBuyOffer(
squeak_hash=offer_squeak_hash,
nonce=nonce,
amount=amount,
preimage_hash=buy_response.preimage_hash,
payment_request=buy_response.payment_request,
pubkey=buy_response.pubkey,
host=buy_response.host,
port=buy_response.port,
)
)
def serve(self):
print('Calling serve...', flush=True)
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))

View file

@ -1,4 +1,19 @@
import os
DATA_KEY_LENGTH = 32
def get_hash(squeak):
return squeak.GetHash()[::-1]
def generate_offer_nonce():
return os.urandom(DATA_KEY_LENGTH)
def bxor(b1, b2): # use xor for bytes
result = bytearray()
for b1, b2 in zip(b1, b2):
result.append(b1 ^ b2)
return bytes(result)