Skip to content

Commit 3249f2b

Browse files
Validate offers format (#2985)
Enforce formatting defined in https://github.com/lightning/bolts/blob/master/12-offer-encoding.md#encoding
1 parent 29ac25f commit 3249f2b

File tree

3 files changed

+89
-1
lines changed

3 files changed

+89
-1
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferTypes.scala

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,8 +314,21 @@ object OfferTypes {
314314
Right(Offer(records))
315315
}
316316

317+
/**
318+
* An offer string can be split with '+' to fit in places with a low character limit. This validates that the string adheres to the spec format to guard against copy-pasting errors.
319+
* @return a lowercase string with '+' and whitespaces removed
320+
*/
321+
private def validateFormat(s: String): String = {
322+
val lowercase = s.toLowerCase
323+
require(s == lowercase || s == s.toUpperCase)
324+
require(lowercase.head == 'l')
325+
require(Bech32.alphabet.contains(lowercase.last))
326+
require(!lowercase.matches(".*\\+\\s*\\+.*"))
327+
lowercase.replaceAll("\\+\\s*", "")
328+
}
329+
317330
def decode(s: String): Try[Offer] = Try {
318-
val triple = Bech32.decodeBytes(s.toLowerCase, true)
331+
val triple = Bech32.decodeBytes(validateFormat(s), true)
319332
val prefix = triple.getFirst
320333
val encoded = triple.getSecond
321334
val encoding = triple.getThird
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
[
2+
{
3+
"comment": "A complete string is valid",
4+
"valid": true,
5+
"string": "lno1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg"
6+
},
7+
{
8+
"comment": "Uppercase is valid",
9+
"valid": true,
10+
"string": "LNO1PQPS7SJQPGTYZM3QV4UXZMTSD3JJQER9WD3HY6TSW35K7MSJZFPY7NZ5YQCNYGRFDEJ82UM5WF5K2UCKYYPWA3EYT44H6TXTXQUQH7LZ5DJGE4AFGFJN7K4RGRKUAG0JSD5XVXG"
11+
},
12+
{
13+
"comment": "+ can join anywhere",
14+
"valid": true,
15+
"string": "l+no1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg"
16+
},
17+
{
18+
"comment": "Multiple + can join",
19+
"valid": true,
20+
"string": "lno1pqps7sjqpgt+yzm3qv4uxzmtsd3jjqer9wd3hy6tsw3+5k7msjzfpy7nz5yqcn+ygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd+5xvxg"
21+
},
22+
{
23+
"comment": "+ can be followed by whitespace",
24+
"valid": true,
25+
"string": "lno1pqps7sjqpgt+ yzm3qv4uxzmtsd3jjqer9wd3hy6tsw3+ 5k7msjzfpy7nz5yqcn+\nygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd+\r\n 5xvxg"
26+
},
27+
{
28+
"comment": "+ can be followed by whitespace, UPPERCASE",
29+
"valid": true,
30+
"string": "LNO1PQPS7SJQPGT+ YZM3QV4UXZMTSD3JJQER9WD3HY6TSW3+ 5K7MSJZFPY7NZ5YQCN+\nYGRFDEJ82UM5WF5K2UCKYYPWA3EYT44H6TXTXQUQH7LZ5DJGE4AFGFJN7K4RGRKUAG0JSD+\r\n 5XVXG"
31+
},
32+
{
33+
"comment": "Mixed case is invalid",
34+
"valid": false,
35+
"string": "LnO1PqPs7sJqPgTyZm3qV4UxZmTsD3JjQeR9Wd3hY6TsW35k7mSjZfPy7nZ5YqCnYgRfDeJ82uM5Wf5k2uCkYyPwA3EyT44h6tXtXqUqH7Lz5dJgE4AfGfJn7k4rGrKuAg0jSd5xVxG"
36+
},
37+
{
38+
"comment": "+ must be surrounded by bech32 characters",
39+
"valid": false,
40+
"string": "lno1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg+"
41+
},
42+
{
43+
"comment": "+ must be surrounded by bech32 characters",
44+
"valid": false,
45+
"string": "lno1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg+ "
46+
},
47+
{
48+
"comment": "+ must be surrounded by bech32 characters",
49+
"valid": false,
50+
"string": "+lno1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg"
51+
},
52+
{
53+
"comment": "+ must be surrounded by bech32 characters",
54+
"valid": false,
55+
"string": "+ lno1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg"
56+
},
57+
{
58+
"comment": "+ must be surrounded by bech32 characters",
59+
"valid": false,
60+
"string": "ln++o1pqps7sjqpgtyzm3qv4uxzmtsd3jjqer9wd3hy6tsw35k7msjzfpy7nz5yqcnygrfdej82um5wf5k2uckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg"
61+
}
62+
]

eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OfferTypesSpec.scala

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,4 +319,17 @@ class OfferTypesSpec extends AnyFunSuite {
319319
}
320320
}
321321
}
322+
323+
case class FormatTestVector(comment: String, valid: Boolean, string: String)
324+
325+
test("string format spec test vectors") {
326+
implicit val formats: DefaultFormats.type = DefaultFormats
327+
328+
val src = Source.fromFile(new File(getClass.getResource(s"/format-string-test.json").getFile))
329+
val testVectors = JsonMethods.parse(src.mkString).extract[Seq[FormatTestVector]]
330+
src.close()
331+
for (vector <- testVectors) {
332+
assert(Offer.decode(vector.string).isSuccess == vector.valid, vector.comment)
333+
}
334+
}
322335
}

0 commit comments

Comments
 (0)