lightning-onion 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +6 -0
- data/.ruby-version +1 -0
- data/README.md +47 -3
- data/lib/lightning/onion.rb +6 -1
- data/lib/lightning/onion/error_packet.rb +13 -0
- data/lib/lightning/onion/failure_messages.rb +181 -0
- data/lib/lightning/onion/failure_messages/amount_below_minimum.rb +18 -0
- data/lib/lightning/onion/failure_messages/channel_disabled.rb +18 -0
- data/lib/lightning/onion/failure_messages/expiry_too_far.rb +17 -0
- data/lib/lightning/onion/failure_messages/expiry_too_soon.rb +18 -0
- data/lib/lightning/onion/failure_messages/fee_insufficient.rb +18 -0
- data/lib/lightning/onion/failure_messages/final_expiry_too_soon.rb +17 -0
- data/lib/lightning/onion/failure_messages/final_incorrect_cltv_expiry.rb +17 -0
- data/lib/lightning/onion/failure_messages/final_incorrect_htlc_amount.rb +17 -0
- data/lib/lightning/onion/failure_messages/incorrect_cltv_expiry.rb +18 -0
- data/lib/lightning/onion/failure_messages/incorrect_payment_amount.rb +17 -0
- data/lib/lightning/onion/failure_messages/invalid_onion_hmac.rb +18 -0
- data/lib/lightning/onion/failure_messages/invalid_onion_key.rb +18 -0
- data/lib/lightning/onion/failure_messages/invalid_onion_version.rb +18 -0
- data/lib/lightning/onion/failure_messages/invalid_realm.rb +17 -0
- data/lib/lightning/onion/failure_messages/permanent_channel_failure.rb +17 -0
- data/lib/lightning/onion/failure_messages/permanent_node_failure.rb +17 -0
- data/lib/lightning/onion/failure_messages/required_channel_feature_missing.rb +17 -0
- data/lib/lightning/onion/failure_messages/required_node_feature_missing.rb +17 -0
- data/lib/lightning/onion/failure_messages/temporary_channel_failure.rb +18 -0
- data/lib/lightning/onion/failure_messages/temporary_node_failure.rb +17 -0
- data/lib/lightning/onion/failure_messages/unknown_next_peer.rb +17 -0
- data/lib/lightning/onion/failure_messages/unknown_payment_hash.rb +17 -0
- data/lib/lightning/onion/sphinx.rb +94 -20
- data/lib/lightning/onion/version.rb +1 -1
- data/lib/lightning/utils/string.rb +14 -0
- data/lightningrb-onion.gemspec +1 -0
- metadata +42 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '079f7534c2a4cbce4c4f7c272b498f08433d2d09'
|
4
|
+
data.tar.gz: d99082b353d85e7c9a32bb7bb3c08ffaf614c094
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4925aecf07e1782b9316b676e2a67c8e669a4fb528a3e6235f397d7e185a4c371d4be75cbd100af07f3668b8276719a1663028c72ba49c2e7987032f6781f8d3
|
7
|
+
data.tar.gz: 256905de4a28f346b9a49172fd0d2dac789230e2096db59e9f9a6328104a0994d8bc51d8d4d95a74df3619240e994fd1a208f29b098fb7dfb6e1fb9f5dc95e5e
|
data/.rubocop.yml
CHANGED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.4.1
|
data/README.md
CHANGED
@@ -9,7 +9,7 @@ TODO: Delete this and the text above, and describe your gem
|
|
9
9
|
Add this line to your application's Gemfile:
|
10
10
|
|
11
11
|
```ruby
|
12
|
-
gem '
|
12
|
+
gem 'lightning-onion'
|
13
13
|
```
|
14
14
|
|
15
15
|
And then execute:
|
@@ -18,11 +18,55 @@ And then execute:
|
|
18
18
|
|
19
19
|
Or install it yourself as:
|
20
20
|
|
21
|
-
$ gem install
|
21
|
+
$ gem install lightning-onion
|
22
22
|
|
23
23
|
## Usage
|
24
24
|
|
25
|
-
|
25
|
+
### Build Onion Packet
|
26
|
+
|
27
|
+
$ ./bin/console
|
28
|
+
irb(main):001:0> irb(main):002:0> session_key = '4141414141414141414141414141414141414141414141414141414141414141'
|
29
|
+
irb(main):003:0> public_keys = [
|
30
|
+
irb(main):004:1* '02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619',
|
31
|
+
irb(main):005:1* '0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c',
|
32
|
+
irb(main):006:1* '027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007',
|
33
|
+
irb(main):007:1* '032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991',
|
34
|
+
irb(main):008:1* '02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145'
|
35
|
+
irb(main):009:1> ]
|
36
|
+
irb(main):010:0> payloads = [
|
37
|
+
irb(main):011:1* '000000000000000000000000000000000000000000000000000000000000000000',
|
38
|
+
irb(main):012:1* '000101010101010101000000010000000100000000000000000000000000000000',
|
39
|
+
irb(main):013:1* '000202020202020202000000020000000200000000000000000000000000000000',
|
40
|
+
irb(main):014:1* '000303030303030303000000030000000300000000000000000000000000000000',
|
41
|
+
irb(main):015:1* '000404040404040404000000040000000400000000000000000000000000000000'
|
42
|
+
irb(main):016:1> ]
|
43
|
+
irb(main):017:0> associated_data = '4242424242424242424242424242424242424242424242424242424242424242'
|
44
|
+
irb(main):018:0> onion, secrets = Lightning::Onion::Sphinx.make_packet(session_key, public_keys, payloads, associated_data)
|
45
|
+
|
46
|
+
### Parse Onion Packet
|
47
|
+
|
48
|
+
irb(main):019:0> private_keys = [
|
49
|
+
irb(main):020:1* '4141414141414141414141414141414141414141414141414141414141414141',
|
50
|
+
irb(main):021:1* '4242424242424242424242424242424242424242424242424242424242424242',
|
51
|
+
irb(main):022:1* '4343434343434343434343434343434343434343434343434343434343434343',
|
52
|
+
irb(main):023:1* '4444444444444444444444444444444444444444444444444444444444444444',
|
53
|
+
irb(main):024:1* '4545454545454545454545454545454545454545454545454545454545454545'
|
54
|
+
irb(main):025:1> ]
|
55
|
+
irb(main):028:0> payload0, next_packet0, = Lightning::Onion::Sphinx.parse(private_keys[0], onion.to_payload)
|
56
|
+
irb(main):029:0> payload1, next_packet1, = Lightning::Onion::Sphinx.parse(private_keys[1], next_packet0.to_payload)
|
57
|
+
irb(main):030:0> payload2, next_packet2, = Lightning::Onion::Sphinx.parse(private_keys[2], next_packet1.to_payload)
|
58
|
+
irb(main):031:0> payload3, next_packet3, = Lightning::Onion::Sphinx.parse(private_keys[3], next_packet2.to_payload)
|
59
|
+
irb(main):032:0> payload4, next_packet4, = Lightning::Onion::Sphinx.parse(private_keys[4], next_packet3.to_payload)
|
60
|
+
irb(main):035:0> payload0.bth
|
61
|
+
=> "000000000000000000000000000000000000000000000000000000000000000000"
|
62
|
+
irb(main):036:0> payload1.bth
|
63
|
+
=> "000101010101010101000000010000000100000000000000000000000000000000"
|
64
|
+
irb(main):037:0> payload2.bth
|
65
|
+
=> "000202020202020202000000020000000200000000000000000000000000000000"
|
66
|
+
irb(main):038:0> payload3.bth
|
67
|
+
=> "000303030303030303000000030000000300000000000000000000000000000000"
|
68
|
+
irb(main):039:0> payload4.bth
|
69
|
+
=> "000404040404040404000000040000000400000000000000000000000000000000"
|
26
70
|
|
27
71
|
## Development
|
28
72
|
|
data/lib/lightning/onion.rb
CHANGED
@@ -2,12 +2,17 @@
|
|
2
2
|
|
3
3
|
require 'lightning/onion/version'
|
4
4
|
|
5
|
-
require '
|
5
|
+
require 'algebrick'
|
6
6
|
require 'bitcoin'
|
7
|
+
require 'rbnacl'
|
8
|
+
|
9
|
+
require 'lightning/utils/string'
|
7
10
|
|
8
11
|
module Lightning
|
9
12
|
module Onion
|
10
13
|
autoload :ChaCha20, 'lightning/onion/chacha20'
|
14
|
+
autoload :ErrorPacket, 'lightning/onion/error_packet'
|
15
|
+
autoload :FailureMessages, 'lightning/onion/failure_messages'
|
11
16
|
autoload :HopData, 'lightning/onion/hop_data'
|
12
17
|
autoload :PerHop, 'lightning/onion/per_hop'
|
13
18
|
autoload :Packet, 'lightning/onion/packet'
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lightning
|
4
|
+
module Onion
|
5
|
+
class ErrorPacket
|
6
|
+
attr_accessor :onion_node, :failure_message
|
7
|
+
def initialize(onion_node, failure_message)
|
8
|
+
@onion_node = onion_node
|
9
|
+
@failure_message = failure_message
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,181 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lightning
|
4
|
+
module Onion
|
5
|
+
module FailureMessages
|
6
|
+
# unparsable onion encrypted by sending peer
|
7
|
+
BADONION = 0x8000
|
8
|
+
|
9
|
+
# permanent failure (otherwise transient)
|
10
|
+
PERM = 0x4000
|
11
|
+
|
12
|
+
# node failure (otherwise channel)
|
13
|
+
NODE = 0x2000
|
14
|
+
|
15
|
+
# new channel update enclosed
|
16
|
+
UPDATE = 0x1000
|
17
|
+
|
18
|
+
TYPES = {
|
19
|
+
invalid_realm: PERM | 1,
|
20
|
+
temporary_node_failure: NODE | 2,
|
21
|
+
permanent_node_failure: PERM | NODE | 2,
|
22
|
+
required_node_feature_missing: PERM | NODE | 3,
|
23
|
+
invalid_onion_version: BADONION | PERM | 4,
|
24
|
+
invalid_onion_hmac: BADONION | PERM | 5,
|
25
|
+
invalid_onion_key: BADONION | PERM | 6,
|
26
|
+
temporary_channel_failure: UPDATE | 7,
|
27
|
+
permanent_channel_failure: PERM | 8,
|
28
|
+
required_channel_feature_missing: PERM | 9,
|
29
|
+
unknown_next_peer: PERM | 10,
|
30
|
+
amount_below_minimum: UPDATE | 11,
|
31
|
+
fee_insufficient: UPDATE | 12,
|
32
|
+
incorrect_cltv_expiry: UPDATE | 13,
|
33
|
+
expiry_too_soon: UPDATE | 14,
|
34
|
+
unknown_payment_hash: PERM | 15,
|
35
|
+
incorrect_payment_amount: PERM | 16,
|
36
|
+
final_expiry_too_soon: 17,
|
37
|
+
final_incorrect_cltv_expiry: 18,
|
38
|
+
final_incorrect_htlc_amount: 19,
|
39
|
+
channel_disabled: 20,
|
40
|
+
expiry_too_far: 21
|
41
|
+
}.freeze
|
42
|
+
|
43
|
+
FailureMessage = Algebrick.type do
|
44
|
+
InvalidRealm = type do
|
45
|
+
fields! type_code: Numeric
|
46
|
+
end
|
47
|
+
TemporaryNodeFailure = type do
|
48
|
+
fields! type_code: Numeric
|
49
|
+
end
|
50
|
+
PermanentNodeFailure = type do
|
51
|
+
fields! type_code: Numeric
|
52
|
+
end
|
53
|
+
RequiredNodeFeatureMissing = type do
|
54
|
+
fields! type_code: Numeric
|
55
|
+
end
|
56
|
+
InvalidOnionVersion = type do
|
57
|
+
fields! type_code: Numeric,
|
58
|
+
sha256_of_onion: String
|
59
|
+
end
|
60
|
+
InvalidOnionHmac = type do
|
61
|
+
fields! type_code: Numeric,
|
62
|
+
sha256_of_onion: String
|
63
|
+
end
|
64
|
+
InvalidOnionKey = type do
|
65
|
+
fields! type_code: Numeric,
|
66
|
+
sha256_of_onion: String
|
67
|
+
end
|
68
|
+
TemporaryChannelFailure = type do
|
69
|
+
fields! type_code: Numeric,
|
70
|
+
channel_update: String
|
71
|
+
end
|
72
|
+
PermanentChannelFailure = type do
|
73
|
+
fields! type_code: Numeric
|
74
|
+
end
|
75
|
+
RequiredChannelFeatureMissing = type do
|
76
|
+
fields! type_code: Numeric
|
77
|
+
end
|
78
|
+
UnknownNextPeer = type do
|
79
|
+
fields! type_code: Numeric
|
80
|
+
end
|
81
|
+
AmountBelowMinimum = type do
|
82
|
+
fields! type_code: Numeric,
|
83
|
+
htlc_msat: Numeric,
|
84
|
+
channel_update: String
|
85
|
+
end
|
86
|
+
FeeInsufficient = type do
|
87
|
+
fields! type_code: Numeric,
|
88
|
+
htlc_msat: Numeric,
|
89
|
+
channel_update: String
|
90
|
+
end
|
91
|
+
IncorrectCltvExpiry = type do
|
92
|
+
fields! type_code: Numeric,
|
93
|
+
cltv_expiry: Numeric,
|
94
|
+
channel_update: String
|
95
|
+
end
|
96
|
+
ExpiryTooSoon = type do
|
97
|
+
fields! type_code: Numeric,
|
98
|
+
channel_update: String
|
99
|
+
end
|
100
|
+
UnknownPaymentHash = type do
|
101
|
+
fields! type_code: Numeric
|
102
|
+
end
|
103
|
+
IncorrectPaymentAmount = type do
|
104
|
+
fields! type_code: Numeric
|
105
|
+
end
|
106
|
+
FinalExpiryTooSoon = type do
|
107
|
+
fields! type_code: Numeric
|
108
|
+
end
|
109
|
+
FinalIncorrectCltvExpiry = type do
|
110
|
+
fields! type_code: Numeric,
|
111
|
+
cltv_expiry: Numeric
|
112
|
+
end
|
113
|
+
FinalIncorrectHtlcAmount = type do
|
114
|
+
fields! type_code: Numeric,
|
115
|
+
incoming_htlc_amt: Numeric
|
116
|
+
end
|
117
|
+
ChannelDisabled = type do
|
118
|
+
fields! type_code: Numeric,
|
119
|
+
flags: String,
|
120
|
+
channel_update: String
|
121
|
+
end
|
122
|
+
ExpiryTooFar = type do
|
123
|
+
fields! type_code: Numeric
|
124
|
+
end
|
125
|
+
variants InvalidRealm,
|
126
|
+
TemporaryNodeFailure,
|
127
|
+
PermanentNodeFailure,
|
128
|
+
RequiredNodeFeatureMissing,
|
129
|
+
InvalidOnionVersion,
|
130
|
+
InvalidOnionHmac,
|
131
|
+
InvalidOnionKey,
|
132
|
+
TemporaryChannelFailure,
|
133
|
+
PermanentChannelFailure,
|
134
|
+
RequiredChannelFeatureMissing,
|
135
|
+
UnknownNextPeer,
|
136
|
+
AmountBelowMinimum,
|
137
|
+
FeeInsufficient,
|
138
|
+
IncorrectCltvExpiry,
|
139
|
+
ExpiryTooSoon,
|
140
|
+
UnknownPaymentHash,
|
141
|
+
IncorrectPaymentAmount,
|
142
|
+
FinalExpiryTooSoon,
|
143
|
+
FinalIncorrectCltvExpiry,
|
144
|
+
FinalIncorrectHtlcAmount,
|
145
|
+
ChannelDisabled,
|
146
|
+
ExpiryTooFar
|
147
|
+
end
|
148
|
+
|
149
|
+
def self.load(payload)
|
150
|
+
type, = payload.unpack('na*')
|
151
|
+
message_class = FailureMessage.variants.find do |t|
|
152
|
+
TYPES[t.name.split('::').last.snake.to_sym] == type
|
153
|
+
end
|
154
|
+
message_class.load(payload)
|
155
|
+
end
|
156
|
+
|
157
|
+
require 'lightning/onion/failure_messages/invalid_realm'
|
158
|
+
require 'lightning/onion/failure_messages/temporary_node_failure'
|
159
|
+
require 'lightning/onion/failure_messages/permanent_node_failure'
|
160
|
+
require 'lightning/onion/failure_messages/required_node_feature_missing'
|
161
|
+
require 'lightning/onion/failure_messages/invalid_onion_version'
|
162
|
+
require 'lightning/onion/failure_messages/invalid_onion_hmac'
|
163
|
+
require 'lightning/onion/failure_messages/invalid_onion_key'
|
164
|
+
require 'lightning/onion/failure_messages/temporary_channel_failure'
|
165
|
+
require 'lightning/onion/failure_messages/permanent_channel_failure'
|
166
|
+
require 'lightning/onion/failure_messages/required_channel_feature_missing'
|
167
|
+
require 'lightning/onion/failure_messages/unknown_next_peer'
|
168
|
+
require 'lightning/onion/failure_messages/amount_below_minimum'
|
169
|
+
require 'lightning/onion/failure_messages/fee_insufficient'
|
170
|
+
require 'lightning/onion/failure_messages/incorrect_cltv_expiry'
|
171
|
+
require 'lightning/onion/failure_messages/expiry_too_soon'
|
172
|
+
require 'lightning/onion/failure_messages/unknown_payment_hash'
|
173
|
+
require 'lightning/onion/failure_messages/incorrect_payment_amount'
|
174
|
+
require 'lightning/onion/failure_messages/final_expiry_too_soon'
|
175
|
+
require 'lightning/onion/failure_messages/final_incorrect_cltv_expiry'
|
176
|
+
require 'lightning/onion/failure_messages/final_incorrect_htlc_amount'
|
177
|
+
require 'lightning/onion/failure_messages/channel_disabled'
|
178
|
+
require 'lightning/onion/failure_messages/expiry_too_far'
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lightning
|
4
|
+
module Onion
|
5
|
+
module FailureMessages
|
6
|
+
module AmountBelowMinimum
|
7
|
+
def to_payload
|
8
|
+
[type_code, htlc_msat, channel_update.htb.bytesize].pack('nq>n') + channel_update.htb
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.load(payload)
|
12
|
+
type_code, htlc_msat, len, rest = payload.unpack('nq>nH*')
|
13
|
+
new(type_code, htlc_msat, rest[0..len * 2])
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lightning
|
4
|
+
module Onion
|
5
|
+
module FailureMessages
|
6
|
+
module ChannelDisabled
|
7
|
+
def to_payload
|
8
|
+
[type_code].pack('n') + flags + [channel_update.htb.bytesize].pack('n') + channel_update.htb
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.load(payload)
|
12
|
+
type_code, flags, len, rest = payload.unpack('na4nH*')
|
13
|
+
new(type_code, flags, rest[0..len * 2])
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lightning
|
4
|
+
module Onion
|
5
|
+
module FailureMessages
|
6
|
+
module ExpiryTooFar
|
7
|
+
def to_payload
|
8
|
+
[type_code].pack('n')
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.load(payload)
|
12
|
+
new(*payload.unpack('n'))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lightning
|
4
|
+
module Onion
|
5
|
+
module FailureMessages
|
6
|
+
module ExpiryTooSoon
|
7
|
+
def to_payload
|
8
|
+
[type_code, channel_update.htb.bytesize].pack('nn') + channel_update.htb
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.load(payload)
|
12
|
+
type_code, len, rest = payload.unpack('nnH*')
|
13
|
+
new(type_code, rest[0..len * 2])
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lightning
|
4
|
+
module Onion
|
5
|
+
module FailureMessages
|
6
|
+
module FeeInsufficient
|
7
|
+
def to_payload
|
8
|
+
[type_code, htlc_msat, channel_update.htb.bytesize].pack('nq>n') + channel_update.htb
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.load(payload)
|
12
|
+
type_code, htlc_msat, len, rest = payload.unpack('nq>nH*')
|
13
|
+
new(type_code, htlc_msat, rest[0..len * 2])
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lightning
|
4
|
+
module Onion
|
5
|
+
module FailureMessages
|
6
|
+
module FinalExpiryTooSoon
|
7
|
+
def to_payload
|
8
|
+
[type_code].pack('n')
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.load(payload)
|
12
|
+
new(*payload.unpack('n'))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lightning
|
4
|
+
module Onion
|
5
|
+
module FailureMessages
|
6
|
+
module FinalIncorrectCltvExpiry
|
7
|
+
def to_payload
|
8
|
+
[type_code, cltv_expiry].pack('nq>')
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.load(payload)
|
12
|
+
new(*payload.unpack('nq>'))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lightning
|
4
|
+
module Onion
|
5
|
+
module FailureMessages
|
6
|
+
module FinalIncorrectHtlcAmount
|
7
|
+
def to_payload
|
8
|
+
[type_code].pack('n')
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.load(payload)
|
12
|
+
new(*payload.unpack('n'))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lightning
|
4
|
+
module Onion
|
5
|
+
module FailureMessages
|
6
|
+
module IncorrectCltvExpiry
|
7
|
+
def to_payload
|
8
|
+
[type_code, cltv_expiry, channel_update.htb.bytesize].pack('nq>n') + channel_update.htb
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.load(payload)
|
12
|
+
type_code, cltv_expiry, len, rest = payload.unpack('nq>nH*')
|
13
|
+
new(type_code, cltv_expiry, rest[0..len * 2])
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lightning
|
4
|
+
module Onion
|
5
|
+
module FailureMessages
|
6
|
+
module IncorrectPaymentAmount
|
7
|
+
def to_payload
|
8
|
+
[type_code].pack('n')
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.load(payload)
|
12
|
+
new(*payload.unpack('n'))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lightning
|
4
|
+
module Onion
|
5
|
+
module FailureMessages
|
6
|
+
module InvalidOnionHmac
|
7
|
+
def to_payload
|
8
|
+
[type_code, sha256_of_onion.htb.bytesize].pack('nn') + sha256_of_onion.htb
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.load(payload)
|
12
|
+
type_code, len, rest = payload.unpack('nnH*')
|
13
|
+
new(type_code, rest[0..len * 2])
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lightning
|
4
|
+
module Onion
|
5
|
+
module FailureMessages
|
6
|
+
module InvalidOnionKey
|
7
|
+
def to_payload
|
8
|
+
[type_code, sha256_of_onion.htb.bytesize].pack('nn') + sha256_of_onion.htb
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.load(payload)
|
12
|
+
type_code, len, rest = payload.unpack('nnH*')
|
13
|
+
new(type_code, rest[0..len * 2])
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lightning
|
4
|
+
module Onion
|
5
|
+
module FailureMessages
|
6
|
+
module InvalidOnionVersion
|
7
|
+
def to_payload
|
8
|
+
[type_code, sha256_of_onion.htb.bytesize].pack('nn') + sha256_of_onion.htb
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.load(payload)
|
12
|
+
type_code, len, rest = payload.unpack('nnH*')
|
13
|
+
new(type_code, rest[0..len * 2])
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lightning
|
4
|
+
module Onion
|
5
|
+
module FailureMessages
|
6
|
+
module InvalidRealm
|
7
|
+
def to_payload
|
8
|
+
[type_code].pack('n')
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.load(payload)
|
12
|
+
new(*payload.unpack('n'))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lightning
|
4
|
+
module Onion
|
5
|
+
module FailureMessages
|
6
|
+
module PermanentChannelFailure
|
7
|
+
def to_payload
|
8
|
+
[type_code].pack('n')
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.load(payload)
|
12
|
+
new(*payload.unpack('n'))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lightning
|
4
|
+
module Onion
|
5
|
+
module FailureMessages
|
6
|
+
module PermanentNodeFailure
|
7
|
+
def to_payload
|
8
|
+
[type_code].pack('n')
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.load(payload)
|
12
|
+
new(*payload.unpack('n'))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lightning
|
4
|
+
module Onion
|
5
|
+
module FailureMessages
|
6
|
+
module RequiredChannelFeatureMissing
|
7
|
+
def to_payload
|
8
|
+
[type_code].pack('n')
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.load(payload)
|
12
|
+
new(*payload.unpack('n'))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lightning
|
4
|
+
module Onion
|
5
|
+
module FailureMessages
|
6
|
+
module RequiredNodeFeatureMissing
|
7
|
+
def to_payload
|
8
|
+
[type_code].pack('n')
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.load(payload)
|
12
|
+
new(*payload.unpack('n'))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lightning
|
4
|
+
module Onion
|
5
|
+
module FailureMessages
|
6
|
+
module TemporaryChannelFailure
|
7
|
+
def to_payload
|
8
|
+
[type_code, channel_update.bytesize].pack('nn') + channel_update.htb
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.load(payload)
|
12
|
+
type_code, len, rest = payload.unpack('nnH*')
|
13
|
+
new(type_code, rest[0..len * 2])
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lightning
|
4
|
+
module Onion
|
5
|
+
module FailureMessages
|
6
|
+
module TemporaryNodeFailure
|
7
|
+
def to_payload
|
8
|
+
[type_code].pack('n')
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.load(payload)
|
12
|
+
new(*payload.unpack('n'))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lightning
|
4
|
+
module Onion
|
5
|
+
module FailureMessages
|
6
|
+
module UnknownNextPeer
|
7
|
+
def to_payload
|
8
|
+
[type_code].pack('n')
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.load(payload)
|
12
|
+
new(*payload.unpack('n'))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lightning
|
4
|
+
module Onion
|
5
|
+
module FailureMessages
|
6
|
+
module UnknownPaymentHash
|
7
|
+
def to_payload
|
8
|
+
[type_code].pack('n')
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.load(payload)
|
12
|
+
new(*payload.unpack('n'))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -8,24 +8,39 @@ module Lightning
|
|
8
8
|
MAC_LENGTH = 32
|
9
9
|
MAX_HOPS = 20
|
10
10
|
HOP_LENGTH = PAYLOAD_LENGTH + MAC_LENGTH
|
11
|
+
MAX_ERROR_PAYLOAD_LENGTH = 256
|
12
|
+
ERROR_PACKET_LENGTH = MAC_LENGTH + MAX_ERROR_PAYLOAD_LENGTH + 2 + 2
|
11
13
|
|
12
14
|
ZERO_HOP = Lightning::Onion::HopData.parse("\x00" * HOP_LENGTH)
|
13
|
-
LAST_PACKET = Lightning::Onion::Packet.new(VERSION, "\x00" *
|
14
|
-
|
15
|
-
def self.loop(hop_payloads, keys, shared_secrets, packet, associated_data)
|
16
|
-
return packet if hop_payloads.empty?
|
17
|
-
next_packet = make_next_packet(hop_payloads.last, associated_data, keys.last, shared_secrets.last, packet)
|
18
|
-
loop(hop_payloads[0...-1], keys[0...-1], shared_secrets[0...-1], next_packet, associated_data)
|
19
|
-
end
|
15
|
+
LAST_PACKET = Lightning::Onion::Packet.new(VERSION, "\x00" * 33, [ZERO_HOP] * MAX_HOPS, "\x00" * MAC_LENGTH)
|
20
16
|
|
21
17
|
def self.make_packet(session_key, public_keys, payloads, associated_data)
|
22
|
-
ephemereal_public_keys, shared_secrets =
|
18
|
+
ephemereal_public_keys, shared_secrets = compute_keys_and_secrets(session_key, public_keys)
|
23
19
|
filler = generate_filler('rho', shared_secrets[0...-1], HOP_LENGTH, MAX_HOPS)
|
24
|
-
last_packet = make_next_packet(
|
25
|
-
|
20
|
+
last_packet = make_next_packet(
|
21
|
+
payloads.last,
|
22
|
+
associated_data,
|
23
|
+
ephemereal_public_keys.last,
|
24
|
+
shared_secrets.last,
|
25
|
+
LAST_PACKET,
|
26
|
+
filler
|
27
|
+
)
|
28
|
+
packet = internal_make_packet(
|
29
|
+
payloads[0...-1],
|
30
|
+
ephemereal_public_keys[0...-1],
|
31
|
+
shared_secrets[0...-1],
|
32
|
+
last_packet,
|
33
|
+
associated_data
|
34
|
+
)
|
26
35
|
[packet, shared_secrets.zip(public_keys)]
|
27
36
|
end
|
28
37
|
|
38
|
+
def self.internal_make_packet(hop_payloads, keys, shared_secrets, packet, associated_data)
|
39
|
+
return packet if hop_payloads.empty?
|
40
|
+
next_packet = make_next_packet(hop_payloads.last, associated_data, keys.last, shared_secrets.last, packet)
|
41
|
+
internal_make_packet(hop_payloads[0...-1], keys[0...-1], shared_secrets[0...-1], next_packet, associated_data)
|
42
|
+
end
|
43
|
+
|
29
44
|
def self.make_next_packet(payload, associated_data, ephemereal_public_key, shared_secret, packet, filler = '')
|
30
45
|
hops_data1 = payload.htb << packet.hmac << packet.hops_data.map(&:to_payload).join[0...-HOP_LENGTH]
|
31
46
|
stream = generate_cipher_stream(generate_key('rho', shared_secret), MAX_HOPS * HOP_LENGTH)
|
@@ -46,16 +61,28 @@ module Lightning
|
|
46
61
|
Lightning::Onion::Packet.new(VERSION, ephemereal_public_key, hops_data, next_hmac)
|
47
62
|
end
|
48
63
|
|
49
|
-
def self.
|
64
|
+
def self.compute_keys_and_secrets(session_key, public_keys)
|
50
65
|
point = ECDSA::Group::Secp256k1.generator
|
51
66
|
generator_pubkey = ECDSA::Format::PointOctetString.encode(point, compression: true)
|
52
67
|
ephemereal_public_key0 = make_blind(generator_pubkey.bth, session_key)
|
53
68
|
secret0 = compute_shared_secret(public_keys[0], session_key)
|
54
69
|
blinding_factor0 = compute_blinding_factor(ephemereal_public_key0, secret0)
|
55
|
-
|
70
|
+
internal_compute_keys_and_secrets(
|
71
|
+
session_key,
|
72
|
+
public_keys[1..-1],
|
73
|
+
[ephemereal_public_key0],
|
74
|
+
[blinding_factor0],
|
75
|
+
[secret0]
|
76
|
+
)
|
56
77
|
end
|
57
78
|
|
58
|
-
def self.
|
79
|
+
def self.internal_compute_keys_and_secrets(
|
80
|
+
session_key,
|
81
|
+
public_keys,
|
82
|
+
ephemereal_public_keys,
|
83
|
+
blinding_factors,
|
84
|
+
shared_secrets
|
85
|
+
)
|
59
86
|
if public_keys.empty?
|
60
87
|
[ephemereal_public_keys, shared_secrets]
|
61
88
|
else
|
@@ -65,7 +92,13 @@ module Lightning
|
|
65
92
|
ephemereal_public_keys << ephemereal_public_key
|
66
93
|
blinding_factors << blinding_factor
|
67
94
|
shared_secrets << secret
|
68
|
-
|
95
|
+
internal_compute_keys_and_secrets(
|
96
|
+
session_key,
|
97
|
+
public_keys[1..-1],
|
98
|
+
ephemereal_public_keys,
|
99
|
+
blinding_factors,
|
100
|
+
shared_secrets
|
101
|
+
)
|
69
102
|
end
|
70
103
|
end
|
71
104
|
|
@@ -103,12 +136,6 @@ module Lightning
|
|
103
136
|
end.pack('C*').bth
|
104
137
|
end
|
105
138
|
|
106
|
-
module KeyType
|
107
|
-
RHO = 0x72686F
|
108
|
-
MU = 0x6d75
|
109
|
-
UM = 0x756d
|
110
|
-
end
|
111
|
-
|
112
139
|
def self.hmac256(key, message)
|
113
140
|
OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, key, message)
|
114
141
|
end
|
@@ -150,6 +177,53 @@ module Lightning
|
|
150
177
|
end
|
151
178
|
[payload, Lightning::Onion::Packet.new(VERSION, next_public_key, hops_data, hmac), shared_secret]
|
152
179
|
end
|
180
|
+
|
181
|
+
def self.make_error_packet(shared_secret, failure)
|
182
|
+
message = failure.to_payload
|
183
|
+
um = generate_key('um', shared_secret)
|
184
|
+
padlen = MAX_ERROR_PAYLOAD_LENGTH - message.length
|
185
|
+
payload = +''
|
186
|
+
payload << [message.length].pack('n')
|
187
|
+
payload << message
|
188
|
+
payload << [padlen].pack('n')
|
189
|
+
payload << "\x00" * padlen
|
190
|
+
forward_error_packet(mac(um, payload.unpack('C*')) + payload, shared_secret)
|
191
|
+
end
|
192
|
+
|
193
|
+
def self.forward_error_packet(payload, shared_secret)
|
194
|
+
key = generate_key('ammag', shared_secret)
|
195
|
+
stream = generate_cipher_stream(key, ERROR_PACKET_LENGTH)
|
196
|
+
xor(payload.unpack('C*'), stream.unpack('C*')).pack('C*')
|
197
|
+
end
|
198
|
+
|
199
|
+
def self.parse_error(payload, node_shared_secrets)
|
200
|
+
raise "invalid length: #{payload.htb.bytesize}" unless payload.htb.bytesize == ERROR_PACKET_LENGTH
|
201
|
+
internal_parse_error(payload.htb, node_shared_secrets)
|
202
|
+
end
|
203
|
+
|
204
|
+
def self.internal_parse_error(payload, node_shared_secrets)
|
205
|
+
raise RuntimeError unless node_shared_secrets
|
206
|
+
node_shared_secret = node_shared_secrets.last
|
207
|
+
next_payload = forward_error_packet(payload, node_shared_secret[0])
|
208
|
+
if check_mac(node_shared_secret[0], next_payload)
|
209
|
+
ErrorPacket.new(node_shared_secret[1], extract_failure_message(next_payload))
|
210
|
+
else
|
211
|
+
internal_parse_error(next_payload, node_shared_secrets[0...-1])
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def self.check_mac(secret, payload)
|
216
|
+
mac = payload[0...MAC_LENGTH]
|
217
|
+
payload1 = payload[MAC_LENGTH..-1]
|
218
|
+
um = generate_key('um', secret)
|
219
|
+
mac == mac(um, payload1.unpack('C*'))
|
220
|
+
end
|
221
|
+
|
222
|
+
def self.extract_failure_message(payload)
|
223
|
+
raise "invalid length: #{payload.bytesize}" unless payload.bytesize == ERROR_PACKET_LENGTH
|
224
|
+
_mac, len, rest = payload.unpack("a#{MAC_LENGTH}na*")
|
225
|
+
FailureMessages.load(rest[0...len])
|
226
|
+
end
|
153
227
|
end
|
154
228
|
end
|
155
229
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class String
|
4
|
+
def camel
|
5
|
+
split('_').map { |w| w[0].upcase + w[1..-1] }.join
|
6
|
+
end
|
7
|
+
|
8
|
+
def snake
|
9
|
+
gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
10
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
11
|
+
.tr('-', '_')
|
12
|
+
.downcase
|
13
|
+
end
|
14
|
+
end
|
data/lightningrb-onion.gemspec
CHANGED
@@ -20,6 +20,7 @@ Gem::Specification.new do |spec|
|
|
20
20
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
21
21
|
spec.require_paths = ['lib']
|
22
22
|
|
23
|
+
spec.add_runtime_dependency 'algebrick'
|
23
24
|
spec.add_runtime_dependency 'bitcoinrb'
|
24
25
|
spec.add_runtime_dependency 'rbnacl'
|
25
26
|
|
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lightning-onion
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Hajime Yamaguchi
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-04-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: algebrick
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: bitcoinrb
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -90,6 +104,7 @@ files:
|
|
90
104
|
- ".gitignore"
|
91
105
|
- ".rspec"
|
92
106
|
- ".rubocop.yml"
|
107
|
+
- ".ruby-version"
|
93
108
|
- ".travis.yml"
|
94
109
|
- CODE_OF_CONDUCT.md
|
95
110
|
- Gemfile
|
@@ -100,11 +115,36 @@ files:
|
|
100
115
|
- bin/setup
|
101
116
|
- lib/lightning/onion.rb
|
102
117
|
- lib/lightning/onion/chacha20.rb
|
118
|
+
- lib/lightning/onion/error_packet.rb
|
119
|
+
- lib/lightning/onion/failure_messages.rb
|
120
|
+
- lib/lightning/onion/failure_messages/amount_below_minimum.rb
|
121
|
+
- lib/lightning/onion/failure_messages/channel_disabled.rb
|
122
|
+
- lib/lightning/onion/failure_messages/expiry_too_far.rb
|
123
|
+
- lib/lightning/onion/failure_messages/expiry_too_soon.rb
|
124
|
+
- lib/lightning/onion/failure_messages/fee_insufficient.rb
|
125
|
+
- lib/lightning/onion/failure_messages/final_expiry_too_soon.rb
|
126
|
+
- lib/lightning/onion/failure_messages/final_incorrect_cltv_expiry.rb
|
127
|
+
- lib/lightning/onion/failure_messages/final_incorrect_htlc_amount.rb
|
128
|
+
- lib/lightning/onion/failure_messages/incorrect_cltv_expiry.rb
|
129
|
+
- lib/lightning/onion/failure_messages/incorrect_payment_amount.rb
|
130
|
+
- lib/lightning/onion/failure_messages/invalid_onion_hmac.rb
|
131
|
+
- lib/lightning/onion/failure_messages/invalid_onion_key.rb
|
132
|
+
- lib/lightning/onion/failure_messages/invalid_onion_version.rb
|
133
|
+
- lib/lightning/onion/failure_messages/invalid_realm.rb
|
134
|
+
- lib/lightning/onion/failure_messages/permanent_channel_failure.rb
|
135
|
+
- lib/lightning/onion/failure_messages/permanent_node_failure.rb
|
136
|
+
- lib/lightning/onion/failure_messages/required_channel_feature_missing.rb
|
137
|
+
- lib/lightning/onion/failure_messages/required_node_feature_missing.rb
|
138
|
+
- lib/lightning/onion/failure_messages/temporary_channel_failure.rb
|
139
|
+
- lib/lightning/onion/failure_messages/temporary_node_failure.rb
|
140
|
+
- lib/lightning/onion/failure_messages/unknown_next_peer.rb
|
141
|
+
- lib/lightning/onion/failure_messages/unknown_payment_hash.rb
|
103
142
|
- lib/lightning/onion/hop_data.rb
|
104
143
|
- lib/lightning/onion/packet.rb
|
105
144
|
- lib/lightning/onion/per_hop.rb
|
106
145
|
- lib/lightning/onion/sphinx.rb
|
107
146
|
- lib/lightning/onion/version.rb
|
147
|
+
- lib/lightning/utils/string.rb
|
108
148
|
- lightningrb-onion.gemspec
|
109
149
|
homepage: https://github.com/Yamaguchi/lightningrb-onion
|
110
150
|
licenses:
|