lightning-onion 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|