timplement bolt-04 onion packet construction - electrum - Electrum Bitcoin wallet
HTML git clone https://git.parazyd.org/electrum
DIR Log
DIR Files
DIR Refs
DIR Submodules
---
DIR commit 47b1bed53933dd1f24f8e68ff97ae8ab778ea9cc
DIR parent 60b77f6a005e45e95435420d305163b151a4684f
HTML Author: SomberNight <somber.night@protonmail.com>
Date: Thu, 3 May 2018 18:29:02 +0200
implement bolt-04 onion packet construction
Diffstat:
M lib/lnbase.py | 129 +++++++++++++++++++++++++++++++
M lib/tests/test_lnbase.py | 32 ++++++++++++++++++++++++++++++-
2 files changed, 160 insertions(+), 1 deletion(-)
---
DIR diff --git a/lib/lnbase.py b/lib/lnbase.py
t@@ -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,
t@@ -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))
DIR diff --git a/lib/tests/test_lnbase.py b/lib/tests/test_lnbase.py
t@@ -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
t@@ -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)
parazyd.org:70 /git/electrum/commit/47b1bed53933dd1f24f8e68ff97ae8ab778ea9cc.gph:205: line too long