electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit 47b1bed53933dd1f24f8e68ff97ae8ab778ea9cc
parent 60b77f6a005e45e95435420d305163b151a4684f
Author: SomberNight <somber.night@protonmail.com>
Date:   Thu,  3 May 2018 18:29:02 +0200

implement bolt-04 onion packet construction

Diffstat:
Mlib/lnbase.py | 129+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/tests/test_lnbase.py | 32+++++++++++++++++++++++++++++++-
2 files changed, 160 insertions(+), 1 deletion(-)

diff --git a/lib/lnbase.py b/lib/lnbase.py @@ -19,7 +19,10 @@ import time import binascii import hashlib import hmac +from typing import Sequence import cryptography.hazmat.primitives.ciphers.aead as AEAD +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms +from cryptography.hazmat.backends import default_backend from .bitcoin import (public_key_from_private_key, ser_to_point, point_to_ser, string_to_number, deserialize_privkey, EC_KEY, rev_hex, int_to_hex, @@ -1295,3 +1298,129 @@ class LNPathFinder(PrintError): path += [(cur_node, edge_taken)] path.reverse() return path + + +# bolt 04, "onion" -----> + +NUM_MAX_HOPS_IN_PATH = 20 +HOPS_DATA_SIZE = 1300 # also sometimes called routingInfoSize in bolt-04 +PER_HOP_FULL_SIZE = 65 # HOPS_DATA_SIZE / 20 +NUM_STREAM_BYTES = HOPS_DATA_SIZE + PER_HOP_FULL_SIZE +PER_HOP_PAYLOAD_SIZE = 32 # PER_HOP_FULL_SIZE - len(realm) - len(HMAC) +PER_HOP_HMAC_SIZE = 32 + + +class OnionPerHop: + + def __init__(self, short_channel_id: bytes, amt_to_forward: bytes, outgoing_cltv_value: bytes): + self.short_channel_id = short_channel_id + self.amt_to_forward = amt_to_forward + self.outgoing_cltv_value = outgoing_cltv_value + + def to_bytes(self) -> bytes: + ret = self.short_channel_id + ret += self.amt_to_forward + ret += self.outgoing_cltv_value + ret += bytes(12) # padding + return ret + + +class OnionHopsDataSingle: + + def __init__(self, per_hop: OnionPerHop): + self.realm = 0 + self.per_hop = per_hop + self.hmac = None + + def to_bytes(self) -> bytes: + ret = bytes([self.realm]) + ret += self.per_hop.to_bytes() + ret += self.hmac if self.hmac is not None else bytes(PER_HOP_HMAC_SIZE) + return ret + + +class OnionPacket: + + def __init__(self, public_key: bytes, hops_data: bytes, hmac: bytes): + self.version = 0 + self.public_key = public_key + self.hops_data = hops_data # also called RoutingInfo in bolt-04 + self.hmac = hmac + + def to_bytes(self) -> bytes: + ret = bytes([self.version]) + ret += self.public_key + ret += self.hops_data + ret += self.hmac + return ret + + +def get_bolt04_onion_key(key_type: bytes, secret: bytes) -> bytes: + if key_type not in (b'rho', b'mu', b'um'): + raise Exception('invalid key_type {}'.format(key_type)) + key = hmac.new(key_type, msg=secret, digestmod=hashlib.sha256).digest() + return key + + +def new_onion_packet(payment_path_pubkeys: Sequence[bytes], session_key: bytes, + hops_data: Sequence[OnionHopsDataSingle], associated_data: bytes) -> OnionPacket: + num_hops = len(payment_path_pubkeys) + hop_shared_secrets = num_hops * [b''] + ephemeral_key = session_key + + # compute shared key for each hop + for i in range(0, num_hops): + hop_shared_secrets[i] = get_ecdh(ephemeral_key, payment_path_pubkeys[i]) + ephemeral_pubkey = bfh(EC_KEY(ephemeral_key).get_public_key()) + blinding_factor = H256(ephemeral_pubkey + hop_shared_secrets[i]) + blinding_factor_int = int.from_bytes(blinding_factor, byteorder="big") + ephemeral_key_int = int.from_bytes(ephemeral_key, byteorder="big") + ephemeral_key_int = ephemeral_key_int * blinding_factor_int % SECP256k1.order + ephemeral_key = ephemeral_key_int.to_bytes(32, byteorder="big") + + filler = generate_filler(b'rho', num_hops, PER_HOP_FULL_SIZE, hop_shared_secrets) + mix_header = bytearray(HOPS_DATA_SIZE) + next_hmac = bytearray(PER_HOP_HMAC_SIZE) + + # compute routing info and MAC for each hop + for i in range(num_hops-1, -1, -1): + rho_key = get_bolt04_onion_key(b'rho', hop_shared_secrets[i]) + mu_key = get_bolt04_onion_key(b'mu', hop_shared_secrets[i]) + hops_data[i].hmac = next_hmac + stream_bytes = generate_cipher_stream(rho_key, NUM_STREAM_BYTES) + mix_header = mix_header[:-PER_HOP_FULL_SIZE] + mix_header = hops_data[i].to_bytes() + mix_header + mix_header = ((int.from_bytes(mix_header, "big") ^ int.from_bytes(stream_bytes[:HOPS_DATA_SIZE], "big")) + .to_bytes(HOPS_DATA_SIZE, "big")) + if i == num_hops - 1: + mix_header = mix_header[:-len(filler)] + filler + packet = mix_header + associated_data + next_hmac = hmac.new(mu_key, msg=packet, digestmod=hashlib.sha256).digest() + + return OnionPacket( + public_key=bfh(EC_KEY(session_key).get_public_key()), + hops_data=bytes(mix_header), + hmac=next_hmac) + + +def generate_filler(key_type: bytes, num_hops: int, hop_size: int, + shared_secrets: Sequence[bytes]) -> bytes: + filler_size = (NUM_MAX_HOPS_IN_PATH + 1) * hop_size + filler = bytearray(filler_size) + + for i in range(0, num_hops-1): # -1, as last hop does not obfuscate + filler = filler[hop_size:] + filler += bytearray(hop_size) + stream_key = get_bolt04_onion_key(key_type, shared_secrets[i]) + stream_bytes = generate_cipher_stream(stream_key, filler_size) + filler = ((int.from_bytes(filler, "big") ^ int.from_bytes(stream_bytes, "big")) + .to_bytes(filler_size, "big")) + + return filler[(NUM_MAX_HOPS_IN_PATH-num_hops+2)*hop_size:] + + +def generate_cipher_stream(stream_key: bytes, num_bytes: int) -> bytes: + algo = algorithms.ChaCha20(stream_key, nonce=bytes(16)) + cipher = Cipher(algo, mode=None, backend=default_backend()) + encryptor = cipher.encryptor() + return encryptor.update(bytes(num_bytes)) diff --git a/lib/tests/test_lnbase.py b/lib/tests/test_lnbase.py @@ -6,7 +6,7 @@ from lib.util import bh2u, bfh from lib.lnbase import make_commitment, get_obscured_ctn, Peer, make_offered_htlc, make_received_htlc, make_htlc_tx from lib.lnbase import secret_to_pubkey, derive_pubkey, derive_privkey, derive_blinded_pubkey, overall_weight from lib.lnbase import make_htlc_tx_output, make_htlc_tx_inputs, get_per_commitment_secret_from_seed -from lib.lnbase import make_htlc_tx_witness +from lib.lnbase import make_htlc_tx_witness, OnionHopsDataSingle, new_onion_packet, OnionPerHop from lib.transaction import Transaction from lib import bitcoin import ecdsa.ellipticcurve @@ -308,3 +308,33 @@ class Test_LNBase(unittest.TestCase): self.assertEqual(0x915c75942a26bb3a433a8ce2cb0427c29ec6c1775cfc78328b57f6ba7bfeaa9c.to_bytes(byteorder="big", length=32), get_per_commitment_secret_from_seed(0x0101010101010101010101010101010101010101010101010101010101010101.to_bytes(byteorder="big", length=32), 1)) + def test_new_onion_packet(self): + payment_path_pubkeys = [ + bfh('02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619'), + bfh('0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c'), + bfh('027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007'), + bfh('032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991'), + bfh('02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145'), + ] + session_key = bfh('4141414141414141414141414141414141414141414141414141414141414141') + associated_data = bfh('4242424242424242424242424242424242424242424242424242424242424242') + hops_data = [ + OnionHopsDataSingle(OnionPerHop( + bfh('0000000000000000'), bfh('0000000000000000'), bfh('00000000') + )), + OnionHopsDataSingle(OnionPerHop( + bfh('0101010101010101'), bfh('0000000000000001'), bfh('00000001') + )), + OnionHopsDataSingle(OnionPerHop( + bfh('0202020202020202'), bfh('0000000000000002'), bfh('00000002') + )), + OnionHopsDataSingle(OnionPerHop( + bfh('0303030303030303'), bfh('0000000000000003'), bfh('00000003') + )), + OnionHopsDataSingle(OnionPerHop( + bfh('0404040404040404'), bfh('0000000000000004'), bfh('00000004') + )), + ] + packet = new_onion_packet(payment_path_pubkeys, session_key, hops_data, associated_data) + self.assertEqual(bfh('0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a71da571226458c510bbadd1276f045c21c520a07d35da256ef75b4367962437b0dd10f7d61ab590531cf08000178a333a347f8b4072e216400406bdf3bf038659793a86cae5f52d32f3438527b47a1cfc54285a8afec3a4c9f3323db0c946f5d4cb2ce721caad69320c3a469a202f3e468c67eaf7a7cda226d0fd32f7b48084dca885d15222e60826d5d971f64172d98e0760154400958f00e86697aa1aa9d41bee8119a1ec866abe044a9ad635778ba61fc0776dc832b39451bd5d35072d2269cf9b040d6ba38b54ec35f81d7fc67678c3be47274f3c4cc472aff005c3469eb3bc140769ed4c7f0218ff8c6c7dd7221d189c65b3b9aaa71a01484b122846c7c7b57e02e679ea8469b70e14fe4f70fee4d87b910cf144be6fe48eef24da475c0b0bcc6565ae82cd3f4e3b24c76eaa5616c6111343306ab35c1fe5ca4a77c0e314ed7dba39d6f1e0de791719c241a939cc493bea2bae1c1e932679ea94d29084278513c77b899cc98059d06a27d171b0dbdf6bee13ddc4fc17a0c4d2827d488436b57baa167544138ca2e64a11b43ac8a06cd0c2fba2d4d900ed2d9205305e2d7383cc98dacb078133de5f6fb6bed2ef26ba92cea28aafc3b9948dd9ae5559e8bd6920b8cea462aa445ca6a95e0e7ba52961b181c79e73bd581821df2b10173727a810c92b83b5ba4a0403eb710d2ca10689a35bec6c3a708e9e92f7d78ff3c5d9989574b00c6736f84c199256e76e19e78f0c98a9d580b4a658c84fc8f2096c2fbea8f5f8c59d0fdacb3be2802ef802abbecb3aba4acaac69a0e965abd8981e9896b1f6ef9d60f7a164b371af869fd0e48073742825e9434fc54da837e120266d53302954843538ea7c6c3dbfb4ff3b2fdbe244437f2a153ccf7bdb4c92aa08102d4f3cff2ae5ef86fab4653595e6a5837fa2f3e29f27a9cde5966843fb847a4a61f1e76c281fe8bb2b0a181d096100db5a1a5ce7a910238251a43ca556712eaadea167fb4d7d75825e440f3ecd782036d7574df8bceacb397abefc5f5254d2722215c53ff54af8299aaaad642c6d72a14d27882d9bbd539e1cc7a527526ba89b8c037ad09120e98ab042d3e8652b31ae0e478516bfaf88efca9f3676ffe99d2819dcaeb7610a626695f53117665d267d3f7abebd6bbd6733f645c72c389f03855bdf1e4b8075b516569b118233a0f0971d24b83113c0b096f5216a207ca99a7cddc81c130923fe3d91e7508c9ac5f2e914ff5dccab9e558566fa14efb34ac98d878580814b94b73acbfde9072f30b881f7f0fff42d4045d1ace6322d86a97d164aa84d93a60498065cc7c20e636f5862dc81531a88c60305a2e59a985be327a6902e4bed986dbf4a0b50c217af0ea7fdf9ab37f9ea1a1aaa72f54cf40154ea9b269f1a7c09f9f43245109431a175d50e2db0132337baa0ef97eed0fcf20489da36b79a1172faccc2f7ded7c60e00694282d93359c4682135642bc81f433574aa8ef0c97b4ade7ca372c5ffc23c7eddd839bab4e0f14d6df15c9dbeab176bec8b5701cf054eb3072f6dadc98f88819042bf10c407516ee58bce33fbe3b3d86a54255e577db4598e30a135361528c101683a5fcde7e8ba53f3456254be8f45fe3a56120ae96ea3773631fcb3873aa3abd91bcff00bd38bd43697a2e789e00da6077482e7b1b1a677b5afae4c54e6cbdf7377b694eb7d7a5b913476a5be923322d3de06060fd5e819635232a2cf4f0731da13b8546d1d6d4f8d75b9fce6c2341a71b0ea6f780df54bfdb0dd5cd9855179f602f917265f21f9190c70217774a6fbaaa7d63ad64199f4664813b955cff954949076dcf'), + packet.to_bytes())