diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala index ab669d25fc..d779b6d0ef 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala @@ -47,6 +47,7 @@ case class Bolt11Invoice(prefix: String, amount_opt: Option[MilliSatoshi], creat require(tags.collect { case _: Bolt11Invoice.PaymentHash => }.size == 1, "there must be exactly one payment hash tag") require(tags.collect { case Bolt11Invoice.Description(_) | Bolt11Invoice.DescriptionHash(_) => }.size == 1, "there must be exactly one description tag or one description hash tag") require(tags.collect { case _: Bolt11Invoice.PaymentSecret => }.size == 1, "there must be exactly one payment secret tag") + require(tags.collect { case f: Bolt11Invoice.FallbackAddress => Try(FallbackAddress(Bolt11Invoice.FallbackAddress.toAddress(f, prefix))) }.forall(_.isSuccess), "invalid fallback address") require(Features.validateFeatureGraph(features).isEmpty, Features.validateFeatureGraph(features).map(_.message)) lazy val paymentHash: ByteVector32 = tags.collectFirst { case p: Bolt11Invoice.PaymentHash => p.hash }.get @@ -269,16 +270,15 @@ object Bolt11Invoice { } def toAddress(f: FallbackAddress, prefix: String): String = { - import f.data f.version match { - case 17 if prefix == "lnbc" => Base58Check.encode(Base58.Prefix.PubkeyAddress, data.toArray) - case 18 if prefix == "lnbc" => Base58Check.encode(Base58.Prefix.ScriptAddress, data.toArray) - case 17 if prefix == "lntb" || prefix == "lnbcrt" || prefix == "lntbs" => Base58Check.encode(Base58.Prefix.PubkeyAddressTestnet, data.toArray) - case 18 if prefix == "lntb" || prefix == "lnbcrt" || prefix == "lntbs" => Base58Check.encode(Base58.Prefix.ScriptAddressTestnet, data.toArray) - case version if prefix == "lnbc" => Bech32.encodeWitnessAddress("bc", version, data.toArray) - case version if prefix == "lntb" => Bech32.encodeWitnessAddress("tb", version, data.toArray) - case version if prefix == "lnbcrt" => Bech32.encodeWitnessAddress("bcrt", version, data.toArray) - case version if prefix == "lntbs" => Bech32.encodeWitnessAddress("tb", version, data.toArray) + case 17 if prefix == "lnbc" => Base58Check.encode(Base58.Prefix.PubkeyAddress, f.data.toArray) + case 18 if prefix == "lnbc" => Base58Check.encode(Base58.Prefix.ScriptAddress, f.data.toArray) + case 17 if prefix == "lntb" || prefix == "lnbcrt" || prefix == "lntbs" => Base58Check.encode(Base58.Prefix.PubkeyAddressTestnet, f.data.toArray) + case 18 if prefix == "lntb" || prefix == "lnbcrt" || prefix == "lntbs" => Base58Check.encode(Base58.Prefix.ScriptAddressTestnet, f.data.toArray) + case version if prefix == "lnbc" => Bech32.encodeWitnessAddress("bc", version, f.data.toArray) + case version if prefix == "lntb" => Bech32.encodeWitnessAddress("tb", version, f.data.toArray) + case version if prefix == "lnbcrt" => Bech32.encodeWitnessAddress("bcrt", version, f.data.toArray) + case version if prefix == "lntbs" => Bech32.encodeWitnessAddress("tb", version, f.data.toArray) } } } @@ -516,11 +516,11 @@ object Bolt11Invoice { val lowercaseInput = input.toLowerCase val separatorIndex = lowercaseInput.lastIndexOf('1') val hrp = lowercaseInput.take(separatorIndex) - val prefix: String = prefixes.values.toSeq.sortBy(_.length).findLast(prefix => hrp.startsWith(prefix)).getOrElse(throw new RuntimeException("unknown prefix")) + val prefix = prefixes.values.toSeq.sortBy(_.length).findLast(prefix => hrp.startsWith(prefix)).getOrElse(throw new RuntimeException("unknown prefix")) val data = string2Bits(lowercaseInput.slice(separatorIndex + 1, lowercaseInput.length - 6)) // 6 == checksum size val bolt11Data = Codecs.bolt11DataCodec.decode(data).require.value val signature = ByteVector64(bolt11Data.signature.take(64)) - val message: ByteVector = ByteVector.view(hrp.getBytes) ++ data.dropRight(520).toByteVector // we drop the sig bytes + val message = ByteVector.view(hrp.getBytes) ++ data.dropRight(520).toByteVector // we drop the sig bytes val recid = bolt11Data.signature.last val pub = Crypto.recoverPublicKey(signature, Crypto.sha256(message), recid) // README: since we use pubkey recovery to compute the node id from the message and signature, we don't check the signature. diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala index dc185b4dca..c74440e386 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala @@ -306,33 +306,6 @@ class Bolt11InvoiceSpec extends AnyFunSuite { assert(invoice.sign(priv).toString == ref) } - test("On mainnet, please send $30 for coffee beans to the same peer, which supports features 8, 14 and 99, using secret 0x1111111111111111111111111111111111111111111111111111111111111111") { - val refs = Seq( - "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqqsgq2a25dxl5hrntdtn6zvydt7d66hyzsyhqs4wdynavys42xgl6sgx9c4g7me86a27t07mdtfry458rtjr0v92cnmswpsjscgt2vcse3sgpz3uapa", - // All upper-case - "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqqsgq2a25dxl5hrntdtn6zvydt7d66hyzsyhqs4wdynavys42xgl6sgx9c4g7me86a27t07mdtfry458rtjr0v92cnmswpsjscgt2vcse3sgpz3uapa".toUpperCase, - // With ignored fields - "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqqsgq2qrqqqfppnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqppnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhpnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqspnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqnp5qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqnpkqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqz599y53s3ujmcfjp5xrdap68qxymkqphwsexhmhr8wdz5usdzkzrse33chw6dlp3jhuhge9ley7j2ayx36kawe7kmgg8sv5ugdyusdcqzn8z9x" - ) - - for (ref <- refs) { - val Success(invoice) = Bolt11Invoice.fromString(ref) - assert(invoice.prefix == "lnbc") - assert(invoice.amount_opt.contains(2500000000L msat)) - assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(invoice.paymentSecret.bytes == hex"1111111111111111111111111111111111111111111111111111111111111111") - assert(invoice.createdAt == TimestampSecond(1496314658L)) - assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) - assert(invoice.description == Left("coffee beans")) - assert(features2bits(invoice.features) == bin"1000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000100000000") - assert(!invoice.features.hasFeature(BasicMultiPartPayment)) - assert(invoice.features.hasFeature(PaymentSecret, Some(Mandatory))) - assert(!invoice.features.hasFeature(TrampolinePaymentPrototype)) - assert(TestConstants.Alice.nodeParams.features.invoiceFeatures().areSupported(invoice.features)) - assert(invoice.sign(priv).toString == ref.toLowerCase) - } - } - test("On mainnet, please send $30 for coffee beans to the same peer, which supports features 8, 14, 99 and 100, using secret 0x1111111111111111111111111111111111111111111111111111111111111111") { val ref = "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q4psqqqqqqqqqqqqqqqqsgqtqyx5vggfcsll4wu246hz02kp85x4katwsk9639we5n5yngc3yhqkm35jnjw4len8vrnqnf5ejh0mzj9n3vz2px97evektfm2l6wqccp3y7372" val Success(invoice) = Bolt11Invoice.fromString(ref) @@ -430,6 +403,8 @@ class Bolt11InvoiceSpec extends AnyFunSuite { "lnbc1p5q54jjpp5fe0dhqdt4m97psq0fv3wjlk95cclnatvuvq49xtnc8rzrp0dysusdqqcqzzsxqrrs0fppqy6uew5229e67r9xzzm9mjyfwseclstdgsp5rnanj9x5rnanj9xnq28hhgd6c7yxlmh6lta047h6lqqqqqqqqqqqrqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6qqqqqqqqqqqqqqqqqqq9kvnknh7ug5mttnqqqqqqqqq8849gwfhvnp9rqpe0cy97", // Invalid min_final_expiry_delta_blocks. "lnbc1p5q54jjpp5fe0dhqdt4m97psq0fv3wjlk95cclnatvuvq49xtnc8rzrp0d5susdqqcqg3vrywwwjsppqy6uew5229e67rzxzzm9mjyfwseclstdgsp5rnanj9xnq28hhgd6c7yxlmh6lta047h6lqqqqqqqqqqqrqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq9kvnknh7ug5mttn2yu5ha6m98cpda2rtwu08849gwfhvnp9rqpqqqqqqqqqg58lts", + // Invalid fallback address. + "lnbc1qzupp9qsp5pvgsuqqpgczuppczc3pcz3syzy8q2xqqqqqqqqqqqqqqqqqygh9qpp5s7zxqqqqqqqqqqyqymqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhp5qs97qqqqqqqpqqyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqptfqptfqptfqptfqptfqptfqptfqptfq95xtfqptfqp3w9chzut3w9chj95xw7tfpp35qqqw9chzuqt3w9chzut3qptfqptfqptfqptfqptfqptfqpqw9cqqqqt28y39", ) for (ref <- refs) { assert(Bolt11Invoice.fromString(ref).isFailure)